为什么 JPEG不用傅里叶
你相册里那几万张照片,压缩用的是「离散余弦变换」,不是傅里叶变换。这个选择不是随便做的,也不是因为余弦更简单——它来自一个很容易被忽略的细节:傅里叶变换会把一块图像的左边和右边接起来。这一章拿 JPEG 标准里那块著名的方块从头算一遍。
JPEG 把图片切成 8 × 8 的小块,对每一块做一次变换,再把高频系数粗暴地量化掉。用的是离散余弦变换(DCT),不是这本书前十七章讲的傅里叶变换(DFT)。
问:为什么?
先把那块方块算完
这是 JPEG 教科书里被引用了几万次的那块 8 × 8 亮度数据(已经减去 128 做过电平搬移):
−76 −73 −67 −62 −58 −67 −64 −55 −65 −69 −73 −38 −19 −43 −59 −56 −66 −69 −60 −15 16 −24 −62 −55 −65 −70 −57 −6 26 −22 −58 −59 −61 −67 −60 −24 −2 −40 −60 −58 −49 −63 −68 −58 −51 −60 −70 −53 −43 −57 −64 −69 −73 −67 −63 −45 −41 −49 −59 −60 −63 −52 −50 −34
做一次 8 × 8 的 DCT,第一行系数:
−415.4 −30.2 −61.2 27.2 56.1 −20.1 −2.4 0.5 左上角那个直流系数精确值: −415.375 (教科书上通常印成 −415.38 —— 那是四舍五入,本机与 numpy 双向核对过)
然后用 JPEG 标准的亮度量化表逐点相除、四舍五入:
量化后第一行: −26 −3 −6 2 2 −1 0 0
整块 64 个系数里,量化后非零的: ⟨20 / 64⟩
也就是说 **44 个系数被直接归零了**。
再反量化、逆变换回来,和原块比:
最大误差 14.6
均方根误差 5.98
PSNR 32.6 dB
64 个数变成 20 个非零的(其中大多数还是很小的整数,熵编码之后更省),而画质损失在一般观看条件下几乎看不出来。这就是 JPEG 的全部主意。
那么,为什么不用 DFT
关键在这本书从第 3 章起就反复出现的那句话:傅里叶变换默认你的数据是周期的。
它眼里的这一行 8 个像素,不是一段孤立的数据,而是一段无限重复的图案——第 7 个像素后面接的是第 0 个像素。于是:
这一块的第一行: −76 −73 −67 −62 −58 −67 −64 −55
DFT 眼里的样子(首尾相接,无限重复):
… −64 −55 │ −76 −73 −67 −62 −58 −67 −64 −55 │ −76 −73 …
↑ ↑
接缝:从 −55 突然跳到 −76,⟨跳了 21⟩
那道跳变本来不存在,是「首尾相接」这个假设造出来的。
而一道跳变意味着**所有高频系数都不为零**(第 12 章)。
DCT 的解法漂亮而朴素:接之前先照镜子。
DCT 眼里的样子(先做镜像延拓,再周期重复):
… −73 −76 │ −76 −73 −67 −62 −58 −67 −64 −55 │ −55 −64 …
↑ ↑
接缝:−76 接 −76,⟨跳变 0⟩ −55 接 −55,跳变 0
镜像之后,两端天然接得上,**一道人造跳变都没有**。
DCT 就是「先把数据镜像延拓一倍,再做傅里叶变换」的等价简写。镜像让延拓后的信号变成偶对称的,而偶对称信号的傅里叶变换只有余弦项(正弦项全部抵消)——所以结果是实数,也所以它叫余弦变换。
选项 A 说的「结果是实数省一半存储」因此是对的,但它是结果不是原因。真正的原因是镜像消掉了接缝的跳变,而实数只是偶对称的副产品。
这个差别值多少钱
本机拿两种块各量了一遍「只保留最大的 k 个系数,重建误差多少」。为了公平,DFT 那一侧按「一共存几个实数」计数(复系数连同它的共轭伙伴一起留,算两个实数):
| 块的类型 | 保留系数 | DCT 误差 | DFT 误差 |
|---|---|---|---|
| 上面那块(有纹理) | 4 | 27.00% | 28.11% |
| 8 | 16.94% | 19.16% | |
| 16 | 8.32% | 8.93% | |
| 一块平滑的渐变 | 3 | 10.733% | 60.522% |
| 4 | 6.286% | 60.522% | |
| 8 | 0.409% | 39.323% |
在有纹理的块上,两者差不多(27.00% vs 28.11%)——因为这种块本身就有很多高频,接缝多出来的那点跳变淹没在里面了。
在平滑的块上,差距是灾难性的:保留 8 个系数,DCT 的误差 0.409%,DFT 是 39.323%,差了将近一百倍。因为这种块本来一个高频都不该有,全部高频系数都是接缝那道假跳变造出来的。
而真实照片里,绝大多数 8 × 8 的块是平滑的。天空、皮肤、墙面、失焦的背景——占据画面面积的绝大部分。DCT 赢就赢在最常见的那种情况上。
顺手看懂那张量化表
JPEG 的亮度量化表左上角是 16,右下角是 99。系数除以这个数再取整,所以除数越大,那个系数被归零的可能性越大。
16 11 10 16 24 40 51 61 ← 左上:低频,除数小,保留得多 12 12 14 19 26 58 60 55 14 13 16 24 40 57 69 56 14 17 22 29 51 87 80 62 18 22 37 56 68 109 103 77 24 35 55 64 81 104 113 92 49 64 78 87 103 121 120 101 72 92 95 98 112 100 103 99 ← 右下:高频,除数大,基本全归零
这张表不是数学算出来的,是拿人眼做实验测出来的——人眼对低频的亮度变化极其敏感,对高频细节的敏感度低得多。JPEG 丢掉的不是「小的数」,是「你看不见的数」。
「质量 50%」这类滑块,做的事就是给整张表乘一个系数。乘得越大,归零的系数越多,文件越小,方块效应越明显。
「JPEG 的方块效应是算法太糙」——不对,它是分块这个决定的直接后果,和 DCT 没关系。
每一块独立量化,相邻两块的误差互不相干,于是块与块的边界上就出现了台阶。为什么非要分块?两个原因:一是成本(8 × 8 的变换极便宜,还能并行);二是局部性(照片不是平稳的,整张图做一次变换会让一处的细节影响到全图)。
后来的标准一直在修这个:H.264 之后的视频编码都带去块滤波器(在解码后专门抹平块边界);JPEG 2000 干脆换成第 12 章的小波,没有块也就没有方块效应;而 HEVC / AV1 用的是可变大小的块(4×4 到 64×64),平滑区域用大块、细节区域用小块。
但 DCT 本身一直没被换掉。2020 年代的 AV1 里仍然是 DCT(外加 DST 和恒等变换供选择)。这个 1974 年提出的变换,撑了半个世纪。
把那块方块从头算完,二十行:
import numpy as np
blk = np.array([
[-76,-73,-67,-62,-58,-67,-64,-55],[-65,-69,-73,-38,-19,-43,-59,-56],
[-66,-69,-60,-15, 16,-24,-62,-55],[-65,-70,-57, -6, 26,-22,-58,-59],
[-61,-67,-60,-24, -2,-40,-60,-58],[-49,-63,-68,-58,-51,-60,-70,-53],
[-43,-57,-64,-69,-73,-67,-63,-45],[-41,-49,-59,-60,-63,-52,-50,-34]], float)
u = np.arange(8)
C = np.cos((2*u[:, None] + 1) * u[None, :] * np.pi / 16)
a = np.where(u == 0, 1/np.sqrt(2), 1.0)
D = 0.25 * np.outer(a, a) * (C.T @ blk @ C) # 二维 DCT-II
print('DC =', D[0, 0]) # -415.3749999999999
Q = np.array([
[16,11,10,16,24,40,51,61],[12,12,14,19,26,58,60,55],
[14,13,16,24,40,57,69,56],[14,17,22,29,51,87,80,62],
[18,22,37,56,68,109,103,77],[24,35,55,64,81,104,113,92],
[49,64,78,87,103,121,120,101],[72,92,95,98,112,100,103,99]], float)
q = np.round(D / Q)
print('量化后第一行:', q[0].astype(int)) # [-26 -3 -6 2 2 -1 0 0]
print('非零系数:', int(np.count_nonzero(q)), '/ 64') # 20 / 64
back = 0.25 * (C @ (np.outer(a, a) * q * Q) @ C.T)
rmse = np.sqrt(((back - blk)**2).mean())
print('RMSE %.2f PSNR %.1f dB' % (rmse, 20*np.log10(255/rmse))) # 5.98 32.6
把 Q 乘 2(相当于质量调低)再跑一遍:非零系数会掉到十个以内,PSNR 掉几个 dB。这就是那个质量滑块在干的事。
接缝的实验更简单,两行就能看到:
row = blk[0]
print('DFT 眼里的跳变:', abs(row[0] - row[-1])) # 21.0
print('DCT 眼里的跳变:', abs(np.r_[row, row[::-1]][0] - np.r_[row, row[::-1]][-1])) # 0.0
python3 -c "import numpy as np;print(abs(-76 - (-55)))"
在线跑:Google Colab。想看真实 JPEG 的内部:djpeg -verbose(libjpeg 自带)能打印量化表;在线工具里 jpegdump 一类的能把每一块的系数导出来。
- 你相册里的每一张照片。1992 年定标准,到今天仍是网络上最主流的图像格式。三十多年,这块 8 × 8 的方块被算了大概几万亿次。
- 你看的每一个视频。H.264、H.265/HEVC、AV1、VP9——全部以 DCT(或它的整数近似)为核心变换。视频还多做一件这本书没讲的事:帧间预测,也就是只编码「这一帧和预测帧的差」。而那个差通常更接近噪声,所以对变换的能量集中能力要求更高。
- 你听的每一首歌。MP3、AAC、Opus 用的 MDCT 是 DCT 的一个变体,多了「相邻窗重叠还能完美重构」这条性质(第 9 章提过的时域混叠抵消)。音频编码器解决「分块」问题的方式和图像不同:它不去块滤波,它让块之间重叠。
- 你手机拍照时的实时预览。ISP 芯片里有专门的 DCT 硬件单元。一个 8 点 DCT 可以用 11 次乘法和 29 次加法算完(Loeffler 算法),做成电路只要几百个门。「算得动」这件事在这里是字面意义的——它决定了能不能做进手机。
「DCT 是比 DFT 更先进的变换,所以图像压缩用它。」
DCT就是 DFT——是「先镜像延拓一倍,再做 DFT」的等价简写。它不是一个更先进的东西,是同一个东西配了一个更合适的边界条件。
这一点很值得咂摸:DCT 相对 DFT 的全部优势,来自一个和「变换」本身无关的决定——怎么处理边界。而边界处理这件事,在第 8 章(泄漏)、第 9 章(窗)、第 14 章(循环卷积的绕回)里已经出现过三次了。这本书里让人栽跟头的东西,一大半都发生在边界上。
反过来说,DCT 也丢掉了一样东西:镜像之后信号是实对称的,相位信息退化成了正负号。所以 DCT不能像 DFT 那样直接用来做卷积加速(第 14 章那条定理在 DCT 域里不成立,需要特殊处理),也不能表示第 4 章那种「平移只改相位」的关系。判据是:要压缩、要能量集中,用 DCT;要做卷积、要看相位、要分析频率,用 DFT。它们不是替代关系。
正确答案是 B:DFT 会把左右边接起来,那道人造跳变(本机量出来是 21)会让所有高频系数都不为零。
A 「结果是实数省一半存储」——说的是事实,但把结果当成了原因。DCT 的结果是实数,正是因为镜像延拓让信号变成了偶对称的(偶函数的傅里叶变换只有余弦项)。如果只是想省存储,实信号的 DFT 本来就有共轭对称性,64 个复数里只有 64 个独立实数,一个比特都没多用。 C 「DCT 算得更快」——恰恰相反,8 点 DCT 的标准快速算法通常是通过 16 点 FFT 或者专门的分解实现的,运算量和同规模 DFT 是一个量级。速度不是选它的理由,能量集中才是。 D 「专利」——JPEG 的历史上确实有过几场专利官司(最著名的是 Forgent 在 2002 年拿一个 1987 年的专利去告了几十家公司,2006 年败诉),但 DCT 本身 1974 年就公开发表了。专利影响的是哪些格式能普及(第 12 章 JPEG 2000 的遭遇),不是变换的选择。这就是所有变换编码的套路,一句话:换到一个「重要性不均匀」的坐标系里,然后按重要性分配比特。
而这个套路的极限在哪,第 12 章已经给过答案:取决于你的数据和你的基配不配。DCT 配「平滑为主、偶有边缘」的自然图像;小波配「有尖锐边缘」的(指纹);而 2020 年代的神经图像压缩干脆把基学出来——那是同一个思路的下一步,只是不再手工挑基了。
这一章的一句话
DCT 就是「先照镜子再做傅里叶」,它相对 DFT 的全部优势来自一个和变换本身无关的决定——边界怎么接;而在真实照片最常见的平滑块上,这个决定值一百倍。
下一章走出信号处理。前十八章的横轴一直是时间或位置,而这台机器根本不在乎横轴是什么——只要「平移一下规则不变」,它就成立。下一章去看它在别处的样子:用它可以在一行之内证明中心极限定理,可以解释一块晶体为什么在底片上留下那样的点阵,还可以让一台量子计算机在多项式时间里找出一个周期。