浅析一下jpeg图片格式及其来源
JPEG 是怎么压缩图片的?
JPEG 的核心目标是:
在人眼不太容易察觉的地方丢掉一些信息,从而大幅减小照片体积。
所以 JPEG 通常是 有损压缩。它不是简单地把文件“打包变小”,而是会真的改变图像数据。
可以把 JPEG 压缩理解成几个步骤:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19RGB 图像
↓
转换成 YCbCr
↓
色度抽样
↓
切成 8×8 小块
↓
DCT 变换
↓
量化
↓
Zigzag 扫描
↓
游程编码
↓
熵编码
↓
生成 JPEG 文件
1. RGB 转成 YCbCr
我们平时看到的图片通常可以理解成 RGB:1
2
3R = 红色
G = 绿色
B = 蓝色
但 JPEG 不直接用 RGB 压缩,而是会先转换成 YCbCr:1
2
3Y = 亮度信息
Cb = 蓝色色度信息
Cr = 红色色度信息
这么做是因为人眼对 亮度变化 很敏感,但对 颜色细节 没那么敏感。
比如一张人脸照片:1
2轮廓、明暗、边缘变化很重要
颜色细节稍微模糊一点,人眼不一定明显察觉
所以 JPEG 会把图像拆成:1
2更重要的亮度通道 Y
没那么重要的颜色通道 Cb / Cr
后面压缩时,颜色通道可以压得更狠一些。
2. 色度抽样
既然人眼对颜色细节没那么敏感,JPEG 就可以减少颜色信息的采样数量。
常见色度抽样有:1
2
34:4:4 不减少颜色信息
4:2:2 横向减少一半颜色信息
4:2:0 横向和纵向都减少颜色信息
最常见的是 4:2:0。
它大概意思是:1
2亮度信息完整保留
颜色信息减少到原来的一部分
举个简化例子:1
2
3
4
5
6原始:
每个像素都有 Y、Cb、Cr
4:2:0 后:
每个像素仍然有 Y
但多个像素共用一组 Cb、Cr
这一步对照片很有效,因为自然照片里颜色通常是连续变化的。
但如果是文字截图、UI 图标、红蓝边缘特别明显的图,色度抽样可能会让边缘发糊、出现彩边。
所以:1
2照片适合 JPEG
截图、文字、Logo 不太适合 JPEG
这里再补充一个,就是4:2:0究竟是啥意思
YCbCr 是啥意思
YCbCr 会把一个颜色拆成三部分:1
2
3Y = 亮度,也就是明暗
Cb = 蓝色色度差,大概表示“偏蓝还是不偏蓝”
Cr = 红色色度差,大概表示“偏红还是不偏红”
注意:Cb / Cr 不是直接等于蓝色和红色,而是 颜色差异信息。
可以粗略理解为:1
2
3Y 管黑白明暗
Cb 管蓝黄方向的颜色变化
Cr 管红绿方向的颜色变化
所以一张 RGB 图片转成 YCbCr 后,可以想象成三张灰度图:1
2
3
4
5原图
↓
Y 图:黑白版,保留轮廓、明暗、细节
Cb 图:颜色偏蓝 / 偏黄的信息
Cr 图:颜色偏红 / 偏绿的信息
其中最重要的是 Y 亮度图。
为什么可以把亮度和颜色分开?
因为人眼看图时,最敏感的是亮度边缘,而不是颜色边缘。
比如一张白底黑字图片:1
白底 + 黑字
你能很清楚看见文字,是因为亮度差异很大。
再比如一张人像照片:1
2
3
4
5脸部轮廓
鼻梁阴影
眼睛边缘
头发明暗
衣服褶皱
这些主要靠 Y 亮度信息 表达。
但颜色细节稍微粗糙一点,比如皮肤颜色、天空蓝色、墙面颜色,人的感知没那么敏锐。
所以 JPEG 的策略是:1
2Y 亮度:尽量保留清楚
Cb / Cr 颜色:可以减少一些
这就是色度抽样的基础。
4. RGB 转 YCbCr 是怎么转
近似公式是:1
2
3Y = 0.299R + 0.587G + 0.114B
Cb = B - Y 的某种缩放
Cr = R - Y 的某种缩放
这里可以看到G是权重最大的,因为人眼对绿色最敏感,所以绿色对亮度感知贡献最大。
例如:1
2
3R = 200
G = 120
B = 80
那么亮度 Y 大概是:1
2
3Y = 0.299 × 200 + 0.587 × 120 + 0.114 × 80
≈ 59.8 + 70.4 + 9.1
≈ 139.3
所以这个像素的亮度大概是 139。
然后 Cb / Cr 负责记录它相对于这个亮度来说,颜色偏向哪里。
简单理解:1
2Y 记录:这个点有多亮
Cb/Cr 记录:在这个亮度基础上,它是什么颜色
刚才说的 4:4:4、4:2:2、4:2:0 是啥意思
色度通道 Cb / Cr 相对于亮度通道 Y,被保留了多少采样点。
先看一个 2×2 像素块。
原始情况下,每个像素都有:1
Y + Cb + Cr
也就是:1
2
3
4像素 A: Y1 Cb1 Cr1
像素 B: Y2 Cb2 Cr2
像素 C: Y3 Cb3 Cr3
像素 D: Y4 Cb4 Cr4
6. 4:4:4:完全不减少颜色信息
4:4:4 表示:1
2每个像素都有自己的 Y
每个像素也都有自己的 Cb / Cr
用 2×2 看:1
2
3
4
5
6
7
8
9
10
11Y:
Y1 Y2
Y3 Y4
Cb:
Cb1 Cb2
Cb3 Cb4
Cr:
Cr1 Cr2
Cr3 Cr4
也就是亮度和颜色分辨率一样高。
所以:1
4:4:4 = 不做色度抽样
质量最好,体积也更大。
7. 4:2:2:横向减少颜色信息
4:2:2 表示:
水平方向的颜色采样减半,垂直方向不减。
可以理解成:横向相邻两个像素共用一组 Cb / Cr。
比如 2×2 像素:1
2
3
4
5
6
7
8
9
10
11Y:
Y1 Y2
Y3 Y4
Cb:
Cb1 Cb1
Cb2 Cb2
Cr:
Cr1 Cr1
Cr2 Cr2
也就是:1
2第一行:两个像素共用一组颜色
第二行:两个像素共用一组颜色
亮度 Y 仍然每个像素都有。
所以:1
4:2:2 = 横向颜色分辨率减半,纵向颜色分辨率不变
如果原来颜色采样是:1
宽 1000,高 1000
4:2:2 的颜色通道大概变成:1
宽 500,高 1000
8. 4:2:0:横向和纵向都减少颜色信息
4:2:0 最容易误解。它不是说“颜色没有了”,也不是说“第二行没有颜色”。
它的意思是:
色度信息在横向减半,纵向也减半。
也就是一个 2×2 像素块共用一组 Cb / Cr。
看 2×2:1
2
3
4
5
6
7
8
9
10
11Y:
Y1 Y2
Y3 Y4
Cb:
Cb1 Cb1
Cb1 Cb1
Cr:
Cr1 Cr1
Cr1 Cr1
也就是说:1
24 个像素各自有自己的亮度 Y
但 4 个像素共用同一组颜色 Cb / Cr
所以颜色通道的分辨率变成原来的:1
2
3宽度 1/2
高度 1/2
总采样点 = 1/4
假设原图是:1
1920 × 1080
Y 通道仍然是:1
1920 × 1080
但 Cb 通道变成:1
960 × 540
Cr 通道也变成:1
960 × 540
这就是“横向纵向都减少颜色信息”。
这是一个前置的理解,那么关于图片格式,也想聊下怎么识别这些格式,光是后缀好像是可以被随意改动,比如这个jpeg的格式,那么我们可以从文件头来看1
FF D8 FF E0 ... 4A 46 49 46
后面就代表的是 JFIF 这是文件头
我们也可以用一个小工具来识别1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function detectImageType(string $path): string
{
$h = file_get_contents($path, false, null, 0, 32);
if ($h === false || $h === '') {
return 'unknown';
}
if (strncmp($h, "\xFF\xD8\xFF", 3) === 0) {
return 'image/jpeg';
}
if (strncmp($h, "\x89PNG\x0D\x0A\x1A\x0A", 8) === 0) {
return 'image/png';
}
if (strncmp($h, "GIF87a", 6) === 0 || strncmp($h, "GIF89a", 6) === 0) {
return 'image/gif';
}
if (strlen($h) >= 12 && substr($h, 0, 4) === "RIFF" && substr($h, 8, 4) === "WEBP") {
return 'image/webp';
}
if (strlen($h) >= 12 && substr($h, 4, 4) === "ftyp") {
$brands = substr($h, 8);
if (str_contains($brands, "avif")) {
return 'image/avif';
}
if (
str_contains($brands, "heic") ||
str_contains($brands, "heix") ||
str_contains($brands, "mif1")
) {
return 'image/heif';
}
}
return 'unknown';
}
echo detectImageType($argv[1] ?? '') . PHP_EOL;
这是通过文件头和brand来一起识别,brand是针对那些跟视频格式一致的可能包含容器的格式
比如HEIC / AVIF,对于JPEG、PNG、GIF、WebP这类常规格式,是一般从文件头识别
而后面的容器型则需要结合容器的brand标识,
比如一个AVIF的文件头是1
00 00 00 20 66 74 79 70 61 76 69 66 00 00 00 00 61 76 69 66 6D 69 66 31
拆开以后就是1
2
3
4
5
600 00 00 20 = box size
66 74 79 70 = ftyp
61 76 69 66 = avif
00 00 00 00 = minor_version
61 76 69 66 = avif
6D 69 66 31 = mif1
也就是1
2
3box type: ftyp
major_brand: avif
compatible_brands: avif, mif1
这样就大致知道了图片格式的识别

