YCbCr 色彩空间

在 JPEG 图像的编码流程中,一个关键步骤是将 RGB 转换为 YCbCr。Y 表示亮度信号(Luma),Cb 和 Cr 是两个色差分量(Color Difference):Cb 是蓝色色差,Cr 是红色色差;两者合起来构成色度(Chrominance,或者Chroma)。这样一来,图像的亮度与颜色信息就被分开了,后面便可以更大胆地压缩颜色信息,例如进行色度降采样(Chroma Subsampling)。

不过,YCbCr 转换本身并没有压缩图像。它只是先把亮度与色度分开,为后续压缩色度创造条件。

YCbCr 也并不是 JPEG 的强制要求:JPEG 本身不规定分量的含义,RGB、CMYK 乃至灰度图像同样可以用 JPEG 编码;YCbCr 只是 JFIF 为彩色图像指定的颜色空间。本文后续的讨论,都建立在“使用 YCbCr 的 JFIF 图像”这一前提上。

有些材料中会把 YCbCr 称为 YUV。但严格来说,YUV 是属于模拟信号时代的术语,在数字时代,用 YCbCr,或者更为严谨的 Y'CbCr (撇号强调 Gamma 校正),才更为合适。不过在现实中这些术语早就已经大量混用了,一般也不需要过于较真。我们这里所说的R、G、B、Y都是指经过 Gamma 校正后的数值,关于 Gamma 校正,我们日后再来讨论。

Y(Luma)

我们先来理解 YCbCr 中的 Y。在明视觉下,人眼对不同波长的光敏感度不同:对绿光最敏感,红光次之,对蓝光最不敏感。换句话说,不同颜色的光对“人眼感知到的亮度”的贡献并不相同。根据这个原理,我们可以把“人眼感知到的亮度”表示为 RRGGBB 三个分量的加权和:

Y=KrR+KgG+KbBY = K_rR + K_gG + K_bB

这里的 RRGGBB 表示取值范围为 [0,1][0,1] 的 RGB 分量(均已经过 Gamma 校正),KrK_rKgK_gKbK_b 则是三个分量的权重。绿色权重最大、红色次之、蓝色最小,正与前面的直觉相呼应。

JFIF 约定使用 Rec.601 的 Luma 系数:

Kr=0.299,Kg=0.587,Kb=0.114K_r = 0.299, \quad K_g = 0.587, \quad K_b = 0.114

因此,

Y=0.299R+0.587G+0.114BY = 0.299R + 0.587G + 0.114B

因为 Kr+Kg+Kb=1K_r + K_g + K_b = 1,且 RRGGBB 的取值范围为 [0,1][0,1],所以 YY 的取值范围也是 [0,1][0,1]

Cb、Cr(Chroma)

有了 YY 之后,颜色信息又该怎么表示呢?这就需要引入色差的概念。色差,简单来说,就是表示某个颜色分量和 Luma 之间的差异。如果某个像素的蓝色分量比它的 Luma 更高,那么它就带有偏蓝的色差;如果它的红色分量比 Luma 更高,那么它就带有偏红的色差。

因此,我们很自然地会想到用下面的方法来表示色差:

Cb=BYC_b = B - Y Cr=RYC_r = R - Y

这样的定义很简单,很直观,但是却存在着一个问题。

我们可以先分析一下 CbC_b 的取值范围。将 YY 的表达式代入:

Cb=BY=B(KrR+KgG+KbB)=KrRKgG+(1Kb)B\begin{align*} C_b &= B - Y \\ &= B - (K_rR + K_gG + K_bB) \\ &= -K_rR - K_gG + (1-K_b)B \\ \end{align*}

R=G=0R=G=0B=1B=1 时,

(BY)max=1Kb(B-Y)_{\text{max}} = 1 - K_b

R=G=1R=G=1B=0B=0 时,

(BY)min=KrKg=(1Kb)(B-Y)_{\text{min}} = -K_r - K_g = -(1-K_b)

也就是说,BYB-Y 的取值范围为:

BY[(1Kb),1Kb]B-Y \in [-(1-K_b), 1-K_b]

用相同方法可得:

RY[(1Kr),1Kr]R-Y \in [-(1-K_r), 1-K_r]

可以看到,蓝色色差和红色色差的取值范围并不相同,这就可能带来很多使用上的不便。为了能更方便地使用它们,我们可以把它们都缩放到 [0.5,0.5][-0.5, 0.5] 区间,于是我们可以改用下面的方法来表示色差:

Cb=12BY1Kb=BY1.7720.564334(BY)C_b = \frac{1}{2}\frac{B - Y}{1 - K_b} = \frac{B - Y}{1.772} \approx 0.564334(B - Y) Cr=12RY1Kr=RY1.4020.713266(RY)C_r = \frac{1}{2}\frac{R - Y}{1 - K_r} = \frac{R - Y}{1.402} \approx 0.713266(R - Y)

YY 代入这两个式子,并整理同类项,就得到了可直接由 RGB 计算 Cb、Cr 的公式:

Cb=0.564334B0.564334(0.299R+0.587G+0.114B)=0.168736R0.331264G+0.5B\begin{align*} C_b &= 0.564334B - 0.564334(0.299R + 0.587G + 0.114B) \\ &= -0.168736R - 0.331264G + 0.5B \end{align*} Cr=0.713266R0.713266(0.299R+0.587G+0.114B)=0.5R0.418688G0.081312B\begin{align*} C_r &= 0.713266R - 0.713266(0.299R + 0.587G + 0.114B) \\ &= 0.5R - 0.418688G - 0.081312B \end{align*}

到这里,亮度和两个色差分量就都能从 RGB 算出来了。

有些朋友可能发现了,YCbCr 的色差分量只有蓝色和红色,并没有单独的绿色分量。这并不意味着绿色信息被丢弃了,只是因为绿色的分量可以由 YYCbC_bCrC_r 计算出来,所以不需要单独储存。

到目前为止,我们已经得知了 YYCbC_bCrC_r 的计算方法:

Y=0.299R+0.587G+0.114BCb=0.168736R0.331264G+0.5BCr=0.5R0.418688G0.081312B\begin{align*} Y &= 0.299R + 0.587G + 0.114B \\ C_b &= -0.168736R - 0.331264G + 0.5B \\ C_r &= 0.5R - 0.418688G - 0.081312B \end{align*}

且它们的取值范围分别是:

Y[0,1]Cb[0.5,0.5]Cr[0.5,0.5]\begin{align*} Y &\in [0,1] \\ C_b &\in [-0.5,0.5] \\ C_r &\in [-0.5,0.5] \end{align*}

YYCbC_bCrC_r 目前都还是方便推导的连续值。为了更容易地处理数字图像,我们希望产生的三个值都是 [0,255][0, 255] 范围内的整数,这样便可以用无符号 8 位整数来分别表示这三个分量。

我们先定义取整、裁剪之前的连续值:

Y8=255YY_8^* = 255Y Cb8=128+255CbC_{b8}^* = 128 + 255C_b Cr8=128+255CrC_{r8}^* = 128 + 255C_r

再对上述三个值进行取整与裁剪:

Y8=clamp(round(Y8),0,255)Cb8=clamp(round(Cb8),0,255)Cr8=clamp(round(Cr8),0,255)\begin{align*} Y_8 &= \operatorname{clamp}(\operatorname{round}(Y_8^*), 0, 255) \\ C_{b8} &= \operatorname{clamp}(\operatorname{round}(C_{b8}^*), 0, 255) \\ C_{r8} &= \operatorname{clamp}(\operatorname{round}(C_{r8}^*), 0, 255) \end{align*}

其中

  • round(x)\operatorname{round}(x) 表示将 xx 取为相近的整数;
  • clamp(x,0,255)\operatorname{clamp}(x,0,255) 表示将小于 0 的值裁剪为 0,将大于 255 的值裁剪为 255。

现在把前面的步骤合在一起,我们便得到了将数字图像从 RGB 转换为 YCbCr 的方法:

[Y8Cb8Cr8]=[0.2990.5870.1140.1687360.3312640.50.50.4186880.081312][R8G8B8]+[0128128]\begin{bmatrix} Y_8^* \\ C_{b8}^* \\ C_{r8}^* \end{bmatrix} = \begin{bmatrix} 0.299 & 0.587 & 0.114 \\ -0.168736 & -0.331264 & 0.5 \\ 0.5 & -0.418688 & -0.081312 \end{bmatrix} \begin{bmatrix} R_8 \\ G_8 \\ B_8 \end{bmatrix} + \begin{bmatrix} 0 \\ 128 \\ 128 \end{bmatrix}

其中 R8R_8G8G_8B8B_8 是由 RRGGBB 缩放取整到 [0,255][0, 255] 区间上的整数值,而 Y8Y_8^*Cb8C_{b8}^*Cr8C_{r8}^* 表示取整、裁剪之前的连续值。由于 Cb8C_{b8}^*Cr8C_{r8}^* 的取值范围实际上是 [0.5,255.5][0.5, 255.5],略微超出 [0,255][0, 255],因此需要按照前面的规则取整并裁剪,才能得到最终的 8 位整数值 Y8Y_8Cb8C_{b8}Cr8C_{r8}

熟悉视频编码的朋友应该知道,Rec.601 的数字视频通常不会用完整的 [0,255][0, 255] 范围,YY 的范围是 [16,235][16, 235]。JFIF 虽然借用了 Rec.601 的转换系数,却没有沿用它的取值范围,而是让 YYCbC_bCrC_r 使用完整的 [0,255][0, 255] 范围。

色度降采样

现在,颜色信息已经分离到 Cb 和 Cr 两个分量,亮度信息则集中在 Y 分量中。因此就可以只降低 Cb、Cr 的空间分辨率,从而减少数据量。这个过程称为色度降采样(chroma subsampling)。

人眼对颜色变化的敏感度,通常不如对亮度变化的敏感度。一张照片的轮廓和纹理,大多由明暗变化来刻画;颜色往往只是附着在亮度结构之上的大块色调。因此,适度降低色度分量的分辨率,通常不会明显影响观感,却能显著减少需要编码的数据量——这正是编码器敢于压缩颜色信息的依据。

不过,当图像里出现颜色对比强烈的高频细节,比如细小的线条、锐利的边缘等时,色度降采样就会导致比较明显的伪影,因此这类内容通常更适合用 4:4:4 保存。

采样比例

色度的降采样有很多种方案,通常用 J:a:bJ:a:b 记号来描述,常见的有 4:4:4、4:2:2、4:1:1、4:2:0 这几种。

这种记号是以一个宽 JJ 像素、高 22 像素的参考块为基准:

  • JJ:参考块的宽度,通常取 4;
  • aa:参考块中第一行的色度采样个数;
  • bb:参考块从第一行到第二行,色度采样发生变化的数量。

下面用一个宽 4、高 2 的像素块示意。每个格子是一个像素;YC 表示该像素同时有亮度和色度采样,Y 表示它只有亮度采样:

4:4:4

  • 参考块第一行有 4 个色度采样,因此 a=4a=4
  • 参考块从第一行到第二行,新增了 4 个色度采样,因此 b=4b=4

换句话说,该方式不降采样,每个像素都有独立的 CbC_bCrC_r,色度分辨率与亮度相同。

YC YC YC YC
YC YC YC YC

4:2:2

  • 参考块第一行有 2 个色度采样,因此 a=2a=2
  • 参考块从第一行到第二行,新增了 2 个色度采样,因此 b=2b=2

换句话说,该方式只在水平方向将色度采样减半,垂直方向不变。

YC Y  YC Y
YC Y  YC Y

4:1:1

  • 参考块第一行有 1 个色度采样,因此 a=1a=1
  • 参考块从第一行到第二行,新增了 1 个色度采样,因此 b=1b=1

换句话说,该方式在水平方向将色度采样减到四分之一、垂直方向不变。它在静态 JPEG 图像中几乎不会用到,主要见于部分视频格式(如 DV)。

YC Y  Y  Y
YC Y  Y  Y

4:2:0

  • 参考块第一行有 2 个色度采样,因此 a=2a=2
  • 参考块从第一行到第二行,新增了 0 个色度采样,因此 b=0b=0

换句话说,该方式在水平和垂直方向都减半,因此每个 2×22\times2 像素块共用一组 CbC_bCrC_r

YC Y  YC Y
Y  Y  Y  Y

值得注意的是,J:a:bJ:a:b 只规定了色度采样的大致数量关系,至于色度采样具体落在参考块的哪个位置,是居中在多个亮度采样中间,还是与某个亮度采样重合,不同格式的约定并不统一。

另外,虽然 J:a:bJ:a:b 是更为普适的记号,但是在现代语境下,人们似乎更喜欢使用水平、垂直采样因子来描述色度采样比例,JFIF 也是如此。将每个分量的水平、垂直采样因子写进码流的 SOF 标记,解码器就能按文件里写明的比例处理。其中 4:2:0 是照片场景中最常见的选择。

数据量减少了多少

在一个 4×24\times2 的参考块里,YY 始终有 8 个采样,不同的比例只改变色度采样的个数。按每个采样 8 位来算:

  • 4:4:4YY 8 个、CbC_b 8 个、CrC_r 8 个,共 24 个采样;
  • 4:2:2YY 8 个、CbC_b 4 个、CrC_r 4 个,共 16 个采样,是 4:4:4 的 23\frac{2}{3}
  • 4:2:0YY 8 个、CbC_b 2 个、CrC_r 2 个,共 12 个采样,是 4:4:4 的 12\frac{1}{2}

也就是说,仅靠降采样这一步,4:2:0 就能把原始的像素数据量减少一半,而且这部分节省发生在任何压缩变换之前。