JPG压缩:删掉你看不见的细节

DCT原理 → 概念展开
JPG的核心技术叫作离散余弦变换(DCT)。它先把一张图切成很多个8×8的小方块,然后对每个方块做二维DCT变换。完整的公式长这样:
\(F(u,v) = \frac{1}{4} C(u)C(v) \sum_{x=0}^{7} \sum_{y=0}^{7} f(x,y) \cos\frac{(2x+1)u\pi}{16} \cos\frac{(2y+1)v\pi}{16}\)
看起来有点复杂,但它的核心思想可以用一个简单的例子讲清楚。我们用一维DCT来理解,把两个相邻像素的亮度值看作一个N=2的序列:
输入 f = [100, 200]。一维DCT公式简化为:
$$F(u) = C(u) \sum_{x=0}^{1} f(x) \cos\left[\frac{\pi u (2x+1)}{4}\right]$$
其中 u=0 时$$C(0)=\sqrt(1/2)≈0.7071$$ ,u=1 时 C(1)=1。
先算F(0)——直流系数(DC),代表「平均亮度」:
$$F(0) = 0.7071 \times (100\cos 0 + 200\cos 0) = 0.7071 \times 300 \approx 212$$
再算F(1)——交流系数(AC),代表「明暗差异」:
$$F(1) = 1 \times \left[100\cos\frac{\pi}{4} + 200\cos\frac{3\pi}{4}\right]$$
查表:$$\cos\frac{\pi}{4} = \frac{\sqrt{2}}{2} \approx 0.7071, \quad \cos\frac{3\pi}{4} = -\frac{\sqrt{2}}{2} \approx -0.7071$$
$$F(1) = 100 \times 0.7071 + 200 \times (-0.7071) = 70.71 – 141.42 \approx -71$$
所以两个像素 [100, 200] 经过DCT后变成了两个系数 [212, -71]。其中212代表整体的亮度水平(平均亮度×√2),-71代表两个像素之间的差异——这就是把「空间信息」转换成了「频率信息」。
量化与有损 → 对比深化
接下来是量化的步骤。人的眼睛对低频(大面积明暗)很敏感,但对高频(细微差异)很迟钝。所以JPG把高频系数除以一个较大的数字再取整,许多高频系数就这样变成了零。
在我们的例子里,假设我们把F(1)设为0(丢弃差异信息),只保留F(0)=212。然后做逆变换看看结果:
$$f'(0) = 0.7071 \times 212 \times \cos 0 + 1 \times 0 \times \cos\frac{\pi}{4} = 150$$ $$f'(1) = 0.7071 \times 212 \times \cos 0 + 1 \times 0 \times \cos\frac{3\pi}{4} = 150$$
原来 [100, 200] 两个差异明显的数值,量化后逆变换回来变成了 [150, 150]——两个值都变成了平均值。这就是「有损压缩」的数学本质:丢弃高频分量来省空间,代价是两个原本不同的像素变得一模一样了。
扩展到真实场景:8×8的方块经过2D DCT后得到64个系数,左上角的F(0,0)叫DC系数(平均亮度),越往右下角频率越高。JPEG对高频系数(右下角)用大的量化数值去除,让它们变成零;对低频系数(左上角)用小的量化值,尽量保留。量化程度越高,零越多,文件越小,画质损失也越大——这就是为什么反复保存JPG会越来越糊。
霍夫曼编码 → 例证展开
量化之后,剩下的非零系数经过Z字形扫描重新排列,再交给霍夫曼编码进一步压缩。霍夫曼编码的核心思想是:给出现频率高的符号分配短编码,给频率低的分配长编码。
举一个简单的例子。假设某张图量化后的系数中,「0」出现了50次,「1」出现了20次,「2」出现了10次,「3」出现了5次,「4」出现了3次。霍夫曼编码会这样建树排序:
| 符号 | 出现次数 | 编码 | 位数 |
|---|---|---|---|
| 0 | 50次 | 0 | 1位 |
| 1 | 20次 | 10 | 2位 |
| 2 | 10次 | 110 | 3位 |
| 3 | 5次 | 1110 | 4位 |
| 4 | 3次 | 1111 | 4位 |
总共88个符号,等长编码(3位)需要264位,霍夫曼编码只需要50×1+20×2+10×3+5×4+3×4=182位,省了31%。
PNG压缩:不丢一个像素的数学魔术
LZ77算法 → 概念展开
PNG走的是完全不同的路:无损压缩。它的核心技术叫DEFLATE算法,由两个阶段组成。第一阶段:LZ77算法——「找重复」。用一个滑动窗口扫描数据,发现当前数据在过去32KB内已经出现过时,就记录一个指针(距离,长度),而不是重复存储相同的字节。
输入字符串:abcabcabc(9个字符)。逐位扫描过程如下:
| 位置 | 当前字符 | 是否出现过? | 输出 |
|---|---|---|---|
| 0 | a | 未出现 → 输出原文 | a |
| 1 | b | 未出现 → 输出原文 | b |
| 2 | c | 未出现 → 输出原文 | c |
| 3 | a | 在位置0出现过。找最长匹配:abc在位置0-2出现过 ✓ | (3, 3) |
| 6 | a | 在位置0出现过。找最长匹配:abc在位置0-2出现过 ✓ | (6, 3) |
结果:a b c (3,3) (6,3),从9个字符压缩到5个token。解压时,(3,3)的意思是「回到前面第3个位置,复制3个字符过来」,完全可以还原成「abcabcabc」。这就是无损压缩的精髓——不删任何信息,只换一种更紧凑的存储方式。
预过滤 → 概念界定
第二阶段是霍夫曼编码,对LZ77输出的字符和指针再做一次编码压缩,原理和JPG中的霍夫曼编码相同。不过图片的像素数据通常不是整齐排列的重复序列。为了提高LZ77找重复的效率,PNG在压缩之前还做了预过滤。
它把原始像素值变成与相邻像素的差值:如果相邻像素颜色相近,差值趋近于零,数据中出现大量重复的零,LZ77的压缩效率一下子就上来了。这就是为什么PNG对文字截图和纯色LOGO压缩效果极好(相邻像素几乎一样,差值全是零),而对复杂的自然照片效果远不如JPG。
三种格式的本质对比
格式对比 → 归纳升华
JPG之所以超小,是因为它通过DCT把人眼不敏感的高频信息直接丢弃——这是数学层面的「取舍策略」。PNG之所以大,是因为它通过DEFLATE不丢任何信息地压缩——这是数学层面的「无损压缩天花板」。截图存PNG更清晰,是因为文字和图标的锐利边缘在JPG的DCT量化中会被模糊,而PNG能精确保留每一个像素的位置和颜色。拍照必须用JPG,则是因为自然照片几乎没有完美的重复模式,PNG的压缩率远不如JPG——在不牺牲视觉质量的前提下,JPG可以做到十倍以上的压缩比。
至于RAW格式,它连「代码」都不是——它是相机传感器捕捉到的原始电信号数据,未经任何数学变换和色彩处理,相当于食材而非菜肴。它的体积最大,但后期调整的空间也最大。
数学方案 → 号召收束
每种格式都是一套数学方案:JPG选择「视觉上差不多就行」,PNG坚持「一个像素都不能少」,而RAW说「我还没开始做菜」。理解了它们背后的数学原理,你就再也不会存错图、用错格式了。


