ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

抠图换深色背景有白边?先分清直通和预乘alpha

抠图换深色背景有白边?先分清直通和预乘alpha 摘要白底图抠出来贴回白底上看不出毛病换成黑色或深蓝背景后主体外面就浮出一圈发灰发白的边。很多人第一反应是抠得不准。其实这圈边的透明度大体是对的错的是它的颜色。本文从两种 alpha 存法讲起直通 alpha 存的是颜色本身预乘 alpha 存的是乘过透明度的颜色。抠图导出的 PNG 是直通存法半透明像素里的 RGB 基本就是原图像素而原图边缘本来就混着白底。「去除背景色边」能把这圈白压下去可它主要动的是透明度接近底色的主体会被一起压透明。前端合成时存法和公式配错边缘还会再亮一倍。文中数字来自两张 Wikimedia Commons 白底图的实测。抠图模型和去色边算法不是我做的涉及实现的部分都是我从导出文件反推的。版本声明实测日期是 2026 年 9 月 30 日机器是一台装着 macOS 26.5.2 的 Apple M4。浏览器是开着 WebGPU 的 Chromium 149.0.7827.55。抠图用的是图映线上「去背景」里的「快速 AI」档。模型是 ISNet INT8状态行显示走 WebGPU。像素分析用 Python 3 加 Pillow 和 NumPy亮度一律按 Rec.709 加权。前端合成那组对照也在同一个 Chromium 里跑完再读回像素计算。换浏览器、换档位、换样本之后数字都可能不一样。适用边界这篇只讲一件事透明 PNG 的半透明边为什么带着原底色以及几种处理方式各自改了什么。样本只有两张网上找的白底图一张小狗和一根插在玻璃瓶里的带白斑叶片的枝条。抠图只测了快速档专业档和骨灰级档没测换档之后的结果我不推断。灰底、彩色底和画笔精修也都没测。文中凡是讲「它内部怎么做」的地方都是我对着导出文件做的推测不是那块代码的说明。怎么选抠图档位、蒙版画笔怎么用都不在这篇里。文章目录1 抠图换黑底为什么会出白边白边只出现在半透明那一圈合成到黑底后边缘亮了约112 直通alpha和预乘alpha各存什么直通存的是颜色本身预乘存的是乘过透明度的颜色两种存法合成结果相同预乘在8位下会丢精度3 抠图导出的PNG是哪种存法PNG规范只允许直通存法不透明像素和原图完全相同低透明度像素偏差最大到1274 半透明边为什么带着原底色原图的边缘像素本来就混了白底模型只给透明度不给主体色换PNG或WebP改变不了边缘5 去色边为什么会改动透明度不透明像素一个都没变色被改的像素大多是透明度变小越接近白色的像素被压得越多去色边是在重估透明度6 去色边会误伤哪些主体白斑叶片被压出黑洞玻璃瓶的光晕换成了台阶硬边小狗前腿交界多出锯齿7 前端合成配错为什么会放大白边把直通当预乘用边缘会亮一倍预乘上传再乘一次会偏暗canvas 2D合成没有偏差8 这一圈白边该怎么处理先看主体里有没有接近底色的部分边缘收缩和柔化帮不上大忙合成端先对齐存法再谈修边9 适用边界与风险提示10 测量方法自身的坑应有亮度只是近似值逐像素对比要先确认解码器一致亮度分档里混着背景残留测试环境的WebGL可能不走显卡11 还没解决的参考资料1 抠图换黑底为什么会出白边白边只出现在半透明那一圈我拿两张网上找的白底图试。第一张是小狗来自 Wikimedia Commons作者 George HodanCC0。第二张是一根插在玻璃瓶里的榕树枝条原图来自 Wikimedia Commons作者 BiuschCC BY-SA 3.0下文用到它的合成图同样按 CC BY-SA 3.0 提供。两张都是纯白背景。它们也都是抠图最常见的那类素材。抠完导出的透明 PNG 贴到白底上完全正常贴到黑底上就不一样了。小狗的毛边外面是一圈发灰的软边。玻璃瓶外沿那一圈更明显。那是一道大约 20 px 宽的灰白光晕。抠图我用的是图映 ImgInghttps://imging.cn/的快速档模型 ISNet INT8两张白底图抠完再合成到黑底和深蓝底比较。图映是我参与的项目我在里面负责端侧编解码和模型加载抠图模型和「去除背景色边」这两块都不是我做的。这篇我没有去翻那两块的代码只按导出文件说话这样谁拿同样的图都能复核。抠图在浏览器本地跑两张图导出六次全程的非 GET 请求是 0。抠完之后精修区有一句原话默认逐像素直接使用 AI 输出不改透明度、不改边缘颜色。这句话后面会反复用到。它说明默认导出的边就是模型给的边没有谁替你修过。这张图看三处。左边是原图。小狗肚皮下面的毛尖和白底之间没有一条清楚的线只有一段慢慢变白的过渡。中间是默认导出贴到黑底的样子那段过渡还在只是底下的白换成了黑毛尖外面留着一层灰。右边是开了去色边之后贴到黑底的样子灰基本没了前腿和肚皮交界的地方却多出一块锯齿状的硬边。三张并排放着。白边和它的代价都在里面。小狗图来自 Wikimedia CommonsCC0。先把「半透明像素」说清楚。8 位 alpha 通道的取值是 0 到 2550 是全透明255 是完全不透明。这篇里半透明像素指 alpha 在 1 到 254 之间的像素白边就长在这些像素上。默认导出里小狗图有 113,438 个半透明像素植物图有 1,732,202 个。我又在不透明区里取了贴边 3 px 以内的一圈当作主体本来颜色的参照。量出来的差距很直观小狗半透明像素的原色平均亮度是 160.1贴边的不透明像素只有 108.6。植物是 200.8 对 123.6。半透明那一圈明显比主体亮。亮出来的部分就是原来的白底。样本半透明像素半透明原色平均亮度边缘内侧平均亮度半透明里亮度 200 的占比小狗 · PNG 默认113,438160.1108.639.9%植物 · PNG 默认1,732,202200.8123.657.8%最后一列更能说明问题。小狗的半透明像素里有 39.9% 亮度超过 200植物是 57.8%。亮度 200 以上在这两张图里基本就是白底的颜色主体本身没有那么亮的毛或叶子。换句话说半透明那一圈里有将近一半像素存着的 RGB 就是白色。合成到黑底后边缘亮了约11光看 RGB 还不够读者最终看到的是合成之后的颜色。我按直通 alpha 的正确公式rgb×a 底色×(1−a)把两张图合成到黑底和深蓝底 #1A2A4A 上。再用贴边不透明像素的平均色当主体色算出这圈边「应该」有多亮两者对比。样本黑底实际黑底应有深蓝底实际深蓝底应有小狗 · 默认75.464.392.181.0植物 · 默认52.540.979.968.2两张图在两种底色上的实际值都比应有值高出约 11 个亮度单位。这个数我一开始以为是巧合毕竟一张是短毛动物、一张是叶片和玻璃边缘形态完全不同。后来想明白了。11 这个量本身没什么特别只是两张图恰好都有这么多白底被带进了边缘。换一张原图这个数会变。偏亮的方向不会变。偏亮的原因只有一个公式是对的alpha 也大体是对的乘进去的 rgb 却不是主体的颜色是主体和白底混在一起的颜色。要讲清楚这一点得先把两种 alpha 存法分开。2 直通alpha和预乘alpha各存什么直通存的是颜色本身直通 alpha 英文叫 straight alpha也叫非预乘或 unassociated alpha。它把一个像素拆成两件互不干扰的事来存RGB 是这个像素「如果完全不透明」应该是什么颜色alpha 是它覆盖了多少面积。一根毛尖只盖住像素的一小部分。直通存法会把 RGB 存成毛的颜色再给 alpha 存一个很小的值。把 alpha 调大它就变成一块实心的毛色。把 alpha 调小颜色不变只是更淡。这种存法对编辑友好。改透明度不用碰颜色改颜色也不用碰透明度。调色、描边、抠图精修都是在这两个量上分别做事。它的麻烦在合成时才出现每次叠到背景上都要先把 RGB 乘以 alpha 再加上背景乘以剩下的部分。公式里 RGB 和 alpha 是分开出场的漏乘一次或多乘一次结果都会错。预乘存的是乘过透明度的颜色预乘 alpha 英文叫 premultiplied alpha也叫 associated alpha。它在存的那一刻就把乘法做完了。RGB 里存的已经是「颜色乘以覆盖率」也就是这个像素对最终画面实际贡献了多少光。同一根毛尖在预乘存法里 RGB 会很暗因为它只贡献了一点点毛色。alpha 照样存覆盖率它在合成时只负责决定背景要留多少。预乘存法在合成和缩放时占便宜。叠加只需要rgb 底色×(1−a)少一次乘法。更重要的是插值时不会串色。全透明像素在预乘里 RGB 一定是 0拿去和邻居做双线性插值也不会把自己的颜色掺进来。直通存法里全透明像素的 RGB 可以是任意值缩放前如果不先处理边缘就会渗出莫名其妙的颜色。我以前做编解码时在 YUV 缩放上吃过类似的亏道理相通参与插值的量必须是能线性相加的物理量。预乘 RGB 是直通 RGB 不是。两种存法合成结果相同这两种存法描述的是同一个像素只要各自配对的公式用对合成出来就一模一样。直通 alpha 的公式是rgb×a 底色×(1−a)。预乘 alpha 的公式是rgb 底色×(1−a)后者的 rgb 里已经带了那个 a。两者之间换算也简单直通转预乘就是 RGB 乘以 alpha预乘转直通就是 RGB 除以 alpha。这里要把一件事说死存法本身不产生白边。白边的根在于 RGB 里装的是什么和它是直通还是预乘无关。一个 RGB 里混着白底的直通像素转成预乘之后照样混着白底。存法只在一种情况下会放大白边就是存法和公式没对上。这个放到第 7 章讲。预乘在8位下会丢精度预乘有一个代价在 8 位存储时尤其明显。RGB 乘以一个很小的 alpha 之后会挤到 0 附近的几个整数上等到转回直通时再除以 alpha原来的颜色已经回不来了。alpha 越小能区分的颜色越少。alpha 只有 1 的时候预乘后的 RGB 只能是 0 或 1转回直通就只剩两档。HTML 规范讲 canvas 的getImageData和putImageData时专门提醒过这一点。画布内部如果按预乘存的话像素写进去再读出来可能对不上。这个代价对看图的人几乎没有影响alpha 为 1 的像素本来就看不见。但它对逐像素比较很重要。因为它会在文件里留下一个可以辨认的痕迹。下一章我就是靠这个痕迹去猜导出文件经过了什么。3 抠图导出的PNG是哪种存法PNG规范只允许直通存法这个问题其实不用猜。PNG 规范写得很清楚带 alpha 的 PNG 一律按非预乘存RGB 是像素本身的颜色alpha 独立表示不透明度。WebP 文件也是按非预乘存的。工具内部不管怎么处理只要最后写成 PNG 或 WebP 文件里就只能是直通 alpha。浏览器解码这类图片时也按直通理解img和 canvasdrawImage都会替你把乘法做对。但知道存法只解决了一半。直通存法说的是「RGB 应该是主体颜色」并没有保证工具真的往 RGB 里写了主体颜色。工具完全可以按直通的格式往 RGB 里写一个并不干净的颜色。要知道 RGB 里到底是什么只能拿导出文件和原图逐像素比。不透明像素和原图完全相同导出文件和原图尺寸一致小狗是 1280×1920、植物是 2734×3355可以逐像素对齐。我比的是 RGB 三个通道里和原图差得最多的那一个。结果很干脆alpha 为 255 的不透明像素 RGB 和原图 100% 完全相同。小狗 503,980 个、植物 2,359,053 个一个都不差。这也顺带说明我用的解码器和浏览器解出来的原图一致否则不透明区不可能全等。半透明像素就不全等了。默认导出里半透明像素和原图完全相同的占比小狗是 57.35%、植物是 65.56%。放宽到差值不超过 2 的话小狗是 86.75%、植物是 76.08%。大部分半透明像素的 RGB 就是原图像素剩下那部分偏差的规律在 alpha 上。低透明度像素偏差最大到127我把默认导出的半透明像素按 alpha 分四档看每档偏离原图的最大值。alpha 档小狗像素数小狗最大差植物像素数植物最大差1–720,9571271,021,9861278–319,8441668,8811632–12715,013471,8984128–25467,6241569,4371两张完全不同的图四档的最大差一模一样127、16、4、1。alpha 越小偏差上限越大上限还随 alpha 成倍地缩。这正是上一章说的 8 位预乘往返误差的形状乘以一个小 alpha 再除回来取整误差被放大的倍数就在 alpha 倒数那个量级。看到这四个数时我先怀疑是自己的脚本写错了两张图换着跑了几遍结果都一样。我的推测是导出之前像素在某处经过了一次 8 位预乘存储再按 PNG 的要求除回直通。浏览器的 canvas 就是这类存储的常见来源。但这只是从文件形状反推的。那块代码不是我写的我也没去核实。另外有一个反例值得记下同批另一组测试把导出的 PNG 用 canvas 2DdrawImage画上去再getImageData读回半透明像素的 RGB 误差是 0连 alpha 小于 32 的也是 0。可见在 Chromium 149 里单纯画一张图再读回不一定丢精度。取整发生在导出链路的哪一步我还说不上来。这个偏差对白边几乎没有贡献。偏差大的集中在 alpha 1 到 7这些像素合成时权重很小肉眼看不到。它在这篇里的意义是另一件事去掉这层取整噪声之后导出文件的 RGB 基本就是原图像素工具没有替边缘重新算颜色。这和精修区那句「不改边缘颜色」对得上。4 半透明边为什么带着原底色原图的边缘像素本来就混了白底回到原图。照片里毛尖或叶子边缘所在的那个像素记录的是这一小格里所有光的平均主体盖住一部分白底占另一部分。抠图领域有一个几十年没变过的式子描述这件事原图像素颜色 alpha × 主体色 (1 − alpha) × 背景色。Smith 和 Blinn 1996 年那篇蓝幕抠像的论文就是从这个式子出发的。照这个式子看原图的边缘像素本身就是一次以白色为背景的「合成」结果。它是主体色和白色按 alpha 混出来的颜色。前面量到的半透明原色亮度 160.1 和 200.8 明显高于主体贴边的 108.6 和 123.6差出来的那部分就是白色的份额。植物半透明像素里 57.8% 亮度超过 200是因为那张图的半透明区里有大量 alpha 很低的背景残留。这些像素几乎就是纯白。模型只给透明度不给主体色ISNet 这类分割模型输出的是一张前景概率图工具把它当 alpha 用。它回答的是「这个像素有多少属于主体」不回答「主体在这个像素里是什么颜色」。前面那个式子里有三个未知量alpha、主体色、背景色。模型只给了一个。这时候工具要写一张直通 PNGRGB 那一栏填什么最省事也最不容易出错的办法是直接把原图像素填进去。默认导出就是这么做的不透明区 100% 等于原图半透明区去掉取整误差后也等于原图。问题在于直通存法约定的是「RGB 等于主体色」填进去的却是「主体色和白底的混合」。约定和内容对不上白边就来了。合成时正确的直通公式把这个混合色乘以 alpha再加上新背景乘以 (1 − alpha)。新背景那一份是对的。但混合色里本来就有一份白底等于旧背景被按 alpha 打了个折带到新画面上。在白底上看旧背景和新背景是同一种白完全看不出来。在黑底上看那份被带过来的白就是高出「应有」的约 11 个亮度单位。深蓝底上同样高出约 11。这说明它和新背景是什么颜色无关只取决于旧背景混进来多少。换PNG或WebP改变不了边缘有人会怀疑是 WebP 有损压缩把边缘弄脏了。图映的去背景模式默认选中的是 WEBP我把默认 WEBP 和默认 PNG 放在一起比。两者的半透明像素数完全相同小狗都是 113,438、植物都是 1,732,202。原色平均亮度的差不超过 0.2合成之后的差不超过 0.1。白边在两种格式里一样宽、一样亮。这和前面的推理对得上。两种格式都是直通存法。工具往里面写的是同一份 RGB 和同一份 alpha格式只是换了个容器。白边是内容问题不是容器问题。换 PNG 解决不了。这张图是植物默认导出合成到黑底的整图看两处。第一处是玻璃瓶的外沿瓶身左右两侧各有一圈约 20 px 宽的灰白光晕是两张样本里白边最重的地方。玻璃本身就是半透明的。模型把瓶身边缘判成了一大片中低 alpha每个像素都带着一份白底。第二处是叶子上的白斑。它们在黑底上显得很亮看起来像叶片本身的颜色其实有一部分是 alpha 不满的白底。上方那片带白斑的叶子在第 6 章还会出场。原图来自 Wikimedia Commons作者 BiuschCC BY-SA 3.0这张图为抠图后合成同样按 CC BY-SA 3.0 提供。5 去色边为什么会改动透明度不透明像素一个都没变色精修区里的「去除背景色边」是个默认关闭的开关旁边标着「可选 · 会改变 AI 原始边缘」。打开之后再导出白边基本消失了。小狗去色边后黑底上的半透明边是 76.0按主体色估算的应有值是 75.6只差 0.4。按字面理解「去除背景色边」应该是把混在边缘里的背景色去掉也就是改颜色。我原本也这么以为。量完之后才发现它主要改的不是颜色。先看不透明像素。默认和去色边两版都不透明的像素里小狗 503,803 个、植物 2,356,103 个颜色变化超过 30 的是 0 个。和原图逐像素比时去色边版的不透明像素 RGB 仍然 100% 等于原图小狗 504,075 个、植物 2,356,500 个。去色边版半透明像素和原图完全相同的占比小狗是 56.8%、植物是 65.55%和默认版的 57.35%、65.56% 几乎一样。颜色这一栏去色边基本没动。被改的像素大多是透明度变小那变的是什么是 alpha。小狗有 76,236 个像素的 alpha 被改了其中 67,485 个变小、8,751 个变大。植物被改了 1,369,864 个其中 1,363,556 个变小、只有 6,308 个变大。半透明像素的数量也跟着大减小狗从 113,438 降到 77,761植物从 1,732,202 降到 583,152。样本半透明像素默认→去色边半透明原色亮度黑底实际 / 应有alpha 被改其中变小小狗113,438 → 77,761160.1 → 118.176.0 / 75.676,23667,485植物1,732,202 → 583,152200.8 → 153.6106.5 / 91.01,369,8641,363,556半透明原色亮度小狗从 160.1 降到 118.1植物从 200.8 降到 153.6。下降的原因是最亮的那批半透明像素被压成了全透明从而退出了「半透明」的统计。剩下的半透明像素本来就比较暗。平均值自然下来了。越接近白色的像素被压得越多哪些像素的 alpha 被压我按原图该像素的亮度分档统计去色边前后 alpha 的平均下降量只看默认导出里 alpha 大于 0 的像素。原图亮度小狗平均下降植物平均下降植物下降 64 的占比0–990.10.10.0%100–1494.85.03.1%150–19921.132.519.4%200–22920.066.034.2%230–25524.811.15.8%暗像素几乎不动两张图亮度低于 100 的档平均只降 0.1。亮度一过 150 下降量就上来了。植物在 200 到 229 这一档平均降了 66.0超过三成像素降了 64 以上相当于四分之一个满量程。原图越像白底 alpha 被压得越狠。植物最亮那一档平均只降了 11.1看起来和趋势相反。原因在这一档的构成它里面大部分是 alpha 本来就很低的背景残留植物 alpha 1 到 7 的像素有 1,021,986 个。这些像素原本的 alpha 只有个位数再怎么压也降不了多少。真正被大幅压低的是那些原本 alpha 不低、颜色却接近白的像素也就是白斑叶片和玻璃瓶身。去色边是在重估透明度把上面三组数放在一起能反推出去色边大致在做什么。这块不是我做的。下面是我从导出结果反推的逻辑不是它的实现说明。回到那个式子原图像素 alpha × 主体色 (1 − alpha) × 背景色。要消掉白边有两条路。第一条是保留 alpha 把 RGB 从混合色改回主体色这需要估计主体色业内一般叫颜色去污染。第二条是保留 RGB 重新判断这个像素到底有多少属于主体颜色非常接近背景色的像素大概率就是背景alpha 应该更小。图映这版去色边走的是第二条路。不透明像素颜色不变、半透明像素颜色基本不变、alpha 大量变小、越接近白色压得越多这四个现象都指向同一个结论。第二条路有它的道理。只动 alpha 不会凭空造出原图里没有的颜色也不会在边缘画出奇怪的色块实现起来更稳。它的代价同样来自那个式子它判断「像不像背景」只能看颜色主体里本来就接近背景色的部分它分不出来。白底图里的白色主体在它眼里就是背景下一章那几个误伤都是从这里来的。6 去色边会误伤哪些主体白斑叶片被压出黑洞植物上方那片带白斑的叶子是两张样本里去色边代价最大的地方。叶区里 alpha 大于 0 的像素有 208,058 个默认导出里其中 151,728 个就已经是 alpha 在 128 到 254 之间的半透明。也就是说快速档本来就把这片浅色叶子判成了「半透明」从上一张整图也能看出叶子的白斑区在黑底上有点发虚。开了去色边之后这 151,728 个里有 70,011 个降到 alpha 小于 12841,644 个直接变成全透明。合成到黑底上叶片的白斑区出现了大块黑洞像被虫啃过。不透明像素的颜色一个都没变。变化超过 30 的是 0。叶子没有被涂黑。它是被判成了背景。白斑的颜色接近白底按「像不像背景」重估透明度时它就被当成了背景。这个例子把取舍说得最清楚白边消了主体的一部分也没了。商品图里白色的瓶盖、浅色的衣服、白色的包装都可能落进同一个坑。玻璃瓶的光晕换成了台阶硬边玻璃瓶是另一种情况。瓶身本身就是半透明的默认导出时外沿那一圈约 20 px 宽的灰白光晕是白边最重的地方。开去色边之后光晕基本消失瓶身外沿却变成了台阶状的硬边原来那段平缓的 alpha 过渡被压成了几级陡峭的台阶。矛盾在于玻璃边缘本来就该是半透明的。透过玻璃看到的白底从物理上说确实是这个像素颜色的一部分去色边把它当成背景压掉之后光晕没了玻璃也少了一层质感。植物整图合成到黑底后去色边版半透明边的实际亮度是 106.5、应有亮度 91.0差 15.5比默认版的 11.6 还大。这个差值在植物上已经不太能说明问题因为 alpha 被大面积改写之后我估算「应有亮度」的前提也跟着变了。这一点第 10 章再说。小狗前腿交界多出锯齿小狗是两张图里去色边效果最好的白边基本消失实际 76.0 对应有 75.6。回头看第 1 章那张三联图的右边前腿和肚皮交界的地方多出一块锯齿状的硬边。那一段原图里是白色肚皮毛和深色前腿的交界颜色本身就接近白底。去色边把其中一部分判成了背景。边缘从一段平滑的过渡变成了一条锯齿线。小狗身上的白色部分不多。它只在交界处露了一点。换一只白色的狗会怎样我不确定。这一组我没测。7 前端合成配错为什么会放大白边把直通当预乘用边缘会亮一倍前面讲的白边是「内容」问题大小约 11 个亮度单位。还有一类白边是「公式」问题它会叠在前者上面而且大得多。如果把直通 alpha 的 PNG 当成预乘去合成也就是用rgb 底色×(1−a)就漏掉了 rgb 乘 alpha 那一步半透明像素会以满亮度出现。我自己写公式算了一遍黑底上半透明边的平均亮度小狗变成 160.1、植物变成 200.8正确合成分别是 75.4 和 52.5。这是我按公式算出来的机制演示。它不是图映的行为。它说明原本「打了折」的白底在公式错了之后完全不打折地贴了上去。真实前端里最容易踩这个坑的是 WebGL。同批另一组对照在 Chromium 149里做输入是小狗默认导出的一块含 43,740 个半透明像素的 480×480 裁块。按直通公式合成到黑底后半透明边的参照亮度是 54.9。合成方式半透明边亮度与公式的差canvas 2D drawImage54.9最大 0WebGL 不预乘上传 SRC_ALPHA 与 ONE_MINUS_SRC_ALPHA54.9平均 0.2WebGL 预乘上传 ONE 与 ONE_MINUS_SRC_ALPHA54.9平均 0.2WebGL 不预乘上传 ONE 与 ONE_MINUS_SRC_ALPHA108.9平均 54.6WebGL 预乘上传 SRC_ALPHA 与 ONE_MINUS_SRC_ALPHA48.8平均 6.3WebGL 上传纹理时有一个开关UNPACK_PREMULTIPLY_ALPHA_WEBGL决定上传时要不要替你把 RGB 乘上 alpha。混合函数blendFunc的第一个参数决定合成时要不要再乘一次。两处各管一次乘法。必须恰好乘一次。不预乘上传配ONE是一次都没乘也就是上面「把直通当预乘」的情况。边缘亮度 108.9。这差不多是正确值的两倍。预乘上传再乘一次会偏暗反过来预乘上传配SRC_ALPHA会让 RGB 被乘两次 alpha。边缘偏暗到 48.8平均比公式低 6.3。偏暗在黑底上不如偏亮扎眼。换到浅色底上就会变成一圈发脏的暗边。两种配错方向相反根子是同一个没搞清楚手里的数据是直通还是预乘公式就跟着猜。只要配对两种组合结果都对平均 0.2 的差来自浮点取整选哪一组都可以。我个人倾向预乘上传配ONE。理由是第 2 章说的插值问题纹理一旦要缩放或做 mipmap预乘数据插值不会串色直通数据会。canvas 2D合成没有偏差浏览器的 canvas 2D 在这组对照里是对的。drawImage到黑底上的半透明边亮度 54.9和公式的最大差是 0。浏览器知道 PNG 是直通存法解码和合成时替你做了正确的乘法。在透明画布上drawImage之后用getImageData读回半透明像素的 RGB 与文件原值误差也是 0。alpha 小于 32 的像素也一样alpha 本身不变。这只代表 Chromium 149这一个版本Firefox 和 Safari 没测。这组数说明一件事。用img、CSS 背景或者 canvas 2D 时看到的白边就是文件里本来那份约 11 的白边前端没有额外添乱。在 WebGL、WebGPU 或者自己写的像素循环里合成时如果看到的边比别人亮一倍先查上传方式和混合公式。8 这一圈白边该怎么处理先看主体里有没有接近底色的部分去色边能不能开取决于主体本身有没有和原背景相近的颜色。小狗这类以深色或中间色为主、白色部分很少的主体开了之后白边基本消失代价是个别交界处的锯齿。带白斑的叶子、白色瓶盖、浅色织物这类主体开了之后会被压出洞。玻璃和薄纱这类本身半透明的主体开了之后光晕会变成台阶硬边。我现在的做法是先把默认导出贴到目标底色上看一眼再看主体里有没有接近原背景的大块区域。没有就开去色边。有就不开接受那一圈约 11 的白边或者只在白边最明显的局部用画笔修。拿不准的时候两版都导出来贴到真实要用的底色上比这比什么理论都快。边缘收缩和柔化帮不上大忙精修区里还有边缘收缩和蒙版柔化两项看起来也能处理白边。同批的补充实测把它们都试了一遍结果不太乐观。边缘收缩的滑杆范围是 -4 到 4正数往里收、负数往外扩。小狗半透明原色亮度黑底实际 / 应有差默认160.175.4 / 64.311.1收缩 1139.971.0 / 62.68.4收缩 -1162.581.7 / 65.915.8收缩 -2173.189.7 / 69.919.8柔化 1 px150.374.9 / 63.211.7柔化 3 px155.372.8 / 59.413.4去色边118.176.0 / 75.60.4收缩 1 对小狗略有改善差从 11.1 降到 8.4。往外扩更白。-2 时差到了 19.8。柔化基本无效。3 px 时差反而变成 13.4。这些结果用第 4 章的式子都能解释收缩和柔化只是把 alpha 的边界往里推、往外推或者抹平RGB 里那份白底一点没少。往里收会切掉最外面那圈最白的像素往外扩会把更多白底拉进来柔化则是把白底摊得更开。植物图上收缩和柔化的差都在 11.2 到 11.6 之间。玻璃瓶这类本身半透明的主体收缩和柔化都去不掉光晕。合成端先对齐存法再谈修边在前端自己合成时先做的事情不是修边是确认存法。PNG 和 WebP 解出来是直通。WebGL 纹理上传要么不预乘配SRC_ALPHA要么预乘配ONE二选一。自己写像素循环时直通数据要先乘 alpha 再加背景。这一步对了边缘上就只剩文件本身那份白边。这一步错了修边工具怎么调都只是在错的底子上补。我会在项目里放一张已知半透明边亮度的固定裁块每次改渲染管线都贴到黑底上读一次像素。和 canvas 2D 的结果差一倍就是漏乘偏暗就是多乘。这个检查比肉眼看靠谱得多。偏暗那种情况肉眼很容易放过。9 适用边界与风险提示先说样本。两张图都是 Wikimedia Commons 上的白底图一张短毛小狗和一根带白斑叶片与玻璃瓶的枝条。白底是白边最典型的来源。灰底、彩色底、复杂背景都没测。背景不是白色时混进边缘的就是另一种颜色去色边判断「像不像背景」的标准也会跟着变误伤的对象可能不同。再说档位。只测了快速档 ISNet INT8。专业档 BEN2 和骨灰级档 BiRefNet HR-Matting 都没测。更精细的模型可能给出更准的 alpha。按第 4 章的推理只要导出时 RGB 还是原图像素半透明边里就还带着原背景这一点换模型不会变。这是推理不是实测。我没有数据支持它在别的档上成立。第 3 章和第 5 章里所有关于「它内部怎么做」的判断都是从导出文件反推的。预乘往返的猜测和去色边重估透明度的猜测都可能和真实实现不一致。数据本身可以复核。解读是我的。去色边在这两张图上主要改 alpha不代表它在所有图上都不改颜色。浏览器只测了 Chromium 149真 Chrome、Safari、Firefox 都没测。WebGL 那组对照在测试环境里可能走的是软件渲染真实显卡上的取整可能略有不同。配错时亮一倍或暗一截的方向不会因为换显卡就变。最后是风险。去色边会改变 AI 原始边缘。界面旁注也写了这一点。导出之后被压成全透明的像素在文件里 alpha 就是 0后续用别的工具处理也找不回来。精修区里有「恢复 AI 原始透明通道」导出之前可以撤回。导出之后能不能恢复取决于你手上还有没有默认版的文件。我一般两版都留着。10 测量方法自身的坑应有亮度只是近似值「应有亮度」是我用贴边 3 px 以内的不透明像素平均色当作主体色估算出来的。真实的主体色每个位置都不一样毛尖和毛根、叶脉和叶缘颜色都不同。这个估算只适合比较默认和去色边两版的相对差不能当作精确的真值。它在植物去色边版上会失效。去色边大面积改写了 alpha原本半透明的叶片区域有一部分变成了全透明贴边不透明像素的构成也变了估算出来的主体色就不再是同一个参照。植物去色边版的差 15.5 看起来比默认的 11.6 还大原因就在这里。我没有把它当成「去色边让植物更白」的证据。逐像素对比要先确认解码器一致第 3 章拿导出文件和原图逐像素比有一个前提。我用 Pillow 解出来的原图 JPEG必须和浏览器里解出来的是同一组像素。JPEG 解码的不同实现在色度上采样和取整上会有细小差别。解码器一旦不一致不透明区就不会 100% 相等后面所有「RGB 等于原图」的结论都站不住。我没有假设它一致。结论是用结果验证的两张图的不透明像素都 100% 完全相同说明至少在这两张图上两边解码结果一致。换一张图或换一个解码库这一步要重新验。亮度分档里混着背景残留第 5 章那张亮度分档表里植物最亮一档的平均下降只有 11.1。初看会以为去色边对最白的像素反而手下留情。原因前面说过这一档里大部分是 alpha 本来就只有个位数的背景残留它们的下降量上限很低平均值被拉了下来。按平均值读表会被带偏。更稳的读法是看「下降超过 64 的占比」或者按 alpha 和亮度两个维度一起分档。我这次只做了亮度一个维度。后者没做。测试环境的WebGL可能不走显卡WebGL 那组对照是在 Chromium 149里跑的。这个环境下 WebGL 可能走软件渲染不走真实的 GPU。软件渲染和硬件渲染在混合时的取整精度不一定相同配对组合那平均 0.2 的差在真显卡上可能是别的数。我认为结论的方向不受影响漏乘一次就是亮一倍多乘一次就是偏暗这是公式层面的事。具体数值要当成「这一个环境里的数」来读。11 还没解决的第一个是那组 127、16、4、1。两张图四档的最大偏差完全相同形状和 8 位预乘往返误差一致可同批另一组测试里 canvas 2D 读回的误差是 0。取整到底发生在导出链路的哪一步我从文件上看不出来。第二个是去色边里那几千个 alpha 变大的像素小狗 8,751 个、植物 6,308 个。如果去色边只是「像背景就压低」这些像素不应该存在。它们可能来自某种平滑或形态学处理也可能是别的逻辑我没有拆开看。第三个是换档位。专业档和骨灰级档给出的 alpha 更精细之后半透明边会变窄还是变宽白边会轻还是重去色边的误伤会不会少这一轮都没有数。手上正好有一张白底抠出来的透明图的话可以先贴到纯黑底上截一块边缘放大看。再用任意图像库把半透明像素的 RGB 和原图同位置的像素比一下。相同的占多数就说明白边来自原底色换格式没用。这时候再按主体里有没有接近底色的部分决定开不开去色边。参考资料PNG SpecificationW3Calpha 通道按非预乘存储的规定。WebP Container SpecificationWebP 中 alpha 数据与 RGB 的关系。Porter 与 DuffCompositing Digital ImagesSIGGRAPH 1984。预乘 alpha 与合成算子的来源。Smith 与 BlinnBlue Screen MattingSIGGRAPH 1996。抠图方程与背景色溢的经典表述。W3C Compositing and Blending Level 1浏览器合成公式。HTML Living Standard 的 canvas 章节getImageData与putImageData在预乘存储下可能丢失精度的说明。WebGL 1.0 SpecificationUNPACK_PREMULTIPLY_ALPHA_WEBGL与blendFunc。Qin 等Highly Accurate Dichotomous Image SegmentationECCV 2022。ISNet 的出处。ITU-R BT.709本文亮度加权系数的来源。样本一Puppy Dog on White。作者 George Hodan来自 Wikimedia CommonsCC0。样本二Ficus cuttings with roots in a bottle, White background。作者 Biusch来自 Wikimedia CommonsCC BY-SA 3.0。
返回列表