我最初的压缩规则很简单:选择仍能通过 SSIMULACRA2 target 60 的最低 AVIF quality。对于较大的图片集合,我允许一个代表性 sample 最低到 58,其余 sample 则必须保持在 60 或以上。
对于 PNG 和正常的 source image,这个规则让我很满意。后来我开始处理一类不同的输入:这些 WebP 在到达我手中之前,就已经从更高质量的 original 做过一次有损压缩。
更高质量的 original: ~2 MB
↓
lossy WebP: ~100 KB
↓
AVIF
第二次转换是否仍然可以只要求相对于 WebP 达到 60?我一开始认为可以。60 还是 60。但真正变化的不是 score,而是 reference image。
Metric 没有错,变化的是 reference
SSIMULACRA2 会比较 reference image 与 distorted image,并对这两个具体输入之间的感知差异打分。它会对 blur、ringing、额外出现的 edge 等压缩损伤作出反应,公开的评估材料也包含 JPEG、WebP、AVIF 等 codec 的 distortion。SSIMULACRA2](https://github.com/cloudinary/ssimulacra2/blob/main/README.md%22>SSIMULACRA2) 文档介绍了这一 metric 以及大致的 quality anchor。
如果我直接从高质量 source 编码 AVIF,比较过程本质上是 source → AVIF。此时 60 分描述的是这一次转换新引入的损失。
但如果 WebP 本身已经是 lossy,真实历史则是:
original
↓ first lossy encode
WebP
↓ second lossy encode
AVIF
SSIMULACRA2 实际只看到 WebP → AVIF。它完全不知道 WebP 之前存在过什么 original。WebP 中已经存在的 artifact 会直接成为 reference 的一部分。
因此 60 分可以告诉我 AVIF 没有离当前 WebP 太远,却不能告诉我最终 AVIF 与已经丢失的 master 相差多少。
有损 transcoding 会产生第二个 quality budget
假设 original 中有一个非常平滑的 gradient。第一次编码加入了一点 banding,但 WebP 看起来仍然可以接受。之后我再把这个 WebP 编成 AVIF。SSIMULACRA2 可以惩罚 AVIF encoder 新增的 degradation,却无法惩罚已经存在于 reference 中的损伤。
这就是为什么从已经 lossy 的图片继续编码,与直接从最佳 source 编码并不等价。libavif](https://github.com/AOMediaCodec/libavif/discussions/2640%22>libavif) 项目的一则讨论也说明了同样的普遍问题:如果输入本身已经压缩,原有的 compression artifact 可能会继续进入新的 AVIF。
这并不意味着 AVIF 一定会放大所有 WebP artifact,也不意味着永远不应该做 transcoding。它只意味着第二个 encoder 开始工作时,original 的 quality budget 已经消耗了一部分。
我最终采用的 policy
- Canonical 或高质量 source:target 60,单个 sample 的 floor 58。
- Lossless WebP:target 60,floor 58。
- 已知 lossy derivative:target 65,floor 63。
关键区别不是 JPEG 对 WebP,而是canonical source 对已知 lossy derivative。
WebP 可以是 lossless。WebP](https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification%22>WebP) lossless specification描述了一种可以精确恢复 pixel value 的模式,因此不存在前一代 lossy loss。反过来,如果我知道某个 JPEG 已经经历过多次有损转换,我也会像对待此前压缩过的 WebP 一样谨慎。
为什么是 65?
SSIMULACRA2 并没有“第二代有损压缩必须增加 5 分”这样的规则。我找不到这样的规则,因为它本来就不存在。65 是 engineering policy,不是 metric 自身的属性。
公开的 quality anchor 可以提供一些背景。大致来说,50 对应 medium 或 fair quality,70 对应 high 或 good quality。因此 60 仍属于比较激进的 web compression 区间,而不是 visually lossless 区间。
直接从良好 source 转换时,我愿意用这样的 perceptual budget 换取更小的文件。对于第二代 lossy encode,我希望允许的额外 distortion 更小。
我也考虑过 70,但这会把所有 transcoded image 推到明显更严格的质量区间。页面一次加载大量图片时,尤其在 mobile connection 上,多出来的 byte 很重要。我没有证据证明所有已压缩图片都强制到 70 能抵消相应成本,所以最终选择 65 作为保守的中间值。
为什么是 65/63,而不是 65/62?
我原来的 policy 是 60/58:允许一个 representative outlier 比 main target 低 2 分。把 main target 提高到 65 后,如果保持同样的 policy,自然就是 65/63。
60 - 58 = 2
65 - 63 = 2
如果用 62,例外范围就会变成 3 分。普通 sample 变得更严格,而最差的那一个 sample 反而获得更大的容差。我找不到技术理由让已经经历过 lossy compression 的输入享受更宽的例外。
63 和 65 都不是神奇数字。真正有价值的是 policy 的内部一致性。
2 MB 变成 100 KB 并不能说明视觉质量
我刻意不根据 compression ratio 推导 SSIMULACRA2 threshold。2 MB 变成 100 KB 看起来非常夸张,但单纯的 file size 对 perceptual degradation 的说明能力其实很弱。
Resolution、image entropy、noise、纯色区域、line art、chroma subsampling 以及之前的 format,都会显著影响 compression efficiency。Google](https://developers.google.com/speed/webp/docs/webp_study%22>Google) 的 WebP compression study是在近似相同的质量水平上比较 codec,而不是假设文件大小相同就代表视觉质量相同。
所以我不会使用 缩小 20× → target 65 这种规则。对我的 policy 而言,关键是当前文件是否是已知 lossy derivative,而不是 size reduction 看起来多么惊人。
如果 original 还在,我不会从 WebP 继续 transcode
如果高质量 original 和较小的 lossy WebP 都存在,我会直接从 original 编码 AVIF,并继续使用普通的 60/58 policy。
preferred:
original → AVIF
avoid when possible:
original → lossy WebP → AVIF
第二代使用更严格 target 并不能恢复第一次 encode 中已经丢失的信息。65 只会让 AVIF 更接近 WebP;70 会更接近。它们都无法重建已经丢失的 original。
实现 bug 比 62 和 63 的争论更重要
在检查这套 policy 时,我发现 encoder logic 中有一个更危险的问题。Code 已经有 format-specific target constant,也有能够为 WebP 返回不同 target 的 helper,因此把 WebP 从 60 改成 65 看起来很简单。
实际上并不是。Adaptive quality decision 仍然使用 global target 和 global worst-score threshold。Format-specific target 后来用于把 individual result 标记成 pass 或 below-target,却不一定真正控制 final AVIF quality 的选择。
这会产生一个很隐蔽的 failure:WebP sample 可以被正确标记成低于 target 65,但 adaptive search 依然接受同一个 quality,因为 global pass condition 仍然是 60。
intended WebP target: 65
actual score: 61.2
format-aware label: below target
global search rule: pass if target is still 60
如果 threshold 不参与真正选择 encoded output 的决策,它就没有实际意义。
更安全的做法是把 threshold 作为 sample policy 的一部分
现在我更愿意把 threshold 当成 source 的 property,而不是装饰性的 format constant。简化后的 pseudocode 如下:
if sample is a known lossy derivative:
target = 65
floor = 63
else:
target = 60
floor = 58
reject if any sample is below its floor
allow at most one sample below its target
关键在于,用来描述 result 的 threshold,也必须真正控制这个 result 是否被 accept。
Sampling policy 同样重要
我不需要在每个 candidate AVIF quality 下测试所有图片。Pipeline 会沿着 bytes-per-pixel 分布选择最多 10 个具有代表性的 JPEG、PNG 或 WebP sample。
对于 10 张或更少的 collection,每个 sample 都必须达到 normal target。对于更大的 collection,一个 sample 可以使用较低 floor,但其他 sample 仍必须达到 main target。
Sampling 让搜索变得可行,但这也是不应把 outlier rule 放得过宽的原因。被选中的 sample 是代表,并不能证明所有未 sampling 的图片都会以完全相同的方式表现。
可以取代 heuristic 的实验
最可靠的答案,是保留一组具有代表性的 true original,然后测试完整链路:
A: original → AVIF, target 60
B: original → lossy WebP → AVIF, target 60
C: original → lossy WebP → AVIF, target 63
D: original → lossy WebP → AVIF, target 65
E: original → lossy WebP → AVIF, target 70
对每个 variant,我会记录 final byte size、相对于 true original 的 SSIMULACRA2、相对于 WebP intermediate 的 SSIMULACRA2,以及最终选择的 encoder quality。难处理的图片还应手动查看。
我还没有在足够有代表性的 preserved-original 数据集上完成这个 controlled experiment,因此不能声称 65 在所有情况下都是 global optimum。这一点限制很重要。
AVIF 并不自动意味着值得再 encode 一次
如果唯一剩下的 source 是 100 KB WebP,而通过 65/63 的 AVIF 仍有 96 KB,我会质疑这次转换的价值。为了省 4 KB 再增加一代 lossy encode 和额外 processing complexity,未必值得。
但如果同一个 100 KB WebP 在仍通过 quality policy 的情况下变成 65 KB AVIF,那么对于图片很多的页面,这个 trade-off 就有吸引力得多。
Codec conversion 应该回答两个不同的问题:额外 distortion 是否可接受?size reduction 是否足够大?满足第一个条件,并不自动意味着第二个条件也成立。
我现在使用的规则
如果我有质量最好的 original,就直接从它 encode,并在这种 web workload 中使用 60/58。如果 WebP 是 lossless,也使用 60/58。如果唯一剩下的文件是 known lossy derivative,就使用更严格的第二代 budget,目前是 65/63。如果 AVIF 几乎没有减小 size,我会考虑保留现有 WebP。
更深层的结论并不是 WebP 需要某个特殊的 SSIMULACRA2 数字。Full-reference quality metric 只能回答其 reference image 所代表的问题。
如果 reference 已经丢失信息,高 score 的含义只是“接近这个 reference”,而不是“接近它之前存在过的图片”。当我开始把 image provenance 作为 compression policy 的一部分时,这些 threshold 不再像任意的 codec 设置,而是变成了不同 generation of loss 的质量预算。