一张照片到底有多大—— 1280×720 未压缩的 RGBA8 图片,需要 3.52MB,这就是为什么必须压缩

你有没有想过一个问题:手机拍一张照片,为什么文件只有几百 KB,而不是几十 MB?一段 720p 的视频,为什么能在网上流畅播放,而不是把你的流量瞬间榨干?

答案就像你猜到的那样——这背后是一整套挺聪明的东西,叫压缩。但它不是魔法,它就是能把那些你不怎么看得到的"废话"给丢掉。

先算账

咱们先来算一算,如果不压缩,一张图片到底有多大。

一张 1280×720 的图,也就是手机上常见的 720p,总共 1280 × 720 = 921,600 个像素,大概九十多万个像素点。

每个像素要存什么呢?至少四个通道:红(R)、绿(G)、蓝(B),还有透明度(A)。每个通道用 8 个比特,也就是 1 个字节。这样每个像素就是 4 个字节。

921,600 像素 × 4 字节 = 3,686,400 字节,大约 3.52 MB。

一张 720p 的图,不压缩就是 3.52 MB。

如果这是一段视频呢?24 帧每秒,就是每秒 24 张这样的图。24 × 3.52 MB = 84.4 MB/秒。一分钟 5 GB,一小时接近 300 GB。一部两小时的电影,不压缩的话大约是 593 GB。

你的家庭宽带千兆网,理论最大下载速度不过 125 MB/秒。只够勉强传一路 720p 的视频,还要扣掉实际损耗。别说 4K 了,连看个家用摄像头都卡得不行。

一张图的字节数、每秒的数据量、两小时电影的大小

这就是为什么必须压缩。不压缩,整个互联网根本跑不动任何图片和视频。

压缩的本质:找废话

压缩不神奇。它就是找到数据里的三种"废话",然后不存它们。这三种废话分别叫:空间冗余、时间冗余、视觉冗余。

空间冗余:一张图片里,相邻的像素通常非常接近。蓝天一片,连续几千个像素的蓝色值几乎一模一样。你说"这里有九千个 128"和存九千个 128,信息是完全一样的,但前者只需要几十分之一的空间。这就是空间冗余——同一个画面里的重复信息。

时间冗余:视频里,前后两帧之间大部分画面没变。一个说话的人,背后的墙没动,桌上的杯子没动,只有嘴巴在动。存一帧完整的,再存一帧说"跟上帧比,嘴巴这里挪了 3 像素",第二帧的数据就省了 99%。这就是时间冗余。

视觉冗余:人眼对亮度的变化非常敏感,对色彩的变化没那么敏感。对大面积平缓的色块很敏感,对细碎的小细节不怎么在意。一张灰度图可以区分 256 个级别,但人眼实际能分辨出来的不过三四十个级别。分辨不出来的那些级别存着干嘛呢?可以不存,因为你看不出来。

空间冗余、时间冗余、视觉冗余的本质

这三类冗余叠加在一起,给了压缩巨大的空间。你看到压缩后的画质"没区别",不是因为它保住了所有信息,而是因为它丢掉的恰好都是你看不见的。

JPEG 怎么压一张图:六步走

JPEG 是我们最常见的图片格式。它处理空间冗余和视觉冗余,一共有六步。

JPEG 压缩六步流程

第一步:颜色空间转换。 把 RGB 转成 YCbCr。Y 是亮度,Cb 和 Cr 是两种色差。因为人眼对亮度敏感、对色差不敏感,可以把每四个像素共用一组色差值(这叫 4:2:0 采样)。颜色信息直接丢掉四分之三,你肉眼看不出区别。这一步就砍掉了一半的数据量。

第二步:切成小块。 把整张图切成 8×8 像素的小块。1280×720 的图切出来总共 14,400 块。为什么要切块?因为相邻像素之间有关联,但隔了老远的像素就没关联了。分成小块,在每一块里搞局部处理,效率才最高。

第三步:DCT 变换。 这是 JPEG 中最硬核的数学操作。它把每个 8×8 小块从"空间域"(这一格的值是多少、那一格的值是多少)转换到"频率域"(这个块由哪些频率的波形组成)。变化完以后,左上角是低频——描述大面积的平缓区域;右下角是高频——描述细节和边缘。自然图像的能量大多集中在低频,高频系数通常很小甚至接近零。这一步本身不压缩数据,但它把数据重新排布了一下——重要的数字集中在左上角,不重要的摊在右下角,为下一步的"丢数据"做了完美的准备。

第四步:量化。 这是整个流程里唯一丢数据的环节,也是"有损压缩"的损字来源。每个 DCT 系数除以量化表里对应的数字,四舍五入取整。量化表是怎么设计的?低频区除的数小,精度保留得高;高频区除的数大,大量高频系数直接变成零。你调的 JPEG 质量从 90 变成 50,本质上就是调这个量化表的步长——除的数越大,丢掉的高频细节越多,文件越小,画质越差。

第五步:Z 字扫描和游程编码。 这一步看起来不起眼,其实很精妙。量化后的 8×8 矩阵,从左上角开始,按 Z 字形斜着扫描到右下角——先把低频部分扫完,最后再扫高频。因为高频大部分已经被量化成零了,扫出来的序列末尾是一大串连续的 0。这时候游程编码(RLE)就能大显身手:一大串连续的 0,不写 0,0,0,0,0,0,0,而是写"7 个 0"。连续的零越长,省的空间越多。

第六步:熵编码。 最后一道处理。经过前面五步,数据里出现了大量的 0 和一些小数值。熵编码的核心逻辑很简单:出现频率高的符号用短的编码,出现频率低的用长的编码。这样大量高频出现的 0 和 1 可以用非常短的比特来表示,进一步榨干了最后一滴水。

六步走完,一张 3.52 MB 的图通常能压到 100-300 KB,压缩 10 到 30 倍。而你肉眼几乎看不出和原图的区别。

视频压缩:多了一维

如果视频只是每一帧用 JPEG 压一次(这叫 MJPEG),那只能拿到大概 7 倍的压缩率——84 MB/s 压到 12 MB/s,还是太慢了。网上看个视频还是得等半天。

真正的视频压缩(H.264、H.265、AV1)比 JPEG 多了一层处理:它会把时间冗余也消灭掉。这就是 I 帧、P 帧、B 帧的由来:

I 帧(关键帧):完整编码,和 JPEG 的方式差不多。这个帧的数据量最大,它作为后面帧的参考基准。通常每秒只有一两个 I 帧。

P 帧(前向预测):不存完整画面,只存"跟上个帧相比哪里变了"。编码器在上一帧里找当前画面的每个小块最接近的位置,算出一个运动向量(比如"往右挪了三个像素"),然后只存这个运动向量加上和实际画面的微小差异。如果画面里的东西只是平移,差异几乎是零,数据量只有 I 帧的三分之一。

B 帧(双向预测):比 P 帧更进一步,它同时参考前一帧和后一帧来做预测。准确度最高、残留数据最少,数据量只有 I 帧的六分之一。代价是需要缓冲,因为得先把后一帧也解出来才能解 B 帧,会有一点延迟。

典型的视频文件里,帧的排列是:I-B-B-P-B-B-P-B-B-P... 每隔 12 到 15 帧放一个 I 帧重新锚定。24fps 的视频里,只有两三帧是完整的,剩下的二十多帧都只存差异,总数据量就被大幅压下来了。

I帧、P帧、B帧在视频流中的排列

其中运动估计是整颗 CPU 最吃力的活儿——编码器要在一帧里对每一块在上一帧里搜索最佳匹配位置。这个搜索的窗口还算不小,每一帧要查几万个块。这就是为什么视频编码特别耗 CPU 和 GPU,实时编码就更不用说了。

不同编码的效果对比

方案 每秒数据量 压缩倍数 用在哪儿
未压缩 84.4 MB/s 基本不会直接用
MJPEG ~12 MB/s 早就有的网络摄像头
H.264 2-4 MB/s 30-50× B站、YouTube 的主力
H.265 1-2 MB/s 60-100× 4K 流媒体、蓝光
AV1 0.5-1.5 MB/s 80-150× 新一代流媒体标准

不同压缩方案的带宽对比

从 84 MB/s 压到 2 MB/s,四十倍的压缩,让你在千兆宽带上能同时传几十路视频都不卡。这就是这三类冗余被一步步搜刮出来的结果。

最后说几句

压缩的本质不是"把数据塞进更小的盒子",而是想清楚哪些数据是信息、哪些是噪音,只保留信息。

空间冗余——"这一片像素都差不多",不用逐个存;时间冗余——"跟上帧几乎没变",只存差异;视觉冗余——"这些颜色差别人眼看不出来",直接丢弃。三者叠加,才有了从 593 GB 到 2 GB 的接近三百倍压缩。

你调的每个画质参数——码率、CRF 值、GOP 大小、B 帧数量——本质上都是在控制这三类冗余各丢掉多少。理解了这个,你就理解了图片和视频为什么能在网上跑得飞快。