MJPEG/JPEG 视频码流参考手册
MJPEG/JPEG 视频码流参考手册
MJPEG(Motion JPEG)不是一种新的压缩算法,而是连续 JPEG 图像组成的视频表示方式。每一帧可独立解码,代价是码率通常高于 H.264/H.265。
本文把 JPEG 图像码流和 MJPEG 连续帧组织分成两层:JPEG 层负责 marker、量化表、Huffman 表和扫描数据;MJPEG 层负责帧边界、时间信息以及 HTTP、AVI、RTP 等承载方式中的组织规则。
第一章 JPEG 与 MJPEG 的层次
基本模型
1 | JPEG(图像 0) + JPEG(图像 1) + JPEG(图像 2) + ... = MJPEG |
MJPEG 本身没有统一的全局文件头。帧边界和时间信息由承载它的格式提供:AVI 通过索引和 chunk,HTTP 通过 multipart boundary,RTP 通过 RTP timestamp 和 JPEG payload header。
第二章 JPEG 图像码流
JPEG 帧结构
常见 JPEG 帧以 FF D8(SOI)开始,以 FF D9(EOI)结束。中间包含 APP、DQT、DHT、SOF、SOS 和压缩扫描数据等 marker。
工程解析时不能只搜索下一个 FF D9 就认定一定是完整帧:需要检查 SOI、长度边界、marker 规则,并防止损坏数据导致越界扫描。
JPEG 与视频帧的关系
MJPEG 的每张 JPEG 都是一个独立图像帧,不依赖前后帧,因此:
- 丢一帧通常只影响一个显示时刻。
- 可以从任意收到的完整 JPEG 开始显示。
- 编辑和抓帧简单。
- 相同画质下码率较高,网络和存储压力较大。
第三章 MJPEG 的承载方式
HTTP multipart MJPEG
摄像头常通过 HTTP multipart 返回 MJPEG:
1 | HTTP/1.1 200 OK |
接收端应优先使用 Content-Length 读取 payload;若协议实现没有长度字段,再使用 boundary 和 JPEG marker 辅助恢复。HTTP 分块传输的 chunk 边界不等于 JPEG 帧边界。
RTP/JPEG 基本承载
RTP 发送 JPEG 时,RTP header 提供序号、时间戳和 marker;JPEG payload header 通常提供 fragment offset、类型、质量和宽高等信息。单帧超过 MTU 时需要按 offset 分片,最后一个分片设置 marker。
接收端应按以下键管理重组缓存:
1 | (SSRC, RTP timestamp, JPEG type) + fragment offset |
不能只按到达顺序拼接,因为 UDP 可能乱序;也不能只看到 marker 就忽略中间缺失的 offset。
RTP/JPEG 与裸 JPEG 的区别
裸 JPEG 只包含 JPEG 文件字节;RTP/JPEG payload 可能传递经过解析和重建的信息,并不一定是可直接保存为 .jpg 的完整文件。接收端可能需要重建 JPEG header,再拼接扫描数据。
时间、缓存与解析
时间戳与帧率
MJPEG 不在 JPEG 图像内部统一定义播放时间。播放系统应从采集时刻或容器/传输层获取时间戳。若目标帧率为 F,理论帧间隔为 1/F 秒;实际系统应优先使用采集时间戳,避免网络抖动改变播放速度。
码率估算
平均码率近似为:
1 | bitrate ≈ 平均 JPEG 大小 × 帧率 × 8 |
JPEG 大小与分辨率、质量因子、场景纹理高度相关,不能仅根据分辨率准确推算码率。
解析与显示流程
1 | 接收 → 找到帧边界 → 校验 JPEG → 排序/丢弃超时帧 → JPEG 解码 → 色彩转换 → 显示 |
解码前应限制最大帧尺寸和最大压缩数据量,避免异常输入导致内存耗尽。
常见错误
- 把 HTTP chunk 边界当成 JPEG 边界。
- 只搜索 EOI,不验证 SOI 和长度。
- RTP 分片按接收顺序直接 memcpy,忽略 offset。
- 把 RTP marker 当作“没有丢包”的证明。
- 用网络到达时间代替采集时间,造成播放抖动。
- 认为 MJPEG 一定使用 RTP,忽略 HTTP、AVI 等其他承载方式。
与 H.264 的取舍
MJPEG 结构简单、延迟低、单帧独立,适合调试、工业预览和资源受限系统;H.264 具有更高压缩效率,但需要参数集、参考帧和更复杂的错误恢复机制。
JPEG marker 字段
| Marker | 后续长度 | 作用 |
|---|---|---|
SOI FFD8 |
无 | 图像开始 |
| APP0/APP1 | 2 字节长度 | JFIF/Exif 等应用数据 |
DQT FFDB |
2 字节长度 | 量化表 |
SOF0 FFC0 |
2 字节长度 | 基线帧、精度、宽高、组件 |
DHT FFC4 |
2 字节长度 | Huffman 表 |
SOS FFDA |
2 字节长度 | 扫描开始 |
DRI FFDD |
2 字节长度 | 重启间隔 |
| RST0~RST7 | 无 | 重启标记 |
EOI FFD9 |
无 | 图像结束 |
除 SOI、EOI、RST 和填充 FF 外,多数 marker 后都有 16 位大端长度,长度字段自身包含在长度计数中。
JPEG 基本帧头中的尺寸和采样
SOF 中包括 sample precision、height、width、component count 及每个 component 的采样因子。常见 4:2:0 使用 Y 分量 2×2、Cb/Cr 各 1×1;实际显示前还要依据采样因子进行色彩转换。
HTTP multipart 解析状态机
1 | READ_HEADERS → READ_BOUNDARY → READ_PART_HEADERS → READ_PAYLOAD → EMIT_FRAME |
状态机必须允许 TCP 一次读取只得到半个 boundary 或半个 header;不能把一次 recv() 当作一条完整消息。Content-Length 缺失时,boundary 搜索应限制最大帧长度并校验 JPEG SOI/EOI。
RTP/JPEG 重组字段
RTP/JPEG 的核心重组信息通常包括:
| 字段 | 作用 |
|---|---|
| RTP sequence number | 检测丢包和排序 |
| RTP timestamp | 标识同一视频时刻 |
| marker | 指示通常是最后一个分片 |
| fragment offset | JPEG 数据在当前帧中的偏移 |
| type/quality | JPEG 类型和质量描述 |
| width/height | 以协议单位描述图像尺寸 |
大帧的 offset 可能要求 24 位处理,不能使用 16 位变量截断。
重组缓存策略
为每个 timestamp 建立 bitmap 或 offset 区间表:收到分片后先检查 offset+length 是否溢出,再复制到目标缓冲区;所有区间完整且收到 marker 后才提交解码。超时帧应丢弃,不能用下一帧数据填补缺口。
JPEG 质量与码率
质量因子不是所有 JPEG/RTP 实现都能直接还原的统一数值。若协议只给 quality/type,接收端可能需要根据标准表重建量化表;若发送端使用私有量化表,则必须额外传输或约定表内容。
验证方法
1 | ffprobe -f mjpeg -show_frames stream.mjpg |
保存单帧后用 jpeginfo 或图像库验证 SOI、marker、尺寸和解码完整性。
MJPEG 数据构成图
1 | MJPEG stream |
MJPEG 没有一个统一的全局 Header;帧边界由承载格式决定:
| 承载方式 | 帧长度/边界来源 | 时间信息来源 |
|---|---|---|
| 裸 MJPEG | SOI/EOI 或外部长度 | 外部协议/默认帧率 |
| HTTP multipart | Content-Length/boundary |
接收时序或私有 Header |
| RTP/JPEG | fragment offset + marker | RTP timestamp |
| AVI | 00dc chunk size |
AVI stream header |
JPEG 帧头字段示例
1 | FF D8 SOI,无长度字段 |
SOF0 中的核心字段为:
| 字段 | 长度 | 作用 |
|---|---|---|
| sample_precision | 1 | 通常为 8 bit |
| height | 2 | 图像高度,大端 |
| width | 2 | 图像宽度,大端 |
| component_count | 1 | 颜色分量数量 |
| component_id | 1 | 分量编号 |
| sampling_factor | 1 | 水平/垂直采样因子 |
| quant_table_id | 1 | 使用的量化表 |
HTTP multipart 字段
| 字段 | 作用 |
|---|---|
boundary |
分隔连续 JPEG 部分 |
Content-Type |
声明当前 payload 是 image/jpeg |
Content-Length |
当前 JPEG 的精确字节长度 |
| JPEG payload | 实际图像数据 |
TCP 的 recv() 可能只返回半个 Header 或半个 JPEG,因此解析器必须维护跨读取调用的缓存。
JPEG 扫描数据的字节规则
JPEG 的 SOS 之后是熵编码扫描数据。扫描数据中 FF 具有特殊含义:如果它后面是 00,通常表示数据中的一个字节值 FF,解析器应将 FF 00 还原为一个数据字节;如果后面是 D0~D7,则是重启标记;如果后面是 EOI,则表示扫描结束。不能用“遇到任意 FF 就当作 marker”的简单算法解析扫描区,也不能在扫描数据内部把 FF 00 当成新的 JPEG 帧边界。
JPEG 帧的可解码性依赖多个表和 marker 的一致性。SOF 提供精度、尺寸和组件采样因子;DQT 提供量化表;DHT 提供 Huffman 表;SOS 指出本次扫描使用哪些组件和表。一个 MJPEG 接收器保存的单帧必须包含完整的这些信息,或者协议必须明确说明量化表和 Huffman 表由外部配置提供。只截取 SOS 后面的压缩数据通常不能独立解码。
采样因子与 YUV 格式
JPEG 常见的 4:4:4、4:2:2 和 4:2:0 不是简单的字符串标签,而是由 SOF 中每个颜色组件的水平/垂直采样因子表达。Y 分量采样因子为 2×2、Cb/Cr 为 1×1 时,表示一个 MCU 中亮度采样更密集。解码器根据最大采样因子和组件因子计算 MCU 尺寸,再把解码后的组件转换成 RGB 或 YUV 输出。MJPEG 文档如果只写“JPEG 帧可以独立解码”而不说明采样因子,就无法解释颜色格式和行缓冲的实际大小。
RTP/JPEG 分片和提交条件
RTP/JPEG 接收器应以 SSRC、timestamp 和图像类型建立重组上下文。每个分片到达后,先验证 fragment offset 与 payload length 的加法不溢出,再把数据放入对应区间;重复分片可以丢弃,重叠分片必须进行一致性检查。只有区间连续覆盖到最后一个分片,且 RTP marker 表示帧尾时,才把帧提交给 JPEG 解码器。marker 只能说明发送端认为这是最后分片,不能证明此前的每个分片都已到达。
HTTP、AVI 和 RTP 的层次区别
同一组 JPEG 图像可以通过 HTTP multipart、AVI chunk 或 RTP payload 发送,但三者解决的问题不同。HTTP 提供连接和 part 分隔,AVI 提供文件索引和轨道时间,RTP 提供序号、时间戳和实时传输语义。JPEG marker 属于图像码流层,不能用 HTTP boundary 替代 SOI/EOI,也不能把 RTP sequence number 写进 JPEG 文件。实现应先完成承载层重组,再把完整 JPEG 交给图像解码器。
MJPEG 的码率和缓存
MJPEG 不使用跨帧预测,因此每帧大小随画面纹理变化明显。发送端应按最大 JPEG 大小配置发送缓存,接收端应限制单帧上限并为等待中的 timestamp 设置超时。只按平均码率分配缓存会在复杂场景中溢出;只按最大帧分配所有 timestamp 的缓存又容易被恶意流耗尽内存。较稳妥的策略是限制同时存在的重组帧数量、每帧最大字节数和总缓存额度。
JPEG 与 MJPEG 的格式边界
第四章 JPEG 标记段的完整语法
标记字节与填充规则
JPEG 码流以 FF 引入 marker。SOI(FF D8)和 EOI(FF D9)没有长度字段;大多数参数段在 marker 后使用两字节大端长度,长度值包含这两个长度字节本身,不包含 marker。压缩扫描数据中如果出现 FF,编码器必须插入 00 作为 byte stuffing;解码器在熵编码区遇到 FF 00 时应还原为数据字节 FF,遇到其他 marker 则结束当前扫描或进入重启处理。
1 | FF D8 // SOI |
解析器不能把扫描数据中的任意 FF 都当成段起点,也不能用小端读取段长度。遇到未知 APP marker 时,可以按长度跳过并保留原始数据;遇到未知的无长度 marker,则必须依据 JPEG marker 类别决定是否允许继续。
APP、COM 和元数据
APP0、APP1、APP2 等段用于 JFIF、Exif、ICC profile 等应用元数据,COM 用于注释。它们通常不参与像素解码,但段长度、边界和嵌套数据仍必须校验。MJPEG 传输可以选择删除 Exif 或缩略图以降低延迟,但删除操作必须同步修正段长度,不应只截断字节。
第五章 SOF、组件与 MCU
SOF 字段
以基线 DCT 的 SOF0 为例,段体包含样本精度 P、图像高度 Y、图像宽度 X、组件数量 Nf,以及每个组件的标识符、采样因子和量化表选择。高度或宽度为零时,尺寸可能在后续 DNL 段中给出;不支持 DNL 的简化实现应明确拒绝,而不是把零尺寸当作合法图像。
| 字段 | 长度 | 作用 |
|---|---|---|
P |
1 byte | 每样本精度,常见为 8 |
Y |
2 bytes | 图像高度,大端 |
X |
2 bytes | 图像宽度,大端 |
Nf |
1 byte | 组件数量 |
| component id | 1 byte | 组件标识 |
| sampling factors | 1 byte | 高 4 位水平、低 4 位垂直因子 |
| quant table id | 1 byte | 该组件使用的量化表 |
MCU 尺寸和边缘填充
最大水平采样因子为 Hmax、最大垂直采样因子为 Vmax 时,一个 MCU 的亮度参考尺寸为 8 × Hmax 像素宽、8 × Vmax 像素高。图像宽高不是 MCU 尺寸的整数倍时,编码器仍需编码边缘 MCU,解码器再根据 SOF 的实际尺寸裁掉填充像素。不能把最后一个 MCU 的完整块尺寸直接当成显示宽度。
量化表与 DCT 系数
DQT 的精度和表编号
DQT 的表头高 4 位表示精度,低 4 位表示表编号。精度为 0 时每个量化值占 1 字节,精度为 1 时占 2 字节。量化表按 JPEG 规定的 zig-zag 顺序存储,而不是按二维矩阵的自然行序存储;显示或编辑工具通常需要先反 zig-zag 才能绘制 8×8 表。
量化值为零是非法的,因为反量化需要相乘。解析器应检查表编号在实现支持范围内、段长度足以容纳完整表,并拒绝零值或超出实现数值范围的表。量化表的数值越大,保留的 DCT 精度越低,文件通常越小但细节损失越多。
量化与有损特性
JPEG 的主要有损步骤是 DCT 系数量化,而不是 marker 或 Huffman 表。每个 8×8 块的 DCT 系数除以量化值并舍入,解码时再乘回量化值;舍入误差不可逆。MJPEG 的每个帧独立执行该过程,因此降低质量会同时影响每一帧,却不会引入跨帧预测误差。
Huffman 表、扫描和熵编码
DHT 的表描述
DHT 先给出表类(DC 或 AC)和表编号,再给出 16 个代码长度计数,最后按长度递增列出符号值。解码器根据这些计数重建 canonical Huffman code。表中符号数必须等于 16 个计数之和,段长度必须覆盖所有符号;不能只读取常见表的固定 12 或 162 个值。
SOS 字段
SOS 段给出本次扫描的组件数量、每个组件使用的 DC/AC Huffman 表编号,以及光谱选择和逐次逼近参数。基线 JPEG 通常使用单次完整扫描,但渐进 JPEG 可能有多个 SOS。只支持 baseline 的 MJPEG 接收器应在发现不支持的渐进参数时报告格式错误,而不是把第一段扫描当成完整图像。
熵编码区与重启标记
扫描数据中的 DC 系数使用前一个块的 DC 预测值,AC 系数使用游程/幅值符号。RST0 到 RST7 重启标记在指定 MCU 间隔出现,用于重置 DC 预测和熵解码状态。标记序列按模 8 递增,解析器遇到不符合预期的 RST 编号时应记录错误并决定是否丢弃当前帧。重启标记可改善丢失局部数据后的恢复边界,但不能让损坏的 MCU 自动恢复。
JPEG 码流解析算法
两阶段解析
实用解析器可以分为结构扫描和熵解码两阶段。结构扫描阶段读取 SOI、APP、DQT、SOF、DHT、SOS、RST 和 EOI,建立表与图像描述;熵解码阶段在确定组件、采样因子和表均存在后,按 MCU 顺序解码。这样即使暂不实现完整像素解码,也能生成可靠的码流诊断报告。
1 | require SOI |
安全边界
所有段长度、图像尺寸、组件数量和采样因子都来自输入,计算 MCU 数量和输出缓冲区大小时必须检查乘法溢出。对于 MJPEG 网络流,应限制单帧最大长度、最大宽高、最大组件数和解码工作内存;解析失败后要丢弃直到下一个 SOI,而不是在未知偏移继续解释旧状态。
MJPEG 承载格式的字段关系
HTTP multipart
HTTP multipart 的 boundary 只分隔 part,不描述 JPEG 内部字段。每个 part 通常包含 Content-Type: image/jpeg 和 Content-Length,但接收器不能只依赖 Content-Length;应同时寻找下一个 boundary,并检查 body 是否以 SOI 开始、以 EOI 结束。头部字段允许折行和额外扩展,解析时不能把所有冒号后文本直接当作长度。
RTP/JPEG
RTP/JPEG 负载通常在每个 RTP 包中放置图像类型、质量、量化表信息和 24 位 fragment offset,再跟随 JPEG 扫描数据。RTP 头的序号负责丢包检测,timestamp 标识同一图像时刻,marker 表示发送端认为帧已结束。接收器必须先按 timestamp/offset 重组,再根据 RTP/JPEG 规则补出 JPEG 头;不能把一个 RTP payload 直接保存为可解码 JPEG 文件。
文件型 MJPEG
AVI 等容器把每个 JPEG 图像作为独立视频 sample 或 chunk,并通过索引记录偏移和关键帧属性。文件读取器应以容器的 sample 边界为主,仍需对每个 JPEG sample 执行 SOI/EOI 和 marker 校验。一个 AVI chunk 中可能没有完整 JFIF APP0 段,只要 SOF、DQT、DHT 和扫描数据完整,仍可能是合法 JPEG;不能把 JFIF 标识误当成 JPEG 必需字段。
质量、延迟与验证
质量因子不是统一字段
很多编码器提供“质量 1 到 100”,但 JPEG 标准实际保存的是量化表。不同编码器将质量数字映射到量化表的算法不同,因此跨编码器比较质量数值没有严格意义。验证应比较 DQT 内容、图像尺寸、采样因子和实际文件大小。
建议的测试样本
测试集合应包含灰度 JPEG、4:4:4、4:2:2、4:2:0、渐进 JPEG、带重启标记的 JPEG、含 APP/COM 的 JPEG、截断文件、扫描数据中出现 FF 00 的文件以及多帧 MJPEG 流。每个样本至少验证 marker 顺序、段长度、表引用、MCU 数量、EOI 位置和解码后尺寸。
JPEG 是单张图像的压缩码流,MJPEG 是连续 JPEG 图像的组织方式。MJPEG 本身不统一规定音频、不统一规定全局时钟,也不规定必须使用某个网络协议。文档中如果出现 HTTP、RTP 或 AVI,应明确它们是承载方式,而不是 JPEG 编码语法的一部分。
第六章 JPEG 编码处理链
从像素到压缩扫描
常见有损 JPEG 编码会把输入 RGB 转换到亮度/色度空间,对色度进行可选下采样,再按组件和 MCU 划分为 8×8 数据单元。每个数据单元执行电平移位、二维离散余弦变换、量化和 Zig-zag 重排,随后对 DC 系数做差分、对 AC 系数做零游程与幅值分类,最后使用 Huffman 或算术熵编码写入扫描数据。
1 | RGB/YUV 输入 |
JPEG 标准定义解码语法和重建流程,但实现可以使用不同精度的 DCT、颜色变换和量化策略。只要输出码流符合语法,不同编码器对同一图片得到不同字节和不同视觉误差是正常的。
电平移位
对 8 位样本,DCT 前通常减去 128,使无符号范围 [0,255] 转到以零为中心的范围。逆变换后再加 128 并限幅回有效样本范围。若解码器忘记 level shift,图像会出现大面积亮度偏移;若在输入已经是有符号中心化数据时重复减 128,也会产生同类问题。
二维 DCT
二维 8×8 DCT 将空间样本映射为一个 DC 系数和 63 个 AC 系数。DC 近似表示该块平均值,低频 AC 描述缓慢变化,高频 AC 描述边缘和纹理。实现可以利用行列可分离性质先做八次一维变换再转置处理,但定点缩放必须与反量化/IDCT 的数值约定配套。
量化和舍入
编码器将 DCT 系数除以对应量化表值并舍入为整数。解码器将量化整数乘回表值,再做 IDCT。量化表索引由 SOF 中组件的 table selector 选择;如果组件引用的表在当前图像中从未通过 DQT 定义,图像不具备完整解码配置。
Zig-zag 与系数顺序
Zig-zag 的作用
Zig-zag 把二维 8×8 量化系数按大致从低频到高频的顺序展开。由于高频量化后常为零,这种顺序会产生长零游程,便于 AC run-length 编码。Zig-zag 是码流顺序,不代表量化表在内存中必须永久以一维顺序保存;解码器通常反映射到自然二维位置。
自然顺序与扫描顺序
DQT 中的 64 个值和熵解码得到的 64 个系数都依据规定的扫描顺序解释。调试时若直接把一维数组按 8×8 行打印,会把低频与高频位置打乱。应使用标准 Zig-zag 映射表,分别显示“码流索引”和“自然矩阵索引”。
第七章 DC 系数编码
差分预测
同一扫描、同一组件的 DC 值以与前一个块 DC 的差值编码。每个组件维护独立 predictor;扫描开始和 restart marker 后 predictor 重置为零。若解码器对所有组件共用一个 predictor,4:2:0 图像通常会出现明显块状亮度/色度错误。
Category 与附加位
DC Huffman 符号表示差值绝对值所需的 bit 数,也称 category 或 size。size 为零表示差值为零,没有附加位;size 非零时随后读取相应数量的 amplitude bits。正负值使用 JPEG 规定的幅值映射,负数不是普通二补码。
1 | size = number_of_bits(abs(diff)) |
解码负幅值时,如果读到的 bits 小于该 size 的正值阈值,应按 JPEG 的 extend 规则映射为负数。把它直接符号扩展为二补码会得到错误系数。
AC 系数编码
Run/Size 符号
AC Huffman 符号高四位表示前导零数量 run,低四位表示非零系数的 size。解码器先跳过 run 个零,再读取 size 个 amplitude bits。若写入位置超过第 63 个 AC 系数,码流非法。
EOB 与 ZRL
0x00 表示 End of Block,剩余所有 AC 位置填零。0xF0 表示 16 个连续零但不结束块,称为 ZRL。ZRL 可以重复出现;每次都要确保仍有足够系数位置。其他 size 为零的 run/size 组合通常不合法或有特定扩展语义,baseline 解析器应严格检查。
Escape 幅值
常见 baseline Huffman AC category 有规范范围,progressive 或算术模式还有不同语法。解析器不应把所有非零 size 都送给统一二补码读取器。码流能力必须由 SOF marker 类型、精度和 scan 参数共同决定。
Canonical Huffman 解码
从 DHT 重建码表
DHT 中 16 个计数分别表示长度 1 到 16 的 code 数量,后面的 symbol 依长度顺序排列。Canonical code 从最短长度开始递增;进入下一长度前左移一位。重建时需要检查同一长度的 code 数量不会使码空间溢出,也要检查表既不过度订阅也不会读取超出 DHT 段。
查表实现
软件解码器常建立一个 8 到 12 位快速查找表,未命中时再逐 bit 扩展到最长 16 位。快速表是实现优化,输出必须与逐 bit tree 解码一致。读取扫描数据时 bit reservoir 还要处理 byte stuffing 和 restart marker,不能先把整个 scan 当成无 marker 的连续 bit string。
非法 Huffman 输入
若读取超过 16 bit 仍没有 symbol、读到表未定义 symbol 或幅值超出系数边界,应终止当前 frame。容错解码器可以跳到下一个 restart marker,但必须重置 bit alignment、DC predictor 和 EOBRUN 等状态,不能直接从任意下一个字节继续。
MCU 和组件遍历
Interleaved Scan
当 SOS 包含多个组件时,一个 MCU 按 SOF 采样因子包含各组件的若干 8×8 block。例如常见 4:2:0 三组件图像可在一个 MCU 中有四个 Y block、一个 Cb block和一个 Cr block。遍历顺序由 scan 中的组件顺序和各自采样因子确定。
Non-interleaved Scan
当 SOS 只包含一个组件时,MCU 通常对应该组件的一个数据单元,遍历尺寸按该组件相对采样关系计算。Progressive JPEG 经常用多个非交错或部分交错扫描逐步传递系数。不能永远套用“一个 MCU 有六块”的 4:2:0 基线模型。
MCU 行列计算
对交错扫描,水平 MCU 数通常为 ceil(width / (8 × Hmax)),垂直 MCU 数为 ceil(height / (8 × Vmax))。计算需使用不会溢出的整数 ceiling 公式,边缘 MCU 的无效输出像素最后裁掉。解码缓冲仍要容纳完整 block 重建结果。
第八章 Baseline Sequential JPEG
SOF0 约束
SOF0 表示基线 DCT 顺序编码,常见精度为 8 bit,使用 Huffman 熵编码。SOS 的 spectral selection 通常为 Ss=0、Se=63,successive approximation 高低位为零。若 SOF0 文件出现不符合 baseline 的 scan 参数,应报告语法矛盾。
单扫描与多扫描
顺序 JPEG 可以按一个扫描包含全部组件,也可以用多个扫描分别传递组件,只要每个系数在所属扫描中按顺序编码。MJPEG 解码器若假定遇到第一个 SOS 后下一个 marker 必然是 EOI,可能无法处理合法多扫描顺序文件。
Progressive JPEG
Spectral Selection
渐进编码可以先传 DC,再分频带传 AC。SOS 的 Ss 和 Se 指定本扫描覆盖的 Zig-zag 系数范围。DC 扫描通常 Ss=Se=0;AC 扫描使用 Ss>0。扫描范围必须符合组件数量和前后扫描状态约束。
Successive Approximation
Ah 和 Al 指定逐次逼近的高/低位。初始扫描写入系数的高位,refinement 扫描补充低位。解码器需要为整幅图像保存量化系数缓冲,直到所有扫描完成或选择输出中间预览。Baseline 的逐块立即 IDCT 管线不能直接复用为完整 Progressive 实现。
EOBRUN
Progressive AC scan 使用 EOBRUN 批量表示多个 block 的尾部全零,并在 refinement 中有额外修正 bit。restart marker 会重置 EOBRUN。错误处理若只重置 DC predictor 而保留 EOBRUN,会让 restart 后多个 block 错位。
渐进显示
每完成一个扫描,解码器可以对当前系数执行 IDCT 输出逐步清晰图像。MJPEG 实时场景通常偏好 baseline 以降低状态和延迟,但“MJPEG 必须 baseline”并不是普遍格式定义;接收能力应在接口中明确。
真实 Progressive JPEG 测试向量
图像参数
下面是一幅真实可解码的 16×16 Progressive JPEG,文件长 569 字节。SOF2 声明 8 bit 精度、三个组件、4:2:0 采样:组件 1 的采样因子为 2×2,组件 2/3 为 1×1。文件包含十个 SOS,覆盖 DC 初扫、AC 频谱初扫以及 DC/AC refinement。
这个向量用于验证 Marker 状态、Scan Script 和系数生命周期,不应把它的十次扫描顺序视为 Progressive JPEG 的唯一合法脚本。编码器可以选择其他合法的频谱分段和 successive approximation 层次。
完整 569 字节
1 | 0000 FF D8 FF E0 00 10 4A 46 49 46 00 01 01 00 00 01 |
SOF2 逐字段解析
SOF2 从 0x009E 开始,长度 17,payload 15 字节:
1 | FF C2 00 11 |
最大采样因子为 2×2,因此一个交错 MCU 覆盖 16×16 像素。图像正好一个 MCU。组件 1 每 MCU 有四个 block,组件 2/3 各一个,共六个 block。后续单组件 AC Scan 的 MCU 定义则按该组件数据单元网格解释,不能始终把一次 Scan MCU 当作六个 block。
十个 Scan 的完整目录
Marker 与熵区间
| Scan | SOS 偏移 | Ns 与组件 |
Ss |
Se |
Ah |
Al |
熵起点 | 熵字节 | 下一 Marker |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0x00DF |
3:C1/C2/C3 | 0 | 0 | 0 | 1 | 0x00ED |
4 | 0x00F1 DHT |
| 1 | 0x0109 |
1:C1 | 1 | 5 | 0 | 2 | 0x0113 |
7 | 0x011A DHT |
| 2 | 0x0131 |
1:C3 | 1 | 63 | 0 | 1 | 0x013B |
2 | 0x013D DHT |
| 3 | 0x0157 |
1:C2 | 1 | 63 | 0 | 1 | 0x0161 |
3 | 0x0164 DHT |
| 4 | 0x017A |
1:C1 | 6 | 63 | 0 | 2 | 0x0184 |
1 | 0x0185 DHT |
| 5 | 0x01A0 |
1:C1 | 1 | 63 | 2 | 1 | 0x01AA |
7 | 0x01B1 SOS |
| 6 | 0x01B1 |
3:C1/C2/C3 | 0 | 0 | 1 | 0 | 0x01BF |
1 | 0x01C0 DHT |
| 7 | 0x01D8 |
1:C3 | 1 | 63 | 1 | 0 | 0x01E2 |
2 | 0x01E4 DHT |
| 8 | 0x01FC |
1:C2 | 1 | 63 | 1 | 0 | 0x0206 |
3 | 0x0209 DHT |
| 9 | 0x0224 |
1:C1 | 1 | 63 | 1 | 0 | 0x022E |
9 | 0x0237 EOI |
文件末尾 EOI 位于 0x0237,结束偏移 0x0239=569,没有尾随字节。每个熵区间都由状态化扫描器找到,而不是在 payload 中盲搜 FF;本向量恰好没有 stuffed FF 00,但实现仍必须支持。
Scan 0:DC First
第一个 SOS 的原始字段为:
1 | FF DA 00 0C 03 |
Ns=3,三个组件都参与。C1 选择 DC0,C2/C3 选择 DC1;AC selector 在 DC Scan 中不承载普通 AC 解码语义。Ss=0, Se=0, Ah=0, Al=1,说明这是三个组件的 DC 初扫,首次传输的 DC 值处于左移一位的精度层。
Scan 1 与 Scan 4:Y 的频谱分段
Scan 1 对 C1 传输 Ss=1..5、Al=2 的 AC First;Scan 4 对同一组件传输 Ss=6..63、Al=2。两个范围连续且不重叠,共同初始化 C1 的全部 AC 系数高层 bit。不能因为它们都选择 C1 就把第二个 Scan 视为重复;频谱范围不同。
Scan 2/3:色度 AC First
C3 和 C2 分别以单组件 Scan 传输 Ss=1..63, Ah=0, Al=1。Progressive AC First 要求单组件,两个 Scan 都满足。组件顺序不必是 C2 后 C3,本向量先 C3 再 C2,验证器不能强制按 SOF 组件列表顺序出现。
Scan 5:Y AC Refinement
Scan 5 的 Ah=2, Al=1,正好 refinement Scan 1/4 初始化的 Al=2 层。它的频谱范围是 1..63,将 C1 全部 AC 频带补到 bit 1。Refinement 语法需要处理已有非零系数 correction bit、新出现 ±1 系数与 EOBRUN,不能调用 AC First 解码函数后简单右移。
Scan 6:DC Refinement
Scan 6 再次包含三个组件,Ss=Se=0, Ah=1, Al=0。它补充 Scan 0 中 Al=1 的下一位。DC Refinement 通常每 block 消费一个修正 bit,不重新解出 DC category/difference;如果错误复用 DC First Huffman 流程,一个字节不可能按预期覆盖六个 block。
Scan 7~9:最终 AC Refinement
三个单组件 Scan 都使用 Ah=1, Al=0,分别细化 C3、C2、C1 的 AC 系数。C3/C2 的 AC First 在 Al=1,C1 的前一轮 refinement 也结束于 Al=1,所以 successive approximation 连续。Scan 9 结束后直接遇到 EOI。
Progressive Scan 状态矩阵
每系数最低已知位
验证器可以为每个组件的 64 个 Zig-zag 系数保存状态:UNSEEN 或当前已传输的最低位 Al。处理 First Scan 时要求目标系数均为 UNSEEN,完成后写入 Al;处理 Refinement 时要求已有状态等于 Ah,并在 Ah=Al+1 时更新为 Al。
1 | apply_scan(component, Ss, Se, Ah, Al): |
真实解码还要按 block 保存实际系数和 EOBRUN;这个矩阵先在 Scan Header 层发现脚本重叠、跳级或 refinement-before-first。
本向量状态演进
| 处理后 | C1 DC | C1 AC 1..5 | C1 AC 6..63 | C2 DC/AC | C3 DC/AC |
|---|---|---|---|---|---|
| Scan 0 | 1 | 未见 | 未见 | DC=1 | DC=1 |
| Scan 1 | 1 | 2 | 未见 | 不变 | 不变 |
| Scan 2 | 1 | 2 | 未见 | 不变 | AC=1 |
| Scan 3 | 1 | 2 | 未见 | AC=1 | AC=1 |
| Scan 4 | 1 | 2 | 2 | 不变 | 不变 |
| Scan 5 | 1 | 1 | 1 | 不变 | 不变 |
| Scan 6 | 0 | 1 | 1 | DC=0, AC=1 | DC=0, AC=1 |
| Scan 7 | 0 | 1 | 1 | 不变 | DC=0, AC=0 |
| Scan 8 | 0 | 1 | 1 | DC=0, AC=0 | DC=0, AC=0 |
| Scan 9 | 0 | 0 | 0 | DC=0, AC=0 | DC=0, AC=0 |
EOI 时三个组件的所有 64 个系数都已传到 bit 0。若流在 Scan 5 后截断,已有 Scan 仍可生成低质量预览,但结构报告必须标记 progression incomplete。
Scan 间表更新
DHT 的生效点
该测试向量在多个 SOS 之前插入小型 DHT,只定义当前 Scan 需要的优化 Huffman 表。每个 Scan 开始时应冻结当时的表版本;后续 DHT 不能反向改变已消费熵数据。表缓存键至少包含 class、ID 和 version/effective offset。
Transactional Scan Decode
若某个 Scan 熵数据损坏,不应把只解到一半的系数状态标记为完整 Al。解码器可以在临时变更日志或独立系数副本中更新,Scan 成功闭合后提交;容错预览若保留部分 MCU,也要记录具体有效区域和不完整状态。
Restart Interval 与错误恢复
DRI 段
DRI 段给出 restart interval,单位为 MCU。值为零表示不使用 restart interval。非零时,扫描数据应在每指定数量 MCU 后出现 RST0 到 RST7,编号循环递增。restart marker 自身没有长度字段,也不进行 FF 00 stuffing。
Marker 边界
熵解码器在到达预期 MCU 数后丢弃当前字节剩余 padding bit,对齐到字节,读取预期 RST marker,随后重置 DC predictor 和相应渐进状态。若 marker 提前或延后出现,严格模式应失败;容错模式可扫描到下一个 RST,但受影响区间应标记损坏。
MJPEG 丢包恢复
Restart interval 只在一个 JPEG 帧内部提供有限恢复点。若 HTTP part、AVI chunk 或 RTP fragment 整体丢失,接收器首先依据承载层确定缺失区间;有 restart marker 时才可能从后续 interval 恢复部分 MCU。没有正确的 scan header、表和帧边界,单独一个 RST marker不足以重建图像。
真实 Restart Marker 测试向量
图像与 MCU 数量
测试图像为 32×16 Baseline JPEG、三组件 4:2:0。最大采样因子为 2×2,所以一个 MCU 覆盖 16×16 像素:
1 | mcu_columns = ceil(32 / 16) = 2 |
编码器设置 Restart Interval 为一个 MCU,因此两个 MCU 之间应出现一个 RST Marker,最后一个 MCU 后直接结束 Scan,不要求额外 RST。
DRI 与 SOS 字节
文件尾部相关字节如下:
1 | 0250 E3 E4 E5 E6 E7 E8 E9 EA F2 F3 F4 F5 F6 F7 F8 F9 |
DRI 从 0x0261 开始:
1 | FF DD 00 04 00 01 |
L=4,两字节 payload 为 0x0001,即每一个 MCU 重启。SOS 位于 0x0267,长度 12,三个组件共同参与,Ss=0, Se=63, Ah=Al=0,是 Baseline Sequential Scan。熵数据从 0x0275 开始。
两个 Restart Interval
第一个熵区间是 [0x0275,0x0296),共 33 字节;0x0296 的 FF D0 是 RST0。第二个区间从 RST0 后的 0x0298 开始,到 EOI 前的 0x02B9,同样 33 字节。
1 | interval 0 data: 0x0275 .. 0x0295, 33 bytes |
两个区间字节数相同只是这个小图和内容产生的结果,Restart Interval 的单位是 MCU,不要求每个区间压缩后字节长度相同。
RST0 不属于熵 Payload
FF D0 不做 byte stuffing,不是两个待送进 Huffman Reservoir 的数据字节。Entropy Byte Reader 返回 Restart 事件;上层验证当前已完成一个 MCU、编号等于预期 0,然后丢弃字节对齐剩余 bit,清空 Reservoir 并重置预测状态。
Restart 后必须重置的状态
Baseline Sequential DCT 至少重置所有参与组件的 DC predictor:
1 | last_dc[C1] = 0 |
Progressive Scan 还要重置 EOBRUN,并按过程处理 refinement 状态;Lossless 则重置预测起始条件。Huffman 表、量化表、SOF 采样因子和已经重建的前面 MCU 不会因 RST 清空。
Restart 序列验证算法
正常路径
1 | restart_interval = DRI value |
条件中的 more MCU remain 很重要。总 MCU 数恰好是 interval 的整数倍时,最后一个 MCU 后可以直接遇到 Scan 结束 Marker;不能机械要求 EOI 前再有一个 RST。
提前 Marker
若处理的 MCU 少于 interval 就遇到 RST,说明熵数据提前结束、MCU 计数推导错误或流损坏。严格解码器终止 Scan。容错器可以把当前 interval 剩余 MCU 标记为缺失,从实际 RST 编号继续,但不能假装它们已成功解码。
缺失 Marker
达到 interval MCU 数后若下一 Entropy Event 仍是普通数据,说明预期 RST 缺失或此前解码已错位。继续解 Huffman 往往会把 padding/下一区间误读为系数。恢复模式可以有界扫描 FFD0..FFD7,但必须跳过 stuffed FF00 并限制最大字节窗口。
编号错误
预期 RST2 却看到 RST5 时,实际 Marker 仍提供一个物理边界,但说明丢失了若干区间或编码器序列错误。容错器可以把 expected_rst 同步到实际编号的下一值,重置 predictor,并把编号跳跃对应范围标记不可信。不要为了“继续显示”而从报告中隐藏跳跃。
DRI 更新
DRI 可在 Scan 之间更新,新的值对后续 Scan 生效。Parser 在 SOS 时把当前 DRI 捕获进 Scan Context;已经开始的 Scan 不读取中途出现的普通 DRI,因为普通带长度 Marker 不应作为熵数据任意插入。
JPEG Marker 参考
无长度 Marker
SOI、EOI、TEM 和 RST0-RST7 不带长度字段。解析器分派 marker 时应先按 marker code 判断类别,再决定是否读取两字节长度;统一读取长度会把后续数据误当成 size。
带长度 Marker
DQT、DHT、SOF、SOS、DRI、DNL、DAC、APPn 和 COM 等通常带两字节大端长度。长度最小为 2,代表只有长度字段、没有 payload;具体 marker 往往要求更大的最小值。验证器应同时检查通用长度和该段专用语法长度。
SOF 类型
不同 SOF marker 表示 sequential/progressive、Huffman/arithmetic、differential/non-differential 及不同精度模式。只实现 SOF0 的解码器看到 SOF2 时应报告 Progressive unsupported,看到算术编码 SOF 时应报告 arithmetic unsupported,而不是都称为“JPEG 头损坏”。
JFIF、Exif 与颜色解释
JFIF APP0
JFIF APP0 通常包含 JFIF\0 标识、版本、密度单位、X/Y density 和可选缩略图。密度描述物理显示比例,不等于像素宽高。缩略图长度由宽高推导,必须在 APP0 段范围内验证。
Exif APP1
Exif APP1 常包含 Exif\0\0 后的 TIFF 结构,内部端序由 TIFF header 指定。Exif orientation 会影响最终显示方向;JPEG SOF 宽高仍描述编码像素矩阵。解析 Exif 时必须在 APP1 payload 边界内进行 TIFF offset 计算,不能把偏移当作从整个文件零点开始。
Adobe APP14
Adobe APP14 的 transform 字段可帮助判断 RGB、YCbCr 或 YCCK 颜色解释。没有 JFIF/Adobe 元数据时,组件 ID 和采样因子只能提供线索,颜色空间可能需要应用约定。解码器不能仅根据三个组件就绝对断言为 YCbCr。
ICC Profile 分片
ICC profile 可跨多个 APP2 段分片,每段带序号和总片数。读取器要按序号收集、检查重复和缺失,再拼成完整 profile。MJPEG 每帧重复 ICC 会增加码率;容器可有外部颜色配置,但删除帧内 ICC 前必须保证接收端仍有等价信息。
颜色变换与采样恢复
YCbCr 到 RGB
常见 JPEG 使用 YCbCr,但具体矩阵和范围可能受元数据或应用约定影响。基础全范围近似转换常写为:
1 | R = Y + 1.402(Cr - 128) |
实现需要选择定点精度、舍入和限幅。若输入实际为 RGB JPEG 却应用 YCbCr 矩阵,颜色会严重偏移。
色度上采样
4:2:2 或 4:2:0 解码后,Cb/Cr 分辨率低于 Y,需要上采样到输出网格。最近邻、线性和高质量滤波会产生不同边缘结果。采样位置(centered/cosited)也影响半像素相位;标准和元数据允许的解释应与编码器保持一致。
RTP/JPEG 字段级重组
Main JPEG Header
RTP/JPEG payload header 常包含 type-specific、24 位 fragment offset、type、Q、width 和 height。width/height 以 8 像素块为单位表示,某些扩展值需要额外尺寸处理。fragment offset 相对于 JPEG scan payload 的起点,而不是 RTP 包或完整 JFIF 文件起点。
Q 字段与量化表
Q 小于某范围时可指示按规定算法生成量化表;较大值可表示动态量化表随首 fragment 携带。接收器应依据 Q 和 quantization table header 决定表来源,验证 precision、length 和表数。不能把 Q 直接当作 JPEG 文件中的“质量百分比”。
Type 与采样
Type 描述 JPEG 采样方式和 restart 等扩展,映射到构造 SOF 时的组件采样因子。重组器必须按 type 生成与实际 scan 相符的 SOF/DQT/DHT/SOS;使用错误 type 即使网络分片完整,也会导致颜色或 MCU 解析错误。
Restart Marker Header
使用 restart 的 RTP/JPEG 类型可在 payload 中携带 restart interval 和 first/last 等状态。它是 RTP payload 格式的重组元数据,不等于 JPEG 码流中的 DRI/RST 字节;接收器需据此构造或验证最终 JPEG restart 结构。
分片区间算法
每个 timestamp 建立一个有界 interval map。收到 fragment 时检查 offset + length 溢出、帧最大值、与已收区间重叠内容是否一致。Marker packet 到达后记录期望末尾,但只有 [0,end) 完整覆盖并且所需量化表/头信息齐全时才提交。
RTP/JPEG 主头的承载映射
精确字节布局
RTP/JPEG Payload Header 固定八字节,所有多字节网络字段按 big-endian:
| Payload 偏移 | 长度 | 字段 | 作用 |
|---|---|---|---|
| 0 | 1 | Type-specific | 特定 profile 的附加解释;标准基本类型通常按规定值处理 |
| 1 | 3 | Fragment Offset | 当前压缩扫描片段相对本 JPEG frame 数据起点的字节偏移 |
| 4 | 1 | Type | 采样方式及 Restart 扩展类型 |
| 5 | 1 | Q | 量化表生成参数或动态表标识 |
| 6 | 1 | Width | 宽度除以 8 |
| 7 | 1 | Height | 高度除以 8 |
24 位偏移组合公式:
1 | fragment_offset = (header[1] << 16) |
它的最大可表示值是 0xFFFFFF。计算 offset + fragment_payload_length 时使用更宽整数并先检查溢出,不能在 24 位域内回绕。
Width 与 Height
普通尺寸由字段乘 8 得到。例如 640×480:
1 | width_field = 640 / 8 = 80 = 0x50 |
基本头只能表达 8 像素粒度。特定零值/扩展尺寸规则按 RTP/JPEG 规范解释,不能把零直接分配为零宽图像,也不能用向下取整悄悄承载非 8 倍数尺寸。
Type 0 与 Type 1
常见基本类型中,Type 0 对应三组件 4:2:2 风格采样,Type 1 对应 4:2:0 风格采样。接收器据此构造 SOF 组件采样因子;它并不是 JPEG 文件里原样存在的一个 Marker 字段。带 Restart 支持的类型使用扩展类型范围并增加四字节 Restart Marker Header。
动态量化表 Header
出现条件
当 Q 指示动态量化表,并且 Fragment Offset 为零时,主头后出现四字节 Quantization Table Header,然后是量化表数据。后续 fragment 不重复表,否则 fragment offset 与数据区间会被混淆。
| 偏移 | 长度 | 字段 | 作用 |
|---|---|---|---|
| 0 | 1 | MBZ | Must Be Zero |
| 1 | 1 | Precision | 每张表使用 8/16 bit 的位掩码 |
| 2 | 2 | Length | 后续量化表字节总长,big-endian |
两张 8-bit、每张 64 项的表,header 为:
1 | 00 00 00 80 |
MBZ 为零,Precision 各表位为零,Length 0x0080=128。接收器再读取 128 字节,前 64 字节为表 0,后 64 字节为表 1。若某张表 precision 位为一,每项占两字节并按网络字节序读取,长度计算必须反映 128 字节/表。
Q 不是质量百分比
较低 Q 范围可按 RFC 规定的缩放过程生成常用量化表;保留范围必须拒绝或由明确 profile 处理;较高 Q 范围用于动态表/表缓存标识。它与图像编辑软件界面上的“75% JPEG Quality”不具有通用一一对应关系。验证报告应输出最终 64 项 DQT,而不只记录 Q 字节。
表缓存
允许缓存复用时,缓存键至少包含 RTP 会话/SSRC 作用域与 Q 标识,且只有完整合法的首 fragment 表才能原子提交。丢失首 fragment 时不能沿用上一帧同 Q 但来源不明的表。会话切换、SSRC 重启或明确配置变化要清空缓存。
三包 RTP/JPEG 重组实例
Frame 参数
假设一帧 640×480、Type 1(4:2:0)、Q=255,压缩扫描数据总长 2500 字节。使用两张动态 8-bit 量化表,共 128 字节。网络层将扫描数据分成 1000、1000、500 字节三片;RTP Timestamp 相同,Sequence Number 连续,最后一包 RTP Marker Bit 为一。
第一包主头:
1 | 00 00 00 00 01 FF 50 3C |
随后是量化表头 00 00 00 80、128 字节表数据,再跟扫描数据 [0,1000)。量化表头和表数据不计入 Fragment Offset 空间。
第二包主头:
1 | 00 00 03 E8 01 FF 50 3C |
00 03 E8 是 big-endian 24 位 1000,后面直接携带扫描数据 [1000,2000),不再有 Quantization Table Header。
第三包主头:
1 | 00 00 07 D0 01 FF 50 3C |
00 07 D0=2000,携带 [2000,2500),外层 RTP Marker Bit 为一。因此期望 Frame End 为 2000+500=2500。
Interval Map
包即使按 2、0、1 的顺序到达,也可以按 Fragment Offset 放入区间表:
1 | after packet 2: [2000,2500), expected_end=2500, marker_seen=true |
只有最后一步才能提交。Marker 包先到并不代表前面的洞已经填满;首包后到之前也没有动态量化表,不能构造可解码 JPEG Header。
重复与重叠
若重传包覆盖 [1000,2000) 且字节完全相同,可以幂等接受;若同一区间内容不同,说明包混帧、传输损坏或发送器违规,应丢弃整个 timestamp frame。部分重叠如 [900,1300) 也要逐重叠字节比较,不能以后到数据静默覆盖。
最大帧限制
即便 24 位 offset 允许接近 16 MiB,接收器仍应设置适合业务的 max_frame_bytes。验证:
1 | require fragment_offset <= max_frame_bytes |
使用减法式检查避免加法回绕。达到并发 frame 上限时,按 timestamp/超时策略丢弃完整上下文,不从多个 timestamp 拼片。
RTP/JPEG Restart Marker Header
四字节布局
带 Restart 的 Type 在八字节主头后增加:
| Bit/字节 | 字段 | 作用 |
|---|---|---|
| 0..15 | Restart Interval | 每个 Restart 区间的 MCU 数,big-endian |
| 16 | F | 当前包从一个 Restart Interval 的第一字节开始 |
| 17 | L | 当前包包含一个 Restart Interval 的最后字节 |
| 18..31 | Restart Count | 14 位区间序号/计数 |
后两个字节组合为:
1 | flags_count = (F << 15) | (L << 14) | (restart_count & 0x3FFF) |
Restart Header 实例
假设 interval 为 10 MCU,当前包完整承载计数为 7 的一个 Restart Interval,因此 F=1, L=1, count=7:
1 | 00 0A C0 07 |
00 0A 是 interval 10;C0 07 为二进制 11 00000000000111。若包只从区间中部开始,F 为零;若区间还会延续到下一包,L 为零。
与 JPEG DRI/RST 的关系
RTP Restart Header 帮助分片边界和错误恢复,但解码 JPEG Scan 时仍需要相应的 DRI/RST 语义。重组器根据 Type 与 interval 构造 DRI,并保证扫描数据中的 Restart Marker/区间边界与 RTP 元数据相容。不能把 00 0A C0 07 四字节直接复制进 JPEG 文件当作 DRI;合法 DRI 段应是:
1 | FF DD 00 04 00 0A |
从 RTP/JPEG 构造交换 JPEG
Header 合成顺序
RTP/JPEG 通常不逐包传输完整 JFIF/JPEG Header。接收器在帧片段完整后,根据 Payload Header 和表状态构造:
1 | SOI |
合成的 JPEG 文件字节不等于发送器编码前可能存在的原始 JFIF 文件,因为 APP 元数据、Marker 排列和表段组织可能未通过 RTP Payload 保留。应声明为像素/码流数据等价的重建,而不是原文件 byte-exact 还原。
SOF 尺寸构造
640×480 写入 SOF 时使用 16 位大端真实像素值 02 80 和 01 E0,而不是 RTP 头中的 50 3C。Type 1 生成 C1 采样 2×2、C2/C3 采样 1×1;Type 0 生成相应 4:2:2 采样。量化表 selector 与收到/派生的表编号一致。
DHT 来源
RTP/JPEG profile 通常依赖规定的 Huffman 表而不在每帧 Payload Header 中携带任意 DHT。接收器构造标准表段时应使用 profile 明确的完整表常量,并用单元测试验证 Canonical Code;不能从图像内容猜表。若发送端实际使用了不受 profile 支持的优化 Huffman 表却未传递,RTP 流无法按该约定正确解码。
完整性验证
构造后再次用独立 JPEG Marker Parser 验证 DQT/SOF/DHT/SOS 引用和 Scan 闭合,并尝试解码像素。RTP 区间完整只证明传输字节无洞,不证明 Type、Q、表缓存或扫描熵本身正确。
HTTP Multipart MJPEG 状态机
Boundary 提取
Content-Type: multipart/x-mixed-replace; boundary=... 中的 boundary 参数可能带引号。正文分隔行使用前导 --,结束 boundary 还带尾随 --。解析器应按 MIME 行规则读取,不能简单在任意 JPEG payload 中搜索 boundary 子串。
Part Header
每个 part header 以空行结束,常见 Content-Type 和 Content-Length 大小写不敏感。Content-Length 合法时可精确读取 body;缺失时需要扫描下一条合法 boundary。扫描期间仍要限制最大 part 长度,并处理 boundary 跨网络 buffer 的情况。
Body 与 JPEG 边界
Content-Length 定义 part body 长度,SOI/EOI 用于验证 JPEG。body 前后可能存在 CRLF,它们属于 multipart framing 而不是 JPEG payload。若长度与 EOI 位置冲突,应报告承载层长度错误,不能无条件以第一个 EOI 截断,因为 APP payload 或损坏数据也可能造成假阳性。
AVI 中的 MJPEG
Chunk 与帧
常见 AVI MJPEG 每个 00dc chunk 保存一个 JPEG frame,chunk size 给出字节长度,RIFF padding 不属于 JPEG。索引关键帧通常可标记每个 MJPEG frame,但仍应验证 SOI、SOF、SOS 和 EOI 完整。AVI 的 strh.dwRate/dwScale 定义帧时间,不由 JPEG marker 提供。
DIB 与 MJPG FourCC
AVI 视频格式块通过 handler 和 biCompression 声明 MJPEG 变体。历史实现可能对场顺序、缺省 Huffman 表、颜色空间或 APP marker 有私有约定。读取器应先遵守明确 FourCC/格式描述,再对 JPEG payload 做标准解析,不应看到 00dc 就一律认定为 MJPEG。
OpenDML 大文件
长时间 MJPEG 录制容易产生很高码率并超过传统 AVI RIFF 大小限制。OpenDML 通过 AVIX 和分层索引扩展容量,JPEG frame 字节本身不改变。容器分段边界必须落在 chunk 边界,不能把单个 JPEG frame 拆成两个 AVIX 段中的半个 chunk。
JPEG 解码器实现架构
Marker Parser
第一层只负责 SOI 到 EOI 的 marker、长度、表和扫描边界,输出帧描述和表对象。它不应在 APP 元数据失败时破坏核心 DQT/SOF/DHT 状态。所有表更新只有在完整段解析成功后提交。
Entropy Decoder
熵解码层接收限定在单个 scan 的 bit source,处理 stuffing、Huffman、restart 和 progressive state,输出量化系数。Marker parser 与 bit source 要共享明确的“下一个 marker”接口,避免双方重复消费 FF 字节。
Reconstruction
重建层完成反量化、IDCT、上采样、颜色转换、方向处理和输出裁剪。它可以按 MCU 行流式输出 baseline 图像;progressive 通常需要保存整帧系数。内存预算应根据 SOF、采样因子、模式和输出格式提前计算。
安全校验与资源限制
尺寸乘法
读取 SOF 后,先检查宽高非零、组件数和采样因子合法,再计算 MCU 数、block 数和输出 stride。所有乘法使用宽整数并与实现上限比较。即使文件字节很小,恶意 SOF 也可能声明巨大图像诱导内存分配。
表和状态数量
量化表、DC/AC Huffman 表和组件 table selector 都有编号范围。解析器应使用固定上限数组或安全映射,拒绝越界 ID。Progressive scan 数量、APP 元数据总量和同时重组的 MJPEG 帧数也需要限制。
不完整帧提交
缺少 EOI 的文件可以在某些容错解码器中尝试输出,但 API 必须把结果标记为 truncated。实时 MJPEG 在下一帧 SOI 到达时,可以丢弃上一不完整帧;不能把新帧的 DQT/SOF 接到旧 scan 后继续解码。
JPEG 验证报告规范
结构报告
报告应列出每个 marker 的绝对偏移、marker code、声明长度、结束偏移和解析状态;列出 SOF 尺寸、精度、组件采样、DQT/DHT 引用、SOS 范围、restart interval 和扫描数量。对于 APP 元数据,至少报告类型和边界,避免把大块二进制直接打印到日志。
码流报告
对每个 scan 可统计 MCU/block 数、DC/AC symbol 数、EOB、ZRL、restart 数、stuffed FF 数和损坏位置。统计与 SOF/DRI 推导值不一致时,应指出第一个偏离 MCU,而不是只输出“decode failed”。
MJPEG 序列报告
序列层应统计帧数、每帧字节数、平均/峰值码率、尺寸或表变化、时间戳间隔、丢帧和重组超时。JPEG 单帧合法不代表序列时间连续,承载层正常也不代表每帧 JPEG 配置完整,两层结果应分开呈现。
第九章 JPEG Coding Process 分类
Sequential DCT
顺序 DCT 模式在某个组件的一个扫描中传输每个系数的最终量化值。它可以使用 Huffman 或算术熵编码,并有不同样本精度类别。Baseline 是顺序 DCT 的受限子集,不等于所有 sequential JPEG。
Progressive DCT
渐进 DCT 通过 spectral selection 和 successive approximation 将同一幅图像分多次扫描传输。它仍是 DCT 有损编码,但解码器需要保存整幅量化系数状态。每个扫描只更新允许的系数范围和位平面。
Lossless
Lossless JPEG 不使用 DCT/量化,而在像素域用邻居预测加差值熵编码。SOF 类型和 SOS 参数选择预测器与 point transform。文件扩展名同样可为 .jpg,因此解码器不能看到 SOI/DHT 就假定存在 DQT/IDCT。
Differential Hierarchical
Hierarchical 模式可用多分辨率 frame,以较低分辨率图像为基础,对上采样预测和实际高分辨率之间的差异编码。Differential SOF 类型用于这些后续 frame。它与普通 MJPEG 的“连续独立 JPEG 图像”概念不同,支持较少。
Huffman 与 Arithmetic
多数常见 JPEG/MJPEG 使用 Huffman coding,但标准也定义 arithmetic coding。算术模式使用 DAC 条件表而不是 DHT Huffman 表来控制概率状态。只支持 Huffman 的实现应按 SOF/SOS 明确返回不支持。
SOF Marker 分类方法
Marker 决定过程
SOF marker code 同时表达 DCT/无损、顺序/渐进、Huffman/Arithmetic、Differential/Non-differential 和样本精度类别。解析器应建立 marker 到 coding process 的映射,并由该映射决定后续是否需要 DQT、DHT/DAC、哪些 SOS 字段范围合法。
不存在的 SOF 编码
Marker 空间中有保留或用于其他目的的值,不是 FFC0 到 FFCF 全都可作为 SOF。DHT、JPG extension、DAC 等位于同一区间。不能用 marker & 0xF0 == 0xC0 简单认定为 frame header。
多 Frame JPEG
Hierarchical JPEG 可在一个编码图像结构中出现多个 frame header,普通交换文件通常一 SOI/EOI 中一个 frame。MJPEG 解析器若只面向常见 baseline,可限制每帧只接受一个 SOF,但报告应说明 unsupported hierarchical,而不是笼统的 duplicate SOF。
Lossless JPEG 语法
Sample Prediction
无损模式对当前样本 X 使用左 Ra、上 Rb、左上 Rc 的组合预测。标准选择若干 predictor:仅 Ra、仅 Rb、仅 Rc、Ra+Rb−Rc、Ra+(Rb−Rc)/2、Rb+(Ra−Rc)/2、(Ra+Rb)/2 等。边界首行/首列有规定初值和简化规则。
Difference Category
编码的是 Diff = X - Px,再按 category 与 amplitude bits 熵编码,概念类似 DC difference。解码后 X = Px + Diff,并按样本精度与 point transform 恢复。没有 8×8 block、AC coefficient 或 EOB。
Point Transform
SOS 的 Al 在 lossless 过程中可指定 point transform,低位可被省略,重建精度相应变化。所谓 lossless 是相对于变换后的样本定义;Al=0 才保留全部指定位精度。验证报告应显示 precision 和 point transform。
Restart
无损模式同样可用 restart interval。restart 后预测状态按规范重新初始化,首样本用默认 predictor 基准。若只重置 DCT DC predictor 的数组但无损解码使用另一状态,恢复后数值会错。
Arithmetic JPEG
DAC 段
Define Arithmetic Conditioning Table 为 DC/AC 算术编码设置 conditioning 值。每项包含 table class/id 和 conditioning byte。引用的表必须在扫描前定义,编号范围和参数值需按过程约束校验。
Probability State
算术解码使用上下文状态、MPS/LPS 和区间寄存器,不读取 DHT variable-length code。DC conditioning 根据差值类别历史选择 context,AC conditioning 根据位置、EOB 和系数状态选择 context。实现不能复用 CABAC 或通用算术库而忽略 JPEG 特定状态机。
Byte Stuffing 与 Marker
Arithmetic scan 仍需处理 marker 边界和 FF 相关规则,但内部 coder 的 byte output/renormalization有专用过程。仅按 Huffman bit reservoir 的 stuffing 逻辑可能错误消费终止字节。若不实现算术模式,应由 SOF 尽早拒绝。
Differential 与 Hierarchical 过程
DHP
Define Hierarchical Progression 可描述层级图像的最大尺寸、组件和采样。后续 frame 可能是第一层非差分图像或差分图像。解析器需要维护层级 frame 状态,不能每遇 SOF 都清空到独立 JPEG。
EXP
Expand Reference Components 段控制参考组件的水平/垂直扩展,为差分 frame 生成预测参考。扩展与插值规则由标准指定。尺寸和采样因子需与当前层级一致。
MJPEG 边界
工程所谓 MJPEG 几乎总把每个 SOI..EOI 当独立显示帧,而不是使用 hierarchical dependency。若输入使用差分 JPEG,随机访问和“每帧独立”假设失效,因此不应在未声明支持时归入普通 MJPEG pipeline。
DNL 与未知高度
SOF 高度为零
JPEG frame header 可把 Y 高度写为零,随后用 DNL marker 定义 scan lines 数。解析器在此之前不能按零高度分配最终输出,也不能拒绝所有零高度为损坏,除非实现明确不支持 DNL。
DNL 段
DNL 带两字节大端 line count,长度字段应符合规定。line count 必须非零并与已解码 MCU 行和采样因子相容。DNL 的允许位置和次数受过程约束,重复冲突值应判错。
流式分配
支持 DNL 的解码器可按 MCU 行动态增长有界缓冲,或等待 line count 后确认。必须设置最大高度和总像素上限,防止无 DNL 流无限积累。遇到 EOI 仍无有效 DNL 时帧不完整。
DRI 与 Restart 完整校验
Interval 的 MCU 单位
DRI 的 16 位值是 restart interval 的 MCU 数,不是字节数或 block 数。4:2:0 交错 scan 一个 MCU 含六 block,interval=10 表示处理十个这样的 MCU 后 restart。
最后一个 Interval
扫描末尾若剩余 MCU 少于 interval,通常不要求在 EOI/下个 marker 前额外 RST。验证器计算预期 marker 数时应使用完整 interval 边界,不能简单 ceil(total_mcu/interval)。
RST 序列
首个预期 marker 通常 RST0,随后按模 8。新的 scan 是否重置序号按标准过程处理。跳过损坏 interval 后,容错器可根据实际 RSTn 重新同步序号,但必须记录缺失/意外 marker。
Entropy Padding
到 restart 或 scan 结束时,熵数据对齐到字节,剩余 padding bit 应具有规范要求的填充值。严格验证可检查;容错解码器仍不能把 padding 当下一个 Huffman code。
Progressive Scan 合法性
DC First
DC 初次扫描要求 Ss=0, Se=0, Ah=0,Al 指定首次传输位位置。可包含多个组件。每个组件 DC 在 refinement 前必须已有 first scan。
DC Refinement
DC refinement 同样 Ss=Se=0,且 Ah/Al 应形成相邻 successive approximation 位关系。它通常为每个 block 传一个 refinement bit,不重新 Huffman 编码完整 DC category。
AC First
AC 初次扫描要求单组件,1 <= Ss <= Se <= 63,Ah=0。它使用 EOBRUN 和 run/size 传频带系数。扫描不能与已传输的同位平面区间非法重叠。
AC Refinement
AC refinement 只补充已有非零系数的低位并可能引入新的 ±1 系数,语法包含 correction bits、EOBRUN 和特殊 run。实现状态机比 baseline AC 解码复杂,不能先解成普通 run/size 再统一右移。
Scan Script 验证
验证器为每个组件、每个系数维护已接收的最低位状态。每个新 scan 检查 spectral range、Ah/Al 连续性和重复。EOI 时若某些期望系数从未初始化,是否可接受取决于扫描脚本/精度;应报告不完整 progression。
第十章 JPEG Table 生命周期
表在图像内重定义
DQT、DHT 和 DAC 可以在扫描前定义,某些过程允许在扫描之间更新供后续 scan 使用。已经解码的系数不受后续表反向改变。解析器应给表保存版本/生效偏移,不能只在 EOI 后使用最终表解码所有扫描。
Abbreviated Format
JPEG 标准允许 abbreviated image/data/table specification,在外部交换约定中复用表而不每幅重复。普通 .jpg 和多数 MJPEG 期望单帧自包含,但某些流可在会话层先发送 tables-only 数据。接收器只有在协议明确规定表缓存生命周期时才可接受缺表 frame。
默认 Huffman 表
部分 MJPEG 生态允许缺少 DHT,并约定使用常见默认表。这个行为不是“任何 JPEG 缺 DHT 都自动合法”。解码器应由 FourCC、传输 profile 或显式兼容选项启用,并在报告中标记 injected default tables。
配置变化
连续 MJPEG frame 可以改变 DQT、DHT、尺寸或采样。每帧自包含解码器自然更新;复用外部表的协议必须明确何时替换缓存。多线程解码不能让后到帧的表覆盖正在解码帧的 table object。
JPEG APP Marker 边界
APP0 多格式
APP0 不只可能是 JFIF,也可能是 JFXX 等扩展。解析器先检查 payload 标识和最小长度,再分派。没有匹配标识的 APP0 仍可作为未知应用段跳过。
Exif TIFF Offset
Exif 中 TIFF offset 相对于 TIFF header 起点,而不是 APP1 marker 或文件起点。每个 IFD entry 包含 tag/type/count/value-or-offset,count × type_size 需检查溢出,链表 next IFD 需防循环。
XMP
XMP 可位于 APP1,包含标准标识和 XML packet;扩展 XMP 可能跨多段。解析器设置元数据总量和 XML 安全限制。XMP 的 orientation/颜色元数据可能与 Exif 冲突,应用要定义优先级。
ICC 顺序
ICC APP2 每段的 sequence number 从 1 到 count。所有段 count 应一致,序号唯一且完整。拼接顺序按序号,而非文件出现顺序。缺片时不要把部分 profile交给色彩管理当完整数据。
大 APP 数据
单 JPEG segment length 为 16 位,因此超大元数据需要分片协议。解析器不能因单段不大就无限累计数千 APP 段;设置每帧 APP 总量上限,并允许跳过不需要的元数据而继续核心图像。
第十一章 MJPEG 场编码变体
一帧一个 Field
历史视频采集可能把每个 JPEG 图像表示一个交错视频 field,容器或 FourCC 约定场序和两 field 如何组成显示 frame。JPEG SOF 本身只看到单幅高度,无法知道它是 top field 还是完整 frame。
两 Field 同 Chunk
某些 AVI MJPEG 变体在一个 chunk 中串联两个 JPEG field,可能有私有 APP marker 指示偏移。通用“从首 SOI 到首 EOI即一个 AVI frame”会丢掉第二 field。解析必须由具体 FourCC/profile 决定。
Field Order
Top-field-first/bottom-field-first 影响显示和去隔行。容器格式块、APP marker 或外部采集配置提供线索。猜错会产生运动锯齿和时间颠倒;JPEG baseline 语法本身不提供通用场序字段。
MJPEG 序列配置管理
尺寸变化
HTTP 摄像头流可能在连接期间改变分辨率。每帧 SOF 是最终依据,解码器在提交新尺寸帧前重建输出缓冲并通知渲染层。旧尺寸 stride 不可复用。
Sampling 变化
编码器可从 4:2:2 改到 4:2:0,输出 RGB 尺寸不变但 MCU、解码缓冲和上采样路径改变。硬件 JPEG decoder 可能要求重新配置。接收器比较组件数与采样因子,而不是只比较宽高。
Table 变化
质量自适应会改变 DQT,优化 Huffman 也可能每帧改 DHT。表属于 frame/config snapshot,不能以“首帧表”永久覆盖后续自包含帧。只有协议规定 abbreviated mode 才缓存缺失表。
时间变化
MJPEG 没有帧内时间字段,帧率变化由 HTTP 到达节奏、RTP timestamp 或容器 time base 表达。不能从 JPEG size 或 EXIF 拍摄时间稳定推导实时展示时间。
JPEG Encoder 输出验证
编码前参数
记录输入像素格式、矩阵/范围、宽高、采样、质量到 DQT 映射、restart interval、Progressive scan script 和元数据策略。质量数字只作为编码器参数,最终报告仍输出实际 DQT。
结构回读
编码完成后用独立 parser 回读 SOI、DQT、SOF、DHT/DAC、DRI、SOS 和 EOI,核对表引用、尺寸、scan 参数和段长度。不要让 writer 与 parser 共享同一结构体序列化代码作为唯一测试。
像素验证
重新解码并检查输出尺寸、颜色、PSNR/SSIM 或逐像素容差。Lossless 模式应在定义精度/point transform 下 bit-exact;有损模式使用由质量目标定义的阈值,同时人工/感知测试关注 ringing、blocking 和色度边缘。
Determinism
编码器若要求可重复输出,需要固定 Huffman optimization、线程归并顺序、浮点舍入和元数据时间字段。视觉等价不代表字节相同;测试契约应提前定义是结构、像素还是 bitstream determinism。
MJPEG 带宽与队列算法
峰值而非平均值
一帧复杂场景可能远大于平均帧。发送队列按最大 frame 与突发数量设置总上限。若只能丢帧,应丢完整 JPEG frame/整个 timestamp,不要丢 frame 中间任意分片后仍提交。
Backpressure
实时预览通常优先最新帧;队列满时可丢最旧未发送完整帧。录像则更重完整性,可能阻塞或降质量/帧率。无论策略如何,RTP sequence/timestamp 或容器 frame count 要反映 discontinuity。
Decode Queue
多线程解码可并行不同独立 JPEG frame,但输出按 timestamp 排序。限制 in-flight frame 数与每帧 memory。尺寸/表变化包含在各 frame context,不能依赖共享可变全局 decoder config。
JPEG 错误分类
Marker 结构错误
包括缺 SOI/EOI、segment length 越界、非法 marker、SOF/SOS 顺序错误和表引用缺失。此类错误可在像素解码前发现。
Entropy 错误
包括无效 Huffman code、系数越界、错误 restart、Progressive 状态矛盾和 scan 提前结束。报告第一个 MCU/block/component 与 bit/byte offset。
Reconstruction 错误
包括不支持的 precision、算术/层级过程、输出分配超限、颜色空间未知。它们可能是能力不足而非输入损坏,错误码区分 unsupported 与 malformed。
Carrier 错误
包括 HTTP Content-Length/boundary、RTP fragment 缺失/乱序、AVI chunk/index 错误。先报告承载错误,再说明因此造成 JPEG frame truncated,避免把根因全归到 JPEG decoder。
第十二章 Marker Segment 通用字节布局
带长度段
除少数独立 marker外,段通常为:
1 | offset +0 0xFF |
L为big-endian,包含长度字段自身2字节,不包含FF xx marker。因此payload bytes=L-2,下一位置=marker_offset+2+L。L小于2非法。
Fill FF
Marker前可出现额外FF fill byte。Parser跳过连续FF直到非FF code,但在entropy-coded segment内还需区分FF 00 stuffing和RST。FF FF不是payload中的字节FF表示法。
00 Code
FF 00在熵数据中表示数据字节FF;在marker扫描的非熵区域不是合法独立marker。Parser状态决定解释,不能全文件统一替换FF 00。
Marker 编码空间与解析状态
独立 Marker 与带长度 Marker
JPEG 的 Marker 由一个或多个 0xFF 前缀字节以及一个非 0x00、非 0xFF 的 Marker code 构成。绝大多数 Marker 后面带一个 16 位大端长度,但以下类型不带长度字段:
| Marker | Code | 是否带长度 | 作用 |
|---|---|---|---|
TEM |
FF 01 |
否 | 算术编码相关的临时标记,普通 Huffman JPEG 很少使用 |
RST0~RST7 |
FF D0~FF D7 |
否 | 熵数据中的重启边界 |
SOI |
FF D8 |
否 | 一幅 JPEG 图像的开始 |
EOI |
FF D9 |
否 | 一幅 JPEG 图像的结束 |
其余已定义的 SOF、DHT、DAC、DQT、DNL、DRI、DHP、EXP、SOS、APPn 和 COM 等 Marker 都具有长度字段。实现不能使用“code 大于某值”推测是否带长度,而应维护明确的分类表;否则遇到保留码、层级过程或算术编码相关段时容易错位。
SOF Marker 的编码过程映射
FFC0~FFCF 范围并不全部等价。不同 SOF code 指明 sequential、progressive、lossless、differential 以及 Huffman/Arithmetic 等编码过程。FFC4 是 DHT、FFC8 是 JPG 扩展保留用途、FFCC 是 DAC,因此它们不是 SOF。常用映射如下:
| Marker | 编码过程 | 熵编码 |
|---|---|---|
SOF0 / FFC0 |
Baseline DCT, sequential | Huffman |
SOF1 / FFC1 |
Extended sequential DCT | Huffman |
SOF2 / FFC2 |
Progressive DCT | Huffman |
SOF3 / FFC3 |
Lossless sequential | Huffman |
SOF5 / FFC5 |
Differential sequential DCT | Huffman |
SOF6 / FFC6 |
Differential progressive DCT | Huffman |
SOF7 / FFC7 |
Differential lossless | Huffman |
SOF9 / FFC9 |
Extended sequential DCT | Arithmetic |
SOF10 / FFCA |
Progressive DCT | Arithmetic |
SOF11 / FFCB |
Lossless sequential | Arithmetic |
SOF13 / FFCD |
Differential sequential DCT | Arithmetic |
SOF14 / FFCE |
Differential progressive DCT | Arithmetic |
SOF15 / FFCF |
Differential lossless | Arithmetic |
只看到 SOF2 就能确定需要 Progressive 状态机,但仍不能仅凭 SOF 推断颜色空间;颜色解释还要结合组件数量、组件 ID、JFIF、Adobe APP14 以及外层容器约定。解码器在不支持某个合法过程时应返回 unsupported coding process,不要把合法但未实现的 SOF 统一报告为“文件损坏”。
Marker 前缀中的 Fill Byte
在允许 Marker 出现的位置,连续 FF FF ... code 中前面的额外 FF 是 fill byte。Marker 的逻辑起始偏移可以定义为第一枚 FF 或紧邻 code 的最后一枚 FF;诊断工具必须选定一种口径并保持一致。若要原样重写文件,还要保存 fill byte 数量,不能规范化后声称输出与输入逐字节一致。
1 | FF FF FF DB 00 43 ... |
在非熵状态中,前缀后遇到 00 是非法 Marker code;在熵状态中,FF 00 却代表一个字面量数据字节 FF。这种差异要求解析器至少维护 SEGMENT 与 ENTROPY 两个状态,不能用全局搜索或全文件反转义预处理。
保留 Marker 的处理策略
遇到未知但具有可判定长度的 APPn 或扩展段时,可以保留原始 payload 并按长度跳过。遇到未分类的保留 code 时,解析器不能凭空假定它带长度,因为错误假设会让整个后续码流失去同步。严格验证器可停止并报告 Marker code 与偏移;面向恢复的扫描器可以在受控最大窗口内寻找新的 SOI,但恢复结果必须标为推测边界。
Segment 长度闭合与整数安全
长度字段的最小值
所有带长度 Marker 的 L 至少为 2,因为长度把自己的两个字节计算在内。L == 2 表示空 payload,是否语义合法由具体 Marker 决定。例如 COM 可以有空 payload,而 DQT、DHT、SOF 和 SOS 显然还需要自己的最小字段。通用层先验证 L >= 2,语义层再验证精确最小长度。
结束偏移公式
假设 Marker 最后一个前缀 FF 位于 m,code 位于 m+1,长度字段位于 m+2:
1 | L = read_u16be(m + 2) |
区间采用左闭右开表示,payload 是 [payload_begin, segment_end)。计算时先检查 m <= limit - 4,再读取长度;随后检查 L <= limit - (m + 2),不能先执行可能溢出的 m + 2 + L。JPEG 单段长度只有 16 位,但外层文件偏移仍可能为 64 位,尤其是从 AVI、MP4 或网络缓存中的大偏移解析嵌入图像时。
精确消费而不是“至少够用”
DQT、DHT 等允许一段内串联多个表,必须循环解析到 segment_end 恰好闭合。SOF、SOS、DRI 等由计数字段或固定语法决定长度,计算得到的期望长度必须等于 L,不能只判断“大于等于”。允许尾随未知字节会把损坏隐藏为所谓扩展,后续重写还可能丢失这些字节。
1 | SOF expected L = 8 + 3 * Nf |
乘法虽然在标准字段范围内很小,统一解析库仍应采用 checked arithmetic。计数字段为零时应先按语法判错,不能让零长度数组绕过组件存在性验证。
DQT 字段布局
Table Specifier
DQT payload可连续包含一张或多张表。每表首字节高4位Pq表示精度,低4位Tq表示table ID:
1 | PqTq = read_u8() |
Baseline通常使用8-bit量化表,16-bit精度用于其他过程。Pq只允许定义值,Tq在标准表编号范围内。
长度
一张8-bit表消耗65字节(specifier+64值),16-bit表消耗129字节。DQT segment的payload长度必须能被逐表解析恰好耗尽。不能由总长度除65断言表数,因为可混合精度。
读取顺序
每个值big-endian(16-bit时)并按zig-zag扫描位置给出。值不能为0。Parser可同时保存scan[64]和映射后的natural[8][8],报告原始顺序防止重写变化。
DQT 实例开头
1 | FF DB 00 43 00 10 0B 0C 0E ... |
0043=67,payload65字节;specifier 00表示8-bit table 0,后面64个值。下一marker位于当前marker偏移+69。
DHT 字段布局
Table Class 与 ID
每表首字节高4位Tc:0为DC,1为AC;低4位Th为表ID。随后16字节Li分别给码长1..16的symbol数量,再跟sum(Li)个symbol。
1 | TcTh |
长度闭合
每表消耗1+16+n字节。Payload可串多表。累计count使用宽整数且受最大symbol范围限制。若剩余不足17字节,DHT截断。
Code Space 验证
按长度构造canonical code时,当前code加count不能超出该长度可表示空间。过度订阅表会让多个symbol共享前缀,非法。Incomplete table是否允许按过程判断,但解码未分配code时必须报错。
Symbol 合法性
DC symbol通常表示category并受样本精度限制;AC symbol的run/size组合受过程限制。DHT结构合法不代表每个symbol在当前coding process合法,第二阶段验证。
SOF 字段布局
固定头
SOF payload前6字节为precision P、height Y、width X、component count Nf:
| Payload 偏移 | 长度 | 字段 |
|---|---|---|
| 0 | 1 | P |
| 1 | 2 | Y big-endian |
| 3 | 2 | X big-endian |
| 5 | 1 | Nf |
随后每组件3字节,所以segment length应为8 + 3×Nf(包含2字节length)。若有规范扩展则按具体process判断,普通SOF严格核对。
Component Spec
1 | C_i : component identifier |
H/V不能为0,并受最大值及总blocks per MCU约束。Component ID在frame内唯一。Tq引用表可稍后在scan前定义,但开始解码时必须存在。
尺寸上限
X为0通常不合法;Y可在支持DNL时暂为0。计算X×Y×components前检查。Nf为0非法,常见灰度1、YCbCr/RGB3、CMYK/YCCK4。
SOF0 实例
1 | FF C0 00 11 08 01 E0 02 80 03 |
长度0x11=17;P=8,Y=480,X=640,Nf=3。组件1采样2×2用量化表0;组件2/3采样1×1用表1,典型4:2:0。
SOS 字段布局
Scan Header
SOS payload先给scan component count Ns,每组件2字节,然后三个扫描参数:
| 字段 | 长度 | 说明 |
|---|---|---|
Ns |
1 | 本scan组件数 |
Cs_j |
1 | 组件selector |
TdTa_j |
1 | DC/AC table ID |
Ss |
1 | spectral start |
Se |
1 | spectral end |
AhAl |
1 | successive approximation |
Segment length=6+2×Ns。Cs_j必须引用SOF组件且scan内唯一。
Table Selector
TdTa高4位DC table,低4位AC table。Lossless/某些scan可能只使用相关部分,但字段仍按process解释。Huffman DCT scan开始前引用表必须定义。
Baseline 参数
Baseline sequential通常Ss=0, Se=63, Ah=0, Al=0。任何偏离需检查SOF过程;SOF0下不可把Progressive参数当合法扩展。
SOS 实例
1 | FF DA 00 0C 03 |
长度12,Ns=3;Y用DC0/AC0,Cb/Cr用DC1/AC1,Ss=0、Se=63、AhAl=0。紧随其后进入熵数据状态,不再按普通segment length寻找下一个字节。
DRI 字段布局
固定长度
DRI segment length应为4:2字节length加2字节restart interval。Payload值big-endian。多余/不足字节按严格模式判错。
1 | FF DD 00 04 00 0A |
表示每10 MCU一个restart marker。值0关闭restart。
更新生效
新的DRI在后续scan生效,可在图像过程中重定义供下一scan使用。正在解码scan的interval使用其开始时活动值,不被scan中非法插入普通段任意改变。
DNL 字段布局
Line Count
DNL与DRI类似固定segment length4,payload为16-bit NL。它补充frame lines:
1 | FF DC 00 04 YY YY |
NL=0非法。解析后更新frame高度并验证已处理MCU行不超过最终值。
APP0 JFIF 字段布局
最小 Payload
JFIF标识后依次是version major/minor、density units、Xdensity、Ydensity、thumbnail width/height,再跟3×Xthumbnail×Ythumbnail字节RGB缩略图。
| 字段 | 长度 |
|---|---|
JFIF\0 |
5 |
| version | 2 |
| units | 1 |
| Xdensity | 2 |
| Ydensity | 2 |
| Xthumbnail | 1 |
| Ythumbnail | 1 |
最小payload14字节,segment length至少16。缩略图乘法检查溢出和剩余长度。
Density
Units 0表示仅比例,1/2表示每英寸/厘米等定义。Density为零的处理按兼容策略,但不能拿density替代图像pixel尺寸或SAR而不说明来源。
APP14 Adobe 字段布局
标识与 Transform
Adobe APP14常以Adobe标识,随后version、flags0、flags1和transform。Transform可帮助区分unknown/RGB/YCbCr/YCCK等约定。先验证最小长度与标识。
与组件数
三/四组件和transform应相容。冲突时不要在熵解码前修改组件数据,只在颜色转换层选择/报告不确定解释。
COM 字段
任意字节
COM payload是注释数据,标准层不保证UTF-8或null终止。按segment length保存,UI尝试解码时提供encoding策略和替换字符。二进制控制字符需转义。
多 COM
可有多个COM,顺序保留。删除/编辑只需重写各segment和整体字节,不存在父容器size,但不能在entropy segment中随意插入未经允许的marker位置。
第十三章 熵数据 Byte Reader
三种返回
Bitstream byte reader可返回DATA(byte)、RESTART(code)或END_MARKER(code)。逻辑:读普通非FF为DATA;读FF后跳fill FF,若00返回DATA(FF),若D0..D7返回RESTART,否则把marker交回上层结束scan。
1 | read_entropy_byte(): |
EOF在任何中间状态都返回truncated。Marker code字节不能丢失,上层需知道其offset并继续segment解析。
Bit Reservoir
DATA byte按MSB-first加入reservoir。Huffman decoder查看前若干bit但只在命中后消费。到restart/end marker时丢弃对齐padding,reservoir清空。Marker不是reservoir数据。
Baseline Block 解码实例流程
DC
从组件DC表解一个category S;若S>0读S个amplitude bit并extend为diff;DC=previousDC+diff并更新该组件predictor。把DC放系数index0。
AC
从k=1循环:解run/size。0x00则EOB;0xF0令k+=16;否则k+=run,检查k<=63,读size amplitude并放到zig-zag index k,然后k++。到64自然结束。
反量化
每个扫描位置系数乘活动DQT对应值,再映射到natural位置。中间值用足够位宽,防乘法溢出。完成64系数后IDCT。
输出
IDCT加level shift并限幅,写组件plane的对应8×8位置。边缘block仍解完整,最终按width/height裁剪。MCU全部组件完成后推进。
Canonical Huffman 构造伪代码
1 | code = 0 |
实际循环边界修正为i < count[length]。每次进入长度前的code状态按JPEG规范构造。报告可打印symbol:length:code验证。
JPEG 完整十六进制审计流程
第一遍
从offset0要求SOI,逐marker记录offset/length,解析表与SOF;遇SOS解析header后进入entropy reader,直到返回非RST marker;继续处理下一scan/EOI。任何段结束不闭合即停止当前frame。
第二遍
基于第一遍snapshot验证每个scan引用、coding process、MCU数量、restart和Progressive状态。APP元数据独立解析,不影响核心marker offset。
第三遍
可选熵解码/像素重建,统计block和输出hash。结构成功但熵失败时保留第一遍结果,错误定位到scan/MCU,而不是丢失整份报告。
8×8 JPEG 完整测试向量
测试向量的用途
下面是一幅由受控编码过程产生、能够被实际 JPEG 解码器读取的 8×8 黑色图像。编码过程为 Baseline DCT,精度 8 位,三个组件均采用 H=1, V=2,只有一个 MCU;码流包含 JFIF、COM、DQT、DHT、SOF0、SOS 和 EOI。这个向量很小,但仍覆盖 Marker 长度、四张 Huffman 表、组件采样、表选择、DC 差分、EOB 与扫描尾部填充,适合解析器的单元测试。
完整 222 字节如下,左侧是十六进制绝对偏移:
1 | 0000 FF D8 FF E0 00 10 4A 46 49 46 00 01 02 00 00 01 |
测试向量只用于解释格式,不应把其中的量化表、Huffman 表或组件采样因子当成所有 MJPEG 帧的固定模板。真实编码器可以选择不同质量、表和采样方式,只要满足所声明编码过程的语法。
Marker 目录
依据 next = marker_offset + 2 + L 计算所有带长度段,可以得到如下目录:
| Marker | 起始偏移 | L |
Payload 长度 | 下一位置 |
|---|---|---|---|---|
SOI FFD8 |
0x0000 |
无 | 无 | 0x0002 |
APP0 FFE0 |
0x0002 |
16 | 14 | 0x0014 |
COM FFFE |
0x0014 |
16 | 14 | 0x0026 |
DQT FFDB |
0x0026 |
67 | 65 | 0x006B |
DHT FFC4 |
0x006B |
75 | 73 | 0x00B8 |
SOF0 FFC0 |
0x00B8 |
17 | 15 | 0x00CB |
SOS FFDA |
0x00CB |
12 | 10 | 0x00D9 |
| 熵数据 | 0x00D9 |
无 | 3 | 0x00DC |
EOI FFD9 |
0x00DC |
无 | 无 | 0x00DE |
最后结束偏移 0x00DE 等于十进制 222,恰好等于文件长度,没有尾随数据。SOS 的下一位置 0x00D9 是熵数据开始,而不是下一个普通段;只有熵读取器发现非 stuffed Marker 后,控制权才回到 Marker 解析器。
APP0 JFIF 解析
APP0 payload 从 0x0006 开始。前五字节 4A 46 49 46 00 是 JFIF\0,版本 01 02 表示 1.02,density unit 为 0,X/Y density 都为 1,缩略图宽高均为 0。因此最小 payload 恰好 14 字节,不存在缩略图 RGB 数据。
1 | 4A 46 49 46 00 01 02 00 00 01 00 01 00 00 |
unit 为 0 时,两个 density 值只表达像素纵横比例;这里都是 1,因此没有由 JFIF 声明的非方形比例。它们不等于图像宽高,不能把 00 01 错读为“一像素图像”。
COM 解析
COM payload 位于 0x0018~0x0025,共 14 字节:
1 | 4C 61 76 63 36 32 2E 32 38 2E 31 30 32 00 |
这里可以按 ASCII 显示,但 COM 语法本身没有规定所有内容必须是 ASCII 或以 NUL 结尾。解析器应由段长度取得 14 字节,再由上层显示策略处理最后的零字节。
DQT 解析
DQT 从 0x0026 开始,L=0x0043=67,payload 长 65。第一个 payload 字节 00 的高半字节 Pq=0,所以每个量化值一字节;低半字节 Tq=0,所以定义量化表 0。剩余恰好 64 字节,没有第二张表。
前若干扫描顺序值为:
1 | 08 04 04 04 04 04 05 05 05 05 05 05 06 06 06 06 ... |
这些值按照 JPEG 的 zig-zag 扫描索引给出,不是简单的二维逐行矩阵。验证器要检查每个值非零,并将第 k 个值映射到 zig-zag 对应的自然坐标。由于三个 SOF 组件的 Tq 都是 0,本帧所有组件共享这张表。
DHT 中四张表的闭合解析
DHT payload 长 73 字节,其中连续放置四张表。每张表都由一字节 TcTh、16 个长度计数和若干 symbol 构成。逐表消费结果如下:
| 表 | Tc |
Th |
非零长度计数 | Symbol |
|---|---|---|---|---|
| DC0 | 0 | 0 | L1=1, L2=1 |
00 08 |
| DC1 | 0 | 1 | L1=1 |
00 |
| AC0 | 1 | 0 | L1=1 |
00 |
| AC1 | 1 | 1 | L1=1 |
00 |
第一张表占 1 + 16 + 2 = 19 字节,其余每张占 1 + 16 + 1 = 18 字节,总数 19 + 18 × 3 = 73,与 DHT payload 完全闭合。
由 Canonical Huffman 规则,DC0 的长度一码字首先获得 code 0,对应 category 0;长度二码字获得 code 10,对应 category 8。DC1、AC0 和 AC1 各只有一个长度一码字,因此 symbol 00 都对应 code 0。AC 表中的 symbol 00 表示 EOB;DC 表中的 00 表示差值 category 0,二者数值相同但语义由 table class 决定。
SOF0 的尺寸与 MCU 几何
SOF0 payload 可拆成:
1 | 08 sample precision P = 8 |
三个组件的最大采样因子为 Hmax=1、Vmax=2,一个 MCU 覆盖的图像区域为 8×Hmax 乘 8×Vmax,即 8×16 像素。图像只有 8×8,因此水平方向一个 MCU、垂直方向一个 MCU,但 MCU 中每个组件仍按自身 H_i×V_i=2 解码两个 8×8 block,超出图像下边缘的重建行在最终输出时裁掉。
1 | mcu_width_pixels = 8 * Hmax = 8 |
采样因子 0x12 的高半字节是一、低半字节是二,不能按 little-endian 或十进制十二解释。此例也说明不能仅依据“输入想要灰度”推断 JPEG 必然为单组件;最终码流的 SOF 才是权威结构。
SOS 的表选择与扫描参数
SOS payload 为:
1 | 03 Ns = 3 |
三个 selector 都能在 SOF0 找到,且扫描内没有重复。Baseline Sequential 要求的 Ss=0、Se=63、Ah=Al=0 均满足。开始读取熵数据前,量化表 0、DC 表 0/1 与 AC 表 0/1 都已经定义。
熵数据逐 bit 解码
熵 payload 三字节为:
1 | 9F C0 07 = 10011111 11000000 00000111 |
本例没有 FF 00 stuffing 或 Restart Marker,直接按 MSB-first 消费。MCU 内依次解码 C1 的两个 block、C2 的两个 block、C3 的两个 block。结果如下:
| Block | 组件 | 消费 bit 范围 | DC Category | DC Diff | AC |
|---|---|---|---|---|---|
| 0 | C1 | 0~10 | 8 | -128 | EOB |
| 1 | C1 | 11~12 | 0 | 0 | EOB |
| 2 | C2 | 13~14 | 0 | 0 | EOB |
| 3 | C2 | 15~16 | 0 | 0 | EOB |
| 4 | C3 | 17~18 | 0 | 0 | EOB |
| 5 | C3 | 19~20 | 0 | 0 | EOB |
第一个 block 的 DC0 code 为 10,因此 category 为 8;接下来的八个 amplitude bit 是 01111111,无符号值 127。由于首位为零,JPEG 的 extend 规则给出:
1 | diff = 127 - (2^8 - 1) = -128 |
随后 AC0 的 code 0 解出 EOB。第二个 C1 block 使用 DC0 code 0 得到 category 0,再用 AC0 code 0 得到 EOB。C2/C3 使用的 DC1 和 AC1 都只有 code 0,所以每个 block 各消费两个零 bit。六个 block 合计消费 21 bit,最后三位 111 是到字节边界的扫描填充,不是新的 Huffman symbol。
DC predictor 对每个组件分别维护。C1 第一个 block 得到 -128,第二个 block diff 为 0,因此其 DC 仍为 -128;C2、C3 分别从零 predictor 得到零。若错误地让三个组件共享一个 predictor,色度 block 会继承 -128,输出颜色会明显错误。
EOI 与完整性判定
熵读取器在消费完一个 MCU 所需的六个 block 后丢弃对齐填充,下一字节对是 FF D9。它不是熵字节,因为 FF 后的 code 为 D9,所以读取器返回 END_MARKER,外层确认 EOI 位于 0x00DC。若只靠 FF D9 搜索,本例恰好也能找到边界,但那不能证明算法适用于带 stuffing、Restart、多 Scan 或损坏输入的情况。
测试向量应验证的断言
把这个向量加入回归测试时,不应只断言“解码成功”。更有价值的断言包括:文件长度为 222、Marker 偏移与上表一致、DHT 恰好闭合为四张表、SOF 派生一个 MCU 和六个 block、熵层消费 21 个有效 bit、剩余三位为一、EOI 后没有尾随字节,以及解码输出尺寸为 8×8。这样能够区分 Marker Scanner、表解析、MCU 几何和 Huffman Reader 的回归来源。
JPEG 结构审计伪代码
Marker 层与 Scan 层分离
下面的伪代码强调两个解析状态。read_marker 只在非熵状态运行;decode_scan_until_marker 处理 stuffing 和 Restart,并把第一个真正 Marker 的偏移保留下来。
1 | parse_jpeg(input, frame_limit): |
read_marker 可以跳过 fill FF,但必须拒绝非熵状态中的 FF 00。当 decode_scan_until_marker 发现 FF D0~FF D7 时,它验证 Restart 顺序、清空 bit reservoir 和 DC predictor,然后继续当前扫描;发现其他 code 时不消费其 Marker 语义,只返回前缀偏移让外层统一处理。
表快照与 Scan 生命周期
JPEG 允许在不同 Scan 之间重新定义表,因此 Scan Context 应在 SOS 时捕获当时引用的 DQT、DHT/DAC 和 DRI 状态,或保证后续重定义不会修改已经开始的 Scan。用一个全局可变 table[4] 并让工作线程直接引用,可能使并行解码中的旧 Scan 读到新表。可靠实现使用不可变表对象或版本化快照。
资源上限
在分配像素平面前,先由 SOF 验证精度、宽高、组件数量和采样因子;计算 MCU 数、block 数和输出 stride 时使用 checked multiplication。除了图像像素上限,还应限制 Marker 数量、APP/COM 元数据总量、Scan 数量、每 Scan 的重启次数和 Progressive 系数状态内存。攻击性文件可以在尺寸很小的同时塞入大量空段或 Scan,因此只限制 width × height 不够。
第十四章 MJPEG Frame Scanner
自包含载体
AVI chunk/HTTP Content-Length已给frame范围时,不需要在payload中搜索下一个SOI来分帧。直接在受限range内解析并检查SOI/EOI。范围后padding/boundary不交给JPEG parser。
无长度字节流
若协议确实无外层长度,可寻找SOI后用完整marker/entropy状态机找到匹配EOI。不能简单搜索首次FF D9,因为损坏或错误stuffing需要报告,且嵌入缩略图/多结构需正确段边界。
连续 SOI
当前frame未EOI又遇到非熵区新SOI,通常前帧截断,可丢前帧并以新SOI恢复。熵数据中未经stuff的FFD8同样会被reader返回marker,属于损坏/恢复候选。
最大 Frame
从SOI累计到EOI受max_frame_bytes限制,避免无EOI流耗尽内存。超过后丢弃到下一个有界候选SOI,并记录carrier/frame error。
第十五章 DCT、量化与系数排列参考
样本平移与二维变换
Baseline JPEG 通常以八位无符号组件样本为输入。进入 DCT 前先执行 level shift,把 0..255 平移为以零为中心的 -128..127。每个组件按 8×8 block 处理,二维 DCT 可视为先对行做一维变换,再对列做一维变换。
1 | f_shifted[x,y] = f_unsigned[x,y] - 128 |
F[0,0] 是 DC 系数,代表 block 的平均亮度或色度基线;其余 63 项是不同水平和垂直频率的 AC 系数。DCT 本身在理想数学模型中可逆,JPEG 的主要有损来源是随后对系数进行量化和取整。实现可以采用整数缩放 DCT,只要量化表缩放、舍入和 IDCT 精度满足目标兼容性要求。
常量 block 的解析意义
若一个 8×8 八位样本 block 全部等于 128,level shift 后全为零,因此全部 DCT 系数为零。量化后的 DC 与 AC 也全为零。若所有样本为 129,平移后均为一,在上述归一化下只有 DC 非零,理论值为 8,其余 AC 为零。这个向量适合检查 level shift、DC 缩放和行列次序。
编码器若漏掉 level shift,会给每个普通 block 叠加极大的 DC;解码器若解码后忘记加回 128,则中灰图会输出接近黑色。输出前还要按样本精度和颜色转换结果执行裁剪,不能让负数转换到无符号类型后回绕。
量化公式
每个 DCT 系数除以对应量化步长并取整,形成待熵编码的有符号整数:
1 | QF[u,v] = round(F[u,v] / Q[u,v]) |
量化表元素为零非法,因为会造成除零且无法定义逆量化。八位精度量化表元素范围为 1..255,十六位精度表可到 65535。Baseline DCT 过程对表精度和表编号有进一步限制;解析器应区分“DQT 语法可表示”和“当前 coding process 可使用”。
DQT 中的 Zig-zag 顺序
DQT payload 不按自然的二维行优先顺序写 64 个值,而按 JPEG 定义的 zig-zag 扫描顺序。该顺序从 DC 开始,沿斜线逐步走向高频,使常见低频系数优先。解析时应使用固定映射把流位置 k 放入自然坐标 (u,v)。
1 | zigzag_to_natural[0..63] = |
这里自然索引按 row * 8 + column。有的库内部使用转置 DCT 或把 u/v 交换,因此它的常量表看起来不同;只要编码、DQT 映射和 IDCT 一致即可。参考解析器输出 DQT 时最好同时给出“流顺序列表”和“8×8 自然矩阵”,避免误判。
DQT 段的多表闭合
一个 DQT segment 可以连续放置多张量化表,每张从一个字节 Pq/Tq 开始。高四位 Pq 决定元素精度,低四位 Tq 是表号。
1 | DQT payload: |
解析前由剩余 payload 判断是否至少容纳完整一张表。若 Pq=0,每张占 65 字节;若 Pq=1,占 129 字节。任何其他 Pq 保留。Tq 必须在标准允许的表编号范围内。段尾剩下不足一张表的数据是截断,不是 padding。
质量参数不是标准字段
常见编码器暴露 quality=1..100,但 JPEG 码流中没有“质量值”字段。质量旋钮只是编码器生成量化表的方法,同一数值在不同库中可能生成不同表。解码器只使用 DQT 的实际 64 个步长,不能从表可靠反推出唯一质量值。
比较 JPEG 压缩质量时应保存实际量化矩阵、采样方式、Huffman 表、颜色转换和 scan 类型。仅比较 UI 的 quality 数字没有标准意义。MJPEG 序列可以逐帧更换 DQT,因此外层轨道级“质量”也不保证恒定。
逆量化溢出边界
量化系数幅度、表元素和乘积都可能超过窄整数。解析 DQT 后先验证 coding process 允许的精度;逆量化使用足够宽的有符号类型,再进入 IDCT。恶意码流可组合极大 Huffman 幅度与十六位量化值,若在 16 位中相乘会回绕成完全不同的频谱。
1 | dequant = checked_wide_multiply(quantized_coefficient, quant_table_value) |
拒绝超出实现数值范围的图像是合理行为,但错误应表述为实现限制或超范围,而不是“Marker 损坏”。
IDCT 与输出裁剪
逆 DCT 将 64 个近似频域系数恢复为 8×8 样本。之后加回 level shift,并裁剪到组件精度范围。对于八位输出:
1 | sample = clamp(round(idct_value) + 128, 0, 255) |
快速 IDCT 可能利用大量高频为零的情况走捷径,例如只有 DC 时整个 block 输出同一值。捷径必须使用与完整路径一致的缩放和舍入。用全零、仅 DC、单个 AC、最大合法幅度和随机 block 对拍完整参考 IDCT,可发现转置、缩放和饱和错误。
组件采样与 block 网格
SOF 为每个组件给出水平、垂直采样因子 Hi、Vi。令 Hmax=max(Hi)、Vmax=max(Vi),一个 MCU 在完整图像坐标中覆盖 8×Hmax 像素宽、8×Vmax 像素高。该 MCU 内,组件 i 含 Hi×Vi 个 block。
1 | mcu_width_pixels = 8 * Hmax |
典型 YCbCr 4:2:0 写作 Y 的 H=2,V=2,Cb/Cr 各 1,1。每 MCU 有四个 Y block、一个 Cb、一个 Cr,共六个。4:2:2 常见为 Y 2,1,色度各 1,1,每 MCU 四个 block。4:4:4 三组件都 1,1,每 MCU 三个 block。
边缘 MCU
宽高不一定是 MCU 尺寸的整数倍。编码器在右边和下边补足 block 输入,常见做法复制边缘样本;码流仍包含完整 MCU 的全部系数。解码器重建完整 MCU 后,只把 SOF 宽高范围内的像素提交输出。不能因为最后 MCU 只显示一部分就少解几个 block,否则熵流会错位。
计算 padded dimensions 时使用 checked ceil division:
1 | mcu_cols = (width + mcu_width_pixels - 1) / mcu_width_pixels |
加法可能在窄类型溢出,生产代码可改用 width / unit + (width % unit != 0)。
非交错 Scan 的单位
当 SOS 只选择一个组件时,MCU 的含义与多组件交错 scan 不同:每个数据单元通常是该组件的一个 block,遍历该组件自己的 block 网格。不能继续按 frame 的 Hmax×Vmax 大 MCU 期待一次出现该组件的 Hi×Vi 个 block。Progressive JPEG 经常使用单组件 scan,因此这一差异会直接影响 restart interval 计数与 scan 终点。
第十六章 DC、AC 与 Huffman 完整解码
DC 差分的原因
相邻 block 的 DC 值通常相关。JPEG 不直接编码当前 DC,而编码 DIFF = DC_current - DC_predictor[component]。每个组件维护独立 predictor,成功解码后更新。scan 开始和每个 Restart Marker 后 predictor 重置为零。
DC 的 Huffman symbol 表示差分幅度所需的附加位数,称为 category 或 SIZE。SIZE 为零表示差分零,不再读取附加位。SIZE 非零时再读取 SIZE 位并按 JPEG 的 extend 规则还原正负数。
Extend 规则
JPEG 不使用普通二进制补码表示负幅度。同一 SIZE 下,最高位为一的区间表示正值,最高位为零表示负值。
1 | extend(bits, size): |
以 SIZE=3 为例,附加位 100..111 对应 +4..+7,000..011 对应 -7..-4。值零只由 SIZE=0 表示。下表给出小范围映射:
| SIZE | bits | 结果 |
|---|---|---|
| 1 | 0 |
-1 |
| 1 | 1 |
+1 |
| 2 | 00 |
-3 |
| 2 | 01 |
-2 |
| 2 | 10 |
+2 |
| 2 | 11 |
+3 |
| 3 | 000 |
-7 |
| 3 | 011 |
-4 |
| 3 | 100 |
+4 |
| 3 | 111 |
+7 |
DC Block 算例
假设当前 Y predictor 为 15,DC Huffman 解出 symbol 3,随后三位为 010。由 extend 规则得到 -5,所以当前量化 DC 为 10,并把 Y predictor 更新为 10。这里 Huffman symbol 自身不是差分值,010 也不能按补码读成 +2。
若下一 block 属于 Cb,则使用 Cb 自己的 predictor,不继承 Y 的 10。每个 restart 后所有参与 scan 的组件 predictor 同时清零。
AC Symbol 的 RUN/SIZE
AC Huffman symbol 是一个字节,高四位 RUN 表示当前非零系数之前的零个数,低四位 SIZE 表示非零幅度附加位数。特殊值 0x00 是 EOB,表示本 block 剩余 AC 全零;0xF0 是 ZRL,表示连续十六个零,并不跟幅度位。
1 | k = 1 |
ZRL 从当前位置跨过十六个零,允许恰好到达 64;若之后还要放非零值则越界。普通 RUN 先跳零,再必须有一个可写位置。所有未显式写入的 block 系数在开始解码时清零。
AC Block 算例
从 k=1 开始,依次解到 symbol 0x02,附加位 10;symbol 0x31,附加位 0;最后 EOB。
0x02 的 RUN=0、SIZE=2,10 解为 +2,因此 zig-zag 位置 1 写 +2,k 变 2。0x31 的 RUN=3、SIZE=1,跳过位置 2、3、4,在位置 5 写 extend(0,1)=-1,k 变 6。EOB 把位置 6..63 留零。自然 8×8 数组的实际位置由 zig-zag 映射决定。
DHT 码表构造
DHT 中每张表先给 Tc/Th,随后十六个字节 Li 表示码长 i 的 symbol 数,再跟所有 symbol,顺序为码长从 1 到 16、同码长按 canonical code 递增。
1 | Tc 4 bit # 0=DC, 1=AC |
构造 canonical code:
1 | code = 0 |
更严格的 oversubscribed 检查用剩余叶节点计数:初始 left=1,每增加一位先 left*=2,再减去该长度 symbol 数,任一步小于零表示前缀树过度订阅。树不完整可以是合法的,只要实际熵数据不使用缺失码。
DHT 语义校验
DC 表 symbol 对 baseline 八位过程应落在允许 category 范围;AC 表 symbol 的 SIZE、RUN 组合也受过程限制,SIZE=0 时只有 EOB 和 ZRL 两种合法组合。即使 canonical 树本身可构造,语义非法 symbol 也必须在使用前拒绝。
同一 DHT segment 可以包含多张表。每张大小为 1 + 16 + sum(L) 字节,循环必须恰好消费 payload。sum(L) 用宽整数累计并先验证不超过剩余字节和标准最大 symbol 数。
熵位读取与 Byte Stuffing
SOS 之后的 entropy-coded segment 按位读取,但字节值 FF 需要特殊处理:编码器在作为普通熵数据的 FF 后插入 00,形成 FF 00。解码器向 Huffman 位读取器只交付数据字节 FF,不交付 stuffing 的 00。
1 | read_entropy_byte(): |
连续 FF 在 Marker 前可作为 fill bytes;实现应保留原 offset 用于诊断。FF D0..D7 是 restart marker,其他 marker 终止当前 entropy-coded segment 并交给外层。FF 01 TEM 的处理取决于过程和语境,不能一律当普通 payload。
Marker 前的 Pad Bits
scan 熵数据结束时,最后一个数据字节可能只使用部分高位,剩余位按 JPEG 规则填充。解析完预期 MCU/block 后,解码器应丢弃 pad bits 到字节边界,并由 marker-aware byte reader 读取后继 marker。不能继续用 Huffman 表把 pad bits 解成额外 symbol。
相反,若尚未完成预期 MCU 就遇到 EOI 或非 restart marker,scan 截断。外层长度或图像尺寸提供的数据单元计数是判断“是否解完”的依据。
快速 Huffman 表
常见优化使用 8 或 9 位一级查找表。表项包含“已匹配 symbol 与码长”或“进入长码子表”。读取器先 peek 足够位,命中后仅消费实际码长。剩余数据不足一级宽度时必须走逐位安全路径。每张 DC/AC 表与 table id 分开缓存,SOS 的 Td/Ta 选择当前 scan 使用的表。
Restart 对熵状态的重置
Restart Marker 出现在 entropy-coded data 的字节边界,不计入 DRI 的 MCU 数。遇到预期 RSTn 后:清空 Huffman bit reservoir;把所有 DC predictor 清零;清除 progressive scan 的 EOBRUN;按过程重置其他规定状态;期望 marker 编号 (n+1) mod 8。量化表、Huffman 表、SOF 配置不会因 restart 清除。
若 marker 编号错误,严格模式报告损坏。恢复模式可接受任一 RST 作为新边界并重新同步编号,但必须记录丢失/重复区间,不能称该 frame 完整有效。
Scan 完成度校验
Sequential 交错 scan 的数据单元数为 frame MCU 行列乘积;非交错 scan 按单组件 block 网格。Progressive scan 使用相同空间遍历,但只更新指定频谱区间和逐次逼近位。解码器在收到 scan 终止 marker 时比较已完成数据单元数。过少是截断,过多通常意味着尺寸/采样理解错误或熵失步。
完整 JPEG 可有多个 scan。每个 SOS 结束后,外层继续解析 DQT/DHT/DRI/APP/COM 或下一 SOS,直到 EOI。不能把第一次 scan 终止误当作整幅图像终止。
第十七章 Progressive JPEG 逐次扫描语法
Spectral Selection 与 Successive Approximation
Progressive DCT JPEG 不在一个 scan 中发送每个 block 的全部量化系数,而把 DC 和不同 AC 频带分成多个 scan,并可用逐次逼近先发送高有效位、后发送修正位。SOS 中四个参数控制当前 scan:Ss 是谱选择起点,Se 是终点,Ah 是上一逼近位位置,Al 是当前最低有效位位置。
| Scan 类型 | Ss |
Se |
Ah / Al 关系 |
|---|---|---|---|
| DC first | 0 | 0 | Ah=0,Al 可大于等于 0 |
| DC refinement | 0 | 0 | Ah=Al+1 |
| AC first | 1..63 | Ss..63 |
Ah=0 |
| AC refinement | 1..63 | Ss..63 |
Ah=Al+1 |
Ss=0, Se>0 不属于普通 progressive scan 的合法划分。AC scan 通常只允许一个组件,以保持 EOBRUN 和 block 遍历语义明确;DC scan 可以交错包含多个组件。验证器要结合 SOF process、SOS component count 和四个参数判断,而不是分别只检查每个值在 0..63 或 0..15 范围。
系数状态矩阵
解码器为每个组件的每个 block 保留完整 64 个量化系数。First scan 初始化某个系数的高位信息,refinement scan 在已有绝对值上增加当前 bit plane。应另存“哪些系数/bit plane 已定义”的验证状态,避免对从未 first-coded 的频带执行 refinement。
1 | CoefficientState[component][block][k] { |
实际内存可只存整数系数和每个 scan 的全局进度,因为合法流的同一频带共享逼近顺序;验证器仍需维护每组件、频带的 last_Al,确保下一 refinement 的 Ah 正好接续此前 Al。
DC First Scan
DC first 与 sequential DC 类似:Huffman symbol 给差分 category,附加位经 extend 得到差分,再加 predictor。但最终值需要左移 Al 位后写入系数:
1 | diff = extend(read_bits(category), category) |
Predictor 工作在尚未左移或标准指定的数据域,不能用已经左移的 block[0] 作为下一差分基准。Restart 时 predictor 清零。若 Al 较大,左移前验证不会超出系数存储范围。
DC Refinement Scan
DC refinement 不使用 DC Huffman category;每个 block 读取一个修正位。若为一,则在当前 DC 系数上设置或增加 1 << Al 对应的低位,符号处理遵循逐次逼近规则。因为已有系数正负可能不同,不能一律做无符号 OR。
1 | bit = read_bit() |
正常合法流中该 bit plane 之前未知,refine 函数只改变当前位而不破坏更高位。重复发送同一 Ah/Al 应在 scan 序列验证阶段拒绝。
AC First Scan
AC first 只处理 Ss..Se。Huffman symbol 仍使用 RUN/SIZE,但多了 EOBRUN:当 SIZE=0 且 RUN 小于 15 时,不是普通单 block EOB,而是编码一段连续 block 在当前 spectral band 剩余位置都为零。
1 | if size == 0: |
后续每处理一个 block,若 EOBRUN>0,无需读取该 block 的 AC Huffman symbol,直接把当前 band 留零并递减。EOBRUN 可以跨 MCU/block,但不能跨 restart interval 或 scan 终点。Restart Marker 前必须完成规定的数据单元并把 EOBRUN 清零。
非零 AC 幅度在 extend 后左移 Al 位写入。RUN 只在当前 Ss..Se 区间计数,不能从 zig-zag 位置 1 起算。ZRL 也必须保证不会越过 Se+1。
EOBRUN 算例
假设 AC first scan 的 band 为 1..5,读到 symbol 高半字节 R=2、低半字节 S=0,再读取两位 01。则初始 run 为 (1<<2)+1=5 个 block,包括当前 block。当前 block 完成后内部计数可保存为四;随后四个 block 不读 Huffman symbol便结束该 band。第五个后继 block 才恢复正常 Huffman 解码。
若 scan 在尚有 EOBRUN 时提前遇到 EOI,是否意味着语法错误要结合剩余 block 数:EOBRUN 必须能够准确覆盖但不能超过 scan 剩余数据单元。解析器应在建立 run 时验证 EOBRUN <= remaining_blocks_in_scan,防止跳过不存在的 block。
AC Refinement Scan
AC refinement 是 JPEG 解码中最复杂的路径。它既可能修正已经非零的系数,也可能通过 Huffman symbol 在零位置引入一个新的 ±(1<<Al) 系数,还支持 refinement 形式的 EOBRUN。RUN 计算只统计仍为零的系数;遍历过程中遇到已有非零系数,需要消费一个 correction bit 并按其符号决定是否增加幅度,但不减少 RUN。
1 | p1 = 1 << Al |
这段伪代码强调状态关系,不替代标准逐句语法。最常见错误是把 RUN 对所有位置递减,导致已有非零系数被误计为“零 run”;或只在新系数之前修正已有系数,却忘记 EOBRUN 状态下仍要读取已有非零系数的 correction bits。
Refinement 的符号更新
对已有正系数,correction bit 为一时增加 p1;对已有负系数则减去 p1,使绝对值增加。只有当该 bit plane 尚未设置时才更新,避免损坏流重复修正导致同一位被加两次。新出现系数只允许 SIZE=1,符号由一个附加位决定为 +p1 或 -p1。
Progressive Scan 序列验证
对每个组件和每个系数区间维护状态:首次传输要求 Ah=0;之后修正要求 Ah 等于此前保存的 Al,且新的 Al=Ah-1。不同 scan 可以用不同区间覆盖,但不应产生重叠 first scan 或跳过必要位级后直接 refine。
1 | for k in Ss..Se: |
标准允许最终并非所有系数都细化到 Al=0,这只影响精度;但从未传输的系数保持零。验证器不应强制每幅 progressive 图必须有固定十个 scan,scan 数和划分由编码器选择。
Progressive Restart
每个 restart interval 以 scan 的 MCU/数据单元计数。遇到 RST 后 DC predictor 清零,EOBRUN 清零,熵 bit buffer 清空。已写入全局 coefficient buffer 的系数保留,因为后续 scan 仍需修正它们。若解码器错误地在 restart 时清空 block 系数,progressive 图会出现周期性块状损坏。
预览与最终输出
Progressive JPEG 的一个优势是收到少量 scan 后可生成低质量预览。预览解码可以对当前系数执行逆量化和 IDCT,但必须把 coefficient buffer 与显示缓存分开:生成预览不能修改量化系数或把 IDCT 结果写回。后续 scan 到达后,只更新受影响 block,再增量重建。
流式网络中若只收到部分文件,解析器可报告“已完成若干 scan、可预览”,但不能报告完整 JPEG 成功,除非 EOI 已闭合且所有已声明 segment/scan 合法。
Progressive 资源上限
Sequential 解码可以一行 MCU 解完即输出,而 progressive 必须在多个 scan 之间保留整幅图的量化系数,内存约与 block_count × 64 × coefficient_size 成正比。分配前用 SOF 尺寸和采样因子计算所有组件 block 数,checked multiply 后与策略上限比较。小像素尺寸配极端采样因子、超多 scan 或大量表重定义也要分别受限。
第十八章 JPEG Marker、段长度与表生命周期
Marker 的字节形式
Marker 由一个或多个 FF 前缀字节和一个非 00、非 FF 的 marker code 构成。通常规范写作 FF xx。SOI、EOI、RST0..RST7 和 TEM 是 standalone marker,不跟两字节长度;其他大多数 marker 后跟十六位大端长度 L,该长度包含长度字段自身的两个字节,但不包含前面的 marker 两字节。
1 | segment_total_bytes = 2(marker) + L |
把 L 当 payload 长度是经典 off-by-two 错误,会让所有后继 marker 偏移两字节。解析器应在读取 payload 前验证 L <= frame_end - length_field_offset。
SOI 与 EOI
SOI FF D8 建立一幅图像的解析状态,必须位于图像起点;它不带长度。EOI FF D9 终止图像,同样无长度。在 MJPEG 外层给定 frame size 时,EOI 后到 frame 边界可能存在容器 padding 或厂商尾数据,策略应明确:严格 JPEG 要求 EOI 后无非允许数据;容器解析器可把余量归外层,但不能悄悄并入下一 JPEG。
遇到嵌套 SOI 通常说明上一帧截断或 APP 中嵌有缩略图被错误当顶层解析。由于 APP payload 有明确长度,正确 segment parser 不会扫描其内部 marker;只有无长度流恢复器才可能看到假候选。
SOF 的共享字段与差异
SOF segment 通常先给样本精度 P、高度 Y、宽度 X、组件数 Nf,然后每组件给 C/HV/Tq。不同 SOF marker code 选择 baseline、extended sequential、progressive、lossless、differential 以及 Huffman/arithmetic 过程。
1 | P 1 byte |
长度应为 8 + 3*Nf,这里 L 包含自身两字节。组件 id 在 frame 内必须唯一;H、V 不能为零,并受 coding process 上限约束;Tq 必须引用合法表号。高度可在允许 DNL 的过程中初始为零,宽度通常必须非零。组件数量、采样乘积和总 block 数都要受资源限制。
SOS 与组件选择
SOS 先给 scan 组件数 Ns,每个选择项包含 Cs 和一个字节 Td/Ta,随后是 Ss、Se、Ah/Al。
1 | Ns 1 byte |
长度应为 6 + 2*Ns。每个 Cs 必须引用 SOF 中存在且当前 scan 不重复的组件。Td/Ta 引用 Huffman 表,lossless 或 arithmetic 过程对字段解释和所需表不同。表可以在 SOS 之前最近一次 DHT/DAC 中定义;不能假设固定使用表 0/1。
DRI 与 interval 生效范围
DRI segment 的 L 固定为 4,payload 是十六位 restart interval,单位为 MCU。值零禁用周期性 restart。新的 DRI 在后续 scan 生效,直到再次定义。它不说明首个 marker 一定是 RST0 之前有多少字节,而是规定每个 interval 完成多少 MCU 后在 entropy-coded data 中出现 RST。
对于非交错 scan,MCU 单位按该 scan 的数据单元规则计算。编码器可以在不同 scan 前更换 DRI;解析器应在 SOS 时快照当前 interval。
DNL 更新高度
DNL FF DC 的 payload 给出行数,允许在 SOF 高度为零时稍后确定图像高度。它出现在规定语境中,长度固定。实现需要延迟依赖最终高度的完整资源计算,或设定流式上限;不能在高度零时分配零大小图像后让 DNL 无限制扩容。若 SOF 已给非零高度,冲突 DNL 应按过程规则处理或拒绝。
DAC 与算术编码条件
DAC 定义 arithmetic conditioning table。它的条目由 table class/id 和 conditioning value 组成。只支持 Huffman JPEG 的解码器仍能通过 segment 长度跳过 DAC,但一旦 SOF 选择 arithmetic coding process,必须明确返回不支持,不能拿 DHT 路径解释熵数据。
DQT/DHT 的重定义
JPEG 允许在 scan 之间重定义量化表和熵表。某个 block 的量化系数如何逆量化取决于其编码时生效的表。Sequential 常在解码后立即逆量化,不受之后重定义影响;progressive 将量化系数保留到多个 scan 后,若同一组件使用的量化表被不恰当地替换,会产生歧义或需要遵守过程约束。
可靠实现为每次 SOS 建立不可变 ScanTables 快照,包含组件引用的 DQT、DC/AC DHT 或 DAC,以及 DRI。表对象按内容或版本引用,后续 DQT/DHT 解析创建新版本,不原地修改正在解码的对象。
未定义表
SOF 的 Tq 和 SOS 的 Td/Ta 只是选择器,不保证表已经出现。进入 scan 前必须验证所有需要的表存在且与 coding process 兼容。某些交换约定允许省略表并由外层提供默认表,例如特定 MJPEG/RTP 场景;这种默认只能由明确 carrier profile 注入,通用 JPEG parser 不应凭经验自动套用“标准质量表”。
APPn 的通用原则
APP0..APP15 都是带长度的应用段。JPEG 核心解码器可以在边界闭合后跳过未知 APP,但元数据解析器须根据 payload signature 再选择具体格式。不能仅凭 marker 号认定 APP1 一定是 Exif:它也可能是 XMP 或其他注册数据;APP2 可能是 ICC profile 分片,也可能是其他用途。
每个 APP parser 都只能访问该段的 payload 子范围。内部 offset、字符串长度和嵌套 TIFF 结构不得跳出 segment,即使整个 JPEG 后面还有字节。
APP0 JFIF
JFIF payload 以 ASCII JFIF 加零终止字节开头,随后为版本、密度单位、X/Y density 和缩略图宽高。若缩略图尺寸为 Xthumbnail × Ythumbnail,紧随 3 × Xthumbnail × Ythumbnail 字节 RGB 数据。
1 | identifier 5 bytes: 4A 46 49 46 00 |
密度为显示/打印元数据,不改变 SOF 像素尺寸。缩略图乘法必须 checked,且结果恰好落在 APP0 payload 内。
APP1 Exif
Exif APP1 通常以 Exif\0\0 开头,后接 TIFF 结构。TIFF 自己声明 II 或 MM 字节序、固定 magic、IFD offset;所有 offset 相对于 TIFF header 起点,而不是 JPEG 文件起点或 APP1 起点。解析每个 IFD entry 时验证 type、count、单项宽度乘积和 value/offset 范围。
Exif orientation 会影响最终显示方向,但不改变 JPEG 熵解码出来的原始 raster。若应用选择自动旋转,宽高、裁剪和像素坐标 API 要明确报告编码方向还是显示方向。Exif 内也可嵌缩略 JPEG,其 SOI/EOI 位于 APP1 payload 内;顶层 frame scanner 绝不能误把它当下一帧。
APP1 XMP
XMP 使用不同 signature,主体通常为 UTF-8 XML。解析器应限制文本大小和 XML 处理能力,禁用外部实体与网络解析。即便不解析 XMP,也应按 APP1 长度安全跳过。Exif 与 XMP 可以同时出现多个 APP1 段,因此元数据模型不能把 marker 号当唯一键覆盖。
APP2 ICC Profile
ICC profile 可能拆分到多个 APP2 段,每段带 ICC_PROFILE\0 signature、sequence number 和 total count。重组前验证 total 在合理范围、sequence 从一到 total、无重复且所有片段属于同一 JPEG。按 sequence 拼接 payload,不能按文件出现顺序盲拼。
缺片或重复片时可保留原片段并报告 profile 不完整,但不能把残缺字节交给颜色管理系统当有效 ICC。重组总大小也需受上限控制。
APP14 Adobe
Adobe APP14 常含 Adobe signature、版本、两个 flags 和 transform。transform 常用于解释三组件数据为 RGB 或 YCbCr、四组件为 CMYK 或 YCCK。它与 JFIF、组件 id、外部容器颜色声明可能冲突。颜色解释层需要记录证据来源和优先策略,而不是由某一个字节绝对决定。
COM 与文本编码
COM 是任意字节序列,标准不保证 UTF-8 或零终止。展示时应限制长度、替换不可打印字节并避免把内容直接注入 HTML。复制或重封装时可以按原始 bytes 保留。多个 COM 合法,不应串联后当一条无界字符串。
Marker 数量与元数据预算
攻击性 JPEG 可以包含数万个最小 APP/COM/DQT 段,即使像素很小。除了单段 L<=65535,还应限制总 segment 数、元数据累计字节、同类表重定义次数、ICC 片数和 scan 数。每次循环至少消费完整 marker 或返回错误,避免零长度逻辑造成死循环。
Marker 目录输出
结构检查工具可以输出:marker 名称、绝对 offset、声明长度、payload 范围、解析摘要和当时表版本。例如:
1 | 00000000 SOI |
目录中的 offset 和长度应能机械验证下一个 segment 起点。对熵段还要报告 stuffing 次数、RST 目录、数据单元数和终止 marker,而不是把从 SOS 到 EOI 全部当作不透明 blob。
第十九章 颜色空间、采样恢复与像素输出
JPEG 核心与颜色语义的分界
JPEG coding process 编码的是若干组件,并不单凭 SOF 就完整规定这些组件一定是 RGB、YCbCr、CMYK 或其他颜色空间。颜色解释来自 JFIF、Adobe APP14、Exif/ICC、组件 id、应用约定和外层容器。解码器应先把每个组件恢复为样本平面,再由颜色管理层决定组合方式。
三组件、id 为 1/2/3 且有 JFIF 时通常解释为 Y/Cb/Cr;三组件 id 为 R/G/B 且无相反元数据时可能是 RGB;Adobe transform 0/1/2 分别提供其他常见线索。现实文件会出现冲突,可靠工具应报告“采用何种解释及证据”,而不是声称 SOF 组件数足以唯一判断。
YCbCr 到 RGB
八位全范围 JPEG 常以 128 为 Cb、Cr 中性值。常见转换可写为:
1 | R = Y + 1.40200 * (Cr - 128) |
系数选择涉及标准颜色矩阵、精度和舍入,具体实现可用定点近似。JPEG 文件通常不像视频容器那样显式给有限/全范围标志;把 JPEG Y 当电视有限范围 16..235 会造成黑白电平错误。ICC profile 或应用约定存在时,颜色管理流程可能更复杂。
定点颜色转换
嵌入式实现可把系数放大 2^N,使用宽整数乘加,最后舍入右移。例如选 N=16:
1 | cr = Cr - 128 |
常量近似只用于说明结构。负值右移和舍入应在目标语言中定义清楚,乘法前扩展位宽,输出再饱和。用全部 256³ 输入穷举或用高精度参考随机对拍,可以证明最大误差边界。
色度上采样坐标
当 Cb/Cr 采样因子低于 Y 时,需要把色度平面上采样到输出像素网格。最近邻最简单但易出现块边;双线性效果更平滑。关键不只滤波器,还有采样位置假设:色度样本相对亮度是居中还是共址会影响半像素相位。
JPEG/JFIF 的常见解释与某些视频 YUV 采样位置并不完全相同。复用视频硬件的 NV12 上采样器时,要确认它的 chroma siting,否则高对比彩色边缘会发生半像素偏移。
4:2:0 Block 到像素的映射
对于 Y 2×2、Cb/Cr 1×1 的 MCU,四个 Y block 覆盖 16×16 亮度像素,一个 Cb block 和一个 Cr block各覆盖逻辑 16×16 区域。若用最近邻,每个色度样本对应 2×2 亮度像素;若双线性,则从邻近色度样本插值。
MCU 内 Y block 顺序由组件采样遍历决定,一般先水平再垂直:左上、右上、左下、右下。把 block 顺序误当 zig-zag 或列优先,会造成明显棋盘拼接。边缘 MCU 仍完整解码,输出时裁掉超出 SOF width/height 的像素。
RGB JPEG
若三组件被解释为直接 RGB,则各组件通常具有相同采样因子,但标准并不允许实现仅凭“相同采样”断定 RGB,因为 4:4:4 YCbCr 同样满足。RGB 组件解码后无需 YCbCr 矩阵,但仍可能需要 ICC 色彩转换。若误将 RGB 当 YCbCr,颜色会严重偏移;若误将 YCbCr 当 RGB,图像通常呈不自然的灰绿或紫色。
CMYK 与 YCCK
四组件 JPEG 常见于印刷工作流。Adobe transform 可帮助区分 CMYK 和 YCCK,且部分编码器存储反相组件值。转换到 RGB 需要采用正确的反相约定和颜色配置;简单公式只能作为无 ICC 时的近似。通用媒体播放器若不支持,应报告不支持颜色模型,而不是丢弃第四组件后当 RGB。
ICC 颜色管理
完整 ICC profile 重组后,应先验证 profile header、声明大小、tag table 范围和所有 tag offset/length,再交给受信任的颜色管理库。颜色转换目标可能是 sRGB、显示 profile 或线性工作空间。ICC 不影响 JPEG 熵解码边界,但影响“最终像素是否正确”的验收。
损坏 ICC 应降级到明确的默认颜色解释并给出警告,不能让元数据错误阻止安全提取原始组件,除非应用要求严格色彩一致性。
Exif Orientation
Orientation 取值描述编码 raster 到显示方向的旋转/镜像。应用可以选择保留原始宽高加 orientation 元数据,或实际变换像素并把 orientation 归一为一。若实际变换,90/270 度会交换显示宽高,stride 和 ROI 坐标也随之变化。
MJPEG 视频通常不期望逐帧变化 orientation;若每帧 JPEG 元数据不同,播放器应制定轨道级策略,以免输出尺寸不断旋转。AVI/HTTP 外层不一定提供等价字段,因此转封装时保留 APP1 是保留语义的最直接方式。
Alpha 通道边界
传统 JPEG coding process 没有通用、标准化的 alpha 通道语义。四组件不等于 RGBA,通常是 CMYK/YCCK。某些厂商通过额外 APP 数据或外层双流保存 alpha,那属于扩展协议。通用解码器不能把第四组件直接映射为透明度。
输出像素格式
解码 API 应明确输出是 planar 原始组件、planar YCbCr、RGB24、BGR24、RGBA、RGB565 还是其他格式;同时给出 width、height、每平面 stride、sample precision、颜色空间、range 和 orientation 策略。仅返回一个 uint8_t* 会让调用方猜测通道顺序与行对齐。
1 | DecodedImage { |
Stride 与整数安全
stride = width * bytes_per_pixel 必须 checked multiply,再按目标对齐向上取整;总缓冲区 stride * height 再次 checked。对 planar 格式分别计算各平面尺寸。不要从压缩文件长度推断解压缓冲大小,两者没有安全比例上界。
行对齐后的 padding 不应暴露未初始化内存。写 BMP、屏幕 framebuffer 或网络输出前要么填零,要么只传有效像素字节。
缩放与解码比例
许多 JPEG IDCT 实现支持 1/2、1/4、1/8 缩小解码,通过输出每个 block 的较少样本降低成本。API 应区分请求的目标尺寸与实际 IDCT scale,边缘裁剪按缩放后的 ceil 规则计算。Exif orientation 在缩放前后应用都可,但坐标换算必须一致。
缩小解码仍需解析全部必要 Huffman 系数,除非实现采用合法的快速丢弃路径;不能按目标像素减少而跳过熵数据中的 block。
颜色验收向量
至少准备:中性灰阶梯;纯红绿蓝块;Cb/Cr 中性 128;4:4:4、4:2:2、4:2:0 彩色边缘;RGB JPEG;带 Adobe transform 的 CMYK/YCCK;带 ICC 的图像;八种 Exif orientation。比较原始组件和最终 RGB 两层结果,以区分熵/IDCT 错误与颜色转换错误。
PSNR 与感知指标的使用
有损编码后像素不应与原图逐字节相等。测试编码器可计算各组件 PSNR、SSIM 或其他指标,同时检查尺寸、颜色空间和无异常块。解码器一致性测试则应对同一码流与高精度参考输出比较,允许标准规定的 IDCT 误差;颜色管理路径需使用同一 profile 和舍入策略。
第二十章 MJPEG 帧模型与外层承载
MJPEG 不是单一文件标准
MJPEG 通常表示“一连串独立 JPEG 图像作为视频帧”,但这个名称本身不唯一规定帧边界、时间戳、索引、音频、元数据或网络恢复。AVI 中由 RIFF chunk 给边界,HTTP multipart 由 MIME boundary 和可选 Content-Length 给边界,RTP/JPEG 由 RTP timestamp、fragment offset 和 marker bit 组织一帧,厂商裸流则可能只依赖 SOI/EOI。
因此实现接口应分为 carrier parser 与 JPEG frame parser。Carrier 输出精确 frame span、时间戳和丢失状态;JPEG parser 只在该 span 内验证 SOI、segments、scans 和 EOI。把所有输入都先扫描 FF D8/FF D9 会丢掉外层已经提供的更强边界,也容易被 APP 缩略图和损坏熵数据干扰。
自包含帧与缩略表帧
最容易互操作的 MJPEG 每帧都是独立完整 JPEG:含 SOI、必要 DQT/DHT/SOF/SOS、entropy data 和 EOI。某些系统为节省带宽只在首帧或带外发送表,后续帧省略 DQT/DHT。这样的帧离开序列上下文不能独立解码。
解析器可在明确 profile 允许时缓存表并注入,但缓存键应绑定流、配置版本和 table id;seek、丢包、轨道切换后不能沿用另一个序列的表。转存成普通 JPEG 文件时必须把当前所需表实际写回每帧。
帧配置变化
MJPEG 可以逐帧改变宽高、采样因子、量化表、Huffman 表和颜色元数据。外层容器可能声明固定轨道尺寸,二者冲突时要区分 coded frame 与 presentation canvas。播放器可拒绝动态尺寸、重新配置输出或把较小帧放入固定画布,但应记录策略。
表变化是正常压缩控制手段,不等于 resolution change。硬件解码器如果只在序列开始配置一次 DQT,后续质量变化会解码错误;每帧提交前需比较硬件要求的配置状态。
帧时间戳
JPEG 内部没有帧率或显示时间戳。AVI 使用 stream header rate/scale 和索引顺序;RTP 使用 90 kHz timestamp;HTTP multipart 常没有标准时间戳,只能由到达时间、服务器 header 扩展或固定帧率配置推导。
不能从 JPEG APP0 density 推导视频帧率,density 描述像素物理密度。也不能用压缩帧大小估计持续时间。转封装时若源无时间信息,必须由用户或采集层提供帧率,并在输出元数据中注明是假定值。
队列与低延迟
MJPEG 每帧独立,实时播放器通常可在积压时丢弃旧完整帧并从下一帧恢复,无需等待关键帧。但丢弃必须以 carrier frame 边界为单位;丢掉半个 HTTP part 或一组 RTP 分片后,重组器要清除该 timestamp 的全部状态。
低延迟队列可按 timestamp 保留有限数量 frame assembly,设置最大帧字节和组装超时。显示线程慢时优先保留最新完整帧,而不是无限积压导致延迟不断增长。录像模式则不能任意丢帧,应记录 gap 或复制时间戳策略。
帧率与码率计算
固定帧率 F、平均 JPEG frame size S 字节时,媒体平均码率近似 8*S*F bit/s,尚未计入 AVI chunk、HTTP header、RTP/UDP/IP 等开销。MJPEG 帧大小随画面复杂度和量化表变化,带宽规划应使用峰值分位和最大帧,而非只看平均值。
1 | instantaneous_frame_bitrate = frame_bytes * 8 / frame_duration |
网络队列和接收缓冲按峰值 burst 设计。即使平均 10 Mbit/s,一张复杂帧也可能在一个帧周期内产生数百 KB 突发。
丢帧与损坏帧
外层完整但 JPEG 内部损坏时,carrier 可继续定位下一帧。若只缺部分 entropy 数据且有 restart markers,解码器可能恢复后续 MCU 区域,但输出应标记 concealed/damaged。没有 restart 时通常丢弃整帧更安全。
RTP 丢一个 fragment 时不应把剩余 fragment 拼成看似完整 JPEG;即使 EOI 所在末包存在,中间 offset gap 仍然证明 frame 不完整。HTTP Content-Length 不足则等待或超时,不能搜索 boundary 前任意 FFD9 当成功。
配置指纹
可为每帧计算不含 entropy payload 的结构指纹:SOF process/尺寸/组件/采样、使用的 DQT/DHT 内容、颜色标识和 restart interval。连续序列中指纹变化用于触发硬件重配置或诊断码率策略。指纹不应包含 APP 时间戳、COM 或 entropy bytes,否则每帧都会无意义变化。
JPEG 帧哈希
字节哈希用于验证无损搬运;结构哈希用于忽略 APP 顺序或表定义位置差异;解码像素哈希用于验证不同合法 JPEG 表达得到相同输出。三种哈希回答不同问题。重封装 AVI 到 HTTP multipart 若不改 frame bytes,字节哈希应相同;若为省表帧补入 DHT,字节不同但解码像素可相同。
音频同步边界
MJPEG 本身没有音频。AVI 可另有音频 stream,RTP 通常用独立 SSRC/媒体会话,HTTP 简单 multipart MJPEG 多数不含同步音频。同步由外层时间轴解决。把 JPEG frame index 直接与音频 packet index 对齐没有依据,必须换算各自 timestamp/timebase。
Seek 与随机访问
自包含 JPEG frame 天然可随机解码,但 carrier 仍需索引到 frame 起点。AVI 用 idx1/OpenDML index 或扫描 movi;HTTP live 通常不能任意 seek;文件化 multipart 可建立 side index。若帧依赖前帧表,随机访问点还必须包含最近表定义或合成完整 JPEG。
MJPEG 轨道验收
轨道级验证应报告总帧数、首尾时间、帧率统计、尺寸/采样/表指纹变化、最大/平均/分位 frame size、损坏/缺失数、重复 timestamp、倒退 timestamp 和 carrier padding。逐帧 JPEG 合法只是其中一项,不能证明外层时间和索引正确。
第二十一章 AVI、HTTP 与 RTP/JPEG 字段映射
AVI 中的视频流声明
AVI 是 RIFF 容器,MJPEG 视频通常由 strh stream header 和 strf 的 BITMAPINFOHEADER 描述。strh.fccType 为 vids,handler/压缩 fourcc 常见 MJPG;strf.biCompression 也应表达对应编码。外层 width/height、rate/scale 与每帧 JPEG SOF、实际时间顺序应交叉验证。
1 | LIST 'strl' |
stream number 00 只是例子。视频压缩数据 chunk 通常使用 dc 后缀,具体 id 由轨道序号决定。RIFF chunk size 不包含八字节 chunk header,也不包含为了偶数字节对齐增加的 pad。JPEG parser 的 frame span 必须恰好是 chunk payload 的 N 字节,pad 由 AVI parser 消费。
AVI Chunk 与 JPEG EOI
对于自包含 MJPEG,chunk payload 应从 SOI 开始并含 EOI。若 EOI 早于 chunk 末尾,剩余 bytes 可能是编码器 padding、附加数据或损坏;应报告并按策略处理。若 chunk size 在 EOI 前结束,则该 frame 截断,即使底层文件后面的 pad 或下一 chunk 中出现 EOI,也不能跨 chunk 拼接。
AVI 索引 offset 指向 chunk header 还是 data,取决于索引类型和基准。索引解析属于 AVI 手册范围,但 MJPEG 验证必须用实际 chunk payload 与索引 size 对照,不能直接把 index 指向位置交给 JPEG decoder 而忽略 chunk id/size。
AVI 时间基准
dwRate/dwScale 定义 stream 样本率。例如 rate=30、scale=1 表示 30 frame/s;rate=30000、scale=1001 表示约 29.97 frame/s。第 n 帧理论时间为 n*scale/rate,还要考虑 stream start 和容器索引顺序。avih.dwMicroSecPerFrame 可作为全局提示,但多流验证时应以每个 stream 的 rate/scale 为核心并检查一致性。
JPEG 内 APP0 density 与 AVI 帧率完全无关。若丢帧但 AVI 没有显式 timestamp,索引位置仍按样本序列连续,是否保留时间 gap 取决于写入器是否插入空/重复样本或调整长度。
AVI 写入步骤
写入 MJPEG AVI 时先准备 RIFF/hdrl,占位需要最终回填的长度;每得到一张完整 JPEG,写对应 ##dc chunk、size、payload 和奇数 pad,并记录索引项;结束后写 idx1 或 OpenDML 索引并回填 frame count、RIFF/LIST sizes。超过传统 AVI 容量时使用 OpenDML AVIX 和分级索引。
MJPEG writer 不应解析后重新编码 JPEG,除非用户要求;可先验证 SOI/EOI 和 SOF 尺寸,再原样复制 payload。若轨道声明固定 1280×720 而某帧 SOF 不同,必须在写入前选择拒绝、统一转码或允许特定动态格式,不能只保留冲突。
HTTP multipart 类型
常见网络摄像头使用:
1 | Content-Type: multipart/x-mixed-replace; boundary=frameboundary |
Boundary 参数可能在 header 中带或不带引号,语法中的实际分隔行前面再加 --。实现应按 MIME/HTTP 规则解析 header,而不是把示例字符串固定为 --frame。Header 名大小写不敏感,行终止通常 CRLF;宽容设备兼容模式可接受 LF,但需要限制 header 总大小和行数。
Boundary 与 Content-Length
有 Content-Length 时,它是最强帧 payload 边界:读取准确字节数,再消费分隔前换行并寻找下一 boundary。JPEG payload 内可以自然包含类似 boundary 的字节,绝不能在已知长度范围中提前搜索文本分隔符。
没有 Content-Length 时,必须扫描 MIME boundary。Boundary 只有在合法行边界并满足前后语法时才算分隔,不能简单做任意子串搜索。扫描器维护跨网络分片的最长前缀状态,设置最大 part bytes;超过上限还没找到 boundary 就丢弃当前 part 并恢复。
HTTP Header 安全
限制单行长度、总 header 字节、header 数量和 Content-Length 最大值。解析十进制长度时逐位 checked multiply/add,拒绝负号、溢出和含混重复值。若多个 Content-Length 不一致,应拒绝该 part,避免请求走私式边界分歧。Content-Type 可验证为 image/jpeg,未知 header 安全跳过但不执行脚本或文件路径。
HTTP 时间与重连
基础 multipart 没有统一帧 timestamp。接收端可记录首字节、完整帧或服务器自定义时间 header 的时刻,但网络抖动使到达时间不等于采集时间。若用于录像,应明确所选时钟,并在断线重连时开始新的 epoch 或保持单调映射。
重连后必须清空未完成 part、boundary 前缀状态和任何依赖前帧的 JPEG 表缓存,除非新响应明确复用同一配置。第一张完整合法 JPEG 才能重新输出。
RTP/JPEG 的 RTP 层
RTP/JPEG 通常依据 RFC 2435,将 JPEG scan 数据分片放入 RTP payload。RTP header 提供 sequence number、timestamp、marker bit、payload type 和 SSRC。相同 timestamp 的包属于同一 JPEG frame;sequence number 用于检测丢包和排序;最后一个包通常设置 marker bit。
RTP timestamp 时钟通常为 90 kHz。它表示采样/呈现时间而非墙钟,32 位会回绕。接收器用无符号差值扩展 timestamp,不能在回绕处判定时间倒退。RTP marker 不是 JPEG EOI marker,两者处于不同层。
RTP/JPEG 八字节主头
RTP header 后首先是八字节 JPEG payload header:
1 | Type-specific 8 bit |
Fragment Offset 是当前 RTP/JPEG payload 数据相对于该 JPEG frame scan payload 起点的字节偏移,不是 RTP 包序号,也不包含 RTP header、八字节 JPEG header、可选 restart/quantization headers。三字节解析要使用宽无符号整数:
1 | offset = (b1 << 16) | (b2 << 8) | b3 |
Width/Height 以八像素为单位,零在协议中可有特殊表达边界;接收器要按规范解释并与实现最大尺寸比较。不能直接把字节值当像素。
Type 与采样模式
Type 表达 RTP/JPEG profile 中的颜色采样和可选 restart 变体,而不是完整 JPEG SOF marker code。常用类型映射到特定 YCbCr 采样布局。只有协议定义的类型才能据此合成 SOF;未知 Type 需要带外扩展或拒绝,不能猜成 4:2:0。
一些类型通过高位或范围指示附加 Restart Marker Header。解析顺序由 Type 决定:八字节主头后可能先有四字节 restart header,首 fragment 且 Q 指定动态表时还可能有 quantization table header,之后才是 fragment data。
Q 与量化表
Q 较小范围可表示用协议算法从质量参数派生量化表;Q 在动态范围时,首 fragment 携带量化表 header 和实际表 bytes。接收器必须按 RFC 规定的 Q 语义生成或读取表,不能用本地图像库任意 quality 算法替代,因为不同算法表值可能不同。
动态量化表头含 MBZ、precision、length。Length 给随后的表字节总数,precision 位图说明各表是八位还是十六位元素。首 fragment offset 必须为零才能可靠建立 frame 表;后继 fragment 通常不重复表。Length 必须在当前 RTP payload 内闭合。
Restart Marker Header
可选四字节 restart header 包含 Restart Interval、F/L 位和 Restart Count。Interval 对应 JPEG DRI;F/L 帮助表示当前 packet 是否包含一个 restart interval 的起点/终点,Count 用于定位。接收器用它增强丢包恢复,但仍要验证 fragment offset 连续性。
合成交换 JPEG 时,应写 DRI segment,并在 entropy payload 中保留实际 RST markers。只有 header 声明 interval 而 scan data 缺 RST,生成文件仍不一致。
RTP 分片重组
为每个 (SSRC,timestamp) 建立 frame assembly。解析可选头后得到 fragment_offset 和 fragment bytes,写入有界稀疏区间或保存区间列表。接收器允许乱序,但拒绝重叠区间内容不一致、offset 加长度溢出、总长度超过上限。
1 | on_packet(packet): |
仅收到 marker 包不表示完整,必须从 offset 0 到 declared_end 无 gap。重复 packet 内容相同可以幂等忽略;相同 range 内容不同表示冲突,应丢弃 frame 或按可信来源策略处理。
RTP/JPEG 到完整 JPEG
RFC payload 通常承载 entropy-coded scan 数据而非完整 SOI..EOI 文件。重组完成后,接收器按 Type、尺寸、Q/量化表和协议默认 Huffman 表合成:SOI、DQT、SOF、DHT、可选 DRI、SOS,再追加完整 scan data 与 EOI。
1 | SOI |
每个 segment length 按实际内容计算,所有多字节 JPEG 字段为大端。若 fragment data 已含 EOI 或发送端违反 profile 发送完整 JPEG,接收器不能无条件再追加一个 EOI;应检测所协商格式并报告非标准变体。
RTP 包容量与 MTU
发送端从路径 MTU 扣除 IP、UDP、RTP、JPEG payload header 和可选扩展后得到每包最大 fragment bytes。首包若携动态量化表,可用空间更小。不要固定假设 1400 字节在 IPv4、IPv6、RTP extension、SRTP 环境都安全。
Fragment Offset 按实际 scan payload 累计,而不是按固定包容量计算。最后一包设置 RTP marker。sequence number 每包递增,timestamp 每帧按 90 kHz 时间基准前进;同一帧所有包 timestamp 相同。
丢包恢复能力
无 restart 的 JPEG scan 缺中间字节后,后续 Huffman bit 边界和 DC predictor 通常不可恢复,因此整帧丢弃。具 restart interval 且 packet 与 restart 边界合理对齐时,可从后续 RST 恢复部分 MCU,但生成的像素需要损坏区域标记。协议 header 的 F/L 信息有助于识别安全区间。
接收器不能通过在随机 fragment bytes 中搜索 FFD0..FFD7 就认定恢复点,因为 stuffing、缺失和乱序语境需要结合 offset、interval 与完整区间。
三种承载的边界对照
| 承载 | 帧边界 | 时间信息 | 乱序/索引 | JPEG 表来源 |
|---|---|---|---|---|
| AVI | RIFF chunk size | rate/scale 与样本序号 | 文件索引 | 通常每帧内部,亦可能约定复用 |
| HTTP multipart | Content-Length 或 MIME boundary | 通常无标准字段 | TCP 有序,无随机索引 | 每个 image/jpeg part 通常自包含 |
| RTP/JPEG | timestamp、offset、marker、连续区间 | 90 kHz RTP timestamp | sequence/offset 支持检测和重排 | Q 派生或首包动态表 |
任何转换都先把源恢复成“完整 JPEG frame + timestamp”,再写目标 carrier。直接把 RTP payload fragment 拼到 AVI chunk,会缺 SOI/DQT/SOF/DHT/SOS;把 AVI chunk 原样当 RFC 2435 fragment 发送,则会把完整 JPEG markers 错当 scan payload。
第二十二章 安全实现、故障注入与最终验证
信任边界
JPEG/MJPEG 解析器处理的所有长度、数量、offset、采样因子、表 id、Huffman symbol 和图像尺寸都不可信。Carrier 边界先限制 JPEG frame,Marker 长度再限制 segment,SOS 和图像几何限制 entropy 数据单元,输出策略限制像素与内存。每层只能缩小可访问范围,不能由内层字段扩张到外层之外。
Checked Arithmetic
以下计算必须 checked:width*height*channels、MCU 行列和乘积、组件 block 数、stride 对齐、ICC 片段总长、segment offset+length、RTP fragment offset+length、AVI chunk header+size+pad、DHT symbol 总数、progressive coefficient buffer。推荐封装统一函数,失败返回结构化 INTEGER_OVERFLOW。
解析循环进度
Marker 循环、DQT/DHT 多表循环、IFD entry 循环、MCU/block 循环、Huffman bit 循环、HTTP boundary 扫描和 RTP assembly 都必须证明单次迭代消耗输入或减少有限计数。遇到非法零长度 section 后继续而不移动 cursor 会造成 CPU 死循环。
资源预算
策略至少包含:最大 JPEG frame bytes;最大 coded pixels;最大组件和采样乘积;最大 APP/COM 累计;最大 marker/scan 数;最大 progressive coefficient bytes;最大 ICC profile;最大同时 RTP frame assembly;最大 HTTP header/part;单帧解码时间预算。超限应在分配或长循环前拒绝。
解码炸弹与高压缩比
极小 JPEG 文件可以声明巨大尺寸并用简单熵数据表示大面积平坦图,解压内存远大于输入。不能使用“输出最多为压缩文件的若干倍”估算。唯一可靠方式是从经验证的 SOF 尺寸、组件和输出格式计算并与绝对资源上限比较。
表驱动边界
所有 table id 在索引静态数组前验证。DQT、DC DHT、AC DHT 分属不同命名空间;SOF Tq 不能索引 DHT,SOS Td/Ta 也不能混用。Progressive scan 的 Ss/Se 在访问 64 系数数组前检查,restart count 在固定八项循环中取模。
首错原则
分析器应报告能确定的第一个结构错误,同时可在安全边界上继续收集后续独立错误。Huffman 失步后,直到下一可靠 restart/scan/frame 边界之间的派生诊断通常不可信,应标为 suppressed。这样 fuzz 测试能稳定比较错误位置,不会因一次位错产生数千条噪声。
故障注入矩阵
Marker 层:删除 SOI/EOI、截断长度字段、L 小于二、L 超 frame、standalone marker 后伪长度、APP 内伪 SOI。表层:DQT 零值/保留精度/残缺表,DHT oversubscribed/非法 symbol/缺 symbol。Frame 层:重复组件 id、零采样、未定义 Tq、尺寸乘法溢出。Scan 层:未知 Cs、缺 DHT、非法 Ss/Se/Ah/Al、过多 scan。
熵层:无终止 Huffman code、附加位截断、AC run 越 63、错误 stuffing、提前 marker、RST 编号错误、interval 不一致。Progressive 层:refine 早于 first、EOBRUN 越 scan、重复 bit plane、新系数 SIZE 非一。Carrier 层:AVI size/index 冲突、HTTP length/boundary 冲突、RTP gap/overlap/offset 溢出/末包先到。
截断测试
对每个合法测试文件,从零到文件长度逐字节截断。每个前缀要么返回 NEED_MORE_DATA(流式调用仍可能补充),要么返回确定的截断错误;不得越界、挂死或错误成功。再对每个 bit 位置翻转,统计首错层、是否安全恢复和是否输出 damaged 标记。
Marker Round-trip
结构 writer 可将解析后的 DQT/DHT/SOF/SOS 重新序列化,再解析并比较语义字段。对于保留 APP/COM 的无损重封装,原始 payload bytes 应相同。Entropy scan 不经解码重编码时必须逐字节保留,包括 stuffing 与 RST;若重新编码,比较解码像素而非码流字节。
几何不变量
从 SOF 推导的每组件 block 网格、每 scan 数据单元数和最终输出矩形应互相一致。所有 MCU 完成后,每个组件应得到预期 block 数;上采样后至少覆盖 coded width/height;裁剪后输出恰好为声明尺寸。Progressive 每个 scan 遍历同一空间网格,只更新系数子集。
熵不变量
每个 sequential block 恰好解一个 DC 并使 AC k 不超过 64;每个 Huffman symbol 码长 1..16 且来自选中表;每个 nonzero amplitude 的 SIZE 与附加位完整;Byte stuffing 只在 entropy data 解释;scan 完成后只剩合法 pad bits再遇 marker;restart interval 之间 MCU 数符合 DRI。
Carrier 不变量
AVI:chunk payload、pad 和索引范围不重叠越界,帧数与可解码视频 chunk 对应。HTTP:每个 Content-Length part 精确闭合,boundary 只在 MIME 行生效。RTP:每帧 range 覆盖从零到 marker 包终点且无 gap/conflict,首包提供所需配置,同 timestamp 的尺寸/Type/Q 一致。
工具验证
可用 JPEG 专用检查器输出 marker 目录,用 ImageMagick、libjpeg 系工具或 FFmpeg 解码,用 ffprobe 检查 AVI/视频流。至少两个独立实现成功并不自动证明所有字段正确,但若自研 parser 的尺寸、采样、表、scan 目录、帧数和 PCM/像素哈希均与独立工具一致,证据显著增强。
1 | ffprobe -v error -show_streams -show_packets input.avi |
命令输出要结合本手册的结构审计:工具可能宽容忽略尾数据、错误 table 或损坏帧。生产验收同时保留严格 parser 报告与实际解码结果。
性能验证
分别测 Marker parse、Huffman、IDCT、upsampling/color conversion、carrier reassembly 和内存复制。输入集包含平坦图、噪声图、最大分辨率、4:2:0/4:4:4、progressive 多 scan、restart 密集、超多 APP 和 RTP 乱序。报告平均与最坏单帧耗时、峰值内存、队列延迟和丢帧策略。
参考报告模板
1 | Frame 238 timestamp=714000/90000 carrier=RTP/JPEG |
对损坏帧增加首错 offset、carrier range、marker/scan 路径、预期与实际值以及恢复动作。报告中的“valid”只在 carrier 完整、JPEG 结构闭合、熵数据单元完成、输出资源合法且时间信息可接受时给出。
最终验收清单
完整 JPEG/MJPEG 实现必须能:按长度闭合所有 Marker segment;逐字段解释 DQT、DHT、SOF、SOS、DRI、DNL 和主要 APP;正确处理 Byte Stuffing、DC 差分、AC RUN/SIZE、ZRL/EOB、Restart;按采样因子遍历 MCU 和恢复颜色;对 progressive first/refinement 与 EOBRUN 保存正确状态;区分 AVI、HTTP multipart、RTP/JPEG 三类边界与时间;在所有尺寸和 offset 上进行 checked arithmetic;对截断、乱序、重叠、未定义表和资源攻击有限失败。
“能显示一张普通 Baseline 图片”只覆盖最小路径。参考手册级实现还应明确未支持的 lossless、arithmetic、hierarchical 或厂商 MJPEG 变体,在遇到其 SOF/扩展时给出可诊断拒绝;不能把未知过程送入 Baseline decoder 后输出无依据像素。只有字段、状态、边界、颜色、时间和故障行为全部可追溯,才能称为规范化的 JPEG/MJPEG 解析与承载实现。
版本化与长期维护
解析器的能力声明应按 coding process、样本精度、组件数、颜色模型和 carrier profile 版本化。新增某个 Marker 或厂商扩展时,不应改变既有严格模式对未知字段的边界处理。测试语料保存原始文件哈希、标准来源、预期结构目录和解码结果,任何优化都同时运行正常、截断和恶意向量。
当外部库升级导致 IDCT 舍入、ICC 颜色转换或错误隐藏结果变化时,应把“标准允许的数值差异”与“结构边界回归”分开审计。Marker offset、segment length、MCU 数、scan 状态和 carrier range 属于应保持精确一致的结构不变量;最终像素则按所采用精度模型设定容差。这样的版本管理可避免为了接受一个异常摄像头流而无意放宽所有输入。