作者:叶余
来源:https://www.cnblogs.com/leisure_chn/p/10290575.html
cnblogs 网站将文本J:a:b
渲染成了J????️b。是否可通过设置博客后台解决此问题?有知道的同学请留言指点一下,谢谢。
1. 色彩空间基础
颜色是不同波长的光对人眼刺激产生的色彩感觉。色彩空间(Color Space)是颜色的数学表示,根据不同的表示方法分为不同的色彩模型。最常用的色彩模型有三类:RGB(用于计算机图形学), YUV(用于视频系统), CMYK(用于彩色印刷)。后文对色彩空间与色彩模型的叫法不作区分。本文仅讨论视频图像处理领域常用的 RGB 色彩空间和 YUV 色彩空间。
颜色与光源特性和人眼视觉特性有密切的关系,与之相关的学科有光度学和色度学。光度学是研究光学计量的学科,定义了光通量、发光强度、照度、亮度等主要光度学参量以及几个光度学重要定律。光度学考虑的是可见光范围,并且考虑人眼主观影响。色度学是研究色彩计量的科学,它定性和定量地研究人眼的颜色视觉规律、颜色测量理论与技术。色度学是研究视频技术的重要理论基础,将色度学中最基础的两个概念摘录如下。本节内容,是理解图像色彩相关知识的基础,已经简洁的不能再简洁。
1.1 光的常用度量
描述光的常用物理量有四个:光通量、光强、照度、亮度。如下两张示意图引用自“如何正确理解照度和亮度的关系?”,图中涉及三个角色:光源、物体(被观察者)与人眼(观察者)。
如下概念的描述,力求通俗,因此并不准确。
1.1.1 光通量
单位时间内由光源所发出或由被照物所吸收的总光能。单位流明。
在第一张图中,灯泡在单位时间内散发的的光的总和即为光通量。光通量衡量光源总的发光量。
1.1.2 光强
光源在给定方向上,每单位立体角内所发出的的光通量。单位坎德拉。
在第一张图中,光源射向照射面的一根光线表示光强。光强衡量光源的发光强度。
1.1.3 照度
物体表面每单位面积入射可见光的光通量。单位勒克斯。
照度在被观察者(被照物体)角度,衡量接受到的光的强弱。
1.1.4 亮度
描述发光面或反光面上光的明亮程度的光度量。单位坎德拉每平方米。
亮度在观察者(人眼)角度,衡量感受到的光的明亮程度。
在第二张图中,理想情况下,被同一光源照射,黑色书和白色书具有同样的照度,却有不同的亮度。
1.2 色度学基础
1.2.1 彩色三要素
光的颜色取决于客观和主观两方面的因素。客观因素是光的功率波谱分布,它影响光源的颜色。主观因素是人眼视频特性,它影响人眼对色彩的感觉。 彩色三要素指亮度(Lightness)、色调(Hue)和饱和度(Saturation),任一色彩都可以用这三个基本参量来表示:
亮度表示颜色明暗的程度,是光作用于人眼时引起的明亮程度的感觉。
色调是指颜色的类别,例如红色、蓝色、绿色指的就是色调。
饱和度指颜色的深浅程度,也称彩度。例如深绿、浅绿指的就是绿色这个色调的饱和度,饱和度越高,颜色越深。
1.2.2 三基色原理
三基色原理指自然界中大部分彩色都可以由三种基色按不同比例混合得到。选择三种基色的前提的:每一种基色都不能由另外两种基色混合得到,这三种基色互相独立。三基色原理主要内容如下:
- 自然界中的绝大部分彩色,都可以由三种基色按一定比例混合得到;反之,任意一种彩色均可被分解为三种基色。
- 作为基色的三种彩色,要相互独立,即其中任何一种基色都不能由另外两种基色混合来产生。
- 由三基色混合而得到的彩色光的亮度等于参与混合的各基色的亮度之和。
- 三基色的比例决定了混合色的色调和色饱和度。
不同颜色混合在一起能产生新的颜色,这种方法称为混色法。色彩空间 RGB 是采用了 R、G、B 三种基色,色彩空间 CMYK 是采用了 C、M、Y 三种基色。
我们看一张包含彩色三要素和 RGB 三原色的示意图:
2. RGB 色彩空间
太深的原理已超出本文的讨论范围和研究目的。这里摘录一些经验观点:
RGB 三原色不是唯一的正交基,还可以有其他选择,只不过 RGB 能组合出来的颜色更为丰富。[8]
不考虑人类视觉的特殊性时,我们平时所认为的“红+绿=黄”之类的“颜色叠加”是不成立的。因为红色和绿色的单色光的混合只有在人类的视觉中才会跟黄色的单色光一样,它们在其它动物的视觉中不一定是一样的。也就是说,不考虑人类视觉的特殊性时,不存在 RGB 颜色空间。[8]
人类有三种视锥细胞用于感知颜色,这三种视锥细胞对红(R)、绿(G)、蓝(B)三种颜色敏感。[9]
人眼看到的物体颜色,是光源照射到物体,物体吸收(还有透射)部分颜色的光,然后从物体表面反射的光线进入人眼后人眼得到的色彩感觉。
人眼看到物体为黑色,是因为没有光线从物体表面反射进入人眼。一种情况是物体将光线完全吸收,没有光从物体表面反射出来(例如白天我们看一件黑衣服);另外一种情况是没有任何光线照射到物体(例如黑夜我们看一张白纸)。
人眼看到物体为白色,是因为在白光源照射下,物体不吸收光线而将光线全部反射(例如白天我们看一张白纸)。
颜色与光源和物体的吸色特性密切相关,基于此,引出混色方法中的加色法和减色法。
加色法利用光源发射特性,将各分色的光谱成分相加得到混合颜色。RGB 色彩空间采用加色法。当无任何光线照射时,R、G、B 三种颜色分量都为 0 时,物体呈现黑色;当 R、G、B 三种颜色分量达到最大时,物体不吸收光线只反射的情况下,物体呈现白色。我们称黑色为最暗,白色为最亮,要达到最亮状态,需要三色分量最大程度混合,因此称为加色。
[11]
加色法用于自发光物体。RGB 颜色空间主要应用于计算机显示器、电视机、舞台灯光等,都具有发光特性。彩色像素在显示器屏幕上不会重叠,但足够的距离时,光线从像素扩散到视网膜上会重叠,人眼会感觉到重叠后的颜色效果。
减色法是利用颜料吸色特性,每加一种颜色的颜料,会吸收掉对应的补色成分。CMYK 色彩空间采用减色法。例如,我们在白纸(白光照射、不吸收、全反射)上涂颜料,黄色颜料能吸收蓝色(黄色的补色),因此在白光照射下显示黄色,当黄(Y)、青(C)、品红(M)三色混在一起且颜色分量都为最大时,它们的补色成分被吸收掉,变成了黑色;当三色分量为 0 即什么也不涂时,白纸显现白色。要达到最大亮度,需要三色分量完全消失,因此称为减色。
印刷时,无法达到理想程度,C、M、Y 最大程度混合后无法得到纯黑色,只能得到深灰色,因此在 C、M、Y 三色之外引入了 K(黑色)。
[12]
减色法用于无法发光的物体。CMYK 颜色空间主要应用于印刷、绘画、布料染色等。
RGB 色彩空间中每个像素点包含了 R、G、B 三种分量。RGB 存储模式也有 packed 和 planar 两类,这两种模式的区分在第 3 节中讲述。RGB 色彩空间及存储模式比较简单,我们挑几个有代表性的存储模式来简述一下:
2.1 存储模式 RGB565
使用 16b(bit)表示一个像素,5b 用于 R,6b 用于 G,5b 用于 B。如下:
[ R G B ] [ R G B ] [ R G B ] [ R G B ] [ R G B ] [ R G B ] [ R G B ] [ R G B ]
以上只是示意图,实际 R、G、B 顺序可能与图中不同。
2.2 存储模式 RGB888
使用 24 位来表示一个像素,每个分量都用 8 位表示。其示意图跟 RGB565 示意图类似。
2.3 存储模式 ARGB8888
使用 32 位来表示一个像素,R、G、B 都用 8 位表示,另外 A(Alpha)表示透明度,也用 8 位表示。
[ A R G B ] [ A R G B ] [ A R G B ] [ A R G B ] [ A R G B ] [ A R G B ] [ A R G B ] [ A R G B ]
以上只是示意图,实际 A、R、G、B 顺序可能与图中不同。
3. YUV 色彩空间
YUV 颜色空间是 PAL、NTSC、SCEAM 三大视频标准使用的颜色空间,主要应用于视频系统。YUV 色彩空间中,Y 表示亮度信息,U 和 V 表示色度(色调和饱和度)信息。使用 YUV 色彩空间,后期出现的彩色电视系统和早期的黑白电视系统兼容,黑白电视机可以只处理彩色电信信号中的 Y 分量,而彩色电视机接收黑白电视信号显示也没有任何问题。
YUV 颜色空间和 RGB 颜色空间可以根据公式相互转换。
经常提到的还有 YPbPr 和 YCbCr。YPbPr 指模拟分量信号(或接口),P(Parallel)表示并行,b 下标表示蓝,r 下标表示红。YCbCr 指的是数字分量信号(或接口),C(Chroma)表示色度。YCbCr 还可指色彩空间,YCbCr 色彩空间是 YUV 色彩空间的缩放和偏移版本。
YUV, YCbCr, YPbPr 所指涉的范围,常有混淆或重叠的情况。从历史的演变来说,其中 YUV 和 Y'UV 通常用来编码电视的模拟信号,而 YCbCr 则是用来描述数字的视频信号,适合影片与图片压缩以及传输,例如 MPEG、JPEG。 但在现今,YUV 通常已经在计算机系统上广泛使用。[14]
3.1 YUV 采样方式与存储模式
人眼有一个特性:对亮度更敏感,对色度没有那么敏感。因此压缩色度分量可以降低数据量,但并不会人眼主观感觉造成太大影响。这是 YUV 采样的理论基础。主流的采样方式有三种,YUV4:4:4,YUV4:2:2,YUV4:2:0。这些采样方式,不压缩 Y 分量,对 UV 分量的压缩程度不同。
同一种采样方式,数据在内存中可以有不同的排布方式。YUV 数据在内存中的排布方式称作 YUV 存储模式。YUV 存储模式分成三大类:
packed:将 Y、U、V 分量交织存放在一起,和 RGB 的存放方式类似。内存中排列形式类似:YVYUYVYUYVYUYVYU...。在具体的存储模式命名中,packed 格式不带后缀 P。
planar:将 Y、U、V 的三个分量分别存放在不同的矩阵(平面)中。内存中排列形式类似:YYYYYY...,UUUUUU...,VVVVVV...。在具体的存储模式命名中,planar 格式带后缀 P。
semi-planar:将 Y、U、V 三个分量放在两个矩阵(平面)中。Y 占用一个平面,UV 共用一个平面。内存中排列形式类似:YYYYYY...,UVUVUV...。在具体的存储模式命名中,semi-planar 格式带后缀 SP。
注意:packed 格式通常用于只含水平采样的采样方式。packed 格式不能处理垂直采样,否则会导致不同的行尺寸不一。
YUV 图像存储模式与采样方式密切相关。一种采样模式下有多种不同的存储模式。
存储模式是一种较粗粒度的划分方式,主要依据不同分量在不同 plane 中的排列顺序来划分存储模式,一种存储模式定义了各分量样本在内存中的布局方式,而更一步的细节并不明确。像素格式则是细粒度的划分方式,一种像素格式中图像数据的内存组织方式的所有细节都是确定的,像素格式在第 4 节讲解。
3.2 采样方式的命名含义
本节内容整理自参考资料[2]。
为方便描述,我们将一个 Y 值称作一个亮度样本。一对 Cb+Cr 值称作一个色度样本。因为采样方式中只对色度进行下采样,不对亮度进行下采样,所以在一幅图像中,亮度样本数等于图像像素总数。
3.2.1 J????️b 命名法
采样方式采用的是 J????️b 命名形式。这个比例指的是在水平方向上的相对采样率,注意此形式的命名中并没有体现垂直方向的采样率。J????️b 表示,一幅图像中总像素数目(奇数行+偶数行的总像素数,也等于总亮度样本数):奇数行中的色度样本数:偶数行中的色度样本数。按照惯例,J 通常都是 4。
以上图来理解,图中 a 部分,一个细线方框表示一个图像像素,一个粗线方框表示一个色度样本(即一对 Cb、Cr)。可以看到 YUV4:4:4 中,一个图像像素对应一个色度样本,而 YUV4:4:0 和 YUV4:2:2 则是两个图像像素对应一个色度样本,YUV4:2:0 和 YUV4:1:1 是四个图像像素对应一个色度样本,YUV4:1:0 是 8 个图像像素对应一个色度样本。这幅图看起来非常直观,一眼能看出一幅图像中大概多少个像素采样出一个色度样本。
图中 b 部分则详解了各种采样方式的命名含义。J????️b 命名形式中,引入了图像参考块的概念,参考块是 J 个像素宽(J 实际是 4)2 个像素高的一个图像块,那么 J????️b 的含义就是,J 个像素宽的参考块中,第一行中有 a 个色度样本,第二行中有 b 个色度样本。此处由参考块解释的 J????️b 的含义,与前文提到的 J????️b 表示图像总像素数目:奇数行中的色度样本数:偶数行中的色度样本数,实际含义是一致的。
3.2.2 h/v 命名法
其实,采样方式可以由两个简单含义的数字描述:水平和垂直采样因子。水平采样因子表示在水平方向每多少个像素采样出一个色度样本,垂直采样因子表示在垂直方向每多少个像素采样出一个色度样本。这种方式可以称作 h/v 命名形式。
J????️b 的命名形式很容易让人迷惑,实际上很多文章中关于采样方式的介绍要么是错的,要么含混不清,让人无法明白 J????️b 中三个数字的含义。而 h/v 的命名形式实际更容易理解。但遗憾的是,现实中广泛使用的是 J????️b 命名法,而未采用 h/v 命名法。
J????️b 命名中,并没有体现出垂直方向采样率相关的信息。但是我们可以列出每一种采样方式的 J????️b 命名形式以及 h/v 命名形式,这样可以很方便地得到此采样方式的水平采样率和垂直采样率,这更便于理解。如下表:
J????️b | h/v | 说明 |
4:4:4 | 1/1 | 水平方向,每 1 个像素采样出 1 个色度样本;垂直方向,每 1 个像素采样出 1 个色度样本 |
4:4:0 | 1/2 | 水平方向,每 1 个像素采样出 1 个色度样本;垂直方向,每 2 个像素采样出 1 个色度样本 |
4:2:2 | 2/1 | 水平方向,每 2 个像素采样出 1 个色度样本;垂直方向,每 1 个像素采样出 1 个色度样本 |
4:2:0 | 2/2 | 水平方向,每 2 个像素采样出 1 个色度样本;垂直方向,每 2 个像素采样出 1 个色度样本 |
4:1:1 | 4/1 | 水平方向,每 4 个像素采样出 1 个色度样本;垂直方向,每 1 个像素采样出 1 个色度样本 |
4:1:0 | 4/2 | 水平方向,每 4 个像素采样出 1 个色度样本;垂直方向,每 2 个像素采样出 1 个色度样本 |
3.3 采样方式 YUV4:4:4
参考图4和表1可知,YUV4:4:4 采样方式中,水平方向,每 1 个像素采样出 1 个色度样本;垂直方向,每 1 个像素采样出 1 个色度样本。以4x4 像素点阵为例,每一个像素都有独立的UV分量可供使用。如下(每个[]为一个像素点):
[ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ] [ Y U V ]
在这种采样方式下,一个像素点包含的完整的亮度信息和色度信息。
3.4 采样方式 YUV4:2:2
参考图4和表1可知,YUV4:2:2 采样方式中,水平方向,每 2 个像素采样出 1 个色度样本;垂直方向,每 1 个像素采样出 1 个色度样本。
以下图4x4 像素点阵为例进行说明。每 2 个像素共用 1 组 UV 分量。
[ Y U ] [ Y V ] [ Y U ] [ Y V ] [ Y V ] [ Y U ] [ Y V ] [ Y U ] [ Y U ] [ Y V ] [ Y U ] [ Y V ] [ Y V ] [ Y U ] [ Y V ] [ Y U ]
若位深是 8,平均算来,一个像素占用的数据宽度为 16b,其中 Y 占 8b,U 占 4b,V 占 4b。后面存储模式命名中的数字 16 指的就是 16b。
在这种采样方式下,一个像素点里含的数据是不完整的,还原出一个像素点,需要相邻的两个像素点数据,如下:
[ Y U ] [ Y V ]
在同一采样模式下,根据分量元素排列顺序的不同,又分为不同的存储模式:
3.4.1 packed 存储模式 YUYV 和 UYVY
YUYV(YUY2) 模式:
[ Y U ] [ Y V ] [ Y U ] [ Y V ] [ Y V ] [ Y U ] [ Y V ] [ Y U ] [ Y U ] [ Y V ] [ Y U ] [ Y V ] [ Y V ] [ Y U ] [ Y V ] [ Y U ]
UYVY 存储模式:
[ U Y ] [ V Y ] [ U Y ] [ V Y ] [ U Y ] [ V Y ] [ U Y ] [ V Y ] [ U Y ] [ V Y ] [ U Y ] [ V Y ] [ U Y ] [ V Y ] [ U Y ] [ V Y ]
3.4.2 planar 存储模式 YUV422P
YU16:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y ------- U U U U U U U U ------- V V V V V V V V
YV16:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y ------- V V V V V V V V ------- U U U U U U U U
3.4.3 semi-planar 存储模式 YUV422SP
NV16:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y ------- U V U V U V U V U V U V U V U V
NV61:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y ------- V U V U V U V U V U V U V U V U
3.5 采样方式 YUV4:2:0
参考图4和表1可知,YUV4:2:0 采样方式中,水平方向,每 2 个像素采样出 1 个色度样本;垂直方向,每 2 个像素采样出 1 个色度样本。
每 4 个像素共用 1 组 UV 分量。若位深是 8,平均算来,一个像素占用的数据宽度为 12b,其中 Y 占 8b,U 占 2b,V 占 2b。后面存储模式命名中的数字 12 指的就是 12b。
[ Y U ] [ Y ] [ Y U ] [ Y ] [ Y V ] [ Y ] [ Y V ] [ Y ] [ Y U ] [ Y ] [ Y U ] [ Y ] [ Y V ] [ Y ] [ Y V ] [ Y ]
在这种采样方式下,还原出一个像素点,需要相邻的四个像素点数据,如下:
[ Y U ] [ Y ] [ Y V ] [ Y ]
在同一采样模式下,根据分量元素排列顺序的不同,又分为不同的存储模式:
3.5.1 planar 存储模式 YUV420P
YU12/IYUV/I420:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y ------- U U U U --- V V V V
YV12:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y ------- V V V V --- U U U U
3.5.2 semi-planar 存储模式 YUV420SP
NV12:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y ------- U V U V U V U V
NV21:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y ------- V U V U V U V U
在 FFmpeg 中,NV12 及 NV21 都是 8 bit 位深,NV12 的 10 bit 版本叫 P010。这几种名称存在于像素格式定义中。
4. FFmpeg 中的像素格式
FFmpeg 中的像素格式是 pixel format,每种像素格式包含有色彩空间、采样方式、存储模式、位深等信息。
4.1 基础概念
与像素格式相关的几个基础概念如下:
在解释具体的函数前,必须理解与像素格式相关的几个基础概念:参“色彩空间与像素格式”一文第 4.1 节
pixel_format:像素格式,图像像素在内存中的排列格式。一种像素格式包含有色彩空间、采样方式、存储模式、位深等信息,其中体现的最重要信息就是存储模式,具体某一类的存储模式参照本文第 2 节、第 3 节。
bit_depth: 位深,指每个分量(Y、U、V、R、G、B 等)单个采样点所占的位宽度。
例如对于 yuv420p(位深是8)格式而言,每一个 Y 样本、U 样本和 V 样本都是 8 位的宽度,只不过在水平方向和垂直方向,U 样本数目和 V 样本数目都只有 Y 样本数目的一半。而 bpp (Bits Per Pixel)则是将图像总比特数分摊到每个像素上,计算出平均每个像素占多少个 bit,例如 yuv420p 的 bpp 是 12,表示平均每个像素占 12 bit(Y占8位、U占2位、V占2位),实际每个 U 样本和 V 样本都是 8 位宽度而不是 2 位宽度。
plane: 存储图像中一个或多个分量的一片内存区域。一个 plane 包含一个或多个分量。planar 存储模式中,至少有一个分量占用单独的一个 plane,具体到 yuv420p 格式有 Y、U、V 三个 plane,nv12 格式有 Y、UV 两个 plane,gbrap 格式有 G、B、R、A 四个 plane。packed 存储模式中,因为所有分量的像素是交织存放的,所以 packed 存储模式只有一个 plane。
slice: slice 是 FFmpeg 中使用的一个内部结构,在 codec、filter 中常有涉及,通常指图像中一片连续的行,表示将一帧图像分成多个片段。注意 slice 是针对图像分片,而不是针对 plane 分片,一帧图像有多个 plane,一个 slice 里同样包含多个 plane。
stride/pitch: 一行图像中某个分量(如亮度分量或色度分量)所占的字节数, 也就是一个 plane 中一行数据的宽度。有对齐要求,计算公式如下:
stride 值 = 图像宽度 * 分量数 * 单样本位宽度 / 水平子采样因子 / 8
其中,图像宽度表示图像宽度是多少个像素,分量数指当前 plane 包含多少个分量(如 rgb24 格式一个 plane 有 R、G、B 三个分量),单位本位宽度指某分量的一个样本在考虑对齐后在内存中占用的实际位数(例如位深 8 占 8 位宽,位深 10 实际占 16 位宽,对齐值与平台相关),水平子采样因子指在水平方向上每多少个像素采样出一个色度样本(亮度样本不进行下采样,所以采样因子总是 1)。
需要注意的是,stride 考虑的是 plane 中的一行。对 yuv420p 格式而言,Y 分量是完全采样,因此一行 Y 样本数等于图像宽度,U 分量和 V 分量水平采样因子是 2(每两个像素采样出一个U样本和V样本),因此一行 U 样本数和一行 V 样本数都等于图像宽度的一半。U 分量和 V 分量垂直采样因子也是 2,因此 U 分量和 V 分量的行数少了,只有图像高度的一半,但垂直方向的采样率并不影响一个 plane 的 stride 值,因为 stride 的定义决定了其值只取决于水平方向的采样率。
若源图像像素格式是 yuv420p(有 Y、U、V 三个 plane),位深是 8(每一个Y样本、U样本、V样本所占位宽度是 8 位),分辨率是 1280x720,则在 Y plane 的一行数据中,有 1280 个 Y 样本,占用 1280 个字节,stride 值是 1280;在 U plane 的一行数据中,有 640 个 U 样本,占用 640 个字节,stride 值是 640;在 V plane 的一行数据中,有 640 个样本,占用 640 个字节,stride 值是 640。
若源图像像素格式是 yuv420p10(有 Y、U、V 三个 plane),位深是 10 (内存对齐后每个样本占 16 位),分辨率仍然是 1280x720,则 Y plane 的 stride 值为 1280 x 16 / 8 = 2560,U plane stride 值为 640 x 16 / 8 = 1280,V plane stride 值为 640 x 16 / 8 = 1280。
若源图像像素格式是 yuv420p16le(有 Y、U、V 三个 plane),位深是 16,分辨率仍然是 1280x720,则 Y plane 的 stride 值为 1280 x 16 / 8 = 2560,U plane stride 值为 640 x 16 / 8 = 1280,V plane stride 值为 640 x 10 / 8 = 1280。
若源图像像素格式是 p010le(有 Y、UV 两个 plane),位深是 10 (内存对齐后,每个样本占 16 位),分辨率仍然是 1280x720,则 Y plane 的 stride 值为 1280 x 16 / 8 = 2560,UV plane stride 值为 640 x 2 x 16 / 8 = 2560。
若源图像像素格式是 bgr24(有 BGR 一个 plane),位深是 8,分辨率仍然是 1280x720。因 bgr24 像素格式是 packed 存储模式,每个像素 R、G、B 三个采样点交织存放,内存区的排列形式为 BGRBGR...,因此可以认为它只有一个 plane,此 plane 中一行图像有 1280 个 R 样本,1280 个 G 样本,1280 个 B 样本,此 plane 的 stride 值为 1280 x 3 x 8 / 8 = 3840。
4.2 数据结构
4.2.1 AVPixelFormat
AVPixelFormat 定义了像素格式 ID,AVPixelFormat 由FFmpeg 内部代码使用,用来标识某一像素格式。例如,在 FFmpeg 转码命令行中指定了 yuv420p 像素格式,则对应此结构体中的 AV_PIX_FMT_YUV420P。几个有代表性的像素格式列举如下,其他格式省略:
enum AVPixelFormat { AV_PIX_FMT_NONE = -1, AV_PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples) AV_PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB... AV_PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR... AV_PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples) AV_PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples) ...... AV_PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V) AV_PIX_FMT_NV21, ///< as above, but U and V bytes are swapped AV_PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB... AV_PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA... AV_PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR... AV_PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA... ...... AV_PIX_FMT_YUV420P10BE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian AV_PIX_FMT_YUV420P10LE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian ...... AV_PIX_FMT_NV16, ///< interleaved chroma YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples) AV_PIX_FMT_NV20LE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian AV_PIX_FMT_NV20BE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian ...... /** * HW acceleration through QSV, data[3] contains a pointer to the * mfxFrameSurface1 structure. */ AV_PIX_FMT_QSV, ...... AV_PIX_FMT_P010LE, ///< like NV12, with 10bpp per component, data in the high bits, zeros in the low bits, little-endian AV_PIX_FMT_P010BE, ///< like NV12, with 10bpp per component, data in the high bits, zeros in the low bits, big-endian ...... AV_PIX_FMT_NB ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions };
4.2.2 AVPixFmtDescriptor
AVPixFmtDescriptor 定义了图像数据在内存中的组织排列形式,此数据结构定义了像素格式在 FFmpeg 中的实现细节。
/** * Descriptor that unambiguously describes how the bits of a pixel are * stored in the up to 4 data planes of an image. It also stores the * subsampling factors and number of components. * * @note This is separate of the colorspace (RGB, YCbCr, YPbPr, JPEG-style YUV * and all the YUV variants) AVPixFmtDescriptor just stores how values * are stored not what these values represent. */ typedef struct AVPixFmtDescriptor { const char *name; uint8_t nb_components; ///< The number of components each pixel has, (1-4) /** * Amount to shift the luma width right to find the chroma width. * For YV12 this is 1 for example. * chroma_width = AV_CEIL_RSHIFT(luma_width, log2_chroma_w) * The note above is needed to ensure rounding up. * This value only refers to the chroma components. */ uint8_t log2_chroma_w; /** * Amount to shift the luma height right to find the chroma height. * For YV12 this is 1 for example. * chroma_height= AV_CEIL_RSHIFT(luma_height, log2_chroma_h) * The note above is needed to ensure rounding up. * This value only refers to the chroma components. */ uint8_t log2_chroma_h; /** * Combination of AV_PIX_FMT_FLAG_... flags. */ uint64_t flags; /** * Parameters that describe how pixels are packed. * If the format has 1 or 2 components, then luma is 0. * If the format has 3 or 4 components: * if the RGB flag is set then 0 is red, 1 is green and 2 is blue; * otherwise 0 is luma, 1 is chroma-U and 2 is chroma-V. * * If present, the Alpha channel is always the last component. */ AVComponentDescriptor comp[4]; /** * Alternative comma-separated names. */ const char *alias; } AVPixFmtDescriptor;
const char *name
像素格式名称。例如 AV_PIX_FMT_YUV420P 的名称为 yuv420p,AV_PIX_FMT_YUV420P 在 FFmpeg 代码中使用,而像素格式名称 yuv420p 则在 FFmpeg 命令行中使用。
uint8_t nb_components
图像分量数,取值范围 1 - 4。例如 AV_PIX_FMT_GRAY8 只有 Y 一个分量,AV_PIX_FMT_YUV420P 有 Y、U、V 三个分量,AV_PIX_FMT_NV12 也有 Y、U、V 三个分量,AV_PIX_FMT_ARGB 有 A、R、G、B 四个分量。
uint8_t log2_chroma_w
移位的位数,表示将亮度样本宽度右移多少位能得到色度样本的宽度,此值等于水平方向色度子采样因子。例如对于 yuv420p 格式,若图像分辨率为 1280 x 720,则亮度样本宽度(水平方向亮度样本数)为 1280,色度样本宽度(水平方向色度样本数)为 1280/2 = 640,log2_chroma_w 值为 1(右移 1 位)。
uint8_t log2_chroma_h
移位的位数,表示将亮度样本高度右移多少位能得到色度样本的高度,此值等于垂直方向色度子采样因子。例如对于 yuv420p 格式,若图像分辨率为 1280 x 720,则亮度样本高度(垂直方向亮度样本数)为 720,色度样本高度(垂直方向色度样本数)为 720/2 = 360,log2_chroma_w 值为 1(右移 1 位)。
uint64_t flags
像素格式标志位组合,形如 AV_PIX_FMT_FLAG_BE | AV_PIX_FMT_FLAG_HWACCEL 。例如,标志 AV_PIX_FMT_FLAG_BE 表示大端格式,AV_PIX_FMT_FLAG_HWACCEL 表示此像素格式用于硬解或硬编等硬件加速场合。
AVComponentDescriptor comp[4]
这个成员非常重要。数组的每个元素表示一个分量,注意是一个分量而不是一个 plane,一个 plane 可能含有多个分量。AVComponentDescriptor 数据结构定义了每个分量的像素数据在内存中的格式,详情参 4.2.4 节。
const char *alias
以逗号分隔的别名列表。在 av_pix_fmt_descriptors[] 数组的定义中可以看到,AV_PIX_FMT_GRAY8 像素格式的名称是 "gray8",alias 值为 "gray8,y8"。
4.2.4 AVComponentDescriptor
AVComponentDescriptor 定义了每个分量在内存中的实际组织形式,包含所有细节。
typedef struct AVComponentDescriptor { /** * Which of the 4 planes contains the component. */ int plane; /** * Number of elements between 2 horizontally consecutive pixels. * Elements are bits for bitstream formats, bytes otherwise. */ int step; /** * Number of elements before the component of the first pixel. * Elements are bits for bitstream formats, bytes otherwise. */ int offset; /** * Number of least significant bits that must be shifted away * to get the value. */ int shift; /** * Number of bits in the component. */ int depth; #if FF_API_PLUS1_MINUS1 /** deprecated, use step instead */ attribute_deprecated int step_minus1; /** deprecated, use depth instead */ attribute_deprecated int depth_minus1; /** deprecated, use offset instead */ attribute_deprecated int offset_plus1; #endif } AVComponentDescriptor;
int plane
当前分量位于哪个 plane 中。
例如 p010 格式有三个分量:Y、U、V,两个 plane:Y、UV。Y plane 的形式为YYYY...
,UV plane 的形式为UVUVUV...
。Y 分量的 plane 值是 0, U 分量和 V 分量的 plane 值是 1,U 样本和 V 样本交织存放在 plane 1中。
int step
步长,表示水平方向连续的两个样本间距是多少个字节(或比特),如果像素格式是比特流格式(标志 AV_PIX_FMT_FLAG_BITSTREAM 有效),此值表示比特数,否则此值表示字节数。
以 p010 格式为例,Y plane 的形式为YYYY...
,UV plane 的形式为UVUVUV...
,位深是 10,考虑考虑对齐后,每一个 Y、每一个 U、每一个 V 都占 2 个字节,因此 Y 分量的 step 是 2(两个 Y 相距两字节),U 分量的 step 是 4(两个 U 相距 4 字节),V 分量的 step 也是 4(两个 V 相距 4 字节)。
int offset
偏移,表示在当前 plane 中,当前分量的第一个样本之前有多少个字节的数据,如果像素格式是比特流格式(标志 AV_PIX_FMT_FLAG_BITSTREAM 有效),此值表示比特数,否则此值表示字节数。
以 p010 格式为例,每一个 U 或 V 都占 2 个字节,第一个 V 样本前有 2 个字节被 U 样本占了,所以 U 分量的 offset 值是 0,V 分量的 offset 值是 2。
int shift
右移位数,表示将对应内存单元的值右移多少位可以得到实际值。
以 p010 格式为例,位深是 10,而内存对齐后每一个 Y、U、V 样本占 16 bit,那么 10 位的数据放在 16 位的内存单元中, 是占据高 10 位还是占据低 10 位,即是由 shift 值决定的。p010 格式中,各分量的 shift 值都是 6 ,表示数据放在高 10 位。从 Y plane 中获取第一个 Y 样本的值,示意代码如下:
1 2 3 uint8_t y_plane[1280*2]; uint16_t *p_y0 = (uint16_t *)y_plane; uint16_t y0 = (*p_y0) >> 6;
yuv420p10le 中分量的 shift 值都是 0 ,表示数据放在低 10 位。 从 Y plane 中获取第一个 Y 样本的值,示意代码如下:
uint8_t y_plane[1280*2]; uint16_t *p_y0 = (uint16_t *)y_plane; uint16_t y0 = (*p_y0) >> 0;
int depth
当前分量每个样本的位宽度,即位深。
上述参数中,plane 表示分量所在的 plane 的序号,offset 表示多个分量交织存放在同一个 plane 中时的排列顺序(如 p010 格式的 UV plane 中 U 在前 V 在后),step、shift 和 depth 则是和内存对齐相关,例如 p010 格式 depth 是 10(bit), step 是 2 字节(16 bit),shift 是 6(bit),表示每个 10 bit 的样本占用 16 bit的内存单元,低 6 位是无用位(高位对齐,靠左对齐)。
4.3 节将详细解释几个常用像素格式各具体参数的含义。
4.2.3 av_pix_fmt_descriptors[]
av_pix_fmt_descriptors 是 FFmpeg 中定义各个像素格式的实际格式的。这个数组非常重要,当不知道具体的某个像素格式的实现细节时,查看此数组中的定义即可明白。常用的以及有代表性的几个像素格式将在下一节具体分析。
static const AVPixFmtDescriptor av_pix_fmt_descriptors[AV_PIX_FMT_NB] = { [AV_PIX_FMT_YUV420P] = { .name = "yuv420p", .nb_components = 3, .log2_chroma_w = 1, .log2_chroma_h = 1, .comp = { { 0, 1, 0, 0, 8, 0, 7, 1 }, /* Y */ { 1, 1, 0, 0, 8, 0, 7, 1 }, /* U */ { 2, 1, 0, 0, 8, 0, 7, 1 }, /* V */ }, .flags = AV_PIX_FMT_FLAG_PLANAR, }, ...... }
4.3 典型像素格式实例分析
FFmpeg 中所有的像素格式都定义在 av_pix_fmt_descriptors[] 数组中。下面只列出最常用到的几个像素格式进行分析:
static const AVPixFmtDescriptor av_pix_fmt_descriptors[AV_PIX_FMT_NB] = { [AV_PIX_FMT_YUV420P] = { .name = "yuv420p", .nb_components = 3, .log2_chroma_w = 1, .log2_chroma_h = 1, .comp = { { 0, 1, 0, 0, 8, 0, 7, 1 }, /* Y */ { 1, 1, 0, 0, 8, 0, 7, 1 }, /* U */ { 2, 1, 0, 0, 8, 0, 7, 1 }, /* V */ }, .flags = AV_PIX_FMT_FLAG_PLANAR, }, ...... }
4.3.1 像素格式 yuv420p
static const AVPixFmtDescriptor av_pix_fmt_descriptors[AV_PIX_FMT_NB] = { [AV_PIX_FMT_YUV420P] = { .name = "yuv420p", .nb_components = 3, // 一共 3 个分量:Y、U、V .log2_chroma_w = 1, // 水平采样因子是 2,pow(2, 1) .log2_chroma_h = 1, // 垂直采样因子是 2, pow(2, 1) .comp = { { 0, 1, 0, 0, 8, 0, 7, 1 }, /* Y */ { 1, 1, 0, 0, 8, 0, 7, 1 }, /* U */ { 2, 1, 0, 0, 8, 0, 7, 1 }, /* V */ }, .flags = AV_PIX_FMT_FLAG_PLANAR, }, ...... }
4.3.2 像素格式 yuv422p
static const AVPixFmtDescriptor av_pix_fmt_descriptors[AV_PIX_FMT_NB] = { ...... [AV_PIX_FMT_YUV422P] = { .name = "yuv422p", .nb_components = 3, // 一共三个分量:Y、U、V .log2_chroma_w = 1, // 水平采样因子是 2,pow(2, 1) .log2_chroma_h = 0, // 垂直采样因子是 1,pow(2, 0) .comp = { { 0, 1, 0, 0, 8, 0, 7, 1 }, /* Y */ { 1, 1, 0, 0, 8, 0, 7, 1 }, /* U */ { 2, 1, 0, 0, 8, 0, 7, 1 }, /* V */ }, .flags = AV_PIX_FMT_FLAG_PLANAR, }, ...... }
4.3.3 像素格式 yuv444p
static const AVPixFmtDescriptor av_pix_fmt_descriptors[AV_PIX_FMT_NB] = { ...... [AV_PIX_FMT_YUV444P] = { .name = "yuv444p", .nb_components = 3, // 一共三个分量:Y、U、V .log2_chroma_w = 0, // 水平采样因子是 1,pow(2, 0) .log2_chroma_h = 0, // 垂直采样因子是 1,pow(2, 0) .comp = { { 0, 1, 0, 0, 8, 0, 7, 1 }, /* Y */ { 1, 1, 0, 0, 8, 0, 7, 1 }, /* U */ { 2, 1, 0, 0, 8, 0, 7, 1 }, /* V */ }, .flags = AV_PIX_FMT_FLAG_PLANAR, }, ...... }
4.3.4 像素格式 nv12 和 nv21
nv12 格式有 Y、U、V 三个分量,Y 分量存放在 plane 0 中,U 和 V 交织存放在 plane 1 中。
static const AVPixFmtDescriptor av_pix_fmt_descriptors[AV_PIX_FMT_NB] = { ...... [AV_PIX_FMT_NV12] = { .name = "nv12", .nb_components = 3, // 一共三个分量:Y、U、V .log2_chroma_w = 1, // 水平采样因子是 2,pow(2, 1) .log2_chroma_h = 1, // 垂直采样因子是 2,pow(2, 1) .comp = { { // Y 分量 0, // plane: plane 0, YYYYYYYY... 1, // step: 两个 Y 间距 1 字节 0, 0, 8, // 8 位宽 0, 7, 1 }, { // U 分量 1, // plane: plane 1, UVUVUVUV... 2, // step: 两个 U 间距 2 字节 0, // offset: U 在前 V 在后 0, 8, // 8 位宽 1, 7, 1 }, { // V 分量 1, // plane: plane 1, UVUVUVUV... 2, // step: 两个 V 间距 2 字节 1, // offset: U 在前 V 在后,因 V 前有 1 个字节的 U 0, 8, // 8 位宽 1, 7, 2 }, }, .flags = AV_PIX_FMT_FLAG_PLANAR, }, ...... }
nv21 格式和 nv12 格式只有一点不同:plane 1 中 U 和 V 的顺序不同,对比下列二者的定义,只有 .comp.offset 成员值不同
[AV_PIX_FMT_NV12] = { .name = "nv12", .nb_components = 3, .log2_chroma_w = 1, .log2_chroma_h = 1, .comp = { { 0, 1, 0, 0, 8, 0, 7, 1 }, /* Y */ { 1, 2, 0, 0, 8, 1, 7, 1 }, /* U */ { 1, 2, 1, 0, 8, 1, 7, 2 }, /* V */ }, .flags = AV_PIX_FMT_FLAG_PLANAR, }, [AV_PIX_FMT_NV21] = { .name = "nv21", .nb_components = 3, .log2_chroma_w = 1, .log2_chroma_h = 1, .comp = { { 0, 1, 0, 0, 8, 0, 7, 1 }, /* Y */ { 1, 2, 1, 0, 8, 1, 7, 2 }, /* U */ { 1, 2, 0, 0, 8, 1, 7, 1 }, /* V */ }, .flags = AV_PIX_FMT_FLAG_PLANAR, },
4.3.5 像素格式 p010le
p010le 和 nv12 格式类似,只是 p010le 位深是 10。
static const AVPixFmtDescriptor av_pix_fmt_descriptors[AV_PIX_FMT_NB] = { ...... [AV_PIX_FMT_P010LE] = { .name = "p010le", .nb_components = 3, // 一共三个分量:Y、U、V .log2_chroma_w = 1, // 水平采样因子是 2,pow(2, 1) .log2_chroma_h = 1, // 垂直采样因子是 2,pow(2, 1) .comp = { { // Y 分量 0, // plane: plane 0, YYYYYYYY... 2, // step: 两个 Y 相距 2 字节 0, // offset: 0 6, // shift: 10 位数据按高位对齐,低 6 位是无效值 10, // depth: 10 位宽 1, 9, 1 }, { // U 分量 1, // plane: plane 1, UVUVUVUV... 4, // step: 两个 U 相距 4 字节 0, // offset: U 在前 V 在后 6, // shift: 10 位数据按高位对齐,低 6 位是无效值 10, // depth: 10 位宽 3, 9, 1 }, { // V 分量 1, // plane: plane 1, UVUVUVUV... 4, // step: 两个 V 相距 4 字节 2, // offset: U 在前 V 在后,因 V 前有 2 个字节被 U 占了 6, // shift: 10 位数据按高位对齐,低 6 位是无效值 10, // depth: 10 位宽 3, 9, 3 }, }, .flags = AV_PIX_FMT_FLAG_PLANAR, }, ...... }
4.3.6 像素格式 yuv420p10le
yuv420p10le 格式有 Y、U、V 三个分量,三个分量分别存放在三个 plane 中。
static const AVPixFmtDescriptor av_pix_fmt_descriptors[AV_PIX_FMT_NB] = { ...... [AV_PIX_FMT_YUV420P10LE] = { .name = "yuv420p10le", .nb_components = 3, // 一共三个分量:Y、U、V .log2_chroma_w = 1, .log2_chroma_h = 1, .comp = { { // Y 分量 0, // plane: plane 0, YYYYYYYY... 2, // step: 两个 Y 相距 2 字节 0, // offset: 0 0, // shift: 10 位数据按低位对齐 10, // depth: 10 位宽 1, 9, 1 }, { // U 分量 1, // plane: plane 1, UUUUUUUU... 2, // step: 两个 U 相距 2 字节 0, // offset: 0 0, // shift: 10 位数据按低位对齐 10, // depth: 10 位宽 1, 9, 1 }, { // V 分量 2, // plane: plane 2, VVVVVVVV... 2, // step: 两个 U 相距 2 字节 0, // offset: 0 0, // shift: 10 位数据按低位对齐 10, // depth: 10 位宽 1, 9, 1 }, }, .flags = AV_PIX_FMT_FLAG_PLANAR, }, ...... }
4.3.7 像素格式 argb
argb 格式有 A、R、G、B 四个分量,交织存放在 plane 0 中,所以 argb 是一种 packed 存储模式。
如是,注意R、G、B、A 各分量的 .comp.offset 值依次是 1、2、3、0,表示 A、R、G、B 在内存中的排列顺序是 ARGBARGBARGB...
static const AVPixFmtDescriptor av_pix_fmt_descriptors[AV_PIX_FMT_NB] = { ...... [AV_PIX_FMT_ARGB] = { .name = "argb", .nb_components = 4, .log2_chroma_w = 0, .log2_chroma_h = 0, .comp = { { 0, 4, 1, 0, 8, 3, 7, 2 }, /* R */ { 0, 4, 2, 0, 8, 3, 7, 3 }, /* G */ { 0, 4, 3, 0, 8, 3, 7, 4 }, /* B */ { 0, 4, 0, 0, 8, 3, 7, 1 }, /* A */ }, .flags = AV_PIX_FMT_FLAG_RGB | AV_PIX_FMT_FLAG_ALPHA, }, ...... }
5. 参考资料
[1] Keith Jack, "Video Demystified" 5th edition, "Chapter 3. Color Spaces"
[2] Douglas A. Kerr, Chrominance Subsampling in Digital Images
[3] 卢官明、宗昉, 《数字电视原理》第 3 版第 1 章
[4] 照明常用术语的定义http://twepc.com.tw/led/k2.htm
[5] 发光强度https://zh.wikipedia.org/wiki/发光强度
[6] 如何正确理解照度和亮度的关系?https://zhuanlan.zhihu.com/p/53136784
[7] https://www.junpin360.com/html/2015-04-12/4088.html
[8] 红绿蓝三色是(唯一的)正交基吗, https://www.zhihu.com/question/24886171
[9] 色彩空间基础,https://zhuanlan.zhihu.com/p/24214731
[10] 色彩空间表示与转换,https://zhuanlan.zhihu.com/p/24281841
[11] 加色法, https://zh.wikipedia.org/wiki/加色法
[12] 减色法, https://zh.wikipedia.org/wiki/減色法
[13] 数字图像处理之 6 大颜色空间,https://zhuanlan.zhihu.com/p/28741691
[14] YUV, https://zh.wikipedia.org/wiki/YUV
[15] 图文详解 YUV420 数据格式, https://www.cnblogs.com/azraelly/archive/2013/01/01/2841269.html
[16] YUV 格式小结, https://www.jianshu.com/p/6a361e86ccd5
[17] YUV#Packed_formats, https://wiki.videolan.org/YUV#Packed_formats
[18] YUV Formats, https://www.cs.auckland.ac.nz/courses/compsci773s1c/lectures/YuY2.htm
6. 修改记录
2019-01-11 V1.0 初稿
2019-02-13 V1.1 增加光的常用度量一节
2020-12-31 V1.2 修改采样方式中不清晰以及错误的描述
2021-01-16 V1.3 增加“FFmpeg 中的像素格式“一节
「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。