这段内容学习自:AVPixFmtDescriptor结构—FFmpeg数据结构详解,后面结合ffmpeg中的代码理解,做个记录。
AVPixFmtDescriptor
结构:
typedef struct AVPixFmtDescriptor {
const char *name;
uint8_t nb_components;
uint8_t log2_chroma_w;
uint8_t log2_chroma_h;
uint64_t flags;
AVComponentDescriptor comp[4];
const char *alias;
} AVPixFmtDescriptor;
const char *name
,这个很明显就是名称。
uint8_t nb_components;
这个字段代表 一个像素由多少个 部分组成,取值范围 1 - 4。像素通常是由 YUV 或者 RGB 组成的,后面可能会有个透明度,所以这个字段通常是 3 或者 4 。一个像素通常由 3 部分,或者 4 部分组成。有些像素格式只有 2 部分,例如 ya16le
uint8_t log2_chroma_w;
,在水平方向上,亮度(luma
)样本数右移多少位 能得到 色度(chroma
)样本数,右移之后的值要向上取整。
uint8_t log2_chroma_h;
,在垂直方向上,亮度(luma
)样本数右移多少位 能得到 色度(chroma
)样本数,右移之后的值要向上取整。
log2_chroma_w
跟 log2_chroma_h
其实不太容易理解,在 YUV420 里面,这两个值都是 1,也就是无论在垂直角度,还是水平角度,色度样本数 都是 亮度样本数的 2 分之一。用下面的图来比较容易理解。
Y Y Y Y Y Y Y Y
UV UV UV UV
Y Y Y Y Y Y Y Y
Y 代表亮度样本,UV代表色度样本,无论从垂直角度还是水平角度,都是2分之一,所以log2_chroma_h是1,右移一位是2。
在 pixdesc.c
里面可以看到 AVPixFmtDescriptor
的初始化,如下:
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 }, /* Y */
{ 1, 1, 0, 0, 8 }, /* U */
{ 2, 1, 0, 0, 8 }, /* V */
},
.flags = AV_PIX_FMT_FLAG_PLANAR,
},
AVComponentDescriptor comp[4];
,这个是 AVPixFmtDescriptor
里面非常重要的一个字段,定义如下:
typedef struct AVComponentDescriptor {
int plane;
int step;
int offset;
int shift;
int depth;
} AVComponentDescriptor;
以 AV_PIX_FMT_YUV420P
为例,如下:
.comp = {
{ 0, 1, 0, 0, 8 }, /* Y */
{ 1, 1, 0, 0, 8 }, /* U */
{ 2, 1, 0, 0, 8 }, /* V */
},
FFmpeg里面有两个英文术语,plane跟components,这两个术语是不一样的,components可以翻译为分量。plane
翻译成中文是平面的意思,数据存储分为packed
和planar
,像NV12是半平面,Y和UV数据分开存储,但是UV是放在一起交错在一起的,所以plane是只有两个,但是存在YUV三个分量,所以components
是3。
step
代表步长,表示水平方向连续的两个的 Y 间隔多少,提醒,我这里的 Y 是泛指分量。以 AV_PIX_FMT_YUV420P 为例,step 的值都是 1,也就是相隔一个字节,因为Y占8位,而且是plane格式。
libavutil/imgutils.c
for (plane = 0; plane < nb_planes; plane++) {
size_t bytewidth = plane_line_bytes[plane];
uint8_t *data = dst_data[plane];
int chroma_div = plane == 1 || plane == 2 ? desc->log2_chroma_h : 0;
int plane_h = ((height + ( 1 << chroma_div) - 1)) >> chroma_div;
for (; plane_h > 0; plane_h--) {
memset_bytes(data, bytewidth, &clear_block[plane][0], clear_block_size[plane]);
data += dst_linesize[plane];
}
}
chroma_div
:计算UV分量的log2_chroma_h
,log2_chroma_h
如前面所说表示在垂直方向上,亮度(luma
)样本数右移多少位能得到UV数据。
((height + (1 << chroma_div) - 1)) >> chroma_div
: (1 << chroma_div) - 1)
是向上取整数
int h, shift;
for (plane = 0; ; plane++) {
shift = (plane == 1 || plane == 2) ? desc->log2_chroma_h : 0;
if (!frame->buf[plane])
break;
h = (frame->height + (1 << shift) - 1) >> shift;
for (k = 0; k < h; k++) {
write(fd, frame->buf[plane]->data + frame->linesize[plane] * k, frame->linesize[plane]);
}
}
plane等于0的时候,shift为0,h等于frame->height
plane等于1和2的时候,shift为1,h等于(frame->height + 1<<1-1) >> 1
[AV_PIX_FMT_NV12] = {
.name = "nv12",
.nb_components = 3,
.log2_chroma_w = 1,
.log2_chroma_h = 1,
.comp = {
{ 0, 1, 0, 0, 8 }, /* Y */
{ 1, 2, 0, 0, 8 }, /* U */
{ 1, 2, 1, 0, 8 }, /* 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 }, /* Y */
{ 1, 2, 1, 0, 8 }, /* U */
{ 1, 2, 0, 0, 8 }, /* V */
},
.flags = AV_PIX_FMT_FLAG_PLANAR,
},
很明显,plane
只有0和1,因为UV是packed存储的,nb_components
是3,指的是有YUV三个分量。
在 C 语言中,可以使用位运算来实现向上取整。具体来说,假设我们要将一个整数 x
除以另一个整数 y
并向上取整,可以使用以下公式:
int ceil_div(int x, int y) {
return (x + y - 1) / y;
}
这个公式的原理和上面提到的一样,当 x
除以 y
时,如果 x
不能被 y
整除,那么向上取整后的结果应该是 x
除以 y
的商加上 1。因此,我们可以将 x
加上 y-1
,这样就可以保证 x
能够被 y
整除时,向上取整后的结果仍然正确。
假设收到比特流157位,利用位运算如何得出占据多少个字节,157+7>>3
得到20字节, 二进制过程为:
10011101 # 157
+ 111 # 7
--------------
10100000 # 164
>>3
--------------
00010100=20
因为:157 / 8 = 19.625
,如果有余数,向上取整,就对结果+1,19+1 = 20
。
这里位运算蕴含的思想是:除以8,在位运算中就是右移3位丢掉最后的3位。
C语言中的移位常数运算是指使用常数值进行移位运算。例如,对于表达式x << 3
,其中的3就是一个移位常数。移位常数运算的意义如下:
左移位常数运算:将一个数的二进制表示向左移动指定的位数,右边空出的位用0填充。移位常数通常用于对某个数进行乘法运算的优化,例如将表达式x * 8
替换为x << 3
,因为8的二进制表示为1000,左移3位后得到的结果也是x乘以8的结果。
右移位常数运算:将一个数的二进制表示向右移动指定的位数,左边空出的位用符号位填充。移位常数通常用于对某个数进行除法运算的优化,例如将表达式x / 4
替换为x >> 2
,因为4的二进制表示为100,右移2位后得到的结果也是x除以4的结果。
移位常数运算可以提高代码的执行效率,因为移位常数的值是固定的,编译器可以将其转换为位运算指令,从而避免了乘法或除法指令的执行。但是需要注意的是,移位常数的值必须是非负整数,否则会导致未定义的行为。