抠图结果在编辑器里看着已经透明,保存下来却像一张白底图。这个现象不能只靠换一个文件后缀解决:有时是查看器的白色底板,有时是导出时合成了底色,也有时,所谓“透明棋盘格”早已变成图片里的普通像素。
我开发的图片猫(PicCat
)www.piccat.cn有智能抠图和批量格式转换两个相关入口。这次沿着现有代码,把预览、蒙版合成和文件编码串起来看,重点讨论怎样判断透明是否还在,以及它在哪一步可能丢失。
1 先区分页面背景和图片像素
在常见的 8 位 RGBA 图像里,R、G、B 表示颜色,A 表示不透明度。A 为 0 是全透明,255 是完全不透明,中间值用来表达半透明边缘。透明图放在白色页面上看起来是白底,并不意味着文件里真的存了白色背景。
图 1 来自 image-smart-cutout 案例目录。中间的棋盘格和右侧的深色底板是为讲解而添加的,图中没有重新调用抠图模型,也不是本次浏览器运行截图。切换底板后,人物之外的区域跟着变色,才能直观展示透明的作用。
SmartCutout.vue 把 checkerboard-bg 放在包裹 canvas 的容器上;renderComposite() 则先 clearRect,再绘制主体。CSS 背景只负责让透明区域容易辨认,Canvas 编码不会自动把容器的背景样式一起保存。
但如果下载的是页面截图,或者把棋盘格主动画进 Canvas,它就成了真实像素。此时导出 PNG 也不会让这些格子自动消失。
2 蒙版怎样变成透明的主体图层
智能抠图组件取得接口结果后,把它画进 maskCanvas,读取 Alpha,并计算非透明区域的包围盒。随后新建局部 objCanvas,先绘制蒙版,再用 source-in 绘入原图。这个合成模式只保留源图像与目标已有像素重叠的部分,并受目标 Alpha 影响。
项目代码摘录:SmartCutout.vue 的主体合成部分。省略了包围盒计算、上下文判空及图层入栈,变量来自上文处理流程,不能单独运行。
objCtx.drawImage(
maskCanvas, minX, minY, width, height,
0, 0, width, height
);
objCtx.globalCompositeOperation = 'source-in';
objCtx.drawImage(
originalImage, minX, minY, width, height,
0, 0, width, height
);
source-in 的完整规则可查阅MDN 合成模式文档。这里新建的是局部画布,处理后的图层会再绘回输出画布。
有一个实现细节不能略过。当前读取蒙版时,组件实际用了下面的二值化逻辑;中间省略的代码只计算包围盒。
const alpha = (data[i + 3] ?? 0) > 0 ? 255 : 0;
// 此处省略包围盒坐标计算
data[i] = 255;
data[i + 1] = 255;
data[i + 2] = 255;
data[i + 3] = alpha;
也就是说,假如接口返回 Alpha=128 的边缘,它在这里会被改成 255。PNG 能保存半透明,不代表这条业务链路保留了接口的半透明信息。对于发丝、纱布或玻璃边缘,直接把所有非零值当成完全不透明,可能造成边缘变硬;实际程度还取决于接口输出。
建议改进方向是先确认接口契约:结果到底是二值分割蒙版、软 Alpha 蒙版,还是用灰度表示概率的图片。只有它确实以 Alpha 表达透明度时,才适合保留原始 Alpha;包围盒检测可以另用阈值,避免低强度噪点把裁剪范围撑大。这是改进建议,当前代码尚未采用。
另一个待完善点是,renderComposite() 会把选中虚线框画进同一个输出 Canvas,而 downloadResult() 直接编码它。按当前绘制路径,选中状态存在把编辑标记带进导出的风险,本文没有做浏览器复现。建议把选择框放在独立覆盖层,或导出时用干净画布只重绘主体图层。
3 PNG 保留透明 JPEG 需要明确的底色
保存时要先决定文件的用途。如果还要把主体放进其他海报,应该保留透明;如果目标系统只接收 JPG,就要明确选择底色,再把前景与背景合成。把 .jpg 改成 .png 只改了名字,不会恢复已经丢失的 Alpha。
输出格式 | 透明能力 | 当前项目的相关行为 |
PNG | 支持透明和半透明 | 智能抠图固定以 image/png 编码;转换核心可输出 PNG |
WebP | 支持透明和半透明 | 转换核心保留 RGBA;当前未设置 lossless=True,不宜称为无损转换 |
JPEG | 普通 JPEG 不保存 Alpha | 转换核心先按指定背景合成 RGB,再编码 |
批量转换组件会提交 targetFormat、quality 和 backgroundColor。服务端 image_convert_core.py 在 EXIF 方向校正后,对 JPEG 单独处理 RGBA、LA 和调色板 P 模式:先建 RGB 底图,再用 Alpha 作为粘贴蒙版。
项目代码摘录:convert_image_data() 中的 JPEG 分支。省略了参数校验、其他格式分支和最终编码;变量 background_rgb 已由背景色参数解析得到。
if normalized_format == 'JPEG' and image.mode in ('RGBA', 'LA', 'P'):
background = Image.new('RGB', image.size, background_rgb)
if image.mode != 'RGBA':
image = image.convert('RGBA')
background.paste(image, mask=image.getchannel('A'))
image = background
elif normalized_format == 'JPEG' and image.mode != 'RGB':
image = image.convert('RGB')
这里不能简单用 convert("RGB") 代替合成。丢掉 Alpha 不等于按白底混合:全透明像素内部也可能带着 RGB 值,直接去掉透明度会把这些颜色露出来。
可以用一个具体像素理解混合:前景 RGB=(200,100,50),Alpha=128,放到白底上后,按通道计算“前景 × 128/255 + 白色 × 127/255”,约为 (227,177,152)。JPEG 还会做有损编码,因此验收颜色时应容许小幅误差。
当前核心对 JPEG 和 WebP 使用质量参数,PNG 走保存逻辑中的 optimize=True,没有把同一个 quality 值当作 PNG 的画质旋钮。WebP 的颜色压缩与 Alpha 是否存在也要分开判断。
格式与保存参数参考:Pillow 官方文件格式文档。具体是否能解码或编码,还取决于部署环境的 Pillow 及底层库支持。
4 一个容易误判的案例素材
检查项目案例库时,可以找到两张外观都像“抠图完成”的 WebP:model_after.webp 和 bonsai_after.webp。实际读取文件后,前者为 RGBA,Alpha 范围为 0–255;后者为 RGB,统一解码成 RGBA 后 Alpha 全部为 255。
盆景图中的灰白格仍然可见,深色底板没有透出来。它适合做效果展示,却不能作为“下载结果包含透明通道”的验收样本。这个发现针对仓库里的这份展示文件,不表示线上抠图接口一定返回同样的内容。
教学简化代码:用 Pillow 读取图片并检查有效 Alpha。替换本地文件路径即可用于单张静态图片;省略了命令行、文件异常与动画逐帧检查。
from PIL import Image
with Image.open("result.webp") as image:
rgba = image.convert("RGBA")
alpha = rgba.getchannel("A")
lo, hi = alpha.getextrema()
print("存在透明或半透明像素:", lo < 255)
print("Alpha 范围:", lo, hi)
转成 RGBA 再检查,比只看 mode 是否等于 RGBA 更稳妥:调色板 PNG 也可能通过透明信息表达透明区域。反过来,即使文件有 Alpha 通道,也可能每个像素都是 255。
这个检查只回答“有没有透明像素”,不回答“抠得准不准”。背景是否漏抠、主体是否被误删,需要另外对照原图。若原始输入完全不透明,格式转换也不会推断主体边界,更不会自动生成可信的透明蒙版。
5 把编码成功和透明正确分开验收
这次在本地 Pillow 12.3.0 环境中,独立加载并运行了 test_image_convert_direct.py 的 ImageConvertCoreTest,6 项核心测试均通过。覆盖了透明 PNG 转指定底色 JPEG、PNG 透明保留、格式组合、非法格式拒绝、质量范围和非法背景色回退;没有执行接口集成测试。
另外,用同色的三个色块构造 Alpha 分别为 0、128、255 的 PNG,调用项目 convert_image_data() 再解码结果,得到下面的中心像素检查结果。色块用于验证编码链路,不是模型效果评测。
本地验证项 | 实际观察 |
PNG 输出 | 三个色块的 Alpha 仍为 0、128、255 |
WebP 输出 | Alpha 仍为 0、128、255;RGB 出现有损压缩差异 |
JPEG 输出并指定白底 | 统一解码后 Alpha 均为 255;半透明块中心 RGB 为 (227,177,152) |
上述结果只证明这组输入在本地转换核心中的行为,不能替代浏览器保存、线上部署或移动端分享的验收。前面展示的两份案例素材也单独读取过文件模式和 Alpha 范围。
建议继续验收的边界
1. 下载后重新解码文件,核对实际格式、像素尺寸和 Alpha;分别放在白底与深色底板上查看边缘,避免只在编辑器内看一眼。
2. 智能抠图在“对象选中”和“取消选中”两种状态下分别保存,检查虚线框是否进入结果。对软蒙版、细发丝和全透明结果另设样例。
3. 当前 downloadResult() 已处理 toBlob 回调得到 null 的情况,但导出异常和下载 Promise 失败的统一收尾仍值得补齐,防止下载状态不能复位。跨域图像还要检查来源授权;污染的 Canvas 不能直接导出。
4. 若以后增加浏览器端 WebP 导出,应检查返回 Blob 的 type,并据此确定文件扩展名。浏览器不支持所请求编码格式时,toBlob 可能回退到 PNG。服务端 WebP 成功不等于每个浏览器都支持 WebP 编码。
后两项的 API 行为参考:MDN toBlob、MDN 跨域图像与 Canvas。当前项目没有在这次核验中做跨浏览器端到端测试。
遇到“抠图后白底”,我会先查文件 Alpha,再查合成步骤,最后查编码和保存路径。沿着这条顺序定位,才能分清是显示方式、透明数据被改写,还是格式本身要求铺底,也能避免把导出问题误判成模型能力问题。