Compare commits
5 Commits
57cd9a1f39
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
278f1e0f69 | ||
|
|
bd0e4a660a | ||
|
|
d7a198fd58 | ||
|
|
76caafc580 | ||
|
|
a1b0d6731c |
92
.gitignore
vendored
92
.gitignore
vendored
@@ -1,36 +1,72 @@
|
||||
# Build and Release Folders
|
||||
bin-debug/
|
||||
bin-release/
|
||||
[Oo]bj/
|
||||
[Bb]in/
|
||||
# Python cache and bytecode
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Other files and folders
|
||||
.settings/
|
||||
# Native extensions
|
||||
*.so
|
||||
|
||||
node_modules/
|
||||
public/
|
||||
myenv/
|
||||
# Packaging
|
||||
.Python
|
||||
build/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
share/python-wheels/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
package.json
|
||||
package-lock.json
|
||||
env/
|
||||
ENV/
|
||||
myenv/
|
||||
.python-version
|
||||
|
||||
# Executables
|
||||
*.swf
|
||||
*.air
|
||||
*.ipa
|
||||
*.apk
|
||||
# Test, coverage, type check, lint caches
|
||||
.coverage
|
||||
.coverage.*
|
||||
.hypothesis/
|
||||
.mypy_cache/
|
||||
.nox/
|
||||
.pyre/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.tox/
|
||||
htmlcov/
|
||||
coverage.xml
|
||||
nosetests.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
|
||||
# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
|
||||
# should NOT be excluded as they contain compiler settings and other important
|
||||
# information for Eclipse / Flash Builder.
|
||||
# Notebook
|
||||
.ipynb_checkpoints/
|
||||
|
||||
*.mov
|
||||
*.mp4
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
*.local
|
||||
|
||||
# IDE and OS files
|
||||
.DS_Store
|
||||
.idea/
|
||||
.settings/
|
||||
.vscode/
|
||||
|
||||
# Project runtime files
|
||||
images/
|
||||
output/
|
||||
|
||||
# Local certificates and keys
|
||||
cert/nginx/*.crt
|
||||
cert/nginx/*.key
|
||||
|
||||
*.jpg
|
||||
*.heic
|
||||
*.jpeg
|
||||
*.png
|
||||
|
||||
|
||||
426
README.md
426
README.md
@@ -1,208 +1,294 @@
|
||||
# 图片去背景工具
|
||||
|
||||
使用rembg库实现的Python去背景工具
|
||||
用于书画、篆刻作品的去背景与去主体补背景脚本。
|
||||
|
||||
默认模式优先走书画专用前景提取逻辑:
|
||||
- 对墨色笔画做局部背景校正与明暗差分
|
||||
- 对印章做更严格的红色检测
|
||||
- 在粗掩码内进一步细化 alpha,尽量减少字边残留底色
|
||||
- 仅在需要时才回退到 `rembg`
|
||||
|
||||
## 功能概览
|
||||
|
||||
- 单张图片去背景,输出透明 PNG
|
||||
- 批量处理单层目录中的图片
|
||||
- 支持 `artwork`、`auto`、`rembg` 三种前景提取模式
|
||||
- 支持 `calligraphy`、`seal`、`auto` 三种书画类型
|
||||
- 支持 AOT-GAN 去主体补背景
|
||||
- 支持常见格式:`jpg`、`jpeg`、`png`、`bmp`、`webp`
|
||||
- 安装 `pillow-heif` 后可读取 `heic` / `heif`
|
||||
|
||||
## 依赖
|
||||
|
||||
项目当前依赖见 [requirements.txt](/Users/gavin/removeback/requirements.txt):
|
||||
|
||||
```txt
|
||||
rembg[gpu]
|
||||
pillow
|
||||
pillow-heif
|
||||
opencv-python
|
||||
```
|
||||
|
||||
如果要使用 AOT-GAN 补背景,还需要额外安装 `torch` / `torchvision`,并准备 AOT-GAN 代码目录与权重文件。
|
||||
|
||||
## 环境准备
|
||||
|
||||
推荐使用现有虚拟环境:
|
||||
|
||||
```bash
|
||||
source ~/venv/bin/activate
|
||||
```
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
如果你需要启用去主体补背景:
|
||||
|
||||
```bash
|
||||
pip install torch torchvision
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 激活虚拟环境
|
||||
source ~/venv/bin/activate
|
||||
处理默认 `images/` 目录中的图片,结果输出到 `output/`:
|
||||
|
||||
# 使用默认参数处理images文件夹
|
||||
```bash
|
||||
python remove_background.py
|
||||
```
|
||||
|
||||
# 处理单个文件
|
||||
处理单张图片:
|
||||
|
||||
```bash
|
||||
python remove_background.py input.jpg output.png
|
||||
|
||||
# 查看所有参数-m, --model)
|
||||
|
||||
不同模型适用于不同场景:
|
||||
|
||||
| 模型名称 | 大小 | 适用场景 | 推荐度 |
|
||||
|---------|------|---------|--------|
|
||||
| **isnet-general-use** | 179MB | 通用场景 | ⭐⭐⭐⭐⭐ 默认推荐 |
|
||||
| birefnet-general | 250MB | 通用场景,质量更高 | ⭐⭐⭐⭐⭐ |
|
||||
| birefnet-portrait | 250MB | 人像专用 | ⭐⭐⭐⭐⭐ |
|
||||
| u2net | 176MB | 经典通用模型 | ⭐⭐⭐⭐ |
|
||||
| u2netp | 4.7MB | 快速处理 | ⭐⭐⭐ |
|
||||
| u2net_human_seg | 176MB | 人物分割 | ⭐⭐⭐⭐ |
|
||||
| isnet-anime | 179MB | 动漫角色 | ⭐⭐⭐⭐ |
|
||||
| silueta | 43MB | 精简快速 | ⭐⭐⭐ |
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
# 使用默认模型
|
||||
python remove_background.py input.jpg
|
||||
|
||||
# 使用人像专用模型
|
||||
python remove_background.py input.jpg output.png -m birefnet-portrait
|
||||
|
||||
# 使用快速模型
|
||||
python remove_background.py input.jpg output.png -m u2netp
|
||||
``
|
||||
```bash
|
||||
ls -lh ~/.u2net/
|
||||
```
|
||||
|
||||
### 模型大小参考
|
||||
|
||||
| 模型名称 | 文件大小 | 特点 |
|
||||
|---------|---------|------|
|
||||
| u2net | 176MB | 通用模型 |
|
||||
| u2netp | 4.7MB | 轻量级,速度快 |
|
||||
| isnet-general-use | 179MB | 新一代通用,推荐 |
|
||||
| birefnet-general | ~250MB | 最新通用模型 |
|
||||
| birefnet-portrait | ~250MB | 人像专用 |
|
||||
|
||||
### 手动下载模型(网络问题时)
|
||||
处理指定目录:
|
||||
|
||||
```bash
|
||||
# 创建目录
|
||||
mkdir -p ~/.u2net/
|
||||
|
||||
# 下载指定模型(以isnet-general-use为例)
|
||||
curl -L "https://github.com/danielgatis/rembg/releases/download/v0.0.0/isnet-general-use.onnx" \
|
||||
-o ~/.u2net/isnet-general-use.onnx
|
||||
python remove_background.py my_images/ my_output/
|
||||
```
|
||||
|
||||
### 清理模型缓存
|
||||
指定为印章场景:
|
||||
|
||||
```bash
|
||||
# 删除所有已下载的模型
|
||||
rm -rf ~/.u2net/
|
||||
|
||||
# 删除特定模型
|
||||
rm ~/.u2net/u2net.onnx
|
||||
python remove_background.py input.jpg output.png --artwork-type seal
|
||||
```
|
||||
|
||||
## 可调整参数说明
|
||||
强制使用 `rembg`:
|
||||
|
||||
### 1. 模型选择 (model_name)
|
||||
|
||||
不同模型适用于不同场景:
|
||||
|
||||
- **u2net** (默认): 通用模型,适合大多数场景
|
||||
- **u2netp**: 轻量版,速度更快但精度稍低
|
||||
- **u2net_human_seg**: 专门用于人物分割
|
||||
- **silueta**: 精简版u2net (43MB),速度快
|
||||
- **isnet-general-use**: 新一代通用模型,效果可能更好
|
||||
- **isnet-anime**: 专门用于动漫角色
|
||||
- **birefnet-general**: 最新的通用模型,推荐尝试
|
||||
- **birefnet-portrait**: 专门用于人像
|
||||
- **birefnet-general-lite**: 轻量版birefnet
|
||||
|
||||
**建议**: 如果u2net效果不好,试试 `isnet-general-use` 或 `birefnet-general`
|
||||
|
||||
### 2. Alpha Matting 参数
|
||||
|
||||
Alpha Matting 是后处理步骤,可以显著改善边缘质量,特别是头发、毛发等细节。
|
||||
|
||||
#### alpha_matting (True/False)
|
||||
- **作用**: 是否启用alpha matting
|
||||
- **默认**: False
|
||||
- **建议**: 如果边缘不自然,启用此选项
|
||||
|
||||
#### alpha_matting_foreground_threshold (0-255)
|
||||
- **作用**: 前景阈值,控制哪些区域被认为是前景
|
||||
- **默认**: 240
|
||||
- **调整建议**:
|
||||
- 值越大(如270): 保留更多细节,但可能保留一些背景
|
||||
- 值越小(如210): 去除更彻底,但可能丢失细节
|
||||
- 如果前景被过度去除,增加此值
|
||||
- 如果背景残留太多,减小此值
|
||||
|
||||
#### alpha_matting_background_threshold (0-255)
|
||||
- **作用**: 背景阈值,控制哪些区域被认为是背景
|
||||
- **默认**: 10
|
||||
- **调整建议**:
|
||||
- 值越大(如20-30): 去除背景更彻底
|
||||
- 值越小(如5): 保留更多过渡区域
|
||||
- 如果背景残留,增加此值
|
||||
|
||||
#### alpha_matting_erode_size (像素)
|
||||
- **作用**: 侵蚀大小,用于平滑边缘
|
||||
- **默认**: 10
|
||||
- **调整建议**:
|
||||
- 值越大(如15-20): 边缘更平滑,但可能损失细节
|
||||
- 值越小(如5-8): 保留更多细节,但边缘可能不够平滑
|
||||
|
||||
### 3. Mask后处理 (post_process_mask)
|
||||
|
||||
- **作用**: 对mask进行额外的后处理
|
||||
- **默认**: False
|
||||
- **建议**: 可以尝试启用看效果是否改善
|
||||
|
||||
## 常见问题解决
|
||||
|
||||
### 问题1: 前景被过度去除
|
||||
**解决方案**:
|
||||
```python
|
||||
alpha_matting = True
|
||||
alpha_matting_foreground_threshold = 270 # 增加此值
|
||||
alpha_matting_background_threshold = 10 # 保持较小
|
||||
```bash
|
||||
python remove_background.py input.jpg output.png --foreground-mode rembg -m isnet-general-use
|
||||
```
|
||||
|
||||
### 问题2: 背景残留太多
|
||||
**解决方案**:
|
||||
```python
|
||||
alpha_matting = True
|
||||
alpha_matting_foreground_threshold = 240 # 保持默认或减小
|
||||
alpha_matting_background_threshold = 20 # 增加此值
|
||||
post_process_mask = True # 启用后处理
|
||||
查看完整参数:
|
||||
|
||||
```bash
|
||||
python remove_background.py -h
|
||||
```
|
||||
|
||||
### 问题3: 边缘不自然、有锯齿
|
||||
**解决方案**:
|
||||
```python
|
||||
alpha_matting = True
|
||||
alpha_matting_erode_size = 15 # 增加平滑程度
|
||||
## 输出规则
|
||||
|
||||
- 单张图片默认输出为 `*_nobg.png`
|
||||
- 处理目录时,输出文件写入你指定的输出目录
|
||||
- 脚本内置的目录批处理只扫描输入目录的第一层文件,不递归子目录
|
||||
- 如果启用 `--remove-subject`,输出文件名改为 `*_bgfill.<原扩展>` 或 `*_bgfill.jpg`
|
||||
|
||||
## 前景提取模式
|
||||
|
||||
### `artwork`
|
||||
|
||||
默认模式。优先适用于书法、国画、篆刻等纸本图像。
|
||||
|
||||
### `auto`
|
||||
|
||||
先尝试书画专用掩码;如果结果明显不可信,再回退到 `rembg`。
|
||||
|
||||
### `rembg`
|
||||
|
||||
强制使用通用抠图模型。适合非书画类图片,或书画专用规则不适合的特殊样本。
|
||||
|
||||
## 书画类型
|
||||
|
||||
### `auto`
|
||||
|
||||
自动兼容书法与印章。
|
||||
|
||||
### `calligraphy`
|
||||
|
||||
更偏重墨色笔画、灰黑色文字。
|
||||
|
||||
### `seal`
|
||||
|
||||
更偏重红章、篆刻印记。
|
||||
|
||||
## 常用参数
|
||||
|
||||
### 书画专用参数
|
||||
|
||||
- `--foreground-mode`
|
||||
- `--artwork-type`
|
||||
- `--artwork-max-size`
|
||||
|
||||
建议:
|
||||
- 大图先尝试 `--artwork-max-size 1600`
|
||||
- 超大图如果速度较慢,可降低到 `1200` 或 `1000`
|
||||
- 红章较多的图片优先试 `--artwork-type seal`
|
||||
- 纯墨迹优先试 `--artwork-type calligraphy`
|
||||
|
||||
### rembg 相关参数
|
||||
|
||||
这些参数只在 `--foreground-mode rembg` 或 `auto` 回退到 `rembg` 时生效:
|
||||
|
||||
- `-m, --model`
|
||||
- `-a, --alpha-matting`
|
||||
- `-ft, --foreground-threshold`
|
||||
- `-bt, --background-threshold`
|
||||
- `-es, --erode-size`
|
||||
- `-p, --post-process`
|
||||
|
||||
当前支持的 `rembg` 模型包括:
|
||||
|
||||
- `u2net`
|
||||
- `u2netp`
|
||||
- `u2net_human_seg`
|
||||
- `silueta`
|
||||
- `isnet-general-use`
|
||||
- `isnet-anime`
|
||||
- `birefnet-general`
|
||||
- `birefnet-general-lite`
|
||||
- `birefnet-portrait`
|
||||
- `birefnet-dis`
|
||||
- `birefnet-hrsod`
|
||||
- `birefnet-cod`
|
||||
- `birefnet-massive`
|
||||
|
||||
### 去主体补背景参数
|
||||
|
||||
启用:
|
||||
|
||||
```bash
|
||||
python remove_background.py input.jpg output.jpg --remove-subject --aot-pretrain experiments/your_model.pt
|
||||
```
|
||||
|
||||
### 问题4: 毛发、头发细节丢失
|
||||
**解决方案**:
|
||||
```python
|
||||
model_name = "birefnet-portrait" # 使用人像专用模型
|
||||
alpha_matting = True
|
||||
alpha_matting_foreground_threshold = 270 # 增加以保留细节
|
||||
alpha_matting_erode_size = 5 # 减小以保留细节
|
||||
常用参数:
|
||||
|
||||
- `--aot-root`
|
||||
- `--aot-pretrain`
|
||||
- `--aot-device`
|
||||
- `--aot-block-num`
|
||||
- `--aot-rates`
|
||||
- `--aot-crop`
|
||||
- `--aot-crop-pad`
|
||||
- `--aot-max-size`
|
||||
- `--aot-noise-prefill`
|
||||
- `--aot-noise-strength`
|
||||
- `--mask-dilate`
|
||||
- `--mask-blur`
|
||||
- `--mask-threshold`
|
||||
- `--edge-grow`
|
||||
- `--save-mask`
|
||||
- `--black-subject`
|
||||
- `--black-threshold`
|
||||
- `--gray-subject`
|
||||
- `--gray-saturation-threshold`
|
||||
- `--gray-value-threshold`
|
||||
- `--feather`
|
||||
- `--feather-radius`
|
||||
|
||||
一个偏保守的示例:
|
||||
|
||||
```bash
|
||||
python remove_background.py "images/inpaint/IMG_9259 2.JPG" output.jpg \
|
||||
--remove-subject \
|
||||
--foreground-mode artwork \
|
||||
--artwork-type auto \
|
||||
--aot-pretrain experiments/G0000000.pt \
|
||||
--aot-crop \
|
||||
--aot-crop-pad 64 \
|
||||
--aot-max-size 900 \
|
||||
--aot-noise-prefill \
|
||||
--aot-noise-strength 1.0
|
||||
```
|
||||
|
||||
## 推荐配置
|
||||
## 典型用法
|
||||
|
||||
### 配置1: 高质量人像
|
||||
```python
|
||||
model_name = "birefnet-portrait"
|
||||
alpha_matting = True
|
||||
alpha_matting_foreground_threshold = 260
|
||||
alpha_matting_background_threshold = 15
|
||||
alpha_matting_erode_size = 10
|
||||
post_process_mask = True
|
||||
书法图去背景:
|
||||
|
||||
```bash
|
||||
python remove_background.py input.jpg output.png \
|
||||
--foreground-mode artwork \
|
||||
--artwork-type calligraphy
|
||||
```
|
||||
|
||||
### 配置2: 通用高质量
|
||||
```python
|
||||
model_name = "birefnet-general"
|
||||
alpha_matting = True
|
||||
alpha_matting_foreground_threshold = 250
|
||||
alpha_matting_background_threshold = 12
|
||||
alpha_matting_erode_size = 10
|
||||
post_process_mask = True
|
||||
印章图去背景:
|
||||
|
||||
```bash
|
||||
python remove_background.py input.jpg output.png \
|
||||
--foreground-mode artwork \
|
||||
--artwork-type seal
|
||||
```
|
||||
|
||||
### 配置3: 快速处理
|
||||
```python
|
||||
model_name = "u2netp"
|
||||
alpha_matting = False
|
||||
post_process_mask = False
|
||||
通用图片走 `rembg`:
|
||||
|
||||
```bash
|
||||
python remove_background.py input.jpg output.png \
|
||||
--foreground-mode rembg \
|
||||
--model birefnet-general
|
||||
```
|
||||
|
||||
## 测试不同参数
|
||||
## 调参建议
|
||||
|
||||
建议按以下顺序调整:
|
||||
如果背景没有去掉:
|
||||
|
||||
1. 先尝试不同的模型
|
||||
2. 启用alpha_matting
|
||||
3. 调整foreground_threshold和background_threshold
|
||||
4. 最后调整erode_size
|
||||
- 先尝试 `--artwork-type calligraphy`
|
||||
- 再尝试 `--foreground-mode auto`
|
||||
- 非书画图直接改用 `--foreground-mode rembg`
|
||||
|
||||
每次修改后运行脚本,对比结果。
|
||||
如果字边仍有底色:
|
||||
|
||||
- 先确认原图是否有严重纸纹、阴影或压缩噪声
|
||||
- 降低 `--artwork-max-size` 可能更快,但通常不利于细节
|
||||
- 对超大图可以保留 `1600`,必要时单独抽样检查结果
|
||||
|
||||
如果去主体补背景不自然:
|
||||
|
||||
- 开启 `--aot-crop`
|
||||
- 增加 `--aot-crop-pad`
|
||||
- 尝试 `--aot-noise-prefill`
|
||||
- 减小 `--mask-dilate`、`--mask-blur`
|
||||
|
||||
## 校验命令
|
||||
|
||||
项目当前可用的检查命令:
|
||||
|
||||
```bash
|
||||
~/venv/bin/python -m pytest -q
|
||||
~/venv/bin/python -m mypy remove_background.py tests/test_remove_background.py
|
||||
~/venv/bin/python -m ruff check remove_background.py tests/test_remove_background.py
|
||||
~/venv/bin/python -m ruff format remove_background.py tests/test_remove_background.py
|
||||
```
|
||||
|
||||
说明:
|
||||
- `mypy` 与 `ruff` 的项目配置见 [pyproject.toml](/Users/gavin/removeback/pyproject.toml)
|
||||
- `images/` 与 `output/` 已在静态检查配置里排除
|
||||
|
||||
## 已知限制
|
||||
|
||||
- CLI 自带的目录批处理不递归子目录
|
||||
- 书画专用规则仍然可能受极端纸色、重阴影、扫描边框影响
|
||||
- 某些透明 PNG 在图片预览器里会显示白底或棋盘底,这是预览器合成效果,不代表 alpha 一定有问题
|
||||
|
||||
## 仓库结构
|
||||
|
||||
```txt
|
||||
remove_background.py 主脚本
|
||||
tests/test_remove_background.py
|
||||
requirements.txt
|
||||
pyproject.toml
|
||||
images/ 输入样例
|
||||
output/ 输出目录
|
||||
experiments/ AOT-GAN 权重示例
|
||||
```
|
||||
|
||||
BIN
__pycache__/remove_background.cpython-312.pyc
Normal file
BIN
__pycache__/remove_background.cpython-312.pyc
Normal file
Binary file not shown.
BIN
experiments/D0000000.pt
Normal file
BIN
experiments/D0000000.pt
Normal file
Binary file not shown.
BIN
experiments/G0000000.pt
Normal file
BIN
experiments/G0000000.pt
Normal file
Binary file not shown.
BIN
experiments/O0000000.pt
Normal file
BIN
experiments/O0000000.pt
Normal file
Binary file not shown.
BIN
output/.DS_Store → images/.DS_Store
vendored
BIN
output/.DS_Store → images/.DS_Store
vendored
Binary file not shown.
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[tool.mypy]
|
||||
python_version = "3.12"
|
||||
ignore_missing_imports = true
|
||||
exclude = '^(generative_inpainting|images|output)/'
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
"AOT-GAN-for-Inpainting",
|
||||
"generative_inpainting",
|
||||
"images",
|
||||
"output",
|
||||
"__pycache__",
|
||||
".venv",
|
||||
"venv",
|
||||
".git",
|
||||
]
|
||||
1394
remove_background.py
1394
remove_background.py
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
rembg[gpu]
|
||||
pillow
|
||||
pillow-heif
|
||||
opencv-python
|
||||
|
||||
BIN
tests/__pycache__/test_remove_background.cpython-312.pyc
Normal file
BIN
tests/__pycache__/test_remove_background.cpython-312.pyc
Normal file
Binary file not shown.
203
tests/test_remove_background.py
Normal file
203
tests/test_remove_background.py
Normal file
@@ -0,0 +1,203 @@
|
||||
import importlib
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _import_remove_background():
|
||||
if "remove_background" in sys.modules:
|
||||
del sys.modules["remove_background"]
|
||||
rembg_stub = types.ModuleType("rembg")
|
||||
rembg_stub.remove = lambda *args, **kwargs: None
|
||||
rembg_stub.new_session = lambda *args, **kwargs: None
|
||||
sys.modules["rembg"] = rembg_stub
|
||||
return importlib.import_module("remove_background")
|
||||
|
||||
|
||||
class RemoveBackgroundTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.mod = _import_remove_background()
|
||||
|
||||
def _make_paper_image(self, width=160, height=120):
|
||||
rng = self.mod.np.random.default_rng(0)
|
||||
base = self.mod.np.full((height, width, 3), 230, dtype=self.mod.np.uint8)
|
||||
noise = rng.integers(-8, 9, size=(height, width, 3), dtype=self.mod.np.int16)
|
||||
image = self.mod.np.clip(base.astype(self.mod.np.int16) + noise, 0, 255).astype(
|
||||
self.mod.np.uint8
|
||||
)
|
||||
return image
|
||||
|
||||
def test_alpha_to_mask_threshold(self):
|
||||
alpha = self.mod.np.array([[0, 9, 10, 255]], dtype=self.mod.np.uint8)
|
||||
mask = self.mod._alpha_to_mask(alpha, threshold=10)
|
||||
self.assertEqual(mask.shape, alpha.shape)
|
||||
self.assertEqual(mask[0, 0], 0)
|
||||
self.assertEqual(mask[0, 1], 0)
|
||||
self.assertEqual(mask[0, 2], 0)
|
||||
self.assertEqual(mask[0, 3], 255)
|
||||
|
||||
def test_prepare_mask_outputs_binary(self):
|
||||
mask = self.mod.np.zeros((5, 5), dtype=self.mod.np.uint8)
|
||||
mask[2, 2] = 255
|
||||
mask_hard, mask_used = self.mod._prepare_mask(mask, mask_dilate=0, mask_blur=3)
|
||||
self.assertEqual(mask_hard.shape, mask.shape)
|
||||
self.assertEqual(mask_used.shape, mask.shape)
|
||||
self.assertTrue(self.mod.np.all(self.mod.np.isin(mask_used, [0, 255])))
|
||||
|
||||
def test_resolve_aot_paths_defaults(self):
|
||||
aot_root, aot_pretrain = self.mod._resolve_aot_paths(
|
||||
"AOT-GAN-for-Inpainting", None
|
||||
)
|
||||
self.assertTrue(str(aot_root).endswith("AOT-GAN-for-Inpainting"))
|
||||
self.assertIsNone(aot_pretrain)
|
||||
|
||||
def test_resolve_aot_paths_relative_pretrain(self):
|
||||
aot_root, aot_pretrain = self.mod._resolve_aot_paths(
|
||||
"AOT-GAN-for-Inpainting", "experiments/foo.pt"
|
||||
)
|
||||
self.assertTrue(str(aot_root).endswith("AOT-GAN-for-Inpainting"))
|
||||
self.assertTrue(str(aot_pretrain).endswith("experiments/foo.pt"))
|
||||
|
||||
def test_parse_aot_rates(self):
|
||||
rates = self.mod._parse_aot_rates("1+2+4+8")
|
||||
self.assertEqual(rates, [1, 2, 4, 8])
|
||||
|
||||
def test_mask_bbox_empty(self):
|
||||
mask = self.mod.np.zeros((3, 3), dtype=self.mod.np.uint8)
|
||||
self.assertIsNone(self.mod._mask_bbox(mask))
|
||||
|
||||
def test_mask_bbox_and_expand(self):
|
||||
mask = self.mod.np.zeros((5, 5), dtype=self.mod.np.uint8)
|
||||
mask[1:3, 2:4] = 255
|
||||
bbox = self.mod._mask_bbox(mask)
|
||||
self.assertEqual(bbox, (2, 1, 3, 2))
|
||||
expanded = self.mod._expand_bbox(bbox, 2, 5, 5)
|
||||
self.assertEqual(expanded, (0, 0, 4, 4))
|
||||
expanded_min = self.mod._expand_bbox_min_size(bbox, 0, 5, 5, 4)
|
||||
self.assertEqual(expanded_min, (1, 0, 4, 3))
|
||||
|
||||
def test_remove_border_frame_components_keeps_inner_content(self):
|
||||
mask = self.mod.np.zeros((12, 12), dtype=self.mod.np.uint8)
|
||||
mask[0, :] = 255
|
||||
mask[-1, :] = 255
|
||||
mask[:, 0] = 255
|
||||
mask[:, -1] = 255
|
||||
mask[4:8, 5:7] = 255
|
||||
|
||||
cleaned = self.mod._remove_border_frame_components(
|
||||
mask,
|
||||
min_width_ratio=0.7,
|
||||
min_height_ratio=0.7,
|
||||
max_fill_ratio=0.35,
|
||||
)
|
||||
|
||||
self.assertEqual(int(cleaned[0, 0]), 0)
|
||||
self.assertGreater(cleaned[4:8, 5:7].mean(), 200)
|
||||
|
||||
def test_ensure_rgba_size_resizes(self):
|
||||
img = self.mod.Image.new("RGB", (2, 3), color=(0, 0, 0))
|
||||
out = self.mod._ensure_rgba_size(img, (4, 5))
|
||||
self.assertEqual(out.mode, "RGBA")
|
||||
self.assertEqual(out.size, (4, 5))
|
||||
|
||||
def test_extract_artwork_mask_detects_calligraphy_strokes(self):
|
||||
image = self._make_paper_image()
|
||||
self.mod.cv2.line(image, (20, 20), (130, 90), (20, 20, 20), thickness=6)
|
||||
self.mod.cv2.line(image, (45, 15), (45, 105), (30, 30, 30), thickness=5)
|
||||
|
||||
mask = self.mod._extract_artwork_mask(
|
||||
self.mod.Image.fromarray(image),
|
||||
artwork_type="calligraphy",
|
||||
max_size=120,
|
||||
)
|
||||
|
||||
self.assertGreater(mask[25:95, 42:49].mean(), 180)
|
||||
self.assertLess(mask[:15, :15].mean(), 20)
|
||||
|
||||
def test_extract_artwork_mask_detects_red_seal(self):
|
||||
image = self._make_paper_image()
|
||||
self.mod.cv2.rectangle(image, (40, 30), (120, 95), (165, 30, 40), thickness=5)
|
||||
self.mod.cv2.line(image, (55, 40), (105, 85), (170, 25, 35), thickness=6)
|
||||
|
||||
mask = self.mod._extract_artwork_mask(
|
||||
self.mod.Image.fromarray(image),
|
||||
artwork_type="seal",
|
||||
max_size=120,
|
||||
)
|
||||
|
||||
self.assertGreater(mask[38:92, 38:122].mean(), 40)
|
||||
self.assertLess(mask[:15, :15].mean(), 20)
|
||||
|
||||
def test_extract_artwork_mask_ignores_warm_paper_cast(self):
|
||||
image = self.mod.np.full(
|
||||
(120, 160, 3), (222, 188, 150), dtype=self.mod.np.uint8
|
||||
)
|
||||
self.mod.cv2.line(image, (30, 20), (30, 100), (28, 28, 28), thickness=6)
|
||||
self.mod.cv2.line(image, (55, 20), (125, 95), (32, 32, 32), thickness=5)
|
||||
|
||||
mask = self.mod._extract_artwork_mask(
|
||||
self.mod.Image.fromarray(image),
|
||||
artwork_type="calligraphy",
|
||||
max_size=120,
|
||||
)
|
||||
|
||||
self.assertGreater(mask[25:100, 26:34].mean(), 180)
|
||||
self.assertLess(mask[:12, :12].mean(), 20)
|
||||
|
||||
def test_mask_to_transparent_image_refines_alpha_inside_coarse_mask(self):
|
||||
image = self.mod.np.full(
|
||||
(120, 160, 3), (224, 190, 156), dtype=self.mod.np.uint8
|
||||
)
|
||||
self.mod.cv2.line(image, (40, 18), (40, 102), (24, 24, 24), thickness=6)
|
||||
self.mod.cv2.rectangle(image, (102, 24), (132, 54), (170, 30, 40), thickness=-1)
|
||||
|
||||
coarse_mask = self.mod.np.zeros((120, 160), dtype=self.mod.np.uint8)
|
||||
coarse_mask[10:110, 20:140] = 255
|
||||
|
||||
out = self.mod._mask_to_transparent_image(
|
||||
self.mod.Image.fromarray(image),
|
||||
coarse_mask,
|
||||
)
|
||||
alpha = self.mod.np.array(out.getchannel("A"))
|
||||
|
||||
self.assertGreater(alpha[25:95, 36:45].mean(), 110)
|
||||
self.assertGreater(alpha[30:50, 108:128].mean(), 90)
|
||||
self.assertLess(alpha[70:90, 70:90].mean(), 25)
|
||||
|
||||
def test_remove_background_artwork_mode_skips_rembg(self):
|
||||
image = self._make_paper_image(width=100, height=80)
|
||||
self.mod.cv2.line(image, (15, 15), (85, 60), (20, 20, 20), thickness=5)
|
||||
|
||||
original_remove = self.mod.remove
|
||||
|
||||
def _unexpected_remove(*args, **kwargs):
|
||||
raise AssertionError("artwork 模式不应调用 rembg")
|
||||
|
||||
self.mod.remove = _unexpected_remove
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
input_path = Path(tmpdir) / "input.png"
|
||||
output_path = Path(tmpdir) / "output.png"
|
||||
self.mod.Image.fromarray(image).save(input_path)
|
||||
|
||||
self.mod.remove_background(
|
||||
str(input_path),
|
||||
str(output_path),
|
||||
foreground_mode="artwork",
|
||||
artwork_type="calligraphy",
|
||||
artwork_max_size=120,
|
||||
)
|
||||
|
||||
out = self.mod.Image.open(output_path)
|
||||
alpha = self.mod.np.array(out.getchannel("A"))
|
||||
self.assertEqual(out.mode, "RGBA")
|
||||
self.assertGreater(alpha[30:55, 35:70].mean(), 70)
|
||||
self.assertLess(alpha[:10, :10].mean(), 10)
|
||||
finally:
|
||||
self.mod.remove = original_remove
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user