第十三章|基础专题三:JPEG 底层原理
沿着 RGB 到 YCbCr、色度下采样、8x8 DCT、量化、Zig-Zag、RLE、Huffman、marker segment 和浏览器解码管线理解 JPEG。
第十三章|基础专题三:JPEG 底层原理
原稿学习节奏:第 3 周。
本周目标不是背诵“JPEG = DCT + Huffman”,而是能够沿着一张图片的完整数据路径,解释每一步为什么存在、丢失了什么信息、生成了哪些字节,以及浏览器或服务端工程中如何正确使用它。
本文以最常见的 Baseline Sequential DCT JPEG(基线顺序 DCT JPEG) 为主。JPEG 1 标准还定义了算术编码、无损、渐进和分层等模式,不能把所有 JPEG 都等同于 Baseline JPEG。
一、学习目标
完成本周后,你应当能够:
- 在白板上画出 JPEG 编码和解码的完整流程。
- 解释 JPEG、JFIF、Exif、
.jpg文件名扩展名之间的关系。 - 说明 RGB 为什么通常先转为
Y'CbCr,以及色度下采样为什么有效。 - 手算一个简单 8×8 块的电平平移、DCT、量化、反量化与 IDCT。
- 解释 DC 系数、AC 系数、Zig-Zag、游程编码、EOB、ZRL 和 Huffman 编码。
- 根据采样因子计算 MCU(Minimum Coded Unit,最小编码单元)的大小和块数量。
- 识别并解释
SOI、APP0、APP1、DQT、SOF0、DHT、SOS、RSTn、EOI。 - 编写一个不会把熵编码数据误判为 marker 的 JPEG marker 解析器。
- 解释 block artifact、ringing、mosquito noise、color bleeding 和 generation loss。
- 说明为什么不同软件中的“质量 80”不一定得到相同画质或文件大小。
- 设计网页中的 JPEG 上传、缩略图、预览、转码和内存管理管线。
- 回答 JPEG 相关的基础、数学、码流、Web 和系统设计面试题。
二、概念地图
2.1 JPEG 位于媒体系统的哪一层
相机 / AI 模型 / Canvas 产生原始像素
│
▼
RGB / RGBA / Y'CbCr 像素平面
│
▼
JPEG 编码过程(算法与语法)
│
▼
JPEG 压缩图像码流 / 文件
│
┌────────┴────────┐
▼ ▼
APP0/JFIF 信息 APP1/Exif 等元数据
│
▼
HTTP、对象存储、数据库、网页资源
│
▼
<img> / createImageBitmap / ImageDecoder
│
▼
解码后的像素或 GPU 纹理
必须严格区分以下概念:
| 层次 | 示例 | 作用 |
|---|---|---|
| 原始像素 | RGB24、RGBA、Y’CbCr 4:2:0 | 未压缩图像数据 |
| 编码标准 | JPEG 1 / ISO/IEC 10918 / ITU-T T.81 | 定义合法编码过程、解码过程和码流语法 |
| 编码器实现 | libjpeg-turbo、mozjpeg、平台硬件编码器 | 在标准允许范围内选择量化表、采样、Huffman 表等策略 |
| 基础码流 | marker segment、scan、entropy-coded segment | 解码器真正解析的压缩字节 |
| 交换格式与元数据 | JFIF、Exif、ICC profile、XMP | 补充颜色解释、方向、拍摄信息等 |
| 文件名扩展名 | .jpg、.jpeg | 文件命名习惯,不是不同编码标准 |
| 浏览器 API | <img>、Canvas、createImageBitmap()、ImageDecoder | 加载、解码、绘制、处理或重新编码图片 |
| 视频容器 | MP4、WebM | 与普通 JPEG 静态图像不是同一层;JPEG 本身不是 MP4 |
2.2 JPEG、JFIF 与 Exif
- JPEG 主要规定压缩编码与解码过程以及压缩数据的表示。
- JFIF(JPEG File Interchange Format,JPEG 文件交换格式) 约定常见颜色解释、像素密度和
APP0信息,使 JPEG 码流更容易交换。 - Exif(Exchangeable Image File Format,可交换图像文件格式) 常放在
APP1中,保存相机、时间、GPS、缩略图、Orientation 等元数据。 - 一个常见
.jpg文件通常是“JPEG 压缩图像 + JFIF 或 Exif 等应用 marker”,但这些概念不能互换。
2.3 标准与实现策略的边界
| 问题 | 属性 |
|---|---|
| 8×8 DCT 块、系数编码语法、marker 语法 | 标准规定 |
| 使用哪张量化表 | 编码器可选择,码流中必须传给解码器 |
| “quality=80”如何映射为量化表 | 编码器实现策略,不是 JPEG 统一标准 |
| 色度下采样使用盒式、双线性或更高级滤波 | 多为实现策略或交换格式约定 |
| 是否优化 Huffman 表 | 编码器实现策略 |
| IDCT 使用浮点、定点、AAN 快速算法或 SIMD | 解码器实现策略,但输出须满足相应一致性要求 |
| 是否保留 Exif、ICC、XMP | 应用策略 |
三、直觉解释
3.1 JPEG 压缩的核心直觉
一张照片中有三类可利用的冗余:
- 颜色冗余:人眼通常比亮度细节更不敏感于高频色度细节,所以可以减少 Cb、Cr 样本。
- 空间相关性:相邻像素通常相近。8×8 块转换到频域后,能量往往集中在少数低频系数。
- 统计冗余:量化后大量高频系数变为 0,可用游程编码和 Huffman 编码减少比特数。
JPEG 的思路不是“把 64 个像素直接压缩”,而是先问:
这 8×8 个像素主要由哪些平滑变化和细节纹理组成?哪些细节可以粗略表达,哪些可以直接丢掉?
3.2 把 8×8 块想成 64 种纹理积木
二维离散余弦变换(DCT)把一个 8×8 像素块表示成 64 个基函数的加权和:
左上角:整体平均亮度、缓慢变化
↓
越向右:水平方向变化越快
越向下:垂直方向变化越快
↓
右下角:高频细碎纹理
自然照片的大片天空、皮肤、墙面往往主要依赖低频系数;毛发、树叶、文字边缘则需要更多高频系数。
3.3 量化才是主要有损步骤
DCT 本身只是可逆的坐标变换。真正大量丢失信息的是量化:
原始 DCT 系数 37
量化步长 10
编码值 round(37 / 10) = 4
解码重建 4 × 10 = 40
误差 +3
如果高频量化步长更大,许多小系数会被舍入成 0。文件更小,但边缘与纹理会损失。
3.4 为什么会出现 8×8 方块
每个块独立变换和量化。当压缩很强时,相邻块对同一缓慢渐变的近似结果可能不连续,于是块边界可见。这就是典型的 blocking artifact(块效应)。
3.5 为什么文字截图不适合强 JPEG
文字和 UI 有锐利、规则、高对比边缘。这些边缘需要许多高频系数才能准确表示。强量化会造成:
- 边缘模糊;
- 字体周围波纹;
- 色彩边缘扩散;
- 多次保存后误差叠加。
因此照片通常适合 JPEG;文字截图、图标、透明 UI 通常更适合 PNG 或其他无损方案。
四、数学原理
4.1 RGB 到 Y’CbCr
常见 JFIF 工作流会把非线性 RGB 分量转换为亮度相关分量 Y' 和两个色度分量 Cb、Cr。一种常见的 8-bit 全范围近似写法为:
[ Y’ = 0.299R’ + 0.587G’ + 0.114B’ ]
[ Cb = -0.168736R’ - 0.331264G’ + 0.5B’ + 128 ]
[ Cr = 0.5R’ - 0.418688G’ - 0.081312B’ + 128 ]
其中:
R'、G'、B'是经过传递函数编码后的非线性颜色分量;Y'是亮度相关分量;Cb、Cr是蓝色差和红色差分量;+128把有符号色差信号平移到 8-bit 无符号范围。
重要边界: JPEG 核心编码是“按分量编码”,并不等于核心标准强制所有文件都使用这一套 RGB↔Y’CbCr 公式;JFIF 等交换约定负责补足常见颜色解释。
小例子:纯红色
令 R'=255, G'=0, B'=0:
[ Y’ \approx 76.245, Cb \approx 84.972, Cr \approx 255.5 ]
取整并限幅后约为 (76, 85, 255)。纯红色并不是“亮度 255”,它的主要色彩信息落在 Cr 上。
4.2 色度下采样
以 4:2:0 为例,一个 2×2 的亮度区域通常对应 4 个 Y 样本、1 个 Cb 样本和 1 个 Cr 样本:
Y00 Y01 Cb0 Cr0
Y10 Y11
同样的 2×2 区域:
- 4:4:4:
4Y + 4Cb + 4Cr = 12个分量样本; - 4:2:0:
4Y + 1Cb + 1Cr = 6个分量样本。
因此在进入 DCT 前,分量样本数约减少一半。代价是颜色边缘空间分辨率降低。
实现注意: 下采样不是简单“随便丢三个像素”。高质量编码器会先低通滤波,降低混叠;具体滤波器通常属于编码器实现策略。
4.3 8-bit 电平平移
Baseline JPEG 中常见 8-bit 样本范围是 [0,255]。进入 DCT 前减去 128:
[ f(x,y)=s(x,y)-128 ]
其中:
s(x,y)是原始样本;f(x,y)是以 0 为中心的样本。
这使输入落在 [-128,127],方便变换和系数范围管理。
小例子
一个像素值为 140,平移后是:
[ 140-128=12 ]
4.4 二维 8×8 DCT
正向二维 DCT 可写为:
[ F(u,v)=\frac14 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} ]
其中:
[ C(k)= \begin{cases} \frac{1}{\sqrt2}, & k=0\ 1, & k>0 \end{cases} ]
x,y是空间位置;u,v是频率索引;F(0,0)是 DC(Direct Current,直流)系数,描述块的平均水平;- 其余 63 个是 AC(Alternating Current,交流)系数,描述不同方向与频率的变化。
可手算例子:常量块
假设 8×8 块所有原始像素都是 140。电平平移后所有值都是 12。
对于 (u,v)=(0,0):
[ F(0,0)=\frac14\cdot\frac1{\sqrt2}\cdot\frac1{\sqrt2}\cdot64\cdot12 =96 ]
所有 AC 系数理论上都是 0。因此常量块只需表达一个 DC 值,极易压缩。
4.5 DCT 的可分离性
二维 DCT 可以先对每一行做一维 DCT,再对每一列做一维 DCT:
[ F = A f A^T ]
这称为可分离性。朴素二维计算需要大量乘加;利用可分离性和快速 DCT 算法,可以显著减少运算,并便于 SIMD(Single Instruction, Multiple Data,单指令多数据)优化。
4.6 量化
对每个 DCT 系数除以对应量化步长并取整:
[ \hat F(u,v)=\operatorname{round}\left(\frac{F(u,v)}{Q(u,v)}\right) ]
其中:
F(u,v)是原始 DCT 系数;Q(u,v)是量化表项;\hat F(u,v)是写入后续编码流程的量化系数。
解码时反量化:
[ F’(u,v)=\hat F(u,v)Q(u,v) ]
小例子
若 F=37, Q=10:
[ \hat F=\operatorname{round}(3.7)=4, \quad F’=4\times10=40 ]
系数误差为 +3。经过 IDCT 后,该误差会分散到多个像素。
“质量参数”为什么不统一
JPEG 码流只传量化表,不传“质量 80”这一抽象语义。编码器把用户给出的质量值映射成量化表,映射曲线、基础表、色度策略可能不同,所以:
软件 A 的质量 80,不保证等于软件 B 的质量 80。
这是编码器实现策略,不是 JPEG 标准中的统一质量刻度。
4.7 Zig-Zag 扫描
量化后的 8×8 系数按大致从低频到高频的顺序展开:
0 1 5 6 14 15 27 28
2 4 7 13 16 26 29 42
3 8 12 17 25 30 41 43
9 11 18 24 31 40 44 53
10 19 23 32 39 45 52 54
20 22 33 38 46 51 55 60
21 34 37 47 50 56 59 61
35 36 48 49 57 58 62 63
目的不是进一步丢失信息,而是把常见的高频 0 聚集到序列尾部,方便游程编码。
4.8 DC 差分编码
相邻块的平均亮度通常相近,因此 JPEG 不直接编码每个块的 DC 值,而编码与同分量前一块 DC 的差:
[ DIFF_i=DC_i-DC_{i-1} ]
例如:
前一块 DC = 100
当前块 DC = 106
DIFF = 6
随后编码:
SIZE:表示绝对值需要多少比特;- Huffman 码:编码
SIZE; - additional bits:编码具体差值。
6 的二进制是 110,因此 SIZE=3。
负数采用 JPEG 规定的幅值映射。比如 -6 在 SIZE=3 时,附加位为 001,可理解为正数幅值位 110 的逐位取反。
4.9 AC 游程编码
Zig-Zag 后的 63 个 AC 系数通常包含大量 0。每个非零系数用 (RUN, SIZE) 表示:
RUN:它前面连续 0 的数量,范围 0~15;SIZE:该非零幅值所需比特数。
组合成一个字节概念:
[ RS=(RUN \ll 4);|;SIZE ]
特殊符号:
EOB = 0x00:End of Block,后续系数全为 0;ZRL = 0xF0:Zero Run Length,表示连续 16 个 0。
小例子
AC 序列开头为:
0, 0, 5, 0, 0, 0, -2, 0, 0, ... 后面全 0
则可编码为:
(2, SIZE(5)=3) + 5 的附加位
(3, SIZE(-2)=2) + -2 的附加位
EOB
4.10 Huffman 编码与理论平均码长
Huffman 编码给高概率符号较短码字、低概率符号较长码字,并保持前缀码性质。
若符号概率为:
| 符号 | 概率 | 码字示例 |
|---|---|---|
| A | 0.5 | 0 |
| B | 0.25 | 10 |
| C | 0.125 | 110 |
| D | 0.125 | 111 |
平均码长为:
[ L=0.5\times1+0.25\times2+0.125\times3+0.125\times3=1.75\text{ bit/symbol} ]
JPEG 的 DC 和 AC 通常使用不同 Huffman 表,亮度和色度也可使用不同表。
4.11 失真与 PSNR
均方误差:
[ MSE=\frac1N\sum_{i=1}^{N}(x_i-y_i)^2 ]
峰值信噪比:
[ PSNR=10\log_{10}\frac{MAX^2}{MSE} ]
8-bit 图像中 MAX=255。若 MSE=100:
[ PSNR\approx10\log_{10}(650.25)\approx28.13\text{ dB} ]
PSNR 适合做客观对比,但不能完整反映视觉质量。相同 PSNR 下,文字边缘破坏和随机纹理误差的主观感受可能差异很大。
五、底层数据结构
5.1 JPEG marker 的基本形式
大多数 marker 以 0xFF 开头,后接一个非 0x00、非 0xFF 的 marker code:
FF D8 SOI
FF E0 APP0
FF DB DQT
FF C0 SOF0
FF C4 DHT
FF DA SOS
FF D9 EOI
对带参数的 marker segment,常见结构为:
+----------+-------------+----------------------+
| Marker | Length | Payload |
| FF xx | 2 bytes BE | Length - 2 bytes |
+----------+-------------+----------------------+
Length 使用大端序,并且包含长度字段自身的 2 个字节,但不包含前面的 2 字节 marker。
5.2 常见 marker 速览
| Marker | 十六进制 | 作用 |
|---|---|---|
| SOI | FFD8 | Start of Image,图像开始 |
| APP0 | FFE0 | 常见 JFIF 信息 |
| APP1 | FFE1 | 常见 Exif 或 XMP 信息 |
| DQT | FFDB | Define Quantization Table,定义量化表 |
| SOF0 | FFC0 | Baseline DCT 帧参数 |
| SOF2 | FFC2 | Progressive DCT 帧参数 |
| DHT | FFC4 | Define Huffman Table,定义 Huffman 表 |
| DRI | FFDD | Define Restart Interval,定义重启间隔 |
| SOS | FFDA | Start of Scan,扫描开始 |
| RST0~RST7 | FFD0~FFD7 | 重启 marker |
| COM | FFFE | 注释 |
| EOI | FFD9 | End of Image,图像结束 |
5.3 DQT:量化表
DQT payload 可包含一张或多张量化表:
Pq/Tq Q0 Q1 Q2 ... Q63
Pq:精度,常见 Baseline 为 0,表示 8-bit 表项;Tq:表编号;- 后续 64 个值按规定顺序传输。
解析器必须根据 Pq 决定每个表项占 1 字节还是 2 字节,不能硬编码所有 JPEG 都是 64 字节表。
5.4 SOF0:帧头
SOF0 典型字段:
Lf 2 bytes 段长度
P 1 byte 样本精度,常见为 8
Y 2 bytes 高度
X 2 bytes 宽度
Nf 1 byte 分量数量
对每个分量:
C_i 1 byte 分量 ID
H_i | V_i 1 byte 水平/垂直采样因子
Tq_i 1 byte 使用的量化表编号
例如常见 4:2:0:
Y : H=2, V=2
Cb: H=1, V=1
Cr: H=1, V=1
最大采样因子为 Hmax=2, Vmax=2,因此一个 MCU 覆盖:
[ 8H_{max}\times8V_{max}=16\times16\text{ 个亮度位置} ]
每个 MCU 内含:
- 4 个 Y 块;
- 1 个 Cb 块;
- 1 个 Cr 块;
- 共 6 个 8×8 块。
5.5 MCU 数量计算
对于宽 W、高 H 的图像:
[ MCU_x=\left\lceil\frac{W}{8H_{max}}\right\rceil, \quad MCU_y=\left\lceil\frac{H}{8V_{max}}\right\rceil ]
例如 1920×1080、4:2:0:
[ MCU_x=\lceil1920/16\rceil=120 ]
[ MCU_y=\lceil1080/16\rceil=68 ]
因为 1080/16=67.5,底部需要补齐到第 68 行 MCU。编码器会按边界扩展或其他合法策略补足块,解码后再裁到真实尺寸。
5.6 DHT:Huffman 表
DHT 中每张表包含:
Tc/Th
L1 L2 ... L16
V1 V2 ... Vk
Tc:表类别,DC 或 AC;Th:表编号;Li:长度恰为i的码字数量;- 后续值数组给出对应符号。
码流不是直接保存“符号→任意 bit 字符串”字典,而是用各码长数量和符号顺序描述规范 Huffman 表,解码器可重建码字。
5.7 SOS:扫描头
SOS 指定本次 scan 包含哪些分量,以及各分量使用哪张 DC/AC Huffman 表。Baseline 顺序 JPEG 常见只有一个 scan,覆盖所有分量与全部系数;Progressive JPEG 会用多个 scan 分批传输系数或精度。
5.8 熵编码段与字节填充
SOS 之后进入 entropy-coded segment(熵编码数据段)。此处的压缩 bitstream 可能自然产生字节 0xFF。为了避免它被误认为 marker 前缀,编码器必须在数据中的 0xFF 后插入 0x00:
原始熵数据字节:FF
文件中写成: FF 00
解析 scan 时:
FF 00:还原为数据字节FF;FF D0~FF D7:Restart marker;- 其他合法
FF xx:scan 结束,进入新 marker。
这也是“只用 indexOf(0xFF) 找 marker”会失败的原因。
5.9 Restart marker
若 DRI 定义每隔若干 MCU 插入重启点,则编码器循环写入 RST0~RST7。在重启点:
- DC 预测值重置;
- Huffman bitstream 对齐到字节边界;
- 解码器可在局部错误后恢复;
- 某些实现可把区间作为并行或分片处理边界。
Restart marker 是标准工具,但是否启用、间隔多大通常是编码器策略。
5.10 APP0、APP1 与未知 APPn
应用 marker 不应影响核心 JPEG 像素解码。健壮解析器应:
- 识别自己关心的 JFIF、Exif、ICC、XMP;
- 对不认识的 APPn 按长度跳过;
- 修改文件时尽量保留未知但合法的元数据;
- 永远先验证长度,防止越界与恶意文件。
六、编码器流程
6.1 Baseline JPEG 完整流程
RGB / RGBA 输入
│
├─ 处理方向、裁剪、缩放、颜色配置
├─ Alpha 合成到指定背景(JPEG 不保存 Alpha)
▼
RGB → Y'CbCr(常见 JFIF 工作流)
▼
色度低通与下采样(可选 4:4:4 / 4:2:2 / 4:2:0)
▼
按 MCU 顺序组织各分量 8×8 block
▼
每个样本减 128
▼
二维 8×8 DCT
▼
按量化表量化
▼
DC 差分 + AC Zig-Zag / 游程编码
▼
Huffman 熵编码
▼
0xFF 字节填充、可选 Restart marker
▼
写入 SOI / APPn / DQT / SOF / DHT / SOS / EOI
▼
JPEG 文件
6.2 输入规范化
编码器首先需要确定:
- 宽、高和位深;
- 输入是 RGB、RGBA、灰度还是 Y’CbCr;
- Alpha 如何处理;
- 是否应用 Exif Orientation;
- 使用哪个颜色空间和 ICC profile;
- 输出是否保留元数据。
JPEG 通常不保存 Alpha 通道。把 RGBA 导出为 JPEG 前必须指定背景色,否则透明区域可能被实现默认合成为黑色、白色或其他结果。
6.3 颜色转换与采样选择
常见选择:
| 模式 | 色度细节 | 文件大小倾向 | 适用内容 |
|---|---|---|---|
| 4:4:4 | 最高 | 较大 | UI、文字、彩色细线、需要后续编辑 |
| 4:2:2 | 水平减半 | 中等 | 某些专业工作流 |
| 4:2:0 | 水平和垂直均减半 | 较小 | 照片、网页大图、普通预览 |
| 灰度 | 仅 Y | 最小 | 黑白图、文档灰度预览 |
编码器实现策略: 即使质量参数相同,编码器选择不同色度采样也会显著改变文字边缘质量和文件大小。
6.4 MCU 遍历
编码器按 MCU 顺序读取块。对于 4:2:0,每个 MCU 通常按类似顺序处理:
Y00 → Y01 → Y10 → Y11 → Cb → Cr
每个分量拥有独立 DC 预测器。Y 块的 DC 与前一个 Y 块比较,Cb 与前一个 Cb 块比较,Cr 同理。
6.5 单块编码伪代码
function encodeBlock(samples[8][8], quantTable, previousDC):
// 1. 电平平移
for y in 0..7:
for x in 0..7:
shifted[y][x] = samples[y][x] - 128
// 2. 变换
coeff = DCT8x8(shifted)
// 3. 量化
for v in 0..7:
for u in 0..7:
qcoeff[v][u] = round(coeff[v][u] / quantTable[v][u])
// 4. DC 差分
dc = qcoeff[0][0]
diff = dc - previousDC
emitHuffman(DC_TABLE, category(diff))
emitAmplitudeBits(diff)
// 5. AC Zig-Zag 与游程编码
sequence = zigZag(qcoeff)[1..63]
zeroRun = 0
for value in sequence:
if value == 0:
zeroRun += 1
continue
while zeroRun >= 16:
emitHuffman(AC_TABLE, ZRL)
zeroRun -= 16
size = category(value)
emitHuffman(AC_TABLE, (zeroRun << 4) | size)
emitAmplitudeBits(value)
zeroRun = 0
if zeroRun > 0:
emitHuffman(AC_TABLE, EOB)
return dc
6.6 Huffman 表选择
编码器可以:
- 使用常见默认表,编码快、无需先统计整图;
- 先统计符号频率,再生成针对该图优化的 Huffman 表,通常文件更小,但需要额外遍历或缓存。
这是编码器策略。解码器不需要猜测,因为实际表由 DHT 传入。
6.7 目标质量与目标文件大小
JPEG 静态图没有视频编码中的 VBV、GOP 或帧间码率控制。常见策略是:
- 固定质量:选择量化表后一次编码;
- 目标文件大小:多次试编码,二分搜索质量或量化缩放;
- 感知优化:针对纹理、边缘或视觉敏感频率调整量化;
- 元数据控制:删除不必要的 Exif 缩略图、GPS、XMP 以减小文件。
目标文件大小通常不是单次公式就能精确得到,因为 Huffman 统计、图像复杂度和 0 系数分布都会影响结果。
6.8 Progressive JPEG 编码
Progressive JPEG 不是把图片简单切成“低清图 + 高清图”,而是用多个 scan 逐步发送系数:
- Spectral selection:先发低频,再发高频;
- Successive approximation:先发系数高位,再逐步细化低位。
优点是网络加载时可较早看到整图轮廓。代价是编码、解码状态与 scan 管理更复杂;最终完全解码后可以达到与相同量化条件下顺序 JPEG 相当的重建结果。
6.9 编码阶段最重要的工程决策
| 决策 | 主要影响 |
|---|---|
| 量化表 | 画质与文件大小的主控制器 |
| 色度采样 | 彩色边缘质量与大小 |
| 下采样滤波器 | 色度混叠、彩边 |
| Huffman 表优化 | 文件大小与编码耗时 |
| Baseline / Progressive | 加载体验、解码复杂度 |
| Restart interval | 错误恢复、分片和少量额外开销 |
| 元数据保留 | 隐私、方向、色彩、文件大小 |
| ICC profile | 跨设备颜色一致性 |
七、解码器流程
7.1 完整流程
读取字节流并验证 SOI
▼
循环解析 APPn / DQT / SOF / DHT / DRI 等段
▼
读取 SOS,建立 scan 配置
▼
按 bit 读取 Huffman 码字
▼
恢复 DC 差分和 AC (RUN, SIZE)
▼
得到每个块的 64 个量化系数
▼
逆 Zig-Zag
▼
反量化
▼
IDCT
▼
加 128、限幅到样本范围
▼
按采样因子拼装 MCU 与分量平面
▼
色度上采样
▼
Y'CbCr → RGB
▼
应用颜色配置、方向和目标像素格式
▼
RGBA / RGB / GPU 纹理
7.2 解析前的安全检查
处理不可信 JPEG 时,必须在分配大块内存前检查:
- 文件至少包含合法 SOI;
- 每个段长度不小于最小合法值;
offset + length不越界;- 宽、高、分量数和采样因子在实现上限内;
- 计算
width × height × bytesPerPixel时防止整数溢出; - DQT、DHT 的表编号和元素数量合法;
- scan 组件引用已定义组件和表;
- Progressive scan 参数组合合法;
- 设置最大像素数、最大内存和最大解码时间。
压缩文件只有几 MB,不代表解码后也只有几 MB。4000×3000 RGBA 像素约占:
[ 4000\times3000\times4\approx45.8\text{ MiB} ]
还未计算中间分量、系数缓冲和渲染副本。
7.3 Huffman bit 解码
解码器逐 bit 扩展当前码字,直到命中表中的合法码长。高性能实现通常不逐 bit 查树,而使用:
- 8~12 bit 快速查表;
- 未命中时再走慢路径;
- SIMD 或批量 bit buffer;
- 对输入不足和非法码字严格报错。
这是实现优化,不改变标准语义。
7.4 恢复 DC
对每个块:
- Huffman 解出
SIZE; - 读取
SIZE个附加位; - 根据 JPEG 幅值规则恢复有符号
DIFF; DC = previousDC + DIFF;- 更新该分量的
previousDC。
在图像开始和每个 Restart marker 后,DC 预测器重置为 0。
7.5 恢复 AC
初始化系数位置 k=1:
- 解出
RS; - 若为
EOB,其余系数填 0; - 若为
ZRL,跳过 16 个 0; - 否则跳过
RUN个 0; - 读取
SIZE个附加位恢复非零值; - 写入 Zig-Zag 位置并继续。
任何使 k 超出 63 的符号序列都应视为错误,不能写出数组边界。
7.6 反量化与 IDCT
反量化把量化系数乘回表项,但被取整丢失的低位无法恢复。随后 IDCT:
[ f’(x,y)=\frac14 \sum_{u=0}^{7}\sum_{v=0}^{7} C(u)C(v)F’(u,v) \cos\frac{(2x+1)u\pi}{16} \cos\frac{(2y+1)v\pi}{16} ]
再执行:
[ s’(x,y)=\operatorname{clip}(\operatorname{round}(f’(x,y)+128),0,255) ]
不同快速 IDCT 可能存在极小舍入差异,但合规实现必须满足标准或一致性测试所要求的误差界限。
7.7 色度上采样
4:2:0 解码得到的 Cb、Cr 分辨率低于 Y,需要上采样到输出大小。可选策略包括:
- 最近邻:快,但易出现块状色边;
- 双线性:常用且平滑;
- 更高阶滤波:质量更好,计算更多。
上采样滤波和 chroma siting(色度样本位置)处理会影响彩色细线和文字边缘。仅仅“DCT 解对了”不保证最终 RGB 一定没有色偏或彩边。
7.8 元数据、方向与颜色
像素解码完成后还可能需要:
- 按 Exif Orientation 旋转或镜像;
- 使用嵌入 ICC profile 做颜色转换;
- 处理 JFIF 密度信息;
- 决定是否保留或清除 GPS、相机序列号等隐私元数据。
常见 Bug 是:预览组件自动应用了 Orientation,但后端像素处理没有应用,导致预览和导出方向不一致。
7.9 Progressive JPEG 解码
Progressive 解码器需保存尚未完成的系数缓冲。每个 scan 只补充部分系数或部分有效位;完成新的 scan 后可生成更清晰的预览。与 Baseline 相比,它通常需要更复杂的状态机和更多系数内存。
7.10 错误恢复
有 Restart marker 时,解码器可跳到后续重启区间继续;没有重启点时,熵码错误可能破坏后续边界。应用层需要决定:
- 严格失败;
- 输出部分图像;
- 用灰块或邻近数据做错误隐藏;
- 记录损坏位置并请求重传。
错误隐藏通常属于解码器或应用策略,不是 JPEG 必须生成某种替代像素的统一规定。
八、复杂度与性能
8.1 时间复杂度
令图像宽高为 W×H。块数与像素数成正比,因此 JPEG 编解码总体可视为:
[ T(W,H)=O(WH) ]
但常数差异很大:
- 高质量色度滤波比简单抽样更贵;
- 优化 Huffman 表可能需要额外遍历;
- Progressive 需要多 scan 状态;
- 高精度 IDCT、颜色管理和缩放会增加成本;
- 编码通常比解码决策更多,但 JPEG 编码搜索远少于现代视频编码。
8.2 内存占用
典型内存项:
| 缓冲 | 是否一定需要整图 |
|---|---|
| 输入 RGB/RGBA | 取决于上游接口 |
| Y、Cb、Cr 平面 | 可按条带处理,不一定整图 |
| 8×8 系数 | Baseline 可流式;Progressive 通常需保存更多系数 |
| 输出 RGBA | 若交给 Canvas 或 UI,常为整图 |
| GPU 纹理 | 可能形成额外副本 |
| Exif/ICC 元数据 | 通常远小于像素,但恶意文件仍需限长 |
高性能解码器可按 MCU 行解码并直接做颜色转换,避免同时保存完整 Y’CbCr 与 RGBA 两份数据。
8.3 CPU、SIMD 与 GPU
CPU 擅长:
- marker 和变长 bitstream 解析;
- Huffman 解码;
- 小块 DCT/IDCT;
- 分支较多的错误检查。
SIMD 擅长:
- DCT/IDCT;
- 颜色转换;
- 上采样;
- 批量像素裁剪和打包。
GPU 擅长:
- 已解码像素的缩放、滤镜、合成;
- 大量图片的纹理处理;
- 与 WebGL/WebGPU 渲染管线衔接。
把一个小 JPEG 上传 GPU 仅为做 Huffman 解码通常不划算;bitstream 分支、传输开销和同步可能抵消收益。实际平台常使用专用图像硬件或高度优化的 CPU/SIMD 库。
8.4 并行化
可并行的部分:
- 独立图片之间;
- Restart interval 之间;
- DCT/IDCT 和颜色转换的块级计算;
- 解码后缩放和滤镜;
- 缩略图批处理。
受依赖限制的部分:
- 同一分量的 DC 差分跨块依赖;
- 单一 Huffman bitstream 是串行字节流;
- 没有 Restart marker 时难以从任意字节安全开始。
8.5 缩放解码
JPEG 的 DCT 结构允许某些解码器直接输出 1/2、1/4、1/8 等近似缩小尺寸,而不必先完整 IDCT 到原图再缩放。这对缩略图服务非常重要:
错误思路:4000×3000 全尺寸解码 → RGBA → 缩到 400×300
更优思路:缩放 IDCT / 解码期降采样 → 接近目标尺寸 → 最终重采样
具体可用比例与质量属于解码器实现能力,应通过所用库的 API 确认。
8.6 常见性能瓶颈
- 解码后 RGBA 占用远大于压缩文件;
- 主线程批量解码导致页面卡顿;
- Canvas
getImageData()触发 GPU→CPU 读回; - 同一图片在
<img>、Canvas、ImageBitmap、RGBA 数组中重复保存; - 没有及时关闭
VideoFrame、ImageBitmap或撤销 Object URL; - 为小缩略图先解码超大原图;
- 每次拖动时间线都重新请求和解码相同缩略图;
- 服务端反复 JPEG 重编码造成 CPU 开销和 generation loss;
- 同步读取 Exif 或 ICC 阻塞上传主路径;
- 不限制像素数,遭遇内存耗尽或解码拒绝服务。
8.7 编码复杂度与解码复杂度
与 H.264/H.265 编码不同,JPEG 没有帧间运动搜索、参考帧、复杂模式决策和 GOP。其编码与解码成本更接近,但编码器仍可能因以下策略更慢:
- 两遍统计并优化 Huffman 表;
- 多次试编码命中文件大小;
- 更好的下采样滤波;
- 感知量化;
- Progressive scan 脚本优化。
九、实际场景
A. AI 生成视频网页
9.1 JPEG 的正确定位
JPEG 适合:
- 生成任务封面;
- 文生视频首帧预览;
- 低成本占位图;
- 结果列表缩略图;
- 审核、检索或相似度系统的静态采样帧;
- 分享卡片和社交预览图。
JPEG 不适合长期替代视频编码:
30 FPS × 每帧独立 JPEG
= 完全不利用帧间相似性
= 带宽、存储、请求和解码开销都很高
MJPEG(Motion JPEG)在某些低延迟摄像或简单流中有价值,但通常不是 AI 生成视频网页最终播放的首选。最终视频应使用合适的视频编码与容器。
9.2 边生成边预览
一种合理分层:
阶段 1:返回小尺寸 JPEG 占位或首帧
阶段 2:定期更新关键预览帧或短视频片段
阶段 3:输出可播放的 fMP4/HLS/DASH 等视频结果
阶段 4:生成封面、缩略图和分享图
不要每生成一帧就无上限塞入页面。应设置:
- 最大待解码图片数;
- 最新帧优先或按序展示策略;
- 过期预览丢弃;
- 解码并发上限;
- Object URL 生命周期;
- 网络和解码 backpressure(背压)。
9.3 预览质量策略
| 阶段 | 建议 |
|---|---|
| 极早预览 | 小尺寸、较强压缩,快速到达 |
| 用户停留后 | 请求更高分辨率或更高质量 |
| 最终封面 | 按展示尺寸编码,保留必要 ICC,清理隐私元数据 |
| 文字密集结果 | 考虑 4:4:4、PNG/WebP/AVIF 等更合适方案,不能盲用 4:2:0 JPEG |
9.4 内存控制
若同时展示 50 张 1024×1024 RGBA 预览,理论像素内存约为:
[ 50\times1024\times1024\times4\approx200\text{ MiB} ]
即使每张 JPEG 文件只有几十或几百 KB,解码后依然可能造成明显内存压力。列表应使用虚拟化、按可见区解码、降低缩略图尺寸并及时释放资源。
B. 在线 Web 视频编辑器
9.5 JPEG 在编辑器中的角色
JPEG 常用于:
- 媒体库封面;
- 时间线视频缩略图;
- Storyboard(故事板)帧;
- 代理素材预览;
- 模板预览和搜索索引;
- 导出前的静态封面。
它不负责视频的:
- PTS/DTS;
- GOP 与关键帧;
- 帧间预测;
- 音视频同步;
- MP4 mux;
- 视频帧精确剪切。
9.6 时间线缩略图
推荐流程:
视频解复用
▼
按目标时间 Seek 到邻近关键帧
▼
解码到目标帧
▼
缩放到时间线所需尺寸
▼
编码为 JPEG 缩略图或组成 sprite sheet
▼
CDN / IndexedDB / 内存缓存
缩略图生成时应以显示尺寸为目标,不应把 4K 帧编码成 4K JPEG 再在 CSS 中缩小。
9.7 帧精确编辑
JPEG 缩略图只用于视觉导航,不能作为帧精确剪切的时间依据。时间线数据模型应保存:
- 媒体时间;
- 源文件标识;
- 轨道和剪辑边界;
- 变速映射;
- 显示缩略图对应的近似时间。
最终剪切仍需依据视频 sample、时间戳和解码结果。
9.8 滤镜与合成
若用户对 JPEG 素材应用多次滤镜:
错误:每一步都 JPEG 解码 → 处理 → JPEG 重编码
正确:JPEG 只解码一次 → 在线性或明确颜色空间的像素/纹理中编辑 → 最终一次编码
这能减少 generation loss,并避免每一步都重复引入 8×8 量化误差。
9.9 Alpha 与透明素材
JPEG 不保存 Alpha。编辑器内的透明贴纸、Logo、字幕栅格层不应以普通 JPEG 作为中间格式。可使用 PNG、带 Alpha 的现代图像格式、原始纹理或矢量表示。
9.10 代理视频与 JPEG 序列
JPEG 序列便于逐帧随机访问,但存储与 I/O 通常较大。是否用作代理格式取决于:
- 是否要求每帧独立;
- 网络还是本地桌面环境;
- 目标分辨率;
- 是否需要 Alpha;
- 解码硬件能力;
- 存储预算。
浏览器在线编辑器通常更适合使用低分辨率代理视频配合时间线缩略图,而不是把整段视频长期转换成 JPEG 序列。
9.11 问题—JPEG 作用矩阵
| 编辑问题 | JPEG 的作用 | 不能解决的部分 |
|---|---|---|
| 视频预览 | 封面、占位图 | 连续播放与音频同步 |
| 边生成边播放 | 早期静态预览 | 最终流媒体组织 |
| 缩略图 | 非常适合照片型小图 | 精确时间戳 |
| Seek | 提供视觉索引 | 视频关键帧定位与解码 |
| 帧精确剪切 | 辅助确认画面 | sample 边界、PTS/DTS |
| 时间线 | 缩略图条 | 核心编辑状态模型 |
| 代理视频 | 可做帧序列但代价高 | 高效帧间压缩 |
| 滤镜与合成 | 输入/最终静态输出 | Alpha、中间无损状态 |
| 浏览器编码 | Canvas 可导出 JPEG | 完整视频编码与封装 |
| 服务端导出 | 封面、缩略图 | MP4/HLS 视频成品 |
| 兼容性降级 | 静态预览兜底 | 视频能力替代 |
| 内存控制 | 小文件利于传输 | 解码后像素仍很大 |
| Backpressure | 可丢弃过期预览 | 仍需应用自行控制队列 |
十、Web 实现视角
10.1 API 选择
| 需求 | 优先方案 |
|---|---|
| 只展示图片 | <img> |
| 异步解码并作为绘制源 | createImageBitmap() |
| 像素级处理 | Canvas 2D、WebGL 或 WebGPU |
| Worker 中缩放并导出 | OffscreenCanvas + convertToBlob() |
| 显式图片解码、渐进状态或帧资源控制 | WebCodecs ImageDecoder,先探测支持 |
| JPEG 编码 | Canvas toBlob() / OffscreenCanvas convertToBlob(),或 WASM/服务端编码器 |
| 读取 Exif/marker | ArrayBuffer + 自定义解析器或可靠库 |
WebCodecs 规范提供 ImageDecoder,但不要求用户代理支持任何特定图像类型。因此不能仅因为浏览器有 ImageDecoder 就假定支持 JPEG;应调用 ImageDecoder.isTypeSupported("image/jpeg")。
10.2 最简单的展示
<img
src="/preview.jpg"
width="640"
height="360"
decoding="async"
alt="生成视频预览"
/>
明确 width 和 height 有助于页面在图片到达前保留布局空间。纯展示场景通常不需要把像素拉进 JavaScript。
10.3 使用 createImageBitmap 生成缩略图
async function decodeForThumbnail(blob: Blob): Promise<ImageBitmap> {
return createImageBitmap(blob, {
resizeWidth: 320,
resizeHeight: 180,
resizeQuality: "high",
imageOrientation: "from-image",
});
}
async function drawThumbnail(blob: Blob, canvas: HTMLCanvasElement) {
const bitmap = await decodeForThumbnail(blob);
try {
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2D canvas is unavailable");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
} finally {
bitmap.close();
}
}
注意:resizeWidth 和 resizeHeight 同时固定会拉伸非同宽高比图片。生产代码应先读取尺寸,按 contain/cover 规则计算目标矩形。
10.4 Canvas 导出 JPEG
function canvasToJpeg(
canvas: HTMLCanvasElement,
quality = 0.82,
): Promise<Blob> {
return new Promise((resolve, reject) => {
canvas.toBlob(
(blob) => blob ? resolve(blob) : reject(new Error("JPEG encode failed")),
"image/jpeg",
quality,
);
});
}
quality 是 0~1 的质量提示,不是跨浏览器、跨编码器统一量化尺度。导出后应检查 blob.type 和实际文件签名,并通过视觉质量、大小和性能测试确定参数。
10.5 Worker + OffscreenCanvas
async function makeJpegPreview(
source: ImageBitmap,
width: number,
height: number,
): Promise<Blob> {
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2D OffscreenCanvas is unavailable");
ctx.drawImage(source, 0, 0, width, height);
return canvas.convertToBlob({
type: "image/jpeg",
quality: 0.8,
});
}
把图片缩放、滤镜和编码移到 Worker 可减轻主线程压力。仍需限制并发,因为 Worker 不会自动解决总内存过高的问题。
10.6 WebCodecs ImageDecoder
async function decodeJpegWithWebCodecs(blob: Blob): Promise<VideoFrame> {
if (!("ImageDecoder" in globalThis)) {
throw new Error("ImageDecoder is unavailable");
}
const supported = await ImageDecoder.isTypeSupported("image/jpeg");
if (!supported) {
throw new Error("image/jpeg is not supported by ImageDecoder");
}
const data = await blob.arrayBuffer();
const decoder = new ImageDecoder({
type: "image/jpeg",
data,
});
try {
const result = await decoder.decode({
frameIndex: 0,
completeFramesOnly: true,
});
// 调用者取得 VideoFrame 所有权,使用完必须 close()。
return result.image;
} finally {
decoder.close();
}
}
调用处:
const frame = await decodeJpegWithWebCodecs(blob);
try {
ctx.drawImage(frame, 0, 0);
} finally {
frame.close();
}
VideoFrame.close() 不是可有可无的风格问题。未及时释放底层媒体资源可能造成内存压力,甚至让后续解码停滞。
10.7 能力探测与降级
ImageDecoder + JPEG 支持
├─ 是:显式解码,获得 VideoFrame
└─ 否:createImageBitmap(blob)
├─ 成功:获得 ImageBitmap
└─ 失败:<img> / 服务端转码 / 错误提示
不要使用 User-Agent 字符串推断编码能力。能力应在运行时探测,并准备降级路径。
10.8 健壮的 marker 解析示例
下面的 TypeScript 示例只做结构扫描,不实现 DCT/Huffman 解码,但正确处理了 FF00 字节填充和 Restart marker:
type JpegMarker = {
offset: number;
code: number;
name: string;
length?: number;
scanDataStart?: number;
scanDataEnd?: number;
};
const MARKER_NAMES: Record<number, string> = {
0xd8: "SOI",
0xd9: "EOI",
0xe0: "APP0",
0xe1: "APP1",
0xdb: "DQT",
0xc0: "SOF0",
0xc2: "SOF2",
0xc4: "DHT",
0xdd: "DRI",
0xda: "SOS",
0xfe: "COM",
};
function readU16BE(data: Uint8Array, offset: number): number {
if (offset + 1 >= data.length) throw new Error("Unexpected end of JPEG");
return (data[offset] << 8) | data[offset + 1];
}
function isStandaloneMarker(code: number): boolean {
return code === 0xd8 || code === 0xd9 || code === 0x01 ||
(code >= 0xd0 && code <= 0xd7);
}
export function parseJpegMarkers(data: Uint8Array): JpegMarker[] {
if (data.length < 2 || data[0] !== 0xff || data[1] !== 0xd8) {
throw new Error("Not a JPEG SOI stream");
}
const result: JpegMarker[] = [];
let i = 0;
while (i < data.length) {
if (data[i] !== 0xff) {
throw new Error(`Expected marker at offset ${i}`);
}
const markerOffset = i;
while (i < data.length && data[i] === 0xff) i++;
if (i >= data.length) throw new Error("Truncated marker");
const code = data[i++];
if (code === 0x00) throw new Error("Unexpected stuffed byte outside scan");
const marker: JpegMarker = {
offset: markerOffset,
code,
name: MARKER_NAMES[code] ??
(code >= 0xd0 && code <= 0xd7 ? `RST${code - 0xd0}` : `0xFF${code.toString(16)}`),
};
result.push(marker);
if (isStandaloneMarker(code)) {
if (code === 0xd9) break;
continue;
}
const length = readU16BE(data, i);
if (length < 2) throw new Error(`Invalid segment length at ${i}`);
if (i + length > data.length) throw new Error("Segment exceeds file size");
marker.length = length;
i += length; // 2 字节 length + (length - 2) payload
if (code !== 0xda) continue;
marker.scanDataStart = i;
// 扫描熵编码数据,直到遇到非 stuffed、非 restart 的 marker。
while (i < data.length) {
if (data[i] !== 0xff) {
i++;
continue;
}
let j = i + 1;
while (j < data.length && data[j] === 0xff) j++;
if (j >= data.length) throw new Error("Truncated scan data");
const next = data[j];
if (next === 0x00) {
i = j + 1; // FF 00 是数据中的 FF
continue;
}
if (next >= 0xd0 && next <= 0xd7) {
i = j + 1; // 跳过 RSTn
continue;
}
marker.scanDataEnd = i;
break; // i 保持在下一个 marker 的 FF 处
}
}
return result;
}
生产级解析器还必须验证每种 marker 的最小字段长度、分量引用、表编号、Progressive 参数和资源上限。
10.9 CORS 与 Canvas 污染
跨源图片若没有正确 CORS 授权,虽然可能可以显示,但绘制到 Canvas 后读取像素或导出会触发安全限制。工程上应:
- 让图片服务器返回正确 CORS 响应;
- 在设置
src前配置crossOrigin; - 或通过同源图片代理获取;
- 不把“能在
<img>显示”误认为“能从 Canvas 导出”。
10.10 元数据保留策略
Canvas 或像素级重编码通常以“解码后的像素”为输入,不会自动完整复制原 JPEG 的 Exif、ICC、XMP、GPS 和相机信息。应明确选择:
- 隐私优先:清理全部非必要元数据;
- 颜色优先:保留或重建 ICC profile;
- 摄影工作流:保留必要 Exif,但移除 GPS;
- 方向规范化:把像素真正旋转到正确方向,再把 Orientation 写为正常或移除。
十一、易错点
- JPEG 不等于“只能有损”。 常见 Baseline JPEG 是有损 DCT 模式,但 JPEG 1 还定义过无损、算术编码和分层模式。
.jpg与.jpeg通常只是扩展名长度不同。 不能据此推断编码质量或模式。- JPEG 不等于 JFIF。 JPEG 是编码标准;JFIF 是交换格式约定。
- DCT 本身不是主要有损来源。 量化和色度下采样才是主要损失来源。
- 4:2:0 不是所有 JPEG 的强制配置。 JPEG 可以是灰度、4:4:4、4:2:2、4:2:0 等采样组合。
- “质量 80”不是标准字段。 文件中保存的是量化表等数据,不是统一含义的 quality 数字。
- Zig-Zag 不丢信息。 它只是重新排列系数,方便把连续 0 聚集起来。
- Huffman 编码是无损步骤。 解出同一符号序列后不会额外引入画质误差。
- EOB 不等于 EOI。 EOB 结束一个块的剩余 AC 系数;EOI 结束整张 JPEG 图像。
- SOF0 不等于 SOS。 SOF0 描述帧尺寸、分量和采样;SOS 开始一次 scan 并指定熵编码表选择。
- MCU 不总是 8×8。 MCU 的空间覆盖由最大水平、垂直采样因子决定;4:2:0 常为 16×16 亮度位置。
- 一个 JPEG 不一定只有一个 scan。 Progressive JPEG 通常有多个 scan。
- APP1 不一定只装 Exif。 也可能装 XMP 等;应检查标识字符串。
- 看到
0xFF不代表一定遇到 marker。 scan 数据中的FF00表示普通数据字节FF。 - Restart marker 不是图像帧边界。 它是熵编码重启点和错误恢复工具。
- JPEG 不支持普通 Alpha。 透明图直接转 JPEG 必须先合成背景。
- 浏览器显示方向正确不代表像素已经旋转。 可能只是渲染阶段应用了 Exif Orientation。
- Progressive 不必然更小或更清晰。 它主要改变传输与重建顺序;大小取决于具体编码策略。
- 压缩文件小不代表内存占用小。 解码后通常是 RGB/RGBA 全尺寸像素。
- 多次 JPEG 保存不是“无损复制”。 每次解码再量化都可能叠加误差。
- JPEG 不是视频关键帧。 视频中的 I picture、IDR、GOP、PTS/DTS 属于另一套概念。
- JPEG 不是 MP4 容器。 把 JPEG 文件改名为
.mp4不会变成视频。 - 颜色偏差不一定来自 DCT。 也可能来自错误的 Y’CbCr 矩阵、范围、ICC、色度位置或上采样。
- 解析 marker 时不能信任长度。 所有长度、尺寸和乘法都必须做边界与溢出检查。
- Canvas 的 quality 值不是可移植画质标尺。 不同实现可使用不同量化、采样和优化策略。
十二、面试题
A. 基础题(10 道)
A1. JPEG 的完整压缩链路是什么?
- 标准回答: 常见 Baseline 流程为 RGB→Y’CbCr、可选色度下采样、8×8 分块、电平平移、DCT、量化、Zig-Zag、DC 差分、AC 游程、Huffman 编码,最后组织为 marker 与 scan 数据。
- 可能追问: 哪一步有损?为什么高频更容易被量化?
- 常见错误: “DCT 把像素压缩了,所以 DCT 是有损的。”
- 高级回答: 区分标准规定和 JFIF/编码器策略;指出色彩转换与采样解释不完全属于 JPEG 核心压缩语法。
A2. 为什么 JPEG 常用 Y’CbCr 而不是直接编码 RGB?
- 标准回答: 把亮度相关信息与色度分离后,可对 Cb、Cr 下采样和更强量化,利用视觉系统对高频色度较不敏感的特点。
- 可能追问:
Y为什么应写作Y'? - 常见错误: “因为 YUV 比 RGB 少一个通道。”
- 高级回答: 两者通常仍是三个分量;收益来自分量去相关和不同采样/量化,而不是通道数量减少。
A3. 4:2:0 表示什么?
- 标准回答: 相对亮度采样,色度在水平和垂直方向都降低采样密度;典型 2×2 亮度区域共享一组 Cb、Cr 样本。
- 可能追问: 4:2:0 的 MCU 有几个块?
- 常见错误: “每四个像素只保留两个颜色通道中的一个。”
- 高级回答: 结合
H/V采样因子说明常见 MCU 为 4 个 Y 块加各 1 个 Cb、Cr 块。
A4. DCT 在 JPEG 中做了什么?
- 标准回答: 把 8×8 空间样本转换为 1 个 DC 和 63 个 AC 频率系数,使自然图像能量更集中到低频。
- 可能追问: 为什么常量块只有 DC?
- 常见错误: “DCT 删除高频。”
- 高级回答: DCT 只改变表示;量化才决定哪些系数被粗化或归零。
A5. 为什么使用 8×8 块?
- 标准回答: 这是压缩效率、计算复杂度、局部适应性和早期硬件能力之间的标准设计折中。
- 可能追问: 8×8 有什么副作用?
- 常见错误: “因为 CPU 一次正好处理 64 字节。”
- 高级回答: 强量化时会显现块边界;现代编码方法使用更多块尺寸和环路滤波来改善这一问题。
A6. JPEG quality=80 的含义是什么?
- 标准回答: 它通常是编码器 API 的质量提示,由实现映射到量化表、采样等策略;JPEG 标准没有统一的“80”。
- 可能追问: 两个编码器如何公平比较?
- 常见错误: “80 表示保留 80% 像素信息。”
- 高级回答: 应在相同内容、目标大小或质量指标下比较,并检查实际 DQT、采样和元数据。
A7. Baseline JPEG 与 Progressive JPEG 有何区别?
- 标准回答: Baseline 顺序模式通常一次 scan 传完各块系数;Progressive 用多个 scan 分批传低频、高频或精度位,可逐步改善整图。
- 可能追问: Progressive 最终一定更清晰吗?
- 常见错误: “Progressive 就是分辨率从小变大。”
- 高级回答: 其核心是系数的 spectral selection 和 successive approximation,不只是缩放图层。
A8. JPEG 适合什么内容,不适合什么内容?
- 标准回答: 适合连续色调照片;不适合透明图、像素艺术、文字/UI 截图和需要反复无损编辑的中间素材。
- 可能追问: 文字截图非要用 JPEG 怎么优化?
- 常见错误: “所有网页图片都应该用 JPEG,因为体积小。”
- 高级回答: 可提高质量、改用 4:4:4、减少重编码,并与 PNG/现代格式做实测选择。
A9. JPEG 为什么不支持 Alpha?
- 标准回答: 常见 JPEG/JFIF 工作流编码的是灰度或颜色分量,没有标准普通 Alpha 通道语义。
- 可能追问: RGBA 转 JPEG 时透明像素怎么办?
- 常见错误: “JPEG 会自动把 Alpha 压缩得更小。”
- 高级回答: 应先在明确颜色空间中合成到指定背景,避免透明边缘黑边或白边。
A10. JPEG、JPG、JFIF、Exif 有什么区别?
- 标准回答: JPEG 是编码标准;JPG/JPEG 是常见扩展名;JFIF 是交换格式约定;Exif 是常置于 APP1 的元数据格式。
- 可能追问: APP1 一定是 Exif 吗?
- 常见错误: “JFIF 是 JPEG 的新版。”
- 高级回答: APP marker 是应用信息容器,必须检查标识符,未知 APPn 应可安全跳过。
B. 底层实现题(10 道)
B1. 如何解析一个 JPEG marker segment?
- 标准回答: 读取
FF xx,若非独立 marker 则读取 2 字节大端长度;长度含自身 2 字节、不含 marker;验证边界后解析或跳过 payload。 - 可能追问: 哪些 marker 没有长度?
- 常见错误: 长度从 marker 起算,导致 offset 错 2 字节。
- 高级回答: 处理连续
FF填充、TEM、RSTn、SOI、EOI,并对算术溢出做检查。
B2. SOS 后为什么不能简单搜索下一个 0xFF?
- 标准回答: 熵编码数据中可出现
FF00,表示数据字节FF;还有 RSTn。只有非 stuffed、非 restart 的FFxx才是后续 marker。 - 可能追问: 为什么需要 byte stuffing?
- 常见错误: 把
FF00当成 markerFF00。 - 高级回答: 说明 bitstream 到字节边界、Restart marker 和 marker 填充字节的状态机处理。
B3. 如何根据 SOF0 计算 MCU 尺寸?
- 标准回答: 找
Hmax、Vmax,MCU 覆盖8Hmax × 8Vmax;每个分量在 MCU 中有Hi×Vi个 8×8 块。 - 可能追问: 4:2:2 与 4:2:0 分别是什么尺寸?
- 常见错误: 认为所有 MCU 都固定为 8×8。
- 高级回答: 再给出 MCU 横纵数量的向上取整公式和边界填充处理。
B4. DC 系数如何编码?
- 标准回答: 对同分量前一块 DC 做差,Huffman 编码差值类别 SIZE,再写具体附加位。
- 可能追问: Restart marker 后预测器如何处理?
- 常见错误: 与紧邻文件中的任意前一个块比较,不区分分量。
- 高级回答: 每分量独立预测,scan 组织和非交错 scan 会影响访问顺序。
B5. AC 系数如何编码?
- 标准回答: Zig-Zag 后用
(RUN,SIZE)表示零游程和下一个非零幅值;EOB结束剩余 0,ZRL表示 16 个 0。 - 可能追问: 连续 35 个 0 后出现非零值怎么编码?
- 常见错误: RUN 可以直接取 35。
- 高级回答: 先发两个 ZRL,再发 RUN=3 的非零符号。
B6. DHT 如何描述 Huffman 表?
- 标准回答: 给出 1~16 bit 各码长的码字数量,再按规范顺序给符号值,解码器据此建立 canonical Huffman codes。
- 可能追问: 为什么不直接保存每个 bit 字符串?
- 常见错误: 把 DHT 当作像素颜色表。
- 高级回答: 讨论快表、慢路径、过订阅或不完整码树的合法性检查。
B7. EOB 与 ZRL 如何区分?
- 标准回答:
0x00是 EOB,表示本块剩余 AC 全 0;0xF0是 ZRL,表示恰好跨过 16 个 0 后继续。 - 可能追问: 最后正好有 16 个 0,需要 ZRL 吗?
- 常见错误: 每 16 个尾部 0 都必须写 ZRL。
- 高级回答: 尾部全 0 通常直接 EOB 更短,ZRL 用于后面仍有非零系数的长零串。
B8. Restart interval 有什么价值?
- 标准回答: 周期性重置 DC 预测与熵解码状态,提供错误恢复和可定位边界,代价是少量 marker 开销。
- 可能追问: 是否能让整张图任意块完全并行?
- 常见错误: “RST 是下一张图片开始。”
- 高级回答: 只能在已知区间边界并具备相关表和 scan 配置时并行,不能忽略 MCU 顺序和文件解析。
B9. Progressive 解码器为什么需要更多状态?
- 标准回答: 多个 scan 分批补充同一块的系数或有效位,解码器需保存系数数组、EOB run 和逐次逼近状态。
- 可能追问: 是否每个 scan 都能独立显示?
- 常见错误: 每个 scan 是完整独立 JPEG。
- 高级回答: 区分 DC first/refine、AC first/refine 和 spectral band 范围。
B10. 如何设计安全的 JPEG 解码入口?
- 标准回答: 限制文件大小、像素数、尺寸、段长度、表数量、内存和时间;检查整数溢出;隔离解码;及时失败。
- 可能追问: 为什么仅限制上传文件为 10 MB 不够?
- 常见错误: 信任 SOF 中的宽高并直接分配。
- 高级回答: 使用成熟库、沙箱/进程隔离、模糊测试、超时和资源配额,并清理敏感元数据。
C. 数学题(10 道)
C1. 一个 8×8 块所有像素都是 140,电平平移后 DC 是多少?
- 标准回答: 平移后均为 12;按常用正交 DCT 归一化,
F(0,0)=8×12=96,AC 全为 0。 - 可能追问: 若所有像素是 128 呢?
- 常见错误: DC 等于像素平均值 140。
- 高级回答: 明确 DCT 归一化约定;不同库内部缩放形式可不同,但编码语义等价。
C2. 系数 37、量化步长 10,量化与重建结果是什么?
- 标准回答:
round(37/10)=4,反量化为 40,系数域误差 +3。 - 可能追问: 系数 -37 呢?
- 常见错误: 量化后仍保存 3.7。
- 高级回答: 讨论舍入规则、死区量化和实现内部定点误差。
C3. 16×16 区域的 4:4:4 与 4:2:0 各有多少分量样本?
- 标准回答: 4:4:4 为
256×3=768;4:2:0 为256Y+64Cb+64Cr=384。 - 可能追问: 这是否意味着文件一定减半?
- 常见错误: 4:2:0 只有 256 个样本。
- 高级回答: 只是进入变换前的样本数变化,最终码率还取决于量化与熵统计。
C4. 1920×1080、4:2:0 的 MCU 数量是多少?
- 标准回答: 常见 MCU 16×16;横向 120,纵向 68,共 8160 个 MCU。
- 可能追问: 为什么不是 67.5 行?
- 常见错误: 向下取整丢掉底部像素。
- 高级回答: 解释边界块补足和解码后裁到 SOF 声明尺寸。
C5. DC 差值 -6 的 SIZE 和附加位是什么?
- 标准回答:
|−6|需要 3 bit,所以 SIZE=3;负数附加位为正幅值110的逐位取反,即001。 - 可能追问: 解码如何判断正负?
- 常见错误: 使用二进制补码
010。 - 高级回答: 给出阈值法:若附加值小于
2^(SIZE-1),则减去(2^SIZE−1)。
C6. AC 中有 35 个 0 后出现值 5,如何表示零游程?
- 标准回答: 两个 ZRL 表示 32 个 0,再用
(RUN=3,SIZE=3)编码 5。 - 可能追问: 若后面不再有非零值呢?
- 常见错误: 写
(35,3)。 - 高级回答: 尾部全 0 时直接 EOB,不必展开全部零游程。
C7. 已知 MSE=100,8-bit 图像 PSNR 约多少?
- 标准回答:
10log10(255²/100)≈28.13 dB。 - 可能追问: MSE=0 时怎么办?
- 常见错误:
255/100=2.55 dB。 - 高级回答: MSE=0 时 PSNR 视为无穷大;PSNR 不能完全代表感知质量。
C8. 为什么二维 DCT 可分两次一维 DCT?
- 标准回答: 二维余弦基是水平基与垂直基的乘积,矩阵形式为
F=A f Aᵀ,具有可分离性。 - 可能追问: 这对复杂度有什么影响?
- 常见错误: 因为 8×8 可以拆成两个 4×4。
- 高级回答: 结合快速 DCT、定点实现、SIMD 和缓存局部性讨论。
C9. 给定符号概率 0.5、0.25、0.125、0.125,码长 1、2、3、3,平均码长是多少?
- 标准回答:
0.5×1+0.25×2+0.125×3+0.125×3=1.75 bit/symbol。 - 可能追问: 与熵下界什么关系?
- 常见错误: 直接平均得到 2.25。
- 高级回答: 该分布熵也是 1.75 bit,示例 Huffman 达到熵;一般 Huffman 平均码长小于
H+1。
C10. 量化步长扩大一倍通常会怎样?
- 标准回答: 量化系数绝对值更小、0 更多、码率下降、失真增大。
- 可能追问: 文件一定恰好减半吗?
- 常见错误: “步长翻倍,文件精确减半。”
- 高级回答: 熵码长度是非线性变化,还受图像分布、Huffman 表和元数据影响。
D. Web 实战题(10 道)
D1. 如何在浏览器中判断能否用 WebCodecs 解 JPEG?
- 标准回答: 先判断
ImageDecoder是否存在,再调用ImageDecoder.isTypeSupported("image/jpeg");失败时降级到createImageBitmap()或<img>。 - 可能追问: 为什么不能只看浏览器版本?
- 常见错误: 有 WebCodecs 就默认所有 codec 都支持。
- 高级回答: 能力探测、实际解码失败处理和服务端转码兜底应同时存在。
D2. 如何高效生成 320×180 的网页缩略图?
- 标准回答: 尽早按目标尺寸解码或缩放,优先
createImageBitmapresize 或解码器缩放能力,再在 Worker/OffscreenCanvas 导出。 - 可能追问: 为什么不先得到完整 RGBA?
- 常见错误: 4K 全解码后长期保留,再用 CSS 缩小。
- 高级回答: 结合缩放 IDCT、对象缓存、并发上限和可见区懒加载。
D3. 上传照片预览方向正确,但服务端处理后旋转了,如何排查?
- 标准回答: 检查 Exif Orientation 是否在浏览器预览阶段自动应用,而服务端像素管线未应用或重复应用。
- 可能追问: 如何永久规范化?
- 常见错误: 修改 CSS transform 就算修复文件。
- 高级回答: 统一为“读取 Orientation→变换像素→写正常方向→移除或重置 Orientation”,并做八种方向测试。
D4. Canvas quality=0.8 为什么不同浏览器文件大小不同?
- 标准回答: quality 是实现提示,不是统一量化表;编码器可选择不同 DQT、色度采样、Huffman 优化和元数据策略。
- 可能追问: 如何保证线上大小上限?
- 常见错误: 假设 0.8 必然是原图 80% 大小。
- 高级回答: 编码后检查大小,必要时按质量和尺寸做有界迭代,并把服务端规范化作为最终约束。
D5. 页面展示大量 JPEG 时如何控制内存?
- 标准回答: 虚拟列表、按可见区加载、按显示尺寸请求、限制解码并发、释放
ImageBitmap/VideoFrame、撤销 Object URL、避免重复像素副本。 - 可能追问: 为什么文件缓存很小仍会 OOM?
- 常见错误: 只统计网络传输字节。
- 高级回答: 建立压缩字节、CPU 像素、GPU 纹理三类预算和 LRU 缓存。
D6. Worker 能自动解决图片处理卡顿吗?
- 标准回答: Worker 可减少主线程阻塞,但不会减少算法总成本或自动限制内存;仍需 backpressure、任务取消和并发控制。
- 可能追问: 用户快速拖动滑块时怎么做?
- 常见错误: 把每次变化都排队,保证“一个不丢”。
- 高级回答: 合并中间状态、取消过期任务、只保留最新预览请求,最终静止后再做高质量编码。
D7. 跨域 JPEG 能显示但 Canvas 无法导出,原因是什么?
- 标准回答: 图片缺少满足 CORS 的响应,绘制后 Canvas 变为非 origin-clean,读取或导出触发安全限制。
- 可能追问: 仅设置
img.crossOrigin是否够? - 常见错误: 认为同为 HTTPS 就是同源。
- 高级回答: 客户端属性与服务器
Access-Control-Allow-Origin都要正确,并注意缓存的Vary: Origin。
D8. AI 视频生成页面为何不应把每一帧作为 JPEG 推给前端?
- 标准回答: JPEG 帧间独立,无法利用时间冗余;高帧率会造成带宽、请求、解码和内存压力。
- 可能追问: 什么时候 MJPEG 仍可接受?
- 常见错误: “JPEG 解码快,所以一定比视频流快。”
- 高级回答: 早期少量预览帧可用 JPEG,持续播放应切换到带时间戳和分片的视频管线。
D9. 视频编辑器时间线缩略图如何设计缓存键?
- 标准回答: 至少包含源资产版本、时间点或采样序号、目标尺寸、裁剪方式、颜色/旋转规范和编码参数版本。
- 可能追问: 为什么不能只用
assetId + time? - 常见错误: 改了缩略图尺寸仍复用旧结果。
- 高级回答: 使用内容哈希和变换参数哈希,支持 CDN、IndexedDB 和内存多级缓存。
D10. 浏览器端处理用户 JPEG 有哪些安全与隐私问题?
- 标准回答: 恶意尺寸/码流导致资源耗尽,解析器越界风险,Exif GPS 和设备信息泄露,跨域 Canvas 限制。
- 可能追问: 上传前预览是否意味着文件安全?
- 常见错误: 浏览器能显示就直接信任并永久保存原文件。
- 高级回答: 服务端重新验证或规范化、限制资源、剥离敏感元数据、隔离解码并保留原件访问控制。
E. 系统设计题(5 道)
E1. 设计一个用户图片上传与规范化服务
- 标准回答: 客户端直传对象存储;服务接收事件后做格式探测、尺寸/像素限制、安全解码、方向规范化、颜色策略、元数据清理,生成原图受控副本和多尺寸派生图。
- 可能追问: 如何保证幂等?如何处理失败文件?
- 常见错误: 只按扩展名判断 JPEG,原文件直接公开。
- 高级回答: 内容哈希幂等键、隔离队列、资源配额、死信队列、派生版本号、可观测质量指标和隐私审计。
E2. 设计大规模 JPEG 缩略图服务
- 标准回答: 请求携带资产与变换参数;优先命中 CDN/对象存储;未命中进入有界任务队列;使用缩放解码、Worker/进程池生成并原子写入缓存。
- 可能追问: 如何防止同一缩略图缓存击穿?
- 常见错误: 每个请求同步全尺寸解码。
- 高级回答: singleflight、负缓存、热图预生成、尺寸白名单、CPU/内存自适应并发和质量回归测试。
E3. 设计 AI 视频边生成边预览
- 标准回答: 任务 API 返回状态;早期用小 JPEG 首帧/关键预览;中期推送分片视频清单或可追加媒体段;最终生成正式视频、封面和缩略图;前端有队列上限和过期结果丢弃。
- 可能追问: 生成失败或片段重试怎么办?
- 常见错误: WebSocket 不断发送全分辨率 JPEG 帧且永不丢弃。
- 高级回答: 明确序列号、幂等片段、初始化段版本、backpressure、断点恢复、质量分层和 CDN 切换。
E4. 设计 Web 视频编辑器的时间线缩略图系统
- 标准回答: 后端或本地媒体 Worker 按采样计划 Seek/解码视频帧,生成显示尺寸 JPEG 或 sprite sheet;时间线按视口懒加载,多级缓存,缩略图只做视觉索引。
- 可能追问: 变速、倒放和裁剪时如何映射时间?
- 常见错误: 把缩略图数组索引当成精确视频帧号。
- 高级回答: 建立 timeline time→source time 映射,缓存键含变换版本,并将精确剪切交给 demux/decode 模型。
E5. 设计批量 JPEG 转码平台
- 标准回答: 对象存储事件进入队列;调度器按像素规模分级;沙箱化解码;流水线完成方向、颜色、缩放、编码和元数据策略;输出校验后原子发布。
- 可能追问: 如何控制成本并发现画质回退?
- 常见错误: 仅按文件字节大小设置并发。
- 高级回答: 以 megapixel、scan 类型和预计内存计费调度;记录 DQT/采样/PSNR 或感知指标;金样本回归、灰度发布与自动回滚。
十三、练习
13.1 手算题
练习 1:常量块
一个 8×8 灰度块所有像素为 150:
- 电平平移后的值是多少?
- 按本文归一化,DC 系数是多少?
- AC 系数理论上是多少?
答案提示: 150-128=22,DC 为 8×22=176,AC 为 0。
练习 2:量化
给定一组 DCT 系数与量化步长:
F = [120, 18, -7, 3]
Q = [ 16, 11, 10, 16]
计算量化值和反量化值,并列出误差。
答案提示: 分别执行 round(F/Q) 与乘回 Q;注意负数舍入规则要先约定。
练习 3:MCU
分别计算以下采样配置的 MCU 覆盖尺寸与块数:
- 灰度
H=1,V=1; - 4:4:4:三个分量均
1×1; - 4:2:2:Y=
2×1,Cb/Cr=1×1; - 4:2:0:Y=
2×2,Cb/Cr=1×1。
练习 4:DC 幅值
写出 DIFF = 0, 1, -1, 5, -5, 12, -12 的 SIZE 和附加位。
练习 5:AC 游程
把下面 AC 序列写成 (RUN,SIZE)、ZRL 和 EOB:
0,0,3, 0×18, -1, 0×42
13.2 码流分析题
练习 6:Marker 清单
任选三张图片:手机照片、网页截图、Progressive JPEG。使用十六进制查看器或你写的解析器输出:
- marker 顺序;
- 宽高;
- 分量采样因子;
- DQT 数量;
- DHT 数量;
- scan 数量;
- APP0/APP1/APP2 标识。
对比三者差异,不要只记录“能打开”。
练习 7:字节填充
在 JPEG 的 SOS 后搜索 FF00、FFD0~FFD7 和下一个非 stuffed marker:
- 统计
FF00数量; - 判断是否有 Restart marker;
- 验证简单搜索
0xFF为什么会误判。
练习 8:质量参数反推
用同一编码器输出质量 30、60、90 三张图:
- 提取 DQT;
- 记录色度采样;
- 比较文件大小;
- 观察纹理、文字边缘和渐变区域;
- 判断质量参数是否只改变了 DQT。
13.3 伪代码题
练习 9:Huffman 快速表
设计一个 9-bit 一级查表:
- 短码直接返回符号和消费 bit 数;
- 长码进入慢路径;
- 非法前缀报错;
- 输入不足时不越界读取。
练习 10:目标大小编码
写一个有上限次数的二分搜索伪代码,使输出 JPEG 小于 500 KB:
- 质量范围
[20,95]; - 最多编码 7 次;
- 若最低质量仍超限,则逐级降低尺寸;
- 返回最终实际大小、尺寸和质量参数。
练习 11:缩略图任务取消
设计前端任务队列:用户拖动视频时间线时,只保留最新 2 个预览请求,并取消或丢弃过期解码结果。
13.4 架构设计题
练习 12:AI 预览架构
画出以下组件和数据流:
浏览器
任务 API
生成服务
预览帧服务
视频分片服务
对象存储
CDN
状态/事件通道
标出 JPEG 首帧、视频初始化段、媒体片段、最终封面分别从哪里产生和缓存。
练习 13:编辑器缩略图架构
设计 2 小时 4K 视频的缩略图系统:
- 时间线每 5 秒一张;
- 视口只显示 30 张;
- 支持缩放时间线后改变采样密度;
- 支持源文件替换后的缓存失效;
- 支持离线 IndexedDB 缓存。
13.5 性能估算题
练习 14:内存预算
一个页面可见 24 张 800×600 图片,每张解码为 RGBA,同时 GPU 纹理再保留一份。忽略其他开销,估算像素内存。
答案提示:
[ 24\times800\times600\times4\times2\approx87.9\text{ MiB} ]
练习 15:吞吐量
服务每秒接收 40 张 12 MP 图片。单 worker 平均每秒处理 2 张,目标平均利用率不超过 70%。估算至少需要多少 worker,并讨论内存是否可能先成为瓶颈。
13.6 Debug 排查题
练习 16:导出偏色
现象:<img> 显示正常,Canvas 导出 JPEG 后偏色。列出至少 6 个检查点:
- ICC profile 是否丢失;
- Canvas color space;
- Y’CbCr↔RGB 矩阵;
- full/limited range 误用;
- 色度上采样;
- 显示器和截图工具的颜色管理;
- 重编码器是否写入正确颜色信息。
练习 17:透明边缘黑边
现象:PNG Logo 转 JPEG 后边缘发黑。解释 straight alpha、premultiplied alpha 与背景合成顺序,并给出修复方案。
练习 18:偶发解析崩溃
现象:自写 marker parser 在某些照片上越界。重点检查:
- APP1 超长或截断;
- DQT 16-bit 精度;
- 一个 segment 内多张表;
FF00;- 连续
FFFF填充; - Progressive 多个 SOS;
- 宽高乘法溢出。
13.7 第三周学习安排(8~10 小时)
| 时间 | 内容 | 产出 |
|---|---|---|
| 1.5 h | 概念地图、颜色转换、4:2:0、MCU | 手画完整编码流程 |
| 2 h | DCT、量化、Zig-Zag、DC/AC 编码 | 完成手算题 1~5 |
| 1.5 h | marker、DQT、SOF0、DHT、SOS | 写 marker parser |
| 1.5 h | 解码流程、伪代码、安全边界 | 完成码流分析题 |
| 1 h | Web API、Canvas、ImageDecoder、内存 | 完成缩略图实验 |
| 1 h | AI 视频网页与编辑器场景 | 画两张架构图 |
| 1.5 h | 面试题复盘与速查表 | 口述 10 个核心问题 |
13.8 本周验收
不看资料完成:
- 5 分钟画出 JPEG 编解码流程;
- 解释一个 4:2:0 MCU 为什么有 6 个块;
- 手算常量块 DC;
- 解释
FF00、EOB、EOI 的区别; - 给出网页缩略图的内存和 backpressure 方案;
- 用解析器列出一张 Baseline 和一张 Progressive JPEG 的 marker。
十四、本章速查表
14.1 一句话流程
颜色去相关与下采样
→ 8×8 DCT 集中能量
→ 量化丢弃细节
→ Zig-Zag 聚集零
→ DC 差分、AC 游程
→ Huffman 去除统计冗余
→ marker + scan 形成 JPEG
14.2 核心名词
| 名词 | 结论 |
|---|---|
| JPEG | 静态连续色调图像编码标准家族 |
| JFIF | 常见 JPEG 文件交换约定 |
| Exif | 常见 APP1 元数据 |
| Block | 单个分量的 8×8 样本/系数单元 |
| MCU | 按采样因子组合的一组 block |
| DC | 块平均水平相关系数 |
| AC | 描述空间变化的 63 个系数 |
| DQT | 传输量化表 |
| DHT | 传输 Huffman 表 |
| SOF0 | Baseline 帧参数 |
| SOS | 开始一次熵编码 scan |
| EOB | 当前块后续 AC 全 0 |
| EOI | 整张图像结束 |
| ZRL | 连续 16 个 AC 零 |
| RSTn | 熵编码重启点 |
14.3 必背公式
颜色转换的一种常见形式:
[ Y’=0.299R’+0.587G’+0.114B’ ]
电平平移:
[ f=s-128 ]
二维 DCT:
[ F(u,v)=\frac14C(u)C(v)\sum_x\sum_y f(x,y) \cos\frac{(2x+1)u\pi}{16} \cos\frac{(2y+1)v\pi}{16} ]
量化与反量化:
[ \hat F=\operatorname{round}(F/Q),\qquad F’=\hat FQ ]
DC 差分:
[ DIFF_i=DC_i-DC_{i-1} ]
MCU 数量:
[ MCU_x=\left\lceil\frac{W}{8H_{max}}\right\rceil, \quad MCU_y=\left\lceil\frac{H}{8V_{max}}\right\rceil ]
PSNR:
[ PSNR=10\log_{10}\frac{255^2}{MSE} ]
14.4 失真现象
| 现象 | 常见原因 | 改善方向 |
|---|---|---|
| Block artifact | 8×8 独立强量化 | 提高质量、换格式、避免重编码 |
| Ringing | 高频被截断/粗量化 | 更温和量化、避免过度锐化 |
| Mosquito noise | 高对比边缘附近 AC 误差 | 提高质量、4:4:4、无损格式 |
| Color bleeding | 色度下采样/上采样 | 4:4:4、更好滤波 |
| Banding | 平滑渐变精度不足 | 更高质量、更高位深格式、抖动 |
| Generation loss | 多次解码重编码 | 中间保持无损或像素/纹理状态 |
| 方向错误 | Exif Orientation 不一致 | 规范化像素与元数据 |
| 偏色 | ICC、矩阵、范围、采样位置错误 | 明确颜色管线 |
14.5 Marker 顺序示意
FFD8 SOI
FFE0 APP0/JFIF 可选
FFE1 APP1/Exif 可选
FFDB DQT 一张或多张表
FFC0 SOF0 Baseline 参数
FFC4 DHT DC/AC Huffman 表
FFDD DRI 可选
FFDA SOS
... entropy-coded data, FF00 stuffing, optional RSTn ...
FFD9 EOI
实际文件的 APPn、DQT、DHT、SOS 数量和顺序可不同,不能只接受这一种模板。
14.6 面试结论
- DCT 负责能量集中,量化负责主要有损,Huffman 负责无损熵压缩。
- JPEG quality 不是标准化刻度,比较编码器要看真实大小、画质、DQT 和采样。
- 4:2:0 降低色度分辨率,照片收益大,文字和彩色细线风险高。
- MCU 大小由采样因子决定,不恒等于 8×8。
- 解析 SOS 后必须处理 FF00 与 RSTn。
- Progressive 是多 scan 的系数渐进,不只是缩略图放大。
- Exif Orientation 和 ICC 属于工程正确性,不处理会出现方向或颜色问题。
- JPEG 文件很小,解码像素仍可能占几十或几百 MiB。
- 网页大规模图片处理要限制并发、按显示尺寸解码、及时释放资源。
- JPEG 适合作为视频网页的封面与缩略图,不应替代正式视频编码和时间戳管线。
十五、参考资料
以下资料优先用于核对标准边界和 Web API 行为:
- JPEG 官方:JPEG 1 Overview
https://jpeg.org/jpeg/ - ITU-T T.81 / ISO/IEC 10918-1:Digital compression and coding of continuous-tone still images — Requirements and guidelines
https://www.itu.int/rec/T-REC-T.81 - ITU-T T.871 / ISO/IEC 10918-5:JPEG File Interchange Format (JFIF)
https://www.itu.int/rec/T-REC-T.871 - ITU-T T.86 / ISO/IEC 10918-4:JPEG APPn markers
https://www.itu.int/rec/T-REC-T.86 - W3C:WebCodecs
https://www.w3.org/TR/webcodecs/ - WHATWG HTML:The canvas element
https://html.spec.whatwg.org/multipage/canvas.html - WHATWG HTML:ImageBitmap and animations
https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html - FFmpeg 官方:Codec documentation(用于观察、转码和实验,不等同于 JPEG 标准本身)
https://ffmpeg.org/ffmpeg-codecs.html
最终学习原则
先把一张 JPEG 当成“可解析的结构化 bitstream”,再把它当成“浏览器里能显示的图片”。
面试时不要只说“JPEG 使用 DCT”。应完整说明:为什么转分量、如何采样、量化怎样制造 0、DC/AC 如何编码、marker 怎样组织、哪些行为是标准规定、哪些是编码器策略,以及这些选择如何影响 Web 产品的画质、延迟与内存。