摘要
白底图抠完以后常常还要二次存成 JPG 才能上传。背景明明是纯白的 255,存完再读像素却发现主体边缘附近出现了 254 和 253。我把同一批白底 PNG 用四种存法各存了一遍,逐个像素数背景里有多少不是纯白。四种存法是 Pillow、sharp 默认、sharp 里打开 mozjpeg,还有图映 ImgIng 的压缩转换。最意外的是前面三个。同一个质量数下,Pillow、sharp 默认和图映解码出来的像素逐个一模一样。只有文件大小不同。
先说结论。三张图在质量 85、4:2:0 采样下,Pillow、sharp 默认和图映的背景都有 0.48% 的像素不是纯白。sharp 打开 mozjpeg 以后降到 0.35%,少了近三成。体积也从 164.9 KB 降到 125.0 KB。代价有三笔。主体的 PSNR 掉了 5.6 dB 左右。存出来的是渐进式 JPG,编一张要慢六到九倍。把 mozjpeg 拆成四个开关再看。管杂点的是网格量化和过冲去振铃,管体积的是量化表和扫描优化。没有哪一种设置能让背景全部是 255。我量到的体积不超过 216.3 KB 的设置里,最干净的是 mozjpeg 加质量 95 加 4:4:4,背景里仍有 0.045% 不是纯白。不计体积的话,Pillow 质量 100 加 4:4:4 是 0.035%,同样没有归零。
几个结论先摆在这里,后面每一条都有对应的章节和数字。数字全部来自三张图在同一台机器上的同一批实验:
- 同一个质量数下 Pillow、sharp 默认、图映压缩转换存出来的画面逐像素相同,体积略有差别(2.2、2.3 节)。
- Pillow 打开
optimize=True以后体积只比 sharp 默认多 18 个字节,体积差来自这一项优化(2.4 节)。 - 质量 85、4:2:0 下,mozjpeg 的背景杂点比 sharp 默认少 27.9%,体积小 24%(2.5、2.6 节)。
- 杂点集中在紧贴主体 8 像素以内,这一圈只有八成多是纯白,远处几乎全是纯白(第三章)。
- 质量从 75 拉到 85 杂点几乎不动,Pillow 拉到 100 加 4:4:4 也只能降到 0.035%(4.2、4.3 节)。
- 同样标 85 分,mozjpeg 的主体 PSNR 比默认低 5.56 dB,对齐保真后杂点仍少一半左右(第五章)。
- mozjpeg 里管杂点的是网格量化和去振铃,管体积的是量化表和扫描优化,扫描优化会存成渐进式 JPG(第六章)。
- mozjpeg 编一张比 sharp 默认慢 6 到 9 倍,我自己会选质量 92 左右、4:2:0(第七章、第八章)。
版本声明
这些都是我在 2026-10-23 这天跑的。电脑是 Apple M4 加 16 GB 内存,系统是 macOS 26.5.2。Python 是 3.9.6,用的 Pillow 11.3.0 和 numpy 2.0.2。Node 是 24.14.1,用的 sharp 0.34.5。它自带的 libvips 版本是 8.17.3,里面带着 mozjpeg。所有解码和统计都用 Pillow 做,不混用别的解码器。图映那部分是 2026-10-23 的线上版本,在电脑上的 Chromium 149 里操作。抠图选的是快速档。体积一律按 1 KB 等于 1024 字节算,和图映界面的算法一致。文中的百分比都是个数除以区域像素总数,保留两到三位小数。
适用边界
样本只有三张图,都是维基共享资源上的实拍照片。它们缩放过,长边在 1280 到 1920 像素之间。这篇所有的数字只对这三张图和这几个版本的库成立,我不会把它们推成所有白底图都这样。背景的定义是抠图蒙版全透明的那一片,存成 JPG 之前这些位置的像素全是 255。我只量了背景杂点和主体的 PSNR。肉眼盲对比没做,成品也没有传到任何电商平台试过。图映只测了压缩转换这一条路。它抠图之后的导出格式里没有 JPG,这一点头一章会交代。
文章目录
一、白底图存成JPG为什么要挑编码器
- 1.1 白底PNG常被二次转成JPG
- 1.2 我用三张实拍图和蒙版量背景
- 1.3 这次一共比较四种存法
二、同一档质量下哪种存法背景最干净
- 2.1 Pillow默认存法的背景并不纯白
- 2.2 sharp默认和Pillow逐像素一样
- 2.3 图映压缩转换同样逐像素一样
- 2.4 Pillow开优化后体积追平sharp
- 2.5 mozjpeg让体积小了约四分之一
- 2.6 mozjpeg让背景杂点少近三成
三、杂点落在背景的哪些位置
- 3.1 紧贴主体的背景只有八成多纯白
- 3.2 离主体远一点的背景几乎全是纯白
- 3.3 杂点大多只比纯白暗一点点
四、色度全采样和拉高质量能压到多低
- 4.1 色度全采样让杂点降到四成多
- 4.2 质量调高一点杂点并没有降
- 4.3 Pillow质量拉满也没有归零
- 4.4 mozjpeg加色度全采样体积不到一半
- 4.5 同样的杂点水平各家要多少体积
五、同一个质量数是同一档保真吗
- 5.1 同样标85分mozjpeg主体保真更低
- 5.2 对齐保真后mozjpeg杂点仍少一半
六、mozjpeg里哪几个开关在管杂点
- 6.1 四个开关一起开和mozjpeg字节相同
- 6.2 网格量化和去振铃在管杂点
- 6.3 量化表和扫描优化在管体积
- 6.4 开了扫描优化会存成渐进式JPG
七、mozjpeg慢多少值不值得
- 7.1 mozjpeg编一张比默认慢好几倍
- 7.2 几十张图批量处理多花的时间感觉不到
八、我最后怎么选
- 8.1 要小又干净就用sharp的mozjpeg
- 8.2 怕渐进式JPG就关掉扫描优化
- 8.3 只能用Pillow时怎么存
- 8.4 图映压缩转换适合谁
- 8.5 检测纯白别只认255
九、适用边界与风险
十、测量方法的坑
十一、还没解决的
参考资料
一、白底图存成JPG为什么要挑编码器
我在内容公司做技术岗,写脚本、剪片子、搭内部小工具什么都沾一点。处理商品图是其中隔三差五会碰到的一块。每次碰到的时候我从来没认真想过存 JPG 这一步该用谁。这篇就是把这个问题补上。
1.1 白底PNG常被二次转成JPG
我手上这个抠图工具导出的是 PNG、WebP 这类格式,可平台收主图的时候格式清单里排在前面的常常是 JPEG。亚马逊卖家平台的帮助页(美国站,2026-10-23 查看)写得很直白。主图要纯白背景,RGB 值是 255、255、255。文件格式一栏里 JPEG 排在最前面,旁边标着推荐。国内平台的白底规则我这次没查到,因此文里只引这一页。其他平台一律以各自后台当前的规则为准。
流程就成了这样:抠图铺白后导出 PNG,再转一次 JPG。转这一次的时候白色背景会发生什么,我以前只凭感觉认为白的还是白的。这回我先写了几行 numpy 去读存完的 JPG。主体边缘附近的背景读出来有 250 到 254 之间的值,个别地方还更暗。这就是这篇的起点。我不是想证明谁好谁坏。我只想弄清楚背景到底脏了多少、脏在哪,换个存法能不能少脏一点。
1.2 我用三张实拍图和蒙版量背景
样本是我在维基共享资源上找的三张无品牌实拍图:一只毛绒泰迪熊、一只波希米亚高脚杯、一双白色运动鞋。它们不是我拍的。作者分别是 Dan Bollinger、Kritzolina 和 JustinLiang522,许可都是 CC BY-SA 4.0。我用的是维基提供的缩放版。三张图的尺寸是毛绒 1920×1440、玻璃杯 1280×1920、白鞋 1280×1707。
我用的是图映 ImgIng(imging.cn/)的去背景功能,在电脑... PNG 就是后面所有实验的输入,每张图一份。
量背景需要一把尺子。我的办法是借抠图蒙版:蒙版里完全透明的位置就是背景。这一片占整张图的比例是毛绒 72.7%、玻璃杯 76.1%、白鞋 55.2%。这些位置在存成 JPG 之前像素值全是 255,我拿数据核对过。存成 JPG 再解码回来,数一数这一片里有多少个像素的三个通道没有同时等于 255。这个数就是我说的杂点。
1.3 这次一共比较四种存法
我最后挑了四种存法。头一种是 Pillow 的 save。质量和采样自己指定,其余全默认。第二种是 sharp 的 jpeg(),同样只设质量和采样。第三种是在 sharp 里加一个 mozjpeg: true。第四种是图映的压缩转换:把白底 PNG 拖进去选 JPG,拉滑杆再点保存。
这里要先交代一件事。图映抠图完成以后,界面上能选的导出格式里没有 JPG。想拿 JPG 得把白底 PNG 再拖进它的压缩转换里。压缩转换导出的 JPG 是 4:2:0 采样,滑杆停在哪就是哪一档质量。对图映来说这条链路实际是两步:抠图出 PNG,再由压缩转换出 JPG。我比较的是第二步的输出。
还有一个小插曲。命令行里的 cjpeg 我也跑了。结果和 Pillow 一个字节都不差,说明两边是同一类编码器。我本机的 cjpeg 是 libjpeg-turbo 3.2.0,不是 mozjpeg。因此 mozjpeg 这条线我只通过 sharp 去测。
二、同一档质量下哪种存法背景最干净
先看最日常的一档:质量 85,采样 4:2:0。质量数是各个库都有的那个滑杆,数字越大越接近原图。采样说的是颜色信息留多少。4:2:0 是大多数库默认或者最常设的组合,第四章会专门拆它。
2.1 Pillow默认存法的背景并不纯白
先写那把尺子,再让 Pillow 存三张图。每张存两种采样。代码放在 ruler.py 里,后面几段也要用它。不想读代码的话,直接看下面的输出也能跟上。
python
import os
import numpy as np
from PIL import Image
PICS = ["teddy", "glass", "shoe"]
def stray_pct(jpg, cut):
"""蒙版全透明的位置,解码后不是纯白的占多少个百分点"""
px = np.asarray(Image.open(jpg).convert("RGB"))
hole = np.asarray(Image.open(cut).convert("RGBA"))[..., 3] == 0
return 100 * (px[hole] != 255).any(axis=1).mean()
if __name__ == "__main__":
for name in PICS:
for label, sub in (("4:2:0", 2), ("4:4:4", 0)):
out = f"out/{name}_pil_q85_{sub}.jpg"
Image.open(f"{name}.png").save(out, quality=85, subsampling=sub)
kb = os.path.getsize(out) / 1024
print(f"{name:6} {label} {kb:5.0f} KB {stray_pct(out, name + '_cut.png'):.3f}%")
text
teddy 4:2:0 216 KB 0.286%
teddy 4:4:4 258 KB 0.123%
glass 4:2:0 93 KB 0.503%
glass 4:4:4 124 KB 0.218%
shoe 4:2:0 216 KB 0.656%
shoe 4:4:4 246 KB 0.290%
代码说明 :teddy_cut.png 这类文件是抠图导出的带透明 PNG,只用来取蒙版。teddy.png 是铺白后的 PNG。stray_pct 把蒙版里 alpha 等于 0 的位置挑出来,看解码后的 RGB 有没有任何一个通道不是 255。有就算一个杂点,最后换成百分比。输出里每行三个数:图名与采样、体积、杂点占比。这一章盯的是 4:2:0 那三行。三个数是 0.286%、0.503%、0.656%,平均下来 0.481%。毛绒最干净,白鞋最脏。我没有去查原因,只猜和主体边缘有多少高对比的线条有关。白鞋的鞋带和鞋底线比毛绒的毛尖硬得多。
2.2 sharp默认和Pillow逐像素一样
接下来让 sharp 存。每张图存默认和加 mozjpeg: true 两种方式。质量取 75、85、95,采样取 4:2:0 和 4:4:4。一共 36 个文件。这一段用 Node 写,文件名按图名、存法、质量、采样排,后面几段要按这个名字去找文件。
js
const sharp = require("sharp");
const pics = ["teddy", "glass", "shoe"];
(async () => {
let n = 0;
for (const name of pics) {
for (const [tag, extra] of [["plain", {}], ["moz", { mozjpeg: true }]]) {
for (const q of [75, 85, 95]) {
for (const cs of ["4:2:0", "4:4:4"]) {
const out = `out/${name}_${tag}_q${q}_${cs.replace(/:/g, "")}.jpg`;
await sharp(`${name}.png`).jpeg({ quality: q, chromaSubsampling: cs, ...extra }).toFile(out);
n++;
}
}
}
}
console.log("写出", n, "个文件");
})();
text
写出 36 个文件
代码说明 :plain 是 sharp 的默认存法,moz 是只多一个 mozjpeg: true。这一段没有别的输出,要看效果得拿下一段去量。下一段一次比四种存法:Pillow、sharp 默认、sharp 加 mozjpeg,外加图映压缩转换导出的文件。图映那份是我在页面上把滑杆拉到 85 存下来的,文件名是 teddy_imging_q85.jpg 这一类。除了体积和杂点,我还顺手比了一件事:解码出来的两张图是不是每个像素都一样。
python
import os
import numpy as np
from PIL import Image
from ruler import PICS, stray_pct
def px(path):
return np.asarray(Image.open(path).convert("RGB"))
def kb(path):
return os.path.getsize(path) / 1024
for name in PICS:
rows = {
"Pillow": f"out/{name}_pil_q85_2.jpg",
"sharp 默认": f"out/{name}_plain_q85_420.jpg",
"sharp mozjpeg": f"out/{name}_moz_q85_420.jpg",
"图映 压缩转换": f"{name}_imging_q85.jpg",
}
print(name)
for label, path in rows.items():
print(f" {label:<14}{kb(path):6.1f} KB 杂点 {stray_pct(path, name + '_cut.png'):.3f}%")
same = lambda a, b: bool((px(rows[a]) == px(rows[b])).all())
print(" 逐像素相同:Pillow=sharp默认", same("Pillow", "sharp 默认"),
"| 图映=sharp默认", same("图映 压缩转换", "sharp 默认"))
text
teddy
Pillow 216.0 KB 杂点 0.286%
sharp 默认 205.8 KB 杂点 0.286%
sharp mozjpeg 155.5 KB 杂点 0.238%
图映 压缩转换 206.3 KB 杂点 0.286%
逐像素相同:Pillow=sharp默认 True | 图映=sharp默认 True
glass
Pillow 93.1 KB 杂点 0.503%
sharp 默认 78.5 KB 杂点 0.503%
sharp mozjpeg 57.8 KB 杂点 0.351%
图映 压缩转换 79.0 KB 杂点 0.503%
逐像素相同:Pillow=sharp默认 True | 图映=sharp默认 True
shoe
Pillow 216.0 KB 杂点 0.656%
sharp 默认 210.2 KB 杂点 0.656%
sharp mozjpeg 161.8 KB 杂点 0.451%
图映 压缩转换 210.8 KB 杂点 0.656%
逐像素相同:Pillow=sharp默认 True | 图映=sharp默认 True
代码说明:每张图四行,每行是存法、体积和杂点。最后一行是整张图逐像素比较的结果,不只比背景。结果很整齐。Pillow、sharp 默认、图映这三行的杂点是同一个数:毛绒 0.286%、玻璃 0.503%、白鞋 0.656%。小数点后三位都没差。最后一行的两个 True 表示这三个文件解码以后整张图每个像素都一样。只有 mozjpeg 那一行不一样。
这个结果我事先没料到。sharp 的文件比 Pillow 的小,玻璃杯那张小了 16%。我原本猜它压得更狠,背景应该更脏才对。结果杂点一个不多一个不少。三个文件里存的画面信息是同一份,差的只是体积。
2.3 图映压缩转换同样逐像素一样
图映这一行我多验了几档,怕只是 85 凑巧。滑杆拉到 75 和 95 各存一次,三张图一共六个文件。再和 Pillow 同一个质量数的文件逐像素比,六对全是一样。图映的滑杆默认停的位置每张图不一样,这三个默认值导出的文件也和 Pillow 存同一个数的逐像素一样。
| 图映滑杆位置 | 毛绒 | 玻璃杯 | 白鞋 |
|---|---|---|---|
| 默认停的位置 | 78 | 88 | 80 |
| 默认值的体积 KB | 183.5 | 83.3 | 192.4 |
| 默认值的杂点 | 0.313% | 0.479% | 0.578% |
| 拉到 75、85、95 | 同 Pillow | 同 Pillow | 同 Pillow |
| 拉到 100 | 同 Pillow 99 | 同 Pillow 99 | 同 Pillow 99 |
滑杆拉到头是个例外。拉到 100 导出的文件和 Pillow 的 100 对不上,反倒和 Pillow 的 99 三张图都逐像素一样。我不知道它为什么这样处理,也没有去翻它的代码。我只猜是把上限收在了 99。这里只记下一个事实:图映的 100 不等于 Pillow 的 100。
到这里我可以把三条路并成一条。在这三张图和这几个质量数上,同一个质量数下的 Pillow、sharp 默认、图映压缩转换存出来的画面像素是一样的。背景杂点一样多,彼此之间差的只是文件体积。
2.4 Pillow开优化后体积追平sharp
那体积到底差在哪?我猜是霍夫曼表的优化,意思是把同样的数据重新排得更紧凑。Pillow 的 save 有一个 optimize 参数默认是关的,我打开试了一次。下面用字节数而不是千字节来比,这样 18 个字节的差别也看得见。
| 字节数 | 毛绒 | 玻璃杯 | 白鞋 |
|---|---|---|---|
| Pillow 默认 | 221,217 | 95,370 | 221,206 |
| Pillow 打开 optimize | 210,800 | 80,449 | 215,307 |
| sharp 默认 | 210,782 | 80,431 | 215,289 |
打开优化以后每张图都只比 sharp 多 18 个字节。这 18 个字节我没追,多半是文件头里写的东西不一样。体积差的来源就很清楚了:sharp 默认带着这一项优化,Pillow 要自己打开。省下来的比例看图而定。大片平坦的玻璃杯省了 15.6%,毛绒省 4.7%,白鞋省 2.7%。这一节里没有一个像素变了。
2.5 mozjpeg让体积小了约四分之一
前面三种存法的像素和杂点完全一样,只有第三种存法才真正让结果不同。三张图平均下来,质量 85、4:2:0 的体积和杂点是这样的:
| 存法 | 体积 KB(三张均值) | 背景杂点 |
|---|---|---|
| Pillow 默认 | 175.1 | 0.481% |
| Pillow 打开 optimize | 164.9 | 0.481% |
| sharp 默认 | 164.9 | 0.481% |
| 图映压缩转换 | 165.4 | 0.481% |
| sharp mozjpeg | 125.0 | 0.347% |
mozjpeg 这一行的体积比 sharp 默认小 24%,比 Pillow 默认小 29%。三张图都小。毛绒从 205.8 降到 155.5 KB,玻璃杯从 78.5 降到 57.8,白鞋从 210.2 降到 161.8。表里第二行是我拿 2.4 节的字节数算出来的均值,没有单独再量杂点。像素一样,杂点也一样。体积小一截,我头一个反应是警惕。是不是压得更狠,把东西丢掉了?这个疑问我放到第五章专门去查,先把杂点这一栏看完。
2.6 mozjpeg让背景杂点少近三成
mozjpeg 这一行的杂点是 0.347%,对着 0.481% 少了 27.9%。说近三成没有夸张。三张图各自的变化不一样。毛绒从 0.286% 降到 0.238%,少了 17%。玻璃杯从 0.503% 降到 0.351%,少了 30%。白鞋从 0.656% 降到 0.451%,少了 31%。原本最脏的两张图降得最多,原本最干净的毛绒降得最少。
我自己的感觉是体积小了四分之一,脏的地方还少了近三成。听着像白捡的便宜。这种事在编码里不常见,我接下来一路都在找它的代价。
三、杂点落在背景的哪些位置
不管用哪种存法,杂点都不是平均撒在整个背景上的。看一眼背景里离主体多远,规律就出来了。我把背景按离蒙版边缘的距离分成两块:8 像素以内算近处,8 像素以外算远处。选 8 是因为 JPEG 按 8×8 的小块处理。这个理由是我看别人文章学的,没有自己拆块去验证。从数据上看这条分界线倒是很清楚。
3.1 紧贴主体的背景只有八成多纯白
下面这张表的数都是三张图的平均值。质量和采样写在存法后面,后两列是占整个背景的比例。近处和远处的分界是离蒙版边缘 8 像素,纯白指三个通道都等于 255。
| 存法 | 近处纯白 | 远处纯白 | 250到254 | 低于250 |
|---|---|---|---|---|
| Pillow 85 加 4:2:0 | 84.26% | 99.9869% | 0.474% | 0.0075% |
| mozjpeg 85 加 4:2:0 | 88.39% | 99.9916% | 0.346% | 0.0009% |
| Pillow 95 加 4:4:4 | 96.06% | 100.0000% | 0.123% | 0.0001% |
| mozjpeg 95 加 4:4:4 | 98.60% | 100.0000% | 0.045% | 0.0001% |
先看头一行。质量 85 的 Pillow 在紧贴主体 8 像素以内的背景里只有 84.26% 是纯白,差不多每六七个像素就有一个不是 255。这一圈就是取色器最容易点到 254 的地方。分开看三张图,近处的纯白比例是下面这样的:
| 近处纯白 | 毛绒 | 玻璃杯 | 白鞋 |
|---|---|---|---|
| Pillow 85 加 4:2:0 | 86.24% | 78.13% | 88.42% |
| mozjpeg 85 加 4:2:0 | 88.49% | 84.63% | 92.06% |
mozjpeg 没有把杂点挪到别处,近处和远处都在变好。玻璃杯最脏也进步最大,毛绒本来就干净所以升得最少。
3.2 离主体远一点的背景几乎全是纯白
8 像素以外的背景 Pillow 85 的纯白比例是 99.9869%,已经高得难再高。换成 mozjpeg 是 99.9916%,只多了一点点。到了 95 加 4:4:4 这一档,远处直接是 100.0000%。一个杂点都没有。
这也解释了一个常见的检查方法为什么会漏掉问题。有人只看图像最外面几圈像素是不是 255,拿它判断背景纯不纯。我用同样的办法查了最外 3 像素。毛绒和玻璃杯都是 100.0000%,白鞋是 99.9943%。那是因为鞋的一个角贴着画面边缘。这个检查几乎全部通过,可同一批文件里近处的背景有一成多不是纯白。只看边缘等于只看了最干净的地方。
3.3 杂点大多只比纯白暗一点点
杂点到底有多脏?表里后两列回答了。Pillow 85 加 4:2:0 的背景里 250 到 254 这一档占 0.474%,低于 250 的只占 0.0075%。前者是后者的 60 多倍。绝大部分杂点只比白色暗一点点。肉眼基本分不出来,取色器和严格比较才抓得到。
低于 250 的杂点也不是没有。Pillow 质量 75 的白鞋图里背景最暗的一个像素是 228,mozjpeg 质量 85 的同一张图最暗是 244。Pillow 和 mozjpeg 换成 95 加 4:4:4 以后,三张图的最暗值都落在 248 到 252 之间。白鞋的两个文件最暗是 249 和 248,逐像素严格卡 250 的话仍然过不了。按占比放行就宽松得多。这个话题放到 8.5 节再讲。
四、色度全采样和拉高质量能压到多低
采样和质量是两个最容易想到的旋钮。我把它们一起拧了一遍,每次只动一个旋钮,别的保持默认,看杂点能压到多低。体积要付出多少,也一并记下来。
4.1 色度全采样让杂点降到四成多
4:2:0 的意思是亮度保留全分辨率,颜色的分辨率砍掉一半。4:4:4 是不砍。同样的质量数把 4:2:0 换成 4:4:4,杂点每一档都降到了原来的四成多。体积涨得没有那么多。下面是 Pillow 的三档对照:
| Pillow 质量 | 杂点 4:2:0 | 杂点 4:4:4 | 体积 4:2:0 | 体积 4:4:4 |
|---|---|---|---|---|
| 75 | 0.479% | 0.202% | 148.9 KB | 180.2 KB |
| 85 | 0.481% | 0.210% | 175.1 KB | 209.5 KB |
| 95 | 0.301% | 0.123% | 257.3 KB | 304.0 KB |
质量 85 的三张图平均体积涨了 20%。sharp 默认涨 13%,mozjpeg 也是 13%。分开看三张图。玻璃杯涨得最多,从 93 KB 涨到 124 KB,多了三分之一。白鞋涨得最少,只有 14%。
为什么 4:4:4 会让背景更干净?我的理解是主体边缘上带颜色的地方,4:2:0 把颜色拉粗以后会晕到旁边的白像素里。这是推断,我没有去分别看三个通道验证。
4.2 质量调高一点杂点并没有降
第二个旋钮是质量数,这里我碰到一件反直觉的事。上面那张表里 Pillow 的质量从 75 拉到 85,4:2:0 的杂点是 0.479% 对 0.481%,几乎没动。4:4:4 是 0.202% 对 0.210%,还小小涨了一点。拉高了 10 档,背景一点没变干净。体积却涨了 18%。
直到 95 以后杂点才真的往下走。4:2:0 的杂点降到 0.301%,体积涨到 257.3 KB。从 75 到 95,杂点少了三成多。体积涨了 73%。要是目标只是背景干净,在 75 和 85 之间纠结没有意义。
4.3 Pillow质量拉满也没有归零
我把 Pillow 的质量一路拉到头,只看 4:4:4,因为我想知道杂点的下限在哪:
| 质量 | 体积 KB(三张均值) | 背景杂点 |
|---|---|---|
| 75 | 180.2 | 0.202% |
| 85 | 209.5 | 0.210% |
| 95 | 304.0 | 0.123% |
| 98 | 387.0 | 0.060% |
| 100 | 537.8 | 0.035% |
质量 100 加 4:4:4 的三张图,杂点分别是 0.022%、0.030% 和 0.052%。体积是 692、297 和 625 KB。杂点已经低到肉眼和取色器都很难撞上,可它仍然不是零。这是整个实验里 Pillow 能做到的最干净状态,代价是体积达到质量 85 加 4:2:0 的三倍多。
越往顶端越不划算。从 95 拉到 98 体积多出 83 KB,杂点少 0.063 个百分点。每少 0.01 个百分点要多花 13 KB 左右。从 98 拉到 100 体积多出 151 KB,杂点只少 0.025 个百分点。每少 0.01 个百分点要多花 60 KB 左右。
回到最初的问题。在这三张图上没有任何一个质量数能让 JPG 里的白背景完整保持 255。我的理解是这和 JPEG 有损压缩的做法有关,不是哪个库的毛病。这一句同样是我的推断。
4.4 mozjpeg加色度全采样体积不到一半
mozjpeg 在这个方向上的表现更让我意外。质量 95 加 4:4:4 的三张图杂点是 0.022%、0.040% 和 0.072%,平均 0.045%。体积平均 216.3 KB。把它放到 Pillow 质量 100 加 4:4:4 的旁边,杂点是 0.045% 对 0.035%。差得不多,体积是 216.3 KB 对 537.8 KB,不到一半。
再往下一档看。mozjpeg 质量 85 加 4:4:4 是杂点 0.114%、体积 141.3 KB。Pillow 质量 95 加 4:4:4 是 0.123%,304.0 KB。杂点差不多,体积只有它的 46%。写到这里我自己都觉得有点太好了,第五章会专门去找它的代价。
4.5 同样的杂点水平各家要多少体积
把所有存法的体积和杂点画在一张图里,会更直观。横轴是三张图的平均体积,纵轴是背景里不是 255 的像素占比,每个点旁边标着质量数。

照片:Dan Bollinger、Kritzolina、JustinLiang522(维基共享资源,CC BY-SA 4.0)。对比图是改编作品,沿用 CC BY-SA 4.0。
这张图要看三件事。头一件,左边 4:2:0 的面板里 Pillow、sharp 默认和图映三条线在纵向上是同一个高度,杂点本来就一样。sharp 默认和图映两条线几乎叠在一起。Pillow 那条在更右边,同样的杂点体积更大。再一件,橙棕色的 mozjpeg 那条线整体在左下方。同样的体积杂点更低,同样的杂点体积更小。还有一件:右边 4:4:4 面板里没有图映,因为我在它的压缩转换里没有找到 4:4:4 的选项。
两个面板的纵轴刻度不一样。左边大约是 0.15% 到 0.48%,右边大约是 0.05% 到 0.21%。别拿一个面板的位置去比另一个。三张图的平均值也会盖住单张的差别。具体到毛绒、玻璃和白鞋各自的杂点,请回头看 2.2 节和 4.4 节里的单张数字。
把所有组合的三张均值放在下面,方便你自己对照。最后一列是三张图里背景最暗的那个像素值。
| 存法 | 质量 | 采样 | 体积 KB | 杂点 | 最暗值 |
|---|---|---|---|---|---|
| Pillow | 75 | 4:2:0 | 148.9 | 0.479% | 228 |
| Pillow | 75 | 4:4:4 | 180.2 | 0.202% | 231 |
| Pillow | 85 | 4:2:0 | 175.1 | 0.481% | 239 |
| Pillow | 85 | 4:4:4 | 209.5 | 0.210% | 240 |
| Pillow | 95 | 4:2:0 | 257.3 | 0.301% | 247 |
| Pillow | 95 | 4:4:4 | 304.0 | 0.123% | 249 |
| Pillow | 98 | 4:2:0 | 320.7 | 0.174% | 250 |
| Pillow | 98 | 4:4:4 | 387.0 | 0.060% | 251 |
| Pillow | 100 | 4:2:0 | 412.3 | 0.076% | 252 |
| Pillow | 100 | 4:4:4 | 537.8 | 0.035% | 252 |
| sharp 默认 | 75 | 4:2:0 | 134.9 | 0.479% | 228 |
| sharp 默认 | 75 | 4:4:4 | 153.2 | 0.202% | 231 |
| sharp 默认 | 85 | 4:2:0 | 164.9 | 0.481% | 239 |
| sharp 默认 | 85 | 4:4:4 | 187.0 | 0.210% | 240 |
| sharp 默认 | 95 | 4:2:0 | 218.4 | 0.301% | 247 |
| sharp 默认 | 95 | 4:4:4 | 255.0 | 0.123% | 249 |
| sharp mozjpeg | 75 | 4:2:0 | 81.1 | 0.349% | 242 |
| sharp mozjpeg | 75 | 4:4:4 | 93.5 | 0.097% | 244 |
| sharp mozjpeg | 85 | 4:2:0 | 125.0 | 0.347% | 244 |
| sharp mozjpeg | 85 | 4:4:4 | 141.3 | 0.114% | 245 |
| sharp mozjpeg | 95 | 4:2:0 | 185.7 | 0.160% | 247 |
| sharp mozjpeg | 95 | 4:4:4 | 216.3 | 0.045% | 248 |
| 图映压缩转换 | 75 | 4:2:0 | 135.4 | 0.479% | 228 |
| 图映压缩转换 | 85 | 4:2:0 | 165.4 | 0.481% | 239 |
| 图映压缩转换 | 95 | 4:2:0 | 218.9 | 0.301% | 247 |
| 图映压缩转换 | 100 | 4:2:0 | 303.9 | 0.106% | 252 |
表里能看到前面几节的结论。同一个质量和采样下 Pillow、sharp 默认、图映的杂点和最暗值完全一样。Pillow 的体积最大。mozjpeg 的体积和杂点都更低。图映的 100 一行杂点是 0.106%,对着 Pillow 4:2:0 的 100 是 0.076%。这就是 2.3 节说的它和 Pillow 的 99 一样而不是和 100 一样。
五、同一个质量数是同一档保真吗
前面几章都是同一个质量数下比,这有一个前提:不同库的 85 是同一档画质。mozjpeg 的体积小了四分之一,我一直在怀疑它是不是偷偷降低了画质。
5.1 同样标85分mozjpeg主体保真更低
我换了一个角度:不看背景,看主体。把蒙版里不透明度大于 0 的位置当主体,再拿存出来的 JPG 和铺白的原始 PNG 逐像素比,比出来的数叫 PSNR。PSNR 是一个衡量失真的数,越大越接近原图。我是中文系出身,这个词是现学的。只当一把参考尺子,不当结论。
质量 85、4:2:0 下三张图主体上的 PSNR 均值是这样的:sharp 默认 46.55 dB,mozjpeg 40.99 dB。差了 5.56 dB。同样标着 85,mozjpeg 在主体上保留的东西比默认少。它体积小了四分之一,有一部分是拿保真换来的。这个结果我没想遮掩。前面几章里同质量下 mozjpeg 更小更干净是真的,可它们不是同一档保真。
5.2 对齐保真后mozjpeg杂点仍少一半
质量数不能对齐,我就用主体 PSNR 对齐。把 mozjpeg 的质量从 85 往上扫一路量到 98,看哪一档的主体 PSNR 追上 sharp 默认。
| 设置 | 体积 KB(三张均值) | 背景杂点 | 主体 PSNR dB |
|---|---|---|---|
| sharp 默认 85 | 164.9 | 0.481% | 46.55 |
| sharp 默认 95 | 218.4 | 0.301% | 49.92 |
| mozjpeg 85 | 125.0 | 0.347% | 40.99 |
| mozjpeg 90 | 153.2 | 0.261% | 45.00 |
| mozjpeg 92 | 162.8 | 0.239% | 45.71 |
| mozjpeg 94 | 182.2 | 0.166% | 47.89 |
| mozjpeg 95 | 185.7 | 0.160% | 49.26 |
拿 mozjpeg 92 对着 sharp 默认 85:体积 162.8 KB 对 164.9 KB,主体 PSNR 45.71 dB 对 46.55 dB。只低 0.84 dB。杂点 0.239% 对 0.481%,少了一半。再拿 mozjpeg 95 对着 sharp 默认 95:体积 185.7 KB 对 218.4 KB,小了 15%。主体 PSNR 49.26 dB 对 49.92 dB,低 0.66 dB。杂点 0.160% 对 0.301%,少了 47%。
对齐保真以后,mozjpeg 的体积优势从小四分之一缩到了一个点到十五个点。杂点那一栏仍然是少一半左右。这个结果比同质量小四分之一可信得多。我没有拿肉眼做盲对比。PSNR 低零点几 dB 肉眼看不看得出来,我说不准。
六、mozjpeg里哪几个开关在管杂点
sharp 的文档把 mozjpeg: true 写成一组开关的简写。我想知道是哪几个开关在起作用,办法是一个一个拆开。
6.1 四个开关一起开和mozjpeg字节相同
先验证简写的说法。我把网格量化、过冲去振铃、扫描优化、量化表 3 号四个选项一起打开,再和 mozjpeg: true 各存一份。三张图的字节数一样、一个字节都不差,分别是毛绒 159,237、玻璃杯 59,215、白鞋 165,647。这一步我另跑了一个几行的小脚本,没有贴在文里。下面的脚本直接用单个开关。
js
const sharp = require("sharp");
// mozjpeg:true 等于四个开关一起开。这里一次只开一个,再加两个组合
const switches = {
plain: {},
trellis: { trellisQuantisation: true },
deringing: { overshootDeringing: true },
scans: { optimiseScans: true },
table3: { quantisationTable: 3 },
tri_der: { trellisQuantisation: true, overshootDeringing: true },
no_scans: { trellisQuantisation: true, overshootDeringing: true, quantisationTable: 3 },
all: { mozjpeg: true },
};
(async () => {
for (const name of ["teddy", "glass", "shoe"]) {
for (const [tag, opts] of Object.entries(switches)) {
await sharp(`${name}.png`)
.jpeg({ quality: 85, chromaSubsampling: "4:2:0", ...opts })
.toFile(`out/${name}_sw_${tag}.jpg`);
}
}
})();
代码说明 :switches 里每一项是一种开关组合,键名就是后面表里的名字。mozjpeg: true 对应 all。每张图存 8 个文件,文件名是 <图名>_sw_<开关>.jpg,下一段 Python 按这个名字去读。这段没有输出。
python
import os
import numpy as np
from PIL import Image
from ruler import PICS, stray_pct
def subject_psnr(name, tag):
src = np.asarray(Image.open(f"{name}.png").convert("RGB")).astype(float)
out = np.asarray(Image.open(f"out/{name}_sw_{tag}.jpg").convert("RGB")).astype(float)
subj = np.asarray(Image.open(f"{name}_cut.png").convert("RGBA"))[..., 3] > 0
mse = ((src[subj] - out[subj]) ** 2).mean()
return 10 * np.log10(255 ** 2 / mse)
tags = ["plain", "trellis", "deringing", "scans", "table3", "tri_der", "no_scans", "all"]
print("开关 体积KB 杂点% 主体PSNR 渐进式 (三张均值)")
for tag in tags:
files = [f"out/{n}_sw_{tag}.jpg" for n in PICS]
kb = np.mean([os.path.getsize(f) / 1024 for f in files])
stray = np.mean([stray_pct(f, n + "_cut.png") for f, n in zip(files, PICS)])
psnr = np.mean([subject_psnr(n, tag) for n in PICS])
prog = all(bool(Image.open(f).info.get("progressive")) for f in files)
print(f"{tag:<10}{kb:7.1f}{stray:7.3f}{psnr:9.2f} {'是' if prog else '否'}")
text
开关 体积KB 杂点% 主体PSNR 渐进式 (三张均值)
plain 164.9 0.481 46.55 否
trellis 161.0 0.412 44.83 否
deringing 164.9 0.419 46.55 否
scans 147.5 0.481 46.55 是
table3 152.0 0.495 42.47 否
tri_der 161.1 0.349 44.83 否
no_scans 139.7 0.343 40.94 否
all 125.0 0.347 40.99 是
代码说明 :这一段用 Python 量体积、杂点、主体 PSNR,再检查文件是不是渐进式。主体 PSNR 是拿存出来的 JPG 对铺白的原始 PNG,只在蒙版不透明的位置算。trellis 是网格量化。deringing 是过冲去振铃。scans 是扫描优化。table3 是量化表 3 号。tri_der 是前两个一起开。no_scans 是除了扫描优化以外的三个一起开。all 就是 mozjpeg: true。每一行都是质量 85、4:2:0 下三张图的均值。这张表是我整篇里最看重的一张,它把 mozjpeg 好这句笼统的话拆成了四个具体的东西。
6.2 网格量化和去振铃在管杂点
plain 就是不开任何开关的默认存法。把每个开关单开的结果和它比一下,变化量是下面这样的:
| 单开的开关 | 体积 | 杂点 | 主体 PSNR |
|---|---|---|---|
| 网格量化 | 小 2.4% | 少 14% | 低 1.72 dB |
| 过冲去振铃 | 不变 | 少 13% | 不变 |
| 扫描优化 | 小 10.5% | 不变 | 不变 |
| 量化表 3 号 | 小 7.8% | 多 3% | 低 4.08 dB |
先看杂点这一栏。网格量化和去振铃各少了一点。两个一起开降到 0.349%,接近 all 的 0.347%。扫描优化对杂点没有帮助,量化表 3 号反而让杂点微微涨了一点。
去振铃这一项最让我意外。它单开的时候体积和主体 PSNR 都和 plain 一样,杂点却少了 13%。光这一个开关就让背景干净了一些,体积和主体保真上都没有付出代价。它的名字叫过冲去振铃,我没读过实现。从名字和结果猜,它专门处理白底这类有硬边缘的图并会把边缘附近超过 255 的部分压回来。这是猜测。网格量化要付一点保真。我对它的理解是编码时多试几种取整方案,挑一个最划算的。这同样是我看文档的理解,没有验证。
6.3 量化表和扫描优化在管体积
体积这一栏的功劳在另外两个开关。单开扫描优化,体积从 164.9 降到 147.5 KB。主体 PSNR 和杂点一点没变。这又是一个不花保真的项。只是它有一个后果,放到下一节讲。量化表 3 号单开,体积降到 152.0 KB。代价是主体 PSNR 掉到 42.47 dB。
5.1 节里主体 PSNR 掉的 5.56 dB,大头就在量化表 3 号。它掉 4.08 dB,网格量化掉 1.72 dB。两个加起来差不多是总数。这也回答了第五章的疑问:mozjpeg 的体积优势里有一部分确实是拿保真换的,可扫描优化那一块没有换。
6.4 开了扫描优化会存成渐进式JPG
表的最后一列是我写这一章时才想起来加的。扫描优化会让文件存成渐进式 JPG,也就是图片先糊后清的那种加载方式。表里 scans 和 all 两行写的是「是」,其余都是「否」。
这个发现让我改了口。我原本打算把结论写成无脑选 mozjpeg,现在不敢了。渐进式 JPG 在我手上的 Pillow 里读取没有问题。可我没有拿任何一个电商后台试过它收不收,也不知道有没有哪家的上传检测会把渐进式当成异常。别人家怎么处理我不知道。我手上只有一个办法:把扫描优化单独关掉,看杂点和体积会怎样。
no_scans 这一行就是这个办法。网格量化、去振铃、量化表 3 号三个开着,扫描优化关着。存出来是普通的基线 JPG。杂点是 0.343%,比 all 的 0.347% 还略低一点点。体积是 139.7 KB,比 all 的 125.0 KB 大 12%。仍然比 sharp 默认的 164.9 KB 小 15%。想避开渐进式,代价是多出 12% 的体积。
七、mozjpeg慢多少值不值得
好东西总有代价。除了前面说的保真和渐进式,还有一项是时间。这一项没有现成的基准可以抄,我自己写了脚本来量。
7.1 mozjpeg编一张比默认慢好几倍
计时我另写了两个小脚本,没有贴在文里。用同一台机器,每张图编 9 次取中位数。只算编码到内存里的时间,不含读写文件。质量是 85,采样是 4:2:0。
| 编一张的中位数 | 毛绒 | 玻璃杯 | 白鞋 |
|---|---|---|---|
| Pillow | 4 毫秒 | 3 毫秒 | 3 毫秒 |
| sharp 默认 | 10 毫秒 | 8 毫秒 | 9 毫秒 |
| sharp mozjpeg | 86 毫秒 | 52 毫秒 | 77 毫秒 |
mozjpeg 比 sharp 默认慢 6 到 9 倍。Pillow 是在 Python 里测的,sharp 是在 Node 里测的。两边的计时环境不一样,我不拿 Pillow 对 sharp 的倍数去下结论。我只看同一个库里默认和 mozjpeg 的比较。
7.2 几十张图批量处理多花的时间感觉不到
我拿这个数算过一笔账。mozjpeg 比 sharp 默认每张多花 44 到 76 毫秒。如果一次处理一千张图,串行的话多花 44 到 76 秒。这是用上面三张图的中位数乘出来的估算,不是我真的处理了一千张。图越大这个数越大,多线程并行跑这个数会变小。
我的结论很实在。每次上架处理几十张图,这点时间我感觉不到。要是在上传接口里现转,每张图多出几十毫秒就得看并发和限时有多紧。这种情况我没有测过。
八、我最后怎么选
把前面的数放在一起,下面是我自己会怎么选。这只是我对这三张图的判断,不是通用结论。先放一张总表,后面每一节再讲理由。
| 场景 | 我的选法 | 体积 KB | 杂点 |
|---|---|---|---|
| 要小又干净 | mozjpeg 92 加 4:2:0 | 162.8 | 0.239% |
| 要最干净 | mozjpeg 95 加 4:4:4 | 216.3 | 0.045% |
| 怕渐进式 | 三个开关 85 加 4:2:0 | 139.7 | 0.343% |
| 只有 Pillow | 95 加 4:4:4 | 304.0 | 0.123% |
| 手动转几张 | 图映压缩转换 85 | 165.4 | 0.481% |
8.1 要小又干净就用sharp的mozjpeg
我会选 sharp 加 mozjpeg: true。质量取 92 左右,采样保持 4:2:0。对照 5.2 节那张表,这个组合体积 162.8 KB 和 sharp 默认 85 差不多大。杂点 0.239%,比默认少一半。主体 PSNR 45.71 dB,比默认低不到 1 dB。如果背景要更干净,质量往 95 加再加 4:4:4。体积 216.3 KB,杂点 0.045%。我量过的设置里,体积不超过 216.3 KB 的没有比它杂点更低的。
我不会选 mozjpeg 质量 85,原因在 5.1 节:同样标 85 分,它的主体 PSNR 低了 5.56 dB。它体积最小,可主体保真掉得最多。只适合对画质不挑、只想要小文件的场合。
8.2 怕渐进式JPG就关掉扫描优化
如果平台或者下游的工具对渐进式 JPG 有顾虑,就用 6.4 节的 no_scans 这组开关。网格量化、去振铃、量化表 3 号开着,扫描优化不开。杂点和 mozjpeg 全开基本一样。体积多出 12%,仍比 sharp 默认小。这一条我没拿真平台验证过,只当备选。
8.3 只能用Pillow时怎么存
项目里已经用了 Pillow、不想引入 Node 的话,我会这样改。打开 optimize=True,质量 85、4:2:0 的三张图体积能省 3% 到 16%。像素一点不变。背景要更干净就把 subsampling 设成 0,质量拉到 95。三张图平均杂点 0.123%,体积 304.0 KB。
想要 mozjpeg 那种效果,Pillow 这边我没找到办法。我在它的 JPEG 插件源码里搜了一遍。能看到 optimize、progressive、qtables、subsampling,没有网格量化和去振铃这类开关。不排除有别的路径能开,只是我没找到。
8.4 图映压缩转换适合谁
压缩转换在电脑浏览器里用:把白底 PNG 拖进去选 JPG,拉滑杆就出文件。同一个质量数下它出来的画面和 Pillow 逐像素一样,杂点等于 Pillow 的水平。体积和 sharp 默认只差零点几 KB。它输出 4:2:0。我没在界面上找到 4:4:4 的选项,也没有 mozjpeg 的开关。
它适合的情形很具体:手上只有一两张图要转,不想装 Python 或 Node。同一个质量数的结果和 Pillow 一样就够了。想把杂点再压一档要么换到 sharp 的 mozjpeg,要么在这一步之前想办法。我把它放进这篇,是因为白底主图抠完以后常常就是这么转的。它的结果和 Pillow、sharp 默认在同一个水平上,没有更脏也没有更干净。
8.5 检测纯白别只认255
最后一个提醒和编码器没关系。不管选哪一种存法,只要是 JPG 背景里就有杂点。如果你在上传前做一道背景是不是纯白的检查,判据就不能写成严格等于 255。
我拿 Pillow 质量 85、4:2:0 的三个文件试了。背景区域按三个通道都等于 255 判白,毛绒 99.71%、玻璃杯 99.50%、白鞋 99.34%。没有一张到 100%,严格判的话三张全部不及格。改成三个通道都不低于 250,三张分别是 99.9991%、99.9970%、99.9815%,几乎满分。两种判法差得很远:严格等于 255 的话三张全部不及格,放到 250 又几乎全部通过。阈值取 250 左右是我从这三张图推断的。样本太少,推荐值要打个问号。
还有一点:在整张图上判不低于 250,会把白色商品自己的亮部也算进来。拿鞋身单独数,有 2.66% 的像素三个通道都不低于 250。因此检测要在背景区域上做,不能直接套在整张图上。转 JPG 之前,我也会在背景上取几处色,确认是 255,不默认上游给的就是纯白。
九、适用边界与风险
最大的边界是样本。三张图每张只有一个版本,都是缩放后的实拍照。三张图的背景依次是蓝色针织毯、灰墙和浅米色地砖。三种背景的颜色和纹理差别不小,可也只代表三种。更大的原图、纯色背景的棚拍图、带阴影的图,我都没有测。杂点占比这个数会随图变。毛绒和白鞋差了一倍多,在我自己的数据里就能看出来。
库的版本也是边界。Pillow 11.3.0、sharp 0.34.5、libvips 8.17.3。换一个版本,mozjpeg 的默认开关和过冲去振铃的行为都可能变。图映那条线是 2026-10-23 的线上版本,它的压缩转换以后怎么改我不知道。
主体保真我只用了 PSNR。这是个数学上的距离,不等于肉眼感受。我没有做盲对比,也没有量 SSIM 之类的指标。因此 mozjpeg 对齐保真后仍然更好这句话,只在 PSNR 这把尺子上成立。平台这一块我只引了亚马逊美国站那一页,日期是 2026-10-23。国内平台的规则没查到。渐进式 JPG 会不会被卡、纯白判据取多少,我都没有拿平台验证过。
十、测量方法的坑
头一个坑是背景的定义。我用的是抠图蒙版里 alpha 等于 0 的位置。这些位置在存成 JPG 之前全是 255,起点是干净的。如果拿看起来是白的这种阈值去圈背景,起点就不干净。杂点的统计会混进去原本就不是纯白的像素。我的三张图刚好满足这个条件,换别的图要先自己核对。
再一个坑是整张图和背景区域的差别。拿整张图算纯白像素占比,白色商品本身的亮部会混进去。8.5 节里引的 2.66% 就是例子。因此所有杂点的数字都只在蒙版背景上算。
第三个坑是解码器。我统一用 Pillow 解码。别的解码器对 4:2:0 的色度做上采样的方式可能不同,同一个文件解出来的像素差一两个灰阶是有可能的。浏览器里看到的数和取色器读到的数也可能和我的统计略有出入,我没有去比较。
第四个坑是三通道和灰度的判法。我用的是三个通道都等于 255 才算白。如果把图转成灰度再比较,背景里等于 255 的比例会比三通道高 0.1 到 0.3 个百分点。白鞋是 99.66% 对 99.34%,两个口径别混着比。
第五个坑是质量数不跨库。同一个 85,Pillow 和 mozjpeg 对应的主体保真差 5.56 dB。图映的滑杆 100 对应 Pillow 的 99。只拿质量数去比较,很容易得出 A 比 B 小一半这种看着很强、实际不公平的结论。
第六个坑是计时。我只算编码到内存里的时间。Pillow 和 sharp 分别在两个运行环境里,不能直接比。每张图只编 9 次取中位数,没有排除系统偶尔抖一下的影响。第七个坑是单位。体积全部按 1 KB 等于 1024 字节算。拿系统文件管理器里看到的数来对,可能因为进位方式不同差一点点。
十一、还没解决的
最想补的是肉眼对比。主体 PSNR 掉了 5.56 dB 的那一档,我只在数字上看。没有把两张图放大了并排看一眼,心里总有点不踏实。下次我会挑边缘最复杂的白鞋,放大到 400% 看鞋带和鞋底线附近有没有明显的涂抹。
再一件是平台的反应。渐进式 JPG 会不会被卡、纯白判据在国内平台上取多少,我这次一家都没验证。只能等有了账号和测试商品再试。还有图映滑杆 100 对应 Pillow 99 的原因。我只确认了像素一样,没追原因,也不知道这是有意的设计还是顺手收的上限。它的压缩转换里有没有 4:4:4 的入口,这次也没找到。可能藏在我没点开的地方。
最后一件是更大的样本。三张图让我看到了趋势,可距离我敢拿它写进团队规范还差得很远。手上有一批真实白底商品图的话,用文中的 ruler.py 照着量一遍就比看我的数字更靠谱。
参考资料
- 亚马逊卖家平台帮助页「Product image guide」(美国站,2026-10-23 查看)
- 样片:维基共享资源「Teddy Bear - 5.jpg」,作者 Dan Bollinger,CC BY-SA 4.0
- 样片:维基共享资源「Bohemian Glass 01.jpg」,作者 Kritzolina,CC BY-SA 4.0
- 样片:维基共享资源「A pair of white shoes.jpg」,作者 JustinLiang522,CC BY-SA 4.0
- sharp 文档:输出选项里 jpeg 的说明,包含 mozjpeg、trellisQuantisation、overshootDeringing、optimiseScans、quantisationTable
- Pillow 文档:JPEG 保存参数 quality、subsampling、optimize
- 本文实测数据:2026-10-23 本机运行记录,环境见版本声明