深色主题的页面上放一张抠好的透明图以后边缘一圈发白。这事做前端的大概都碰到过。我的第一反应是抠得不干净,于是去拖边缘收缩的滑杆。结果往外扩了一格,白边反而更重了。这就有点尴尬了。我不太服气,手边能用的几招我量了个遍。
量的办法很土。我拿两张 Wikimedia Commons 上的白底图做样本:一张是 George Hodan 的小狗(CC0)、一张是 Biusch 的玻璃瓶插枝(CC BY-SA 3.0)。这次抠图我用的是图映 ImgIng(imging.cn/)的快速档,底下跑的模… ISNet INT8,抠完的图统一叠在黑底上比。抠图在浏览器本地跑完,六次导出没有发出一个非 GET 请求。它精修区的说明写得很直白:「默认逐像素直接使用 AI 输出,不改透明度、不改边缘颜色」。我写了个脚本数像素:边缘半透明那圈在黑底上的平均亮度减去按主体颜色估出来的应有亮度就是差。这个差越大白边越重,不过应有亮度是估的近似值,只适合横向比。
下面这张小狗的表里每一行只动了一项设置。
| 小狗 | 半透明像素 | 黑底实际 / 应有 | 差 |
|---|---|---|---|
| 默认 | 113,438 | 75.4 / 64.3 | 11.1 |
| 收缩 +1 | 117,397 | 71.0 / 62.6 | 8.4 |
| 收缩 -1 | 111,251 | 81.7 / 65.9 | 15.8 |
| 收缩 -2 | 110,118 | 89.7 / 69.9 | 19.8 |
| 柔化 1 px | 116,210 | 74.9 / 63.2 | 11.7 |
| 柔化 3 px | 131,289 | 72.8 / 59.4 | 13.4 |
| 去色边 | 77,761 | 76.0 / 75.6 | 0.4 |
先说我踩的那一下:这个滑杆从 -4 到 4,正数往里收而负数往外扩。我当时以为负数是收,其实往外扩等于把原来白底里的像素也圈进来。-1 和 -2 的差分别到了 15.8 和 19.8。往里收 +1 确实好一点,差从 11.1 降到 8.4。代价是主体被削掉一圈:不透明像素从 503,980 个少到 491,682 个。换到植物上收缩 +1 的差是 11.5,跟默认的 11.6 几乎一样。
这张是小狗肚皮下沿在黑底上放大 3 倍的样子。三栏分别对应默认、往里收一格、往外扩两格。坦白说三张肉眼差别很小,我来回切了几次。唯一明显的是最右边那张底部多出的一道细亮线。那就是往外扩带出来的白。表里 8.4 和 19.8 的差距在图上远没有数字那么吓人。这也是我后来改成先数像素的原因。
第二个直觉是柔化,想着把边糊一糊就看不出来了。结果也不行。表里 1 px 和 3 px 的差都比默认还大一点。植物柔化 3 px 的 11.2 也基本没动。柔化只是把半透明的那圈拉宽,里面的白还是那些白。
第三个直觉是格式,我原来隐约觉得 PNG 比 WebP 保真。实测两种导出的半透明像素数完全一样,原色亮度差不超过 0.2。白边跟格式无关。体积上 PNG 反而吃亏:同一只小狗 PNG 1,338,889 字节,WebP 109,722 字节。顺带一提我拿小狗默认导出的一块裁图在 canvas 2D 上用 drawImage 合成到黑底,结果和公式算的差是 0。浏览器 2D 画布没有额外把白边放大。自己写 WebGL 合成的另说。上传方式和混合公式配错会让边缘亮一倍,这块我没深挖。
最后是「去除背景色边」这个开关。表里它把差压到 0.4,白边几乎看不见。效果确实好。它的原理从结果看是改透明度:小狗有 76,236 个像素的 alpha 被改了,不透明像素里颜色变化超过 30 的一个也没有。代价就出在这里:颜色接近白底的主体会被一起改成透明。那张插枝里带白斑的叶子有 208,058 个可见像素,其中 41,644 个变成了全透明。黑底上直接出了黑洞。小狗前腿和肚皮交界处也冒出一块锯齿硬边。下面这张玻璃瓶也出自那张插枝图:原图来自 Wikimedia Commons,作者 Biusch,CC BY-SA 3.0,下图为抠图后合成,同样按 CC BY-SA 3.0 提供。
玻璃瓶是最难的一类:左边是白底原图,中间的默认导出和右边开了去色边的都合成在黑底上。默认导出的瓶沿外侧飘着一层 20 px 左右的灰白光晕。玻璃本身就是半透明的,收缩和柔化都去不掉它。右边光晕是没了,瓶沿却变成了一级一级的台阶。
所以我现在的顺序是这样的:不透明又跟底色差得远的主体直接开去色边。开完放大检查交界处有没有锯齿。主体里有浅色块的不开,改成收缩 +1。滑杆别往负数拖。玻璃这类我还没找到靠谱的办法。范围交代一下:模型只用了快速档,样本只有白底,收缩档位只碰了 +1、-1、-2。换底色之前先合成到深色背景上放大看一遍边缘再上线。