AVPixFmtDescriptor结构理解和应用

AVPixFmtDescriptor结构理解和应用

这段内容学习自: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_wlog2_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 翻译成中文是平面的意思,数据存储分为packedplanar,像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_hlog2_chroma_h如前面所说表示在垂直方向上,亮度(luma)样本数右移多少位能得到UV数据。

((height + (1 << chroma_div) - 1)) >> chroma_div: (1 << chroma_div) - 1)是向上取整数

从avframe中dump YUV数据

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

  • yuv420p,因为plane分别是0,1,2,UV分别在plane[1]或者plane[2],UV数据分开copy过去。
  • nv21 & nv12,plane分别是0,1,1,UV共用plane[1],所以UV数据同时copy过去。

NV12和NV21的定义

    [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语言中的移位常数运算


C语言中的移位常数运算是指使用常数值进行移位运算。例如,对于表达式x << 3,其中的3就是一个移位常数。移位常数运算的意义如下:

  1. 左移位常数运算:将一个数的二进制表示向左移动指定的位数,右边空出的位用0填充。移位常数通常用于对某个数进行乘法运算的优化,例如将表达式x * 8替换为x << 3,因为8的二进制表示为1000,左移3位后得到的结果也是x乘以8的结果。

  2. 右移位常数运算:将一个数的二进制表示向右移动指定的位数,左边空出的位用符号位填充。移位常数通常用于对某个数进行除法运算的优化,例如将表达式x / 4替换为x >> 2,因为4的二进制表示为100,右移2位后得到的结果也是x除以4的结果。

移位常数运算可以提高代码的执行效率,因为移位常数的值是固定的,编译器可以将其转换为位运算指令,从而避免了乘法或除法指令的执行。但是需要注意的是,移位常数的值必须是非负整数,否则会导致未定义的行为。

你可能感兴趣的:(音视频,ffmpeg)