同样一张图片,按不同顺序排列压缩数据,会得到总长度相近但「读取体验」不同的文件。基线编码把像素按扫描顺序依次压缩,解码器必须读到接近结尾才能画出完整画面;渐进编码则把信息按重要程度重新排序,让解码器读到文件前部时就能给出可辨认的近似结果。对图片浏览器而言,这直接决定了加载过程如何呈现。
两种组织的差别不在压缩算法本身,而在扫描顺序与传输单元的划分。下面分别看 JPEG 与 PNG 的做法。
基线编码的数据组织
JPEG 基线编码把图像划分为 8×8 的块,对每个块做 DCT、量化、熵编码,然后按从左到右、从上到下的顺序输出。解码器需要同时处理的是当前 MCU 行,配合重启标记,可以在不保留整幅系数的情况下边解边输出。
PNG 属于无损格式,没有 DCT 与量化,压缩依赖逐行滤波与 zlib 的 LZ77 加霍夫曼编码。逐行排列的 PNG 按行顺序写入 IDAT,解码器解出一行即可交给显示层。
渐进式 JPEG 的扫描组织
渐进式 JPEG 放弃了「一次把所有系数发完」的做法,改为把码流切成多个扫描(scan),按重要程度分批发送。它使用两个正交的维度来组织扫描:
- 谱选择(spectral selection):把 64 个 DCT 系数分成若干频带,按频带分批发送。低频系数承载主要的结构信息,先发;高频系数承载细节,后发。
- 逐次逼近(successive approximation):同一频带分多次发送,每轮只发送若干位平面。首轮发送若干高位,后续扫描补充低位,使系数逐步精确。
一个扫描由若干参数描述:作用在哪些分量上、覆盖哪个谱段、本轮发送哪一段位精度。下面的片段以 JSON 形式示意这类参数的组织:
{
"scans": [
{ "components": ["Y"], "spectral_start": 0, "spectral_end": 0, "high_bit": 1, "low_bit": 0 },
{ "components": ["Cb"], "spectral_start": 0, "spectral_end": 63, "high_bit": 2, "low_bit": 1 },
{ "components": ["Y"], "spectral_start": 1, "spectral_end": 5, "high_bit": 2, "low_bit": 1 }
]
}
编码器会把低频与高位精度的扫描排在前面。解码器每读完一个扫描,就可以用已获得的系数重建一幅近似图像,因此前几个扫描结束时画面轮廓已经可辨认,随后的扫描只是逐轮细化。整个流程重复执行「系数 → 逆变换 → 上采样 → 输出」,其中上采样所用的插值方式可参见图像缩放插值算法。
交错 PNG 的 Adam7 扫描
PNG 的交错方式称为 Adam7,把图像按 8×8 的像素格划分为 7 遍扫描,每一遍覆盖格子中特定位置的像素:
| 遍 | 起始 x | 起始 y | 水平步长 | 垂直步长 |
|---|---|---|---|---|
| 1 | 0 | 0 | 8 | 8 |
| 2 | 4 | 0 | 8 | 8 |
| 3 | 0 | 4 | 4 | 8 |
| 4 | 2 | 0 | 4 | 4 |
| 5 | 0 | 2 | 2 | 4 |
| 6 | 1 | 0 | 2 | 2 |
| 7 | 0 | 1 | 1 | 2 |
前几遍只包含稀疏的像素,读完后即可给出整幅图的粗略预览;第 7 遍补齐剩余像素后图像完整。遍历位置可以用一段循环生成:
passes = [(0, 0, 8, 8), (4, 0, 8, 8), (0, 4, 4, 8), (2, 0, 4, 4),
(0, 2, 2, 4), (1, 0, 2, 2), (0, 1, 1, 2)]
def iter_pass(width, height, x0, y0, dx, dy):
for y in range(y0, height, dy):
for x in range(x0, width, dx):
yield y * width + x # 该遍扫描包含的像素索引
每一遍都要单独做行滤波与 zlib 压缩。由于同一条扫描行内的相邻像素在物理上并不相邻,滤波的预测效果下降,压缩率通常低于逐行版本;多次压缩流也带来额外开销,因此交错 PNG 的总字节数一般大于同内容的逐行文件。
可辨认画面出现时间与总字节数
渐进编码的核心取舍可以概括为一张表:
| 维度 | 基线 / 逐行 | 渐进 / 交错 |
|---|---|---|
| 可辨认画面初次出现的位置 | 接近文件末尾 | 文件前部即可 |
| 总字节数 | 相对较小 | 通常略大 |
| 达到完整质量所需数据 | 全部 | 全部 |
| 传输中断时的可用性 | 几乎不可用 | 可保留中间质量 |
渐进编码并不减少总数据量,它改变的是数据到达顺序。在带宽受限、或传输可能中断的场景下,可辨认内容出现得更早;在文件完整到达后才显示的场景下,到达顺序不影响显示结果,反而要为略大的体积付出代价。
解码复杂度与内存占用
- 渐进式 JPEG:多个扫描作用于同一组系数,解码器必须保留整幅图像的系数缓冲直到全部扫描完成,内存占用高于基线;若扫描数较多,重建近似图像也需要重复执行逆变换。
- 交错 PNG:像素按 7 遍散布到全图,解码器需要一块完整的图像缓冲区,且必须缓存尚未凑齐整行的样本,无法像逐行方式那样只保留少量行。
- 基线 JPEG:配合重启标记可以按 MCU 行流水处理,行缓冲的数量取决于实现,内存占用与图像宽度相关而非与整幅面积相关。
在内存有限的设备上,这些差异决定了能否直接解码某些文件,或必须改用分块解码。