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次。霍夫曼编码会这样建树排序:

符号出现次数编码位数
050次01位
120次102位
210次1103位
35次11104位
43次11114位

总共88个符号,等长编码(3位)需要264位,霍夫曼编码只需要50×1+20×2+10×3+5×4+3×4=182位,省了31%。

PNG压缩:不丢一个像素的数学魔术

LZ77算法 → 概念展开

PNG走的是完全不同的路:无损压缩。它的核心技术叫DEFLATE算法,由两个阶段组成。第一阶段:LZ77算法——「找重复」。用一个滑动窗口扫描数据,发现当前数据在过去32KB内已经出现过时,就记录一个指针(距离,长度),而不是重复存储相同的字节。

输入字符串:abcabcabc(9个字符)。逐位扫描过程如下:

位置当前字符是否出现过?输出
0a未出现 → 输出原文a
1b未出现 → 输出原文b
2c未出现 → 输出原文c
3a在位置0出现过。找最长匹配:abc在位置0-2出现过 ✓(3, 3)
6a在位置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说「我还没开始做菜」。理解了它们背后的数学原理,你就再也不会存错图、用错格式了。

常见问题(FAQ)

JPG和PNG的压缩方式有什么本质区别?
JPG使用有损压缩,核心是离散余弦变换(DCT):把图像从空间域转换到频率域,然后丢弃人眼不敏感的高频分量,文件极小但有画质损失。PNG使用无损压缩,核心是DEFLATE算法(LZ77+霍夫曼编码):寻找数据中的重复模式并替换为指针,不丢任何信息,但文件较大。
什么是DCT中的DC系数和AC系数?
DC系数(直流系数)F(0,0)代表8×8方块的平均亮度,是整块图像最基础的亮度信息。AC系数(交流系数)代表不同频率的明暗变化细节,频率越高对应的AC系数越靠向右下角,越容易被量化丢弃。
为什么反复保存JPG会越来越糊?
每次保存JPG都会重新做一次DCT变换和量化。量化会丢弃高频细节(边缘、纹理等),保存次数越多,被丢弃的细节越多。这就是你看到越存越糊的原因——它不是在叠加压缩,而是在一次次丢弃信息。
PNG为什么对文字截图压缩效果好,对照片效果差?
PNG的预过滤会把像素值变成与相邻像素的差值。文字截图有大量纯色区域(相邻像素一样,差值=0),大量零让DEFLATE的LZ77能高效找重复。而自然照片相邻像素差异大,差值不趋近零,重复模式少,压缩率有限。
0 讨论
热门最新
总结
暂无总结
0 / 600

你的照片其实是一道数学题——图片压缩背后的数学原理

段, 誉