跳到主要内容
浏览器
导航
图片浏览器

图像解码管线与色彩空间转换

从字节到像素的完整解码链路:文件头识别与容器、编码分离,熵解码与逆量化,YCbCr 到 RGB 的矩阵变换,色度子采样的信息损失,以及色彩空间、传递函数、alpha 与位深的处理。

发布于 5 分钟阅读 1719 字

一张 JPEG 或 PNG 文件在磁盘上是一段按特定语法排列的字节。要把这段字节变成屏幕上可以显示的像素,需要经过一条由若干步骤组成的解码管线。管线的输入是压缩码流,输出是位于某个色彩空间、某个位深、按特定通道顺序排列的像素数组。

解码器并不关心文件从哪里来,它只关心字节的语法:先识别文件类型,再按该类型定义的容器结构定位压缩数据,然后熵解码、反变换、组装成像素。理解这条管线可以解释两个常见的现象:同一张图片在不同工具中显示出的颜色可能不一致,以及打开大图时内存占用为何会明显高于文件体积。

文件头识别与容器、编码的分离

多数图像格式以固定长度的魔数开头,解码器读取前若干字节与已知签名比对即可确定格式:

  • PNG:89 50 4E 47 0D 0A 1A 0A
  • JPEG:以 FF D8 起始,以 FF D9 结束
  • GIF:ASCII 文本 GIF87a 或 GIF89a
  • WebP:RIFF 之后跟 WEBP

识别出格式之后,还要区分「容器」与「编码」两层。JPEG 的容器是一串以 FF 开头的标记段,其中承载压缩数据的是 SOS 之后的熵编码码流;PNG 采用长度—类型—数据—校验四段式的块结构,IDAT 块承载 zlib 压缩后的扫描行数据。容器回答「数据在哪、有多大、属于哪一部分」,编码回答「这些数据如何还原成样本」。关于扫描的组织方式,可参见渐进式 JPEG 与交错 PNG 的编码原理。

/* JPEG 标记段遍历:标记 2 字节 + 长度 2 字节(长度字段不含标记本身) */
while (pos < len) {
    if (data[pos] != 0xFF) { pos++; continue; }                    /* 填充字节 */
    uint8_t marker = data[pos + 1];
    if (marker == 0xD8 || marker == 0xD9) { pos += 2; continue; }  /* SOI / EOI */
    uint16_t seg_len = ((uint16_t)data[pos + 2] << 8) | data[pos + 3];
    handle_segment(marker, data + pos + 4, seg_len - 2);
    pos += 2 + seg_len;
}

熵解码与逆量化

JPEG 的压缩数据先经霍夫曼编码(基线)或算术编码,配合游程编码把量化后的 DCT 系数写成变长码。熵解码把这些码还原成系数块,块尺寸通常为 8×8。随后进行逆量化:把系数乘以量化表中的对应值,再做逆离散余弦变换,得到空间域样本。

逆量化是误差已经固化的环节。量化发生在编码端,解码端只能按同一张量化表还原,误差无法消除,因此反复「另存为 JPEG」会累积新的量化误差。

PNG 一侧的路径不同。IDAT 数据经 zlib 解压后得到的是滤波后的扫描行,解码器要按行识别滤波器类型并做逆滤波。PNG 定义了 Sub、Up、Average、Paeth 等滤波器以提高压缩率,逆滤波依赖上一行或左侧样本,必须按行顺序推进,因此这一环节不易任意并行。

从 YCbCr 到 RGB

JPEG 的样本通常存放在 YCbCr 空间中:Y 表示亮度,Cb 与 Cr 表示蓝色差与红色差。显示器需要的是 RGB,于是解码流程中还要做一次矩阵变换。BT.601 与 BT.709 使用不同的系数:

标准亮度定义(近似系数)常见出处
BT.6010.299 R + 0.587 G + 0.114 B标清电视时代的色度定义,部分 JPEG 码流按此标注
BT.7090.2126 R + 0.7152 G + 0.0722 B高清电视的色度定义

两者都对 Y 与色差做有限范围的偏移与缩放(一种常见做法是把 16–235 映射到 0–255)。系数或范围选错,画面就会出现色偏或对比度变化。整数实现通常用定点运算加移位代替浮点:

/* 简化示意:BT.601 全范围的定点近似,系数为缩放后的整数 */
int r = (y * 256 + 359 * (cr - 128) + 128) >> 8;
int g = (y * 256 -  88 * (cb - 128) - 183 * (cr - 128) + 128) >> 8;
int b = (y * 256 + 454 * (cb - 128) + 128) >> 8;

不同实现采用的缩放位数与舍入方式并不相同,上面只是定点思路的示意。

色度子采样与信息损失

人眼对亮度细节的分辨力通常高于对色度,因此多数格式在存储前对色度通道做下采样。记号「4:2:0」描述的是采样比例:

采样格式水平方向垂直方向每个 2×2 亮度像素块对应的色度样本
4:4:4不降采样不降采样4
4:2:2减半不降采样2
4:2:0减半减半1

还原时解码器要把色度上采样回亮度分辨率,常用做法是在邻域内做插值。这一步不可逆:被丢弃的色度信息无法由插值复原,因此在饱和度高、细节密集的边缘(例如彩色文字的轮廓)容易出现色边。

色彩空间、色域与传递函数

样本数值本身没有颜色含义,要配合色彩空间与传递函数才能确定颜色。sRGB 与 Display P3 都定义了白点、原色坐标与传递函数,但原色坐标与可表示范围不同,同一组 RGB 数值在两者中对应的实际颜色也不同。因此解码器需要读取嵌入的 ICC 描述文件或标准声明,再决定如何解释这些数值。

传递函数描述数值与相对亮度之间的非线性映射,sRGB 的传递函数近似为 γ≈2.2 的幂函数与一段线性段的组合。解码得到的样本通常位于编码域,如果直接在这些数值上做加权平均,结果会与在相对亮度域中运算不同,这也是图像缩放插值算法要先转入线性域的原因之一。

alpha 与位深

带 alpha 通道的图像有预乘与非预乘两种表示。预乘指把 RGB 各通道先乘以 alpha,使半透明像素在逐项相加合成时得到正确结果;非预乘数据在合成前必须先乘上 alpha,否则边缘会出现偏暗的轮廓。

位深决定每个通道可用的离散级别。8 位通道提供 256 级;解码器内部常以更高位深(如 16 位定点)完成矩阵变换与缩放,再转回 8 位输出,以减少中间步骤的舍入误差。提高内部位深不会增加原始信息,但能减少处理环节引入的新误差。

小结

解码管线把压缩码流还原为像素:识别容器 → 熵解码 → 逆量化与逆变换 → 色度上采样 → 色彩空间变换 → 组装像素。每一步都有自己的误差来源:量化误差在编码端固定,色度子采样丢弃了部分信息,矩阵系数与传递函数的选取决定了颜色如何被解释,位深与舍入则影响处理精度。

图片浏览器 · 相关文章