YCbCr,YUV和RGB

Last Updated: 2023-04-12 00:46:51 Wednesday

-- TOC --

YCbCr是一种常见的颜色模式color model,它根据人眼的特性,用算法将luminance(亮度)chroma(颜色)进行分离,通过chroma subsampling,来实现颜色空间的有损压缩

YCbCr基本概念

color model这个概念,表示一种颜色表示机制,有很多不同的color model,非常常见和直观的color model是RGB,YCbCr也是一种在视频处理领域常用的color model,它利用了人眼的生理特性:我们人眼视觉系统的特点是:对明暗程度的敏感度超过了颜色,而在颜色中,又对绿色最为敏感。

YCbCr color model uses Y to represent the brightness and two color channels Cb (chroma blue) and Cr (chroma red). The YCbCr can be derived from RGB and it also can be converted back to RGB.

Y表示亮度,Cb表示蓝色,Cr表示红色,YCbCr可以和RGB相互转换。

YCbCr与RGB分别表示两种不同的颜色模式,即用什么方式来表示每个pixel的颜色数据。YCbCr来自YUV这个术语(有人说YUV用于模拟时代),现在这两个词我感觉基本上指同样的东西(似乎YUV更流行,因为它非常容易发音)。YCbCr的出现(或者YUV的出现),是为了彩色电视和黑白电视的兼容,Y表示luminance,视频码流中的亮度分量,黑白电视只能显示这个,再加上两个颜色分量Cb和Cr,就是彩色的了。

下图来自wiki百科,直观显示了从原图分离出来的Y,Cb和Cr三个分量:

ycbcr_sample.jpg

https://en.wikipedia.org/wiki/YUV

YCbCr色域介绍

YCbCr是在世界数字组织视频标准研制过程中,作为ITU-R BT.601建议的一部分,其实是YUV经过缩放和偏移的翻版。YCbCr由Y(Luminance)、Cb(Chrominance-Blue)和Cr(Chrominance-Red)组成,其中Y表示颜色的明亮度和浓度,而Cb和Cr则分别表示颜色的蓝色浓度偏移量和红色浓度偏移量。

医学研究证明,人的肉眼对视频的Y分量更敏感,因此在通过对色度分量进行subsampling来减少色度分量后,肉眼将察觉不到图像质量的变化。如果只有Y信号分量,而没有U、V分量,那么这样表示的图像就是黑白灰度图像。彩色电视采用YUV空间正是为了用亮度信号Y解决彩色电视机与黑白电视机的兼容问题,使黑白电视机也能接收彩色电视信号。我们通常把YUV和YCbCr概念混在一起,但其实这两者还是有挺大的区别,主要区分介绍如下:

  1. YUV是模拟信号,其色彩模型源于RGB模型,即亮度与色度分离,适合图像算法的处理,常应用于在模拟广播电视中,其中 Y∈[0,1],U,V∈[-0.5,0.5]。
  2. YCbCr是数字信号,其色彩模型源于YUV模型,它是YUV压缩和偏移后的版本(所谓偏移就是从[-0.5,0.5]偏移到[0,1],因此计算时候会加128),在数字视频领域应用广泛,是计算机中应用最多的格式,包括JPEG,MPEG,H.264/5,AVS等都采用YCbCr格式,我们通常广义上讲的YUV,严格来说,应该就是YCbCr。

YCbCr格式可以继续细分,有2种格式:TV rangeFull range,主要区别如下:

  1. TV range:Y∈[16,235],Cb∈[16-240] ,Cr∈[16-240] ,主要是广播电视采用的数字标准。
  2. Full range:Y、Cb、Cr∈[0-255] ,主要是PC端采用的标准,所以也称PC range。

关于为何TV range要量化到16-235,主要是由于YUV最终在模拟域传输,因此为了防止数模转换时引起过冲现象,于是将数字域限定在16-235。至于为什么选择16和235,可自行了解Gibbs Phenomenon吉布斯现象,这里不再继续展开,我也不懂。

关于ffmpege的AV_PIX_FMT_YUV420P和AV_PIX_FMT_YUVJ420P

YUVJ420P和YUV420P格式上是一致的,只是颜色空间上的不同。YUVJ420P使用的是JPEG转换公式,范围是0-255,而YUV420P的范围是16-235。

libjpeg-turbo提供的转yuv的接口,得到的效果,与YUVJXXXP相同!

YUV与RGB互转

现在的计算机显示器,颜色的显示都是RGB模式,而视频编解码使用的大多都是YCbCr信号(YCbCr提供了最初的数据压缩,主流的420采样模式,直接就砍掉了一半的视频数据)。因此,就有了相互转换的需要。

根据ITU-R组织发布的BT.601(SDTV)标准,从RGB到YCbCr为:

Y = 0.299R + 0.587G + 0.114B
Cb = 0.564(B - Y)
Cr = 0.713(R - Y)

注意计算Y的公式,3个系数之和为1(这3个系数就成了一种权重),Y本质上就是从原图得到的一张grayscale灰度图,这也是很多人使用的从原图得到灰度图的计算方法。G(Green)的系数最大,这符合人眼视觉系统对绿色最敏感的特性。

从YCbCr到RGB为:

R = Y + 1.402Cr
G = Y - 0.344Cb - 0.714Cr
B = Y + 1.772Cb

考虑到计算速度,用矩阵计算的方式来转换也是可以的,下面是用Python和NumPy来示例:

>>> to_yuv
array([[ 0.299,  0.587,  0.114],
       [-0.169, -0.331,  0.5  ],
       [ 0.5  , -0.419, -0.08 ]])
>>>
>>> to_rgb
array([[ 1.   ,  0.   ,  1.402],
       [ 1.   , -0.344, -0.714],
       [ 1.   ,  1.772,  0.   ]])
>>>
>>> a = np.array((12,23,34))
>>> a
array([12, 23, 34])
>>> to_rgb @ to_yuv @ a
array([12.052486, 22.972402, 34.005148])

这个计算有性能需求,基本思路就是想办法避免浮点数计算,避免乘法,或者采用查表的方法!

YUV和RGB互转要考虑两个问题:

  1. 转换Matrix,是BT601还是BT709,还是4K时代的BT2020,不同的matrix的色彩范围不一样,数值也有差;
  2. 范围Range,YUV里TV和PC显示器的显示数值范围不一样,在8bit位深的情况下,TV的range是16-235(Y)、16-240(UV),而PC的range是0-255,RGB没有range之分,全是0-255!

BT.601 YUV转RGB

用上面理论部分的矩阵计算得到的数字总是不对,翻阅了一些资料,发现当RGB或YUV都是8bit数据的时候,计算公式是有所不同的。这部分需要参考:https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion

下面是8bit yuv数据转8bit rgb数据的算法:

yuv2rgb.png

按照这个公式算完后,小于0的数据取0,大于255的数据取255。

下面给出两个可以用算法代码。

这个是float计算方式,未优化:

# convert to rgb
# yuv dtype is uint8
yuv = yuv/255

for i in range(oheight):
    for j in range(owidth):
        c = yuv[i,j,0] * 256 - 16
        d = yuv[i,j,1] * 256 - 128
        e = yuv[i,j,2] * 256 - 128

        r = (298*c + 409*e + 128) // 256
        g = (298*c - 100*d - 208*e + 128) // 256
        b = (298*c + 516*d + 128) // 256

        if r < 0: yuv[i,j,0] = 0
        elif r > 255: yuv[i,j,0] = 255
        else: yuv[i,j,0] = r

        if g < 0: yuv[i,j,1] = 0
        elif g > 255: yuv[i,j,1] = 255
        else: yuv[i,j,1] = g

        if b < 0: yuv[i,j,2] = 0
        elif b > 255: yuv[i,j,2] = 255
        else: yuv[i,j,2] = b

yuv = yuv/255

下面是另一种计算方式,未优化:

for i in range(oheight):
    for j in range(owidth):
        c = yuv[i,j,0] - 16
        d = yuv[i,j,1] - 128
        e = yuv[i,j,2] - 128

        r = int((255/219)*c + (255/224)*1.402*e)
        g = int((255/219)*c - (255/224)*1.772*(0.114/0.587)*d - (255/224)*1.402*(0.299/0.587)*e)
        b = int((255/219)*c + (255/224)*1.772*d)

        if r < 0: yuv[i,j,0] = 0
        elif r > 255: yuv[i,j,0] = 255
        else: yuv[i,j,0] = r

        if g < 0: yuv[i,j,1] = 0
        elif g > 255: yuv[i,j,1] = 255
        else: yuv[i,j,1] = g

        if b < 0: yuv[i,j,2] = 0
        elif b > 255: yuv[i,j,2] = 255
        else: yuv[i,j,2] = b

最后上一个彻底优化后的版本,去掉了所有的循环:

# convert to rgb
a = np.empty_like(yuv, dtype=np.int32)
a[:,:,0] = yuv[:,:,0].astype(np.int32) - 16
a[:,:,1:] = yuv[:,:,1:].astype(np.int32) - 128
x1 = 255 / 219
x2 = (255/224) * 1.402
x3 = (255/224) * 1.772
x4 = x3 * (0.114/0.587)
x5 = x2 * (0.299/0.587)
x = np.array(([x1,x1,x1],
               [0,-x4,x3],
               [x2,-x5,0]))
#for i in range(oheight):
#    for j in range(owidth):
#        t1 = a[i,j,:] @ x
#        t1 = np.where(t1<0, 0, t1)
#        yuv[i,j,:] = np.where(t1>255, 255, t1)
#for i in range(oheight):
#        t1 = a[i,:,:] @ x
#        t1 = np.where(t1<0, 0, t1)
#        yuv[i,:,:] = np.where(t1>255, 255, t1)
t1 = a.reshape(380*620,3) @ x
t1 = np.where(t1<0, 0, t1)
t1 = np.where(t1>255, 255, t1)
yuv = t1.reshape(380,620,3).astype(np.uint8)

如果是转换BT709或BT2020,不一样的地方只是系数。

Chroma Subsampling

这个压缩技术,利用了人眼对明暗的敏感程度超过颜色的现实,原理是,保留全部Y,但Cb和Cr只取一部分。

Chroma subsampling is the practice of encoding images by implementing less resolution for chroma information than for luma information, taking advantage of the human visual system's lower acuity for color differences than for luminance. It is used in many video encoding schemes – both analog and digital – and also in JPEG encoding.

JPEG编码也用到了YCbCr的chroma subsampling。

我们在讨论subsampling scheme的时候(注意采样是不涉及Y的),都会采用J:a:b的形式,比如4:2:0,4:2:2等等。这个地方有点费解,我也是仔细研究了好一会儿才明白。

J:水平采样参考,一般取4,

a:Cb和Cr每行采样a个,

b:在第1行和第2行之间的变化,针对Cb和Cr,

还是看这个WiKi的图来理解吧:

chroma_subsampling.jpg

应用YCbCr color model后,虽然有一些损失,但是一般情况人眼难以觉察,或者说这种压缩后的视频质量是在可接受的范围内的。

4:4:4 means no downsampling of the chroma channels.

4:2:2 means 2:1 horizontal downsampling, with no vertical downsampling. Every scan line contains four Y samples for every two U or V samples.

4:2:0 means 2:1 horizontal downsampling, with 2:1 vertical downsampling.

4:1:1 means 4:1 horizontal downsampling, with no vertical downsampling. Every scan line contains four Y samples for every U or V sample. 4:1:1 sampling is less common than other formats.

YUV420和YUV444比较常用,YUV422也能见到。

什么是YUV呢?

「Y」表示明亮度(Luminance),「U」和「V」则是色度、浓度(Chrominance、Chroma)。其实YUV是模拟信号的称呼,数字信号应该是表示为YCbCr。不过,现在还是能够在很多地方看到YUV这个称呼。

常见的颜色模型中,RGB主要用于电子系统里表达和显示颜色,CMYK印刷四色模式用于彩色印刷,而YUV是被欧洲电视系统所采用的一种颜色编码方法。

使用YUV的优点有两个:

  1. YUV主要用于优化彩色视频信号的传输,使其向后兼容老式黑白电视,这一特性用在于电视信号上;
  2. YUV数据总尺寸小于RGB格式(但用YUV444的话,和RGB888一样都是24bits,除了它)

YUV存储格式(原始视频文件格式)

YUV的存储格式有两大类:packedplanar,还有SemiPlanar

对于packed的YUV格式,每个像素点的Y,U,V是连续交错存储的。对于planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V。对于SemiPlanar,则是先连续存储所有像素点的Y,再连续交错U和V。

packed

UYVY422:422采样下,每2个pixel共用1个U和1个V,UYVY表示顺序,先放一个U,然后一个Y,然后一个V,然后一个Y,这4个值刚好对应2个pixel。

uyvy422.jpg

搞定了UYVY422,也就直接搞定了 YUYV422(YUY2),YVYU422。

YUV420:420采样下,每4个pixel共用1个U和1个V,存储方式如下图:

yuv420.jpg

planar和semiplanar

YUV422P:P就是Planar的意思,存储形式如下图:

yuv422p.jpg

YUV422SP,S就是Semi的首字母,直接看图:

yuv422sp.jpg

YUV420P(也叫I420):

yuv420p.jpg

YVU420P,它叫YV12,12表示12 bit per pixel。

YUV420SP(也叫NV12):

yuv420sp.jpg

YVU420SP,也叫NV21。

I420: YYYYYYYY UU VV => YUV420P
YV12: YYYYYYYY VV UU => YUV420P
NV12: YYYYYYYY UVUV  => YUV420SP
NV21: YYYYYYYY VUVU  => YUV420SP

planar存储方式,并不一定是对速度最友好的!

比如常见的YUV420P,写C代码读取这样格式的文件,需要3个for循环,而如果是YUV420SP,或NV12的存储方式,C代码中只需要2个for循环,在后一个for循环中,可以同时读取Cb和Cr的数据。

同理,读取YUV444P存储格式的文件,也需要3个for循环。

ffmpeg -pix_fmts可以看到更多不同的存储方式代码。

本文链接:https://cs.pynote.net/ag/image/202204032/

-- EOF --

-- MORE --