Last Updated: 2023-04-12 00:46:51 Wednesday
-- TOC --
YCbCr
是一种常见的颜色模式color model
,它根据人眼的特性,用算法将luminance(亮度)
和chroma(颜色)
进行分离,通过chroma subsampling,来实现颜色空间的有损压缩
。
color model这个概念,表示一种颜色表示机制,有很多不同的color model,非常常见和直观的color model是RGB
,YCbCr也是一种在视频处理领域常用的color model,它利用了人眼的生理特性:我们人眼视觉系统的特点是:对明暗程度的敏感度超过了颜色,而在颜色中,又对绿色最为敏感。
YCbCr color model uses Y to represent the
brightness
and two color channels Cb (chromablue
) and Cr (chromared
). 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三个分量:
https://en.wikipedia.org/wiki/YUV
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概念混在一起,但其实这两者还是有挺大的区别,主要区分介绍如下:
YCbCr格式可以继续细分,有2种格式:TV range
与Full 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相同!
现在的计算机显示器,颜色的显示都是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互转要考虑两个问题:
BT601
还是BT709
,还是4K时代的BT2020
,不同的matrix的色彩范围不一样,数值也有差;用上面理论部分的矩阵计算得到的数字总是不对,翻阅了一些资料,发现当RGB或YUV都是8bit数据的时候,计算公式是有所不同的。这部分需要参考:https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
下面是8bit yuv数据转8bit rgb数据的算法:
按照这个公式算完后,小于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,不一样的地方只是系数。
这个压缩技术,利用了人眼对明暗的敏感程度超过颜色的现实,原理是,保留全部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的图来理解吧:
应用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也能见到。
「Y」表示明亮度(Luminance),「U」和「V」则是色度、浓度(Chrominance、Chroma)。其实YUV是模拟信号的称呼,数字信号应该是表示为YCbCr
。不过,现在还是能够在很多地方看到YUV这个称呼。
常见的颜色模型中,RGB主要用于电子系统里表达和显示颜色,CMYK印刷四色模式用于彩色印刷,而YUV是被欧洲电视系统所采用的一种颜色编码方法。
使用YUV的优点有两个:
YUV的存储格式有两大类:packed
和planar
,还有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,也就直接搞定了 YUYV422(YUY2),YVYU422。
YUV420:420采样下,每4个pixel共用1个U和1个V,存储方式如下图:
planar和semiplanar
YUV422P:P就是Planar的意思,存储形式如下图:
YUV422SP,S就是Semi的首字母,直接看图:
YUV420P(也叫I420):
YVU420P,它叫YV12,12表示12 bit per pixel。
YUV420SP(也叫NV12):
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 --