📦 JPEG、HEIF、PNG 是怎么把照片变小的?
大白话 + 一步一步算给你看 · danialwang

一张 3300 万像素的照片,如果一点都不压缩,每个像素要记红、绿、蓝 3 个数,每个数占 1 个字节:
3300 万 × 3 字节 ≈ 99 MB。但相机里一张 JPEG 只有十几 MB,HEIF 更小。剩下那八九十 MB 去哪了?这一页就把这件事从头讲清楚。

0先用三句话记住三种格式

📷 JPEG(1992 年)

"丢掉眼睛看不出的细节"。有损压缩,照片变小 5–10 倍,所有设备都能打开。

🎬 HEIF(2015 年)

"用拍视频的技术压照片"。也是有损,但更聪明:先猜,只记猜错的部分。同样画质比 JPEG 小约一半。

🖼️ PNG(1996 年)

"一个像素都不丢"。无损压缩,解压后和原图一模一样。适合截图、文字、图标、透明背景;存照片会很大。

1先搞懂:省空间只有两条路

无损路线一:换个更短的写法

信息一点不丢,只是写得更紧凑。比如把 "2 2 2 2 2 2 2" 写成 "2 重复 7 次"。能还原得一模一样。

👉 PNG 只走这条路。JPEG 和 HEIF 的最后一步也用它。

有损路线二:扔掉不重要的

把人眼不敏感的信息直接扔掉,再也找不回来。但扔得巧,人根本看不出来。

👉 JPEG 和 HEIF 主要靠这条路,所以才能压得那么小。

无损就像把衣服叠整齐放进箱子,拿出来还是那几件;有损就像出门前把不常穿的衣服直接留在家里,箱子轻多了,但那几件就真的没带。

2JPEG 第 1 步:把"亮"和"颜色"分开,颜色存少一点

人眼有个特点:对明暗很敏感,对颜色的细节很迟钝。所以 JPEG 先把每个像素的"红绿蓝"换成"亮度 + 两个颜色值",然后颜色只存一部分。

黑白电视时代,电视台只播亮度。后来加彩色时,就是在黑白信号上"再叠一层粗糙的颜色"——效果照样很好,因为眼睛看清的主要是黑白轮廓。

换算公式(只看第一行就够)

亮度 Y = 0.299 × 红 + 0.587 × 绿 + 0.114 × 蓝 三个系数加起来正好等于 1。绿色系数最大,因为人眼对绿色最敏感;蓝色最小,因为人眼对蓝色最不敏感
✏️ 算一个天蓝色像素:红 100、绿 150、蓝 220
红的贡献:0.299 × 100 = 29.9
绿的贡献:0.587 × 150 = 88.05
蓝的贡献:0.114 × 220 = 25.08
加起来:29.9 + 88.05 + 25.08 = 143.03 → 亮度 Y ≈ 143(满分 255,中等偏亮)
另外两个颜色值用类似的公式算出:偏蓝程度 Cb ≈ 171,偏红程度 Cr ≈ 97。128 代表"不偏",171 比 128 大 → 偏蓝;97 比 128 小 → 不偏红。和"天蓝色"完全对得上 ✔

颜色怎么"存少一点":色度子采样

亮度每个像素都存。颜色则几个像素共用一份:

4:4:4 不省4:2:2 左右两个共用4:2:0 四个共用 4 个亮度 + 4 份颜色数据 100% 4 个亮度 + 2 份颜色约 67%(你相机 HEIF 可选) 4 个亮度 + 1 份颜色50%(JPEG 最常用) 灰格 = 亮度橙点 = 一份颜色
✏️ 4:2:0 为什么正好省一半?看 4 个像素
不省时:每个像素存 1 个亮度 + 2 个颜色值 = 3 个数,4 个像素 4 × 3 = 12 个数
4:2:0 时:4 个亮度照存 = 4 个数;4 个像素共用 1 份颜色 = 2 个数;一共 4 + 2 = 6 个数
6 ÷ 12 = 50% 一下子少了一半,眼睛却几乎看不出来

有损这是 JPEG 第一次丢信息。

3JPEG 第 2 步:DCT——把"一格一格的像素"改写成"渐变 + 花纹"

JPEG 把整张图切成很多 8×8 的小方块,一块一块处理。这一步叫 DCT(离散余弦变换),名字吓人,意思其实很简单:换一种说法描述同一块图。

描述一段楼梯,你可以说"第 1 级高 150、第 2 级高 152、第 3 级高 154……"念 8 个数;也可以说"平均高 157,每级往上升 2"——只要 2 个数。DCT 就是自动帮你找出这种"更省话"的说法。

它把方块拆成"标准花纹"的组合

DCT 64 种标准花纹
JPEG 的 64 种"标准花纹"(用公式实际生成)。左上角黄框是一块纯色,代表"平均亮度";往右是横向越来越密的条纹,往下是竖向越来越密的条纹。任何一个 8×8 方块,都能由这 64 种花纹按不同的分量叠加出来——DCT 做的就是算出每种花纹要放多少。
关键发现:照片里绝大多数方块都是平缓的(天空、墙面、皮肤),只需要左上角那几种"粗花纹"就能拼出来,右下角那些"细花纹"的分量几乎都是 0。一堆 0 是非常好压缩的——这就是 DCT 有用的原因。DCT 这一步本身不丢任何信息,算回去和原来一模一样。

一步一步算:一排 8 个天空像素

为了好算,我们只看方块里的一排(真正的 JPEG 是横竖各算一次,道理完全一样)。这排天空从左到右慢慢变亮:

原始亮度150152154156158160162164
✏️ 第一个数:平均亮度有多少("纯色"花纹的分量)
JPEG 规定先把每个值减 128,让数字围绕 0(暗于中灰为负、亮于中灰为正):
22 24 26 28 30 32 34 36
全部加起来:22+24+26+28+30+32+34+36 = 232
乘一个固定系数 1/√8 ≈ 0.354(这个系数让正算和反算对称,记住"固定"就行):232 × 0.354 ≈ 82.0
所以"纯色"分量 = 82.0。它其实就是"平均亮度"换了个单位:平均值 232÷8 = 29,29 × √8 ≈ 82 ✔
✏️ 第二个数:"从左到右慢慢变化"的分量
拿一条标准的"半个波"花纹,它的 8 个值是(从左边 +1 慢慢降到右边 −1):
0.98 0.83 0.56 0.20 −0.20 −0.56 −0.83 −0.98
把像素和花纹一一对应相乘,看它们"像不像":
22×0.98=21.6 24×0.83=20.0 26×0.56=14.4 28×0.20=5.5
30×(−0.20)=−5.9 32×(−0.56)=−17.8 34×(−0.83)=−28.3 36×(−0.98)=−35.3
加起来:21.6+20.0+14.4+5.5−5.9−17.8−28.3−35.3 ≈ −25.8
乘固定系数 √(2/8) = 0.5:−25.8 × 0.5 ≈ −12.9。负号的意思是"和花纹方向相反"——花纹是左亮右暗,我们的天空是左暗右亮 ✔
其余 6 种更细的花纹,用同样的方法一乘一加,结果都非常接近 0。

DCT 之后,这排 8 个数变成了:

DCT 结果82.0−12.90−1.40−0.40−0.1

原来 8 个数都很大,现在只剩前 2 个比较大,后面都接近 0。信息还是那些信息,但"重点"全挤到前面了。

4JPEG 第 3 步:量化——把数字"约个大概"

这一步是 JPEG 真正"丢东西"的地方:每个数除以一个"步长",然后四舍五入成整数。

问你身高,你不会说"173.46 厘米",会说"一米七几"。精度降低了,但意思没变,而且说起来短得多。量化就是这样"约个大概"。
存下来的数 = 四舍五入(DCT 结果 ÷ 步长) 步长越大,存的数越小越省空间,但"约"得越粗,丢的细节越多

聪明在哪:粗花纹约得细,细花纹约得粗

JPEG 有一张事先定好的步长表。人眼对"大面积明暗"敏感,所以这些花纹步长小(约得细);对"细碎纹理"不敏感,所以步长大(约得粗,常常直接变 0)。比如标准表第一行是:

步长1611101624405161
✏️ 接着算那排天空(用中等质量的步长)
纯色分量:82.0 ÷ 16 = 5.13 → 四舍五入 → 5
渐变分量:−12.9 ÷ 11 = −1.17 → 四舍五入 → −1
后面 6 个:比如 −1.4 ÷ 16 = −0.09 → 0;其余也全都变成 0
最后要存的只有:5 −1 0 0 0 0 0 0 8 个大数变成了 2 个小整数 + 6 个 0
✏️ 解压时还原回去,看看误差多大
先乘回步长:5 × 16 = 80,−1 × 11 = −11(原来是 82.0 和 −12.9,有点误差,这就是丢掉的部分)
再用 DCT 反算回 8 个像素,得到:
原图150152154156158160162164
还原后151152153155157159161162

每个像素最多差 2(满分 255),也就是不到 1%。人眼完全分辨不出来,但要存的数从 8 个大数变成了"5、−1 和一串 0"。

相机里的"画质:超精细 / 精细"、软件里的"质量 1–100",调的就是这张步长表。质量越高,步长越小,还原越准,文件越大。质量 90 时,开头那个 16 会缩到 3,几乎不丢东西。
JPEG 质量对比
压得太狠会怎样?同一张图(徕卡相机局部,放大 2 倍)的实测:质量 95 → 46KB,30 → 9KB,8 → 5KB。质量太低时,步长大到把每个 8×8 方块的花纹全约成 0,只剩"平均色",于是出现马赛克一样的方块——这就是 JPEG 最典型的毛病。

有损这是 JPEG 第二次(也是最主要的一次)丢信息。

5JPEG 第 4 步:把数字写得更短(无损)

现在每个方块剩下一串"几个小数字 + 一大堆 0"。最后一步是把它们写成尽量短的 0 和 1。无损

① 一大串 0,用一个"结束"记号代替

按"之字形"从左上(粗花纹)读到右下(细花纹),读出来是 5, −1, 0, 0, 0, 0, …。后面那一长串 0 不用一个个写,直接写一个"本块结束"记号。

② 常见的写短,少见的写长:哈夫曼编码

发电报时,最常用的字母 E 在摩尔斯电码里只有一个点"·",而很少用的 Q 是"— — · —"。常用的短、少用的长,平均下来最省。
✏️ 编一串 10 个符号:A 出现 5 次、B 3 次、C 1 次、D 1 次
笨办法:4 种符号,每个都用 2 位(00、01、10、11)表示:10 × 2 = 20 位
哈夫曼:A 最常见给 0(1 位),B 给 10(2 位),C 给 110、D 给 111(各 3 位)
总长:5×1 + 3×2 + 1×3 + 1×3 = 5 + 6 + 3 + 3 = 17 位
比笨办法少了 (20 − 17) ÷ 20 = 15% 而且一个信息都没丢 ✔
哈夫曼的局限:每个符号至少要 1 位。如果某个符号出现得特别多(比如 90% 都是它),理论上每个只需要零点几位,哈夫曼却必须给满 1 位,有浪费。HEIF 用的方法能突破这一点,下一节讲。
① 分开亮度/颜色颜色存一半(有损) ② 切 8×8 + DCT换种说法(不丢) ③ 量化约个大概(有损) ④ 哈夫曼写得更短(不丢) JPEG 文件约小 5–10 倍 JPEG 全流程回顾:真正丢信息的只有 ① 和 ③

6HEIF:用拍视频的技术压照片,四个"更聪明"

先澄清一点:HEIF 只是一个"文件盒子",盒子里装着照片、缩略图、拍摄参数等。真正负责压缩的是盒子里的 HEVC(也叫 H.265)——就是手机拍 4K 视频用的那套技术。它把你的照片当成"视频里的一帧"来压。

大流程和 JPEG 一样(分亮度颜色 → 切块 → 变换 → 量化 → 写短),但每一步都升级了:

聪明 ①:先"猜",只记猜错的部分(最重要的一招)

老师批改作文,不会把整篇抄一遍,只在写错的地方用红笔标出来。HEIF 也是:先根据旁边已经处理好的像素猜这一块长什么样,然后只记"猜错了多少"。

JPEG 的每个方块都是从零开始描述,完全不看邻居。但照片里相邻的地方往往很像(天空挨着天空,墙挨着墙),HEIF 就利用了这一点。它有 35 种猜法:

平均猜法

"这块大概和上边、左边的平均颜色一样"——适合纯色区域。

平滑猜法(Planar)

"从上边过渡到左边,像渐变一样"——适合天空、皮肤、虚化背景。

33 种方向猜法

"沿着这个方向延续下去"——适合电线杆、雪山棱线、栏杆这种有方向的线条。

✏️ 举个例子:一个"竖条纹"块(比如一排栏杆),用"从上往下延续"的猜法(数字为示意)
这块上方相邻、已经处理好的一行像素是:80 200 80 200(暗、亮、暗、亮)
"往下延续"的意思:猜这块每一行都和上面那行一样,即 4 行都是 80 200 80 200
假设这块实际是:前 3 行都是 80 200 80 200,第 4 行是 82 198 80 201
只需要存"实际 − 猜测":前 3 行全是 0,第 4 行是 2 −2 0 1
16 个大数(80、200……)变成了 13 个 0 + 3 个很小的数。JPEG 遇到这种条纹,要用好几个"细花纹"才描述得出来,费很多位。
✏️ 再算一个天空渐变块,用"平滑猜法"——数字不会刚好全是 0
一块 4×4 的天空,实际亮度从左上 100 慢慢变到右下 118
平滑猜法用上边一行和左边一列"拉渐变",猜出来从 100 变到 111 左右
"实际 − 猜测"的 16 个数是:0 0 0 1 / 0 1 2 3 / 1 3 4 6 / 1 4 6 9
原来要记 100 多的数,现在只要记 0 到 9 的小数。数越小,后面变换、量化之后越容易变成 0,越省空间

聪明 ②:方块大小随画面变

JPEG:不管画面,一律 8×8 → 一大片蓝天也被切成几千个小块,每块都要记一遍 HEIF:从 64×64 开始,需要时才切小 • 平滑的蓝天:保持大块,一块只记几个数 • 雪山轮廓、草地:一直切到很小,保住细节 切不切怎么定?编码器每种都试一遍: "画质损失" + "多花的空间 × 一个汇率" 哪个最小选哪个

这个"汇率"表示"多花 1 位空间值多少画质"。用户选高画质,汇率就低(舍得花空间);选高压缩,汇率就高(能省则省)。JPEG 没有这种"试一试再决定"的过程。

聪明 ③:写短的方法更好——能做到"一个符号不到 1 位"

猜谜游戏:如果你知道答案 90% 是"是",那每次猜对的信息量就很少——"意料之中的事不值得多说"。HEIF 的编码方法(算术编码)就利用这一点:越常见的东西,花的位越少,可以少到零点几位。
✏️ 存一串 10 个"开关":9 个"关"、1 个"开"("关"的概率 90%)
JPEG 的哈夫曼:每个至少 1 位,10 × 1 = 10 位
HEIF 的算术编码:从一根长度为 1 的"尺子"开始,每来一个符号就按概率截一段——是"关"就留下 90%,是"开"就留下 10%
9 个"关":1 × 0.9 × 0.9 × … (9 次)≈ 0.387;再来 1 个"开":0.387 × 0.1 ≈ 0.0387
尺子剩 0.0387 长。要在 0 到 1 之间精确指到这么窄的一段,大约需要 log₂(1 ÷ 0.0387) ≈ 4.7 位(每多 1 位,能分辨的精度翻一倍:2⁴ = 16,2⁵ = 32,1÷0.0387 ≈ 26 落在中间)
10 位 → 约 4.7 位,省了一半多。而且它会边压边学:根据旁边的块,随时调整"哪种情况更常见"的估计。

聪明 ④:自带"磨皮"——把方块边缘抹平

JPEG 压狠了会出现方块和字边脏点(见第 4 节的图)。HEIF 在解压时会自动检查每个方块的边缘:如果两边都很平滑,只在边界上突然跳一下,那多半是压缩造成的"假边",就把它抹平;如果本来就是物体轮廓,就保留不动。另外还会修正锐利边缘旁边一圈一圈的"波纹"。

四招加起来,同样画质下 HEIF 通常比 JPEG 小 40%–50%(这是业界测试的常见范围,会随照片内容浮动)。代价是计算量大很多,要靠相机和手机里专门的芯片来算,所以老电脑、老软件可能打不开。

7PNG:一个像素都不丢的压缩

PNG 在 1996 年诞生,本来是为了替代当时有专利问题的 GIF。它的原则是:解压后必须和原图一模一样,一位都不能差。所以它不能"扔东西",只能想办法写得更短。

它分两步:① 过滤(把像素改写成"差值")→ ② DEFLATE 压缩(找重复 + 哈夫曼)。这两步都可以原样还原。

第 ① 步:过滤——记"和旁边差多少",而不是记本身

记录每天的体重:65.0, 65.2, 65.1, 65.3……,不如记成"第一天 65.0,之后 +0.2、−0.1、+0.2……"。差值都很小,而且常常重复,后面就好压了。
✏️ 还是那排天空,用 PNG 的"左差"过滤(每个像素减它左边那个)
原始:150 152 154 156 158 160 162 164
第 1 个左边没有像素,原样保留:150
第 2 个:152 − 150 = 2;第 3 个:154 − 152 = 2;……每一个都是 2
过滤后:150 2 2 2 2 2 2 2 8 个各不相同的数,变成了"1 个数 + 7 个一样的 2"
还原时反过来加:150,150+2=152,152+2=154……一模一样 ✔

PNG 有 5 种过滤方法,每一行可以挑最合适的那种:

名字做法适合
None 不过滤原样存本来就很杂乱的行
Sub 左差减去左边像素横向渐变(如上例)
Up 上差减去上边像素竖向渐变、和上一行很像
Average 平均差减去(左 + 上)÷ 2斜向平滑区域
Paeth 智能猜在左、上、左上三个里挑一个"最可能"的来减大多数复杂图像
✏️ Paeth 是怎么"挑"的?假设左边 152、上边 153、左上 150
先估一个值:左 + 上 − 左上 = 152 + 153 − 150 = 155(如果画面是平滑倾斜的,这个估计最准)
看三个邻居谁离 155 最近:左 |155−152| = 3,上 |155−153| = 2,左上 |155−150| = 5
上边最近,就用上边的 153 来做减法 "挑最像的邻居",差值就最小

第 ② 步:DEFLATE——找重复 + 哈夫曼

找重复(LZ77)

发现一段内容前面出现过,就写成 "往回数 X 个,照抄 Y 个"。
例:150 2 2 2 2 2 2 2 → 150, 2, [往回 1 个, 抄 6 个]。
这和压缩包(.zip)用的是同一套方法。

哈夫曼编码

和 JPEG 第 4 步一样:常见的写短,少见的写长。

过滤到底有多大用?我实测了一张 600×400 的渐变图:不过滤直接压缩 → 2123 字节;先用"左差"过滤再压缩 → 1426 字节,又小了约 1/3。原始数据是 240000 字节,所以 PNG 对这种平滑图压缩了 160 多倍。

那为什么 PNG 存照片会很大?

照片里到处是传感器的细小噪点、树叶草地的杂乱纹理,几乎找不到完全一样的重复,差值也不小。PNG 又不许"约个大概",就压不下去。实测同一张照片:

测试图(实测)PNGJPEG(质量 90)谁小
📷 照片:徕卡相机 960×644585 KB126 KBJPEG 小约 4.6 倍
📱 截图:手机设置页面(文字 + 纯色)71 KB86 KBPNG 更小,而且无损
PNG 与 JPEG 文字对比
文字截图放大对比(实测)。JPEG 的"约个大概"在锐利的黑白边缘最容易出问题:字的周围出现一片脏点(专业叫法是"振铃")。PNG 一个像素都不丢,边缘始终干净。所以截图、文字、图标、表格用 PNG,照片用 JPEG/HEIF。

PNG 的另外两个特长

🪟 透明背景

PNG 可以给每个像素多存一个"透明度"值(0 = 完全透明,255 = 完全不透明)。抠好的人像、logo、贴纸都靠它。JPEG 做不到透明。

🎨 最高 16 位

PNG 每个颜色可以存 16 位(65536 级),适合专业修图的中间文件。但这样文件会更大。

88 位和 10 位:为什么 HEIF 的天空更顺

8 位就像只有 256 级台阶的楼梯,10 位是 1024 级的楼梯。同样从一楼走到二楼,台阶越多,每一级就越矮,走起来越像一个平滑的坡;台阶少,每一级就很高,一眼就能看出"一级一级"的。
✏️ 夜空为什么容易出"一圈一圈"的色带?
8 位:2 × 2 × … × 2(8 个 2)= 256 级;10 位:2¹⁰ = 1024 级,正好是 8 位的 4 倍
夜空很暗,亮度只占总范围的一小段,比如从 8% 到 14%,也就是 6% 的范围
8 位能用的台阶:256 × 6% ≈ 15 级;10 位能用的台阶:1024 × 6% ≈ 61 级
一张照片横向约 7000 个像素:8 位时每级台阶宽 7000 ÷ 15 ≈ 470 像素,10 位只有 7000 ÷ 61 ≈ 115 像素
如果在手机上把这张夜景调亮 4.5 倍,8 位的台阶也跟着被放大,就变成了明显的一条条色带;10 位台阶本来就细,调亮后依然平滑 ✔
8位与10位色带对比
按上面的数字实际生成的模拟图:同一段暗夜空,分别存成 8 位和 10 位,再调亮 4.5 倍。上方 8 位出现一条条竖向色带,下方 10 位依然平滑。

要说清楚的一点:真实照片里有细小噪点,会把台阶"糊"掉一些,所以 8 位 JPEG 不调亮时通常也看不出问题。10 位的好处主要在两种情况:后期调亮或调色,以及大片平滑的渐变(蓝天、夜空、夕阳、虚化背景)。

格式位数
JPEG只有 8 位
HEIF(你的相机)10 位
PNG8 位或 16 位

9附:HLG 为什么在普通屏幕上发灰

HLG 不是压缩方法,而是一种"怎么分配亮度台阶"的规则,专门为 HDR 屏幕设计。它分两段:

暗部和中间调:和普通照片一样

用的是"开根号"那样的曲线:信号 = √(3 × 亮度)。普通照片也差不多是这么分配的,所以这部分在普通屏幕上看着正常。

高光:被"压扁"存进去

太阳、雪山反光这些特别亮的地方,用对数曲线压缩后存进去。HDR 屏幕会把它们"展开",显示得特别亮;普通屏幕不会展开,就显得灰、平、没精神。

✏️ 两段在哪里接上?
两段的分界在亮度 = 1/12 处。代入第一段:√(3 × 1/12) = √(1/4) = 0.5
标准里第二段的几个常数,就是特意算好、让它在这一点也正好等于 0.5 的 两段无缝连接,不会突然跳一下 ✔
也就是说:信号的下半部分(0 到 0.5)给暗部和中间调,上半部分(0.5 到 1)全留给高光。普通照片可不会给高光留这么多空间。

这就是之前建议你不开 HLG 的原因:你主要在手机和微信里看照片、发照片,开了反而容易发灰。

10总结:三种格式怎么选

JPEGHEIFPNG
丢不丢信息有损有损无损,一位不差
核心方法8×8 块 + DCT + 量化 + 哈夫曼先猜 + 方块大小可变 + 更好的编码 + 自动去方块记差值 + 找重复 + 哈夫曼
存照片的大小中等最小(约为 JPEG 的一半)很大(常是 JPEG 的 4–5 倍)
存截图、文字字边有脏点还可以最好,边缘干净
色彩位数8 位10 位8 或 16 位
透明背景不支持支持支持,最常用
兼容性所有设备都能打开部分电脑、老软件、网页打不开几乎都能打开

📷 相机日常拍照

HEIF(你现在的设置)。最小、渐变最顺,iPhone 能直接看。

📤 发给别人原图 / 冲印

JPEG。谁都能打开,冲印店也都收。

🖥️ 截图、文字、图标、抠图

PNG。一个像素不丢,可以透明。

🎯 一句话:JPEG 靠"约个大概"变小;HEIF 在此基础上还会"先猜再记差",所以更小更顺;PNG 什么都不丢,只靠"记差值、找重复"变小,适合截图、不适合照片。你的相机保持 HEIF + 4:2:2 + 不开 HLG,就是对你最合适的设置。