GIF / 动图 透明背景抠图
需收集的输入
- 源文件: GIF、PNG 序列还是视频。视频走 ffmpeg 抽帧(脚本已内置)
- 看图底色: 用户是在什么背景上看这张图的?深色界面还是白底页面会暴露完全不同的残留。 不知道就默认做全套底色验证(脚本已内置),不要跳过
- 主体是否有接近背景色的浅色部件(白手套、白靴、白边)? 这是能力天花板,见下方 "先做可解性判断"
先做可解性判断(跳过这步会白干几十分钟)
抠图是信息恢复问题,不是算法调优问题。当主体浅色部件与背景在颜色上接近时,后处理 无法把它们分开 —— 色度、平滑度、时间中位数、背景曲面拟合全都会误伤主体。
别靠肉眼估颜色,跑一下:
.venv-bg\Scripts\python scripts\cutout.py --input <源文件> --work work --stage solvability
它用"相机静止 + 主体会动"分离出背景板和主体,输出三个数:
- 背景板中位色 / 主体最亮色调 / 两者差值
- 落在背景色 ±25/255 以内的主体像素占比 —— 这个数最可解释
实测这份机器人素材:背景 (212,207,210),主体最亮 (250,251,250),差值 40.7,
但32.6% 的主体像素落在背景色 25 以内 → 打印 AMBIGUOUS。
看到 AMBIGUOUS 就要先告诉用户:
这份素材的浅色部件和背景颜色非常接近,纯抠图做不到像素级干净,顶多做到"放大后有极细残留"。 有源工程的话,换高饱和纯绿/品红幕布重渲一次,我就能做到完全无残留; 只有成品文件的话我只能做到逼近干净。
然后问用户要不要继续,别闷头跑 20 分钟。
注意:这个检查是预警信号,不是判决。最终以跑完之后的残留量化为准 —— "能分"也可能因为模型边缘不准而出残留,"不能分"也可能实际抠得很干净。
流程
-
建工作区并装环境
python -m venv .venv-bg && .venv-bg\Scripts\python -m pip install rembg onnxruntime pillow scipy(scipy 来自 rembg 依赖链,引导滤波要用它 —— 见下) -
跑管线
.venv-bg\Scripts\python scripts\cutout.py --input <源文件> --outdir out --scale 2--input接受视频文件、GIF/PNG 文件,或一个已抽好帧的 PNG 序列目录。--scale 2不是可选项:肢体与躯干之间的缝只有 5–15px,720p 下分割模型根本看不见。 个别帧还顽固就--stage matte单独重跑那几帧、--scale 3。纯 CPU 下 70 帧 720p 跑 2× 约 20 分钟(约 17.6s/帧,onnxruntime 无 GPU)。 提前告诉用户这个量级,别让人以为卡死。
-
验收(见下节),不通过就定位阶段重跑,不要整体重跑。
-
交付: GIF + WebP + APNG 三份都给,并说明各自适用场景。
硬规则(违反过、代价都很大)
- 拓扑处理只用 opening,绝不用 closing。
binary_closing会填掉 ≤2px 的背景缝, 把手臂和躯干之间的窄缝焊死,之后再多的处理也救不回来。实测这一条比其它所有问题加起来 造成的返工都多。 - 空洞填充阈值取小值(60px 量级,不是几百)。真正的肢体缝隙有几千像素,针孔只有几十。
- 引导滤波半径 ≤2,且默认不要中值滤波。3×3 中值和半径 4 的引导滤波都会抹掉几像素宽的缝。
- 不要对 alpha 做非线性压缩。曾经写过
clip((clip((a-0.04)/0.92)-0.06)/0.88)这种映射, 意图是收紧边缘,实际在 alpha≈0.7 处反而抬高了 alpha,等于亲手把残留补得更实。 - GIF 不要用有序抖动(Bayer)。静帧上边缘更柔,但那个网点锁在屏幕坐标系里, 轮廓一动穿过去就"沸腾"闪烁 —— 动画里反而更糟。GIF 走清理后的硬阈值, 柔边 alpha 留给 WebP / APNG。
- 模型选 BiRefNet。isnet / u2net 会把地面镜面倒影判成实体抠进去;换成
birefnet-general-lite后 alpha 在靴底处直接归零。 - 不要为了"更干净"去擦背景色。用背景曲面/色度判据大面积擦残留,会连主体浅色部件一起擦掉。 残留的正确解法是提高 matte 分辨率,不是改阈值。
输出契约
out/<name>.gif—— 透明索引 255、transparency=255、disposal=2,1-bit 硬边。 拓扑清理(去散点)只作用于 GIFout/<name>.webp—— 保留连续 alpha,不做拓扑清理,所以会留一层柔边暖色毛边。 叠复杂背景优先用它,这个差异是刻意的,别当成 bugout/<name>.png—— APNG,浏览器兼容性最好- 保持源片的画布尺寸、帧数、每帧时长、循环设置不变;在回复里列出这几项的实测值 (动画 WebP 的逐帧时长 PIL 读不出来,遇到这种情况照实说"以 GIF 实测为准",别编)
- 用
<media>标签交付文件,并说明 GIF 与 WebP 各自的适用场景
失败处理
- GIF 在浏览器里显示成灰底: 先别改文件。查 VP8X 的 ALPHA 位与 ANIM 背景色
(
verify.py会打印),两者都正常的话是查看器给透明区铺了底色。 若verify.py报ANMF ... blend=0,那才是真问题 ——cutout.py导出时会自动改成blend=1,如果这份文件的 blend 是 0,说明它不是本脚本产的,或走了外部工具。 - 主体被啃掉: 几乎都是背景判据太宽。回退到
final/那一层重新导出,不要在擦除逻辑上继续调参。 - 某几帧缝隙没扣开:
--stage matte --scale 3只重跑那几帧,单帧成本约 20 秒。 - rembg 模型下载慢: rembg 2.x 把模型放在
~/.rembg/models/<model>/<model>.onnx(不是老版的~/.u2net),birefnet-general-lite 约 214MB。首次运行要等下载。
验收(这一步不能省)
必须在用户真实观看的那块底色上逐帧看,否则等于没验:
.venv-bg\Scripts\python scripts\verify.py out\<name>.gif --backdrop grey --ref work\final
它会输出:散点数、逐帧不透明占比(判断有无闪烁)、最低不透明行、GIF 相对源 matte 的 颜色量化误差、WebP 容器标志,并生成四底色对比图 + 全帧接触表 + 指定区域放大图。
判断标准:
| 指标 | 通过 |
|---|---|
| 散点数 | 0 |
| 不透明占比逐帧极差 | < 0.05 |
| 最低不透明行 | 逐帧一致(反射残留会让它忽高忽低或顶到画面底) |
| 颜色量化平均误差 | < 3/255(缩略图看不出丢色相,只能靠这个数) |
| 四底色对比 | 每块底色都能透出来,没有统一浅灰 |
| 接触表 | 逐帧无残影、无噪点、肢体缝隙通透 |
肉眼只看单帧、或只在黑底上看,都会漏掉问题 —— 暖色残留叠在深灰底上才明显。
Windows (win32) 平台说明
- 解释器用
py或python,python3在 Windows 上不存在;别用 PATH 上的python(可能指向无 pip 的 hermes venv),优先用 venv 里的.venv-bg\Scripts\python.exe - 路径含中文时,用
Write/Read工具写脚本文件,不要用Set-Content拼内容 - PIL 的
ImageFilter.GaussianBlur不支持 mode"F"和 float RGB;对浮点图做模糊用scipy.ndimage.gaussian_filter(rembg 已经装了 scipy) Image.fromarray(float32_array, "RGB")会直接报错,先.astype(np.float32)再按通道处理- 不要用
Remove-Item清理;cutout.py --clean已经封装了目录清理
示例
输入: "把这段 GIF 的背景抠透明"
→ cutout.py --input 动作.gif --outdir out --scale 2,再 verify.py,
→ 交付 GIF/WebP/APNG 三份,列出 720×720 / 70 帧 / 每帧 100ms / 总 7000ms。
输入: "抠干净点,边缘还有噪点,手臂和身体之间还有没扣的地方"
→ 不要去调阈值。先跑 verify.py --zoom <框选坐标> 确认缺陷位置,
→ 散点多就加 --median 3,缝隙不通用就 --stage matte --scale 3 重跑对应帧,
→ 再验收。回复里说清是哪一类缺陷、用了哪条修复,别只说"已优化"。