FFmpeg像素格式转换

前面使用 SDL 显示了一张 YUV 图片以及 YUV 视频。接下来使用 Qt 中的 QImage 来实现一个简单的 YUV 播放器,查看 QImage 支持的像素格式,你会发现 QImage 仅支持显示 RGB 像素格式数据,并不支持直接显示 YUV 像素格式数据,但是 YUV 和 RGB 之间是可以相互转换的,我们将 YUV 像素格式数据转换成 RGB 像素格式数据就可以使用 QImage 显示了。

YUV 转 RGB 常见有三种方式:
1、使用 FFmpeg 提供的库 libswscale
优点:同一个函数实现了像素格式转换和分辨率缩放以及前后图像滤波处理;
缺点:速度慢。
2、使用 Google 提供的 libyuv:
优点:兼容性好功能全面;速度快,仅次于 OpenGL shader;
缺点:暂无。
3、使用 OpenGL shader:
优点:速度快,不增加包体积;
缺点:兼容性一般。

下面主要介绍如何使用 FFmpeg 提供的库 libswscale 进行转换,其他转换方式将会在后面介绍。

1、像素格式转换核心函数 sws_scale

sws_scale函数主要是用来做像素格式和分辨率的转换,每次转换一帧数据:

int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
              const int srcStride[], int srcSliceY, int srcSliceH,
              uint8_t *const dst[], const int dstStride[]);

参数说明:
c:转换上下文,可以通过函数 sws_getContext 创建;
srcSlice[]:输入缓冲区,元素指向一帧中每个平面的数据,以 yuv420p 为例,{指向每帧中 Y 平面数据的指针,指向每帧中 U 平面数据的指针,指向每帧中 V 平面数据的指针,null}
srcStride[]:每个平面一行的大小,以 yuv420p 为例,{每帧中 Y 平面一行的长度,每帧中 U 平面一行的长度,每帧中 U 平面一行的长度,0}
srcSliceY:输入图像上开始处理区域的起始位置。
srcSliceH:处理多少行。如果 srcSliceY = 0,srcSliceH = height,表示一次性处理完整个图像。这种设置是为了多线程并行,例如可以创建两个线程,第一个线程处理 [0, h/2-1] 行,第二个线程处理 [h/2, h-1] 行,并行处理加快速度。
dst[]:输出的图像数据,和输入参数 srcSlice[] 类似。
dstStride[]:和输入参数 srcStride[] 类似。

注意:sws_scale 函数不会为传入的输入数据和输出数据创建堆空间。

2、获取转换上下文函数
struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
                                  int dstW, int dstH, enum AVPixelFormat dstFormat,
                                  int flags, SwsFilter *srcFilter,
                                  SwsFilter *dstFilter, const double *param);

参数说明:
srcW, srcH, srcFormat:输入图像宽高和输入图像像素格式(我们这里输入图像像素格式是 yuv420p);
dstW, dstH, dstFormat:输出图像宽高和输出图像像素格式(我们这里输出图像像素格式是 rgb24),不仅可以转换像素格式,也可以分辨率缩放;
flag:指定使用何种算法,例如快速线性、差值和矩阵等等,不同的算法性能也不同,快速线性算法性能相对较高。只针对尺寸的变换。

/* values for the flags, the stuff on the command line is different */
#define SWS_FAST_BILINEAR     1
#define SWS_BILINEAR          2
#define SWS_BICUBIC           4
#define SWS_X                 8
#define SWS_POINT          0x10
#define SWS_AREA           0x20
#define SWS_BICUBLIN       0x40
#define SWS_GAUSS          0x80
#define SWS_SINC          0x100
#define SWS_LANCZOS       0x200
#define SWS_SPLINE        0x400

srcFilter, stFilter:这两个参数是做过滤器用的,目前暂时没有用到,传 nullptr 即可;
param:和 flag 算法相关,也可以传 nullptr;

返回值:成功返回转换格式上下文指针,失败返回 NULL;

注意:sws_getContext 函数注释中有提示我们最后使用完上下文不要忘记调用函数 sws_freeContext 释放,一般函数名中有 create 或者 alloc 等单词的函数需要我们释放,为什么调用 sws_getContext 后也需要释放呢?此时我们可以参考一下源码:
ffmpeg-4.3.2/libswscale/utils.c

libswscale 源码

发现源码当中调用了 sws_alloc_set_opts,所以最后是需要释放上下文的。当然我们也可以使用如下方式创建转换上下文,最后同样需要调用 sws_freeContext 释放上下文:

ctx = sws_alloc_context();
av_opt_set_int(ctx, "srcw", in.width, 0);
av_opt_set_int(ctx, "srch", in.height, 0);
av_opt_set_pixel_fmt(ctx, "src_format", in.format, 0);
av_opt_set_int(ctx, "dstw", out.width, 0);
av_opt_set_int(ctx, "dsth", out.height, 0);
av_opt_set_pixel_fmt(ctx, "dst_format", out.format, 0);
av_opt_set_int(ctx, "sws_flags", SWS_BILINEAR, 0);

if (sws_init_context(ctx, nullptr, nullptr) < 0) {
     // sws_freeContext(ctx);
     goto end;
}
3、创建输入输出缓冲区

首先我们创建需要的局部变量:

// 输入/输出缓冲区,元素指向每帧中每一个平面的数据
uint8_t *inData[4], *outData[4];
// 每个平面一行的大小
int inStrides[4], outStrides[4];
// 每一帧图像的大小
int inFrameSize, outFrameSize;

// 此处需要注意的是下面写法是错误的,*是跟着最右边的变量名的:
uint8_t *inData[4], outData[4];
// 其等价于:
uint8_t *inData[4];
uint8_t outData[4];

我们创建好了输入输出缓冲区变量,然后需要为输入输出缓冲区各开辟一块堆空间(sws_scale函数不会为我们开辟输入输出缓冲区堆空间,可查看源码),FFmpeg 为我们提供了现成的函数 av_image_alloc

ret = av_image_alloc(inData, inStrides, in.width, in.height, in.format, 1);
ret = av_image_alloc(outData, outStrides, out.width, out.height, out.format, 1);

// 最后不要忘记释放输入输出缓冲区
av_freep(&inData[0]);
av_freep(&outData[0]);

建议 inData 数组和 inStrides 数组的大小是 4,虽然我们目前的输入像素格式 yuv420p 有 Y 、U 和 V 共 3 个平面,但是有可能会有 4 个平面的情况,比如可能会多 1 个透明度平面。有多少个平面取决于像素格式。

yuv420p 像素格式数据举例:

// 每一帧的 Y 平面数据、U 平面数据和 V 平面数据是紧挨在一起的
// inData[0] -> Y 平面数据
// inData[1] -> U 平面数据
// inData[2] -> V 平面数据
inData[0] = (uint8_t *)malloc(inFrameSize);
inData[1] = inData[0] + 每帧中 Y 平面数据长度;
inData[2] = inData[0] + 每帧中 Y 平面数据长度 + 每帧中 U 平面数据长度;

关于 inStrides 的理解,inStrides 中存放的是每个平面每一行的大小,以当前输入数据举例(视频宽高:640x480 像素格式:yuv420p):

Y 平面:
------ 640列 ------
YY...............YY |
YY...............YY |
YY...............YY 
................... 480行
YY...............YY 
YY...............YY |
YY...............YY |

U 平面:
--- 320列 ---
UU........UU |
UU........UU 
............ 240行
UU........UU 
UU........UU |

V 平面:
--- 320列 ---
VV........VV |
VV........VV 
............ 240行
VV........VV 
VV........VV |

inStrides[0] = Y 平面每一行的大小 = 640
inStrides[1] = U 平面每一行的大小 = 320
inStrides[2] = V 平面每一行的大小 = 320

我们也可以参考前面用到的开辟输入输出缓冲区函数 av_image_alloc,调用函数时 我们把 inStrides 传给了参数 linesizes,linesizes 就很好理解了是每一帧平面一行的大小。

int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
                   int w, int h, enum AVPixelFormat pix_fmt, int align);

outData 和 outStrides 是同样的道理。输出像素格式 rgb24 只有 1 个平面(yuv444 packed 像素格式也只有一个平面)。

示例代码:
在 .pro 中引入库:

macx {
    INCLUDEPATH += /usr/local/ffmpeg/include
    LIBS += -L/usr/local/ffmpeg/lib -lavutil -lswscale
}

ffmpegutils.h:

#ifndef FFMPEGUTILS_H
#define FFMPEGUTILS_H

extern "C" {
    #include 
}

typedef struct {
    const char *filename;
    int width;
    int height;
    AVPixelFormat format;
} RawVideoFile;

typedef struct {
    char *pixels;
    int width;
    int height;
    AVPixelFormat format;
} RawVideoFrame;

class FFmpegUtils
{
public:
    FFmpegUtils();
    // file -> file
    static void convretRawVideo(RawVideoFile &in, RawVideoFile &out);
    // pixels -> pixels,默认传入一帧数据,输出一帧数据
    static void convretRawVideo(RawVideoFrame &in, RawVideoFrame &out);
};

#endif // FFMPEGUTILS_H

ffmpegutils.cpp:

#include "ffmpegutils.h"

#include 
#include 

extern "C" {
    #include 
    #include 
    #include 
}

FFmpegUtils::FFmpegUtils()
{

}

// file -> file
void FFmpegUtils::convretRawVideo(RawVideoFile &in, RawVideoFile &out)
{
    int ret = 0;
    // 转换上下文
    SwsContext *ctx = nullptr;
    // 输入/输出缓冲区,元素指向每帧中每一个平面的数据
    uint8_t *inData[4], *outData[4];
    // 每个平面一行的大小
    int inStrides[4], outStrides[4];
    // 每一帧图片的大小
    int inFrameSize, outFrameSize;

    // 输入文件
    QFile inFile(in.filename);
    // 输出文件
    QFile outFile(out.filename);

    // 创建输入缓冲区
    ret = av_image_alloc(inData, inStrides, in.width, in.height, in.format, 1);
    if (ret < 0) {
        char errbuf[1024];
        av_strerror(ret, errbuf, sizeof (errbuf));
        qDebug() << "av_image_alloc inData error:" << errbuf;
        goto end;
    }

    // 创建输出缓冲区
    ret = av_image_alloc(outData, outStrides, out.width, out.height, out.format, 1);
    if (ret < 0) {
        char errbuf[1024];
        av_strerror(ret, errbuf, sizeof (errbuf));
        qDebug() << "av_image_alloc outData error:" << errbuf;
        goto end;
    }

    // 创建转换上下文
    // 方式一:
    ctx = sws_getContext(in.width, in.height, in.format,
                         out.width, out.height, out.format,
                         SWS_BILINEAR, nullptr, nullptr, nullptr);
    if (!ctx) {
        qDebug() << "sws_getContext error";
        goto end;
    }

    // 方式二:
    // ctx = sws_alloc_context();
    // av_opt_set_int(ctx, "srcw", in.width, 0);
    // av_opt_set_int(ctx, "srch", in.height, 0);
    // av_opt_set_pixel_fmt(ctx, "src_format", in.format, 0);
    // av_opt_set_int(ctx, "dstw", out.width, 0);
    // av_opt_set_int(ctx, "dsth", out.height, 0);
    // av_opt_set_pixel_fmt(ctx, "dst_format", out.format, 0);
    // av_opt_set_int(ctx, "sws_flags", SWS_BILINEAR, 0);

    // if (sws_init_context(ctx, nullptr, nullptr) < 0) {
    //     qDebug() << "sws_init_context error";
    //     goto end;
    // }

    if (!inFile.open(QFile::ReadOnly)) {
        qDebug() << "open in file failure";
        goto end;
    }

    if (!outFile.open(QFile::WriteOnly)) {
        qDebug() << "open out file failure";
        goto end;
    }

    // 计算一帧图像大小
    inFrameSize = av_image_get_buffer_size(in.format, in.width, in.height, 1);
    outFrameSize = av_image_get_buffer_size(out.format, out.width, out.height, 1);

    while (inFile.read((char *)inData[0], inFrameSize) == inFrameSize) {
        // 每一帧的转换
        sws_scale(ctx, inData, inStrides, 0, in.height, outData, outStrides);
        // 每一帧写入文件
        outFile.write((char *)outData[0], outFrameSize);
    }

end:
    av_freep(&inData[0]);
    av_freep(&outData[0]);
    sws_freeContext(ctx);
}

// pixels -> pixels,默认传入一帧数据,输出一帧数据
void FFmpegUtils::convretRawVideo(RawVideoFrame &in, RawVideoFrame &out)
{
    int ret = 0;
    // 转换上下文
    SwsContext *ctx = nullptr;
    // 输入/输出缓冲区,元素指向每帧中每一个平面的数据
    uint8_t *inData[4], *outData[4];
    // 每个平面一行的大小
    int inStrides[4], outStrides[4];
    // 每一帧图片的大小
    int inFrameSize, outFrameSize;

    // 创建输入缓冲区
    ret = av_image_alloc(inData, inStrides, in.width, in.height, in.format, 1);
    if (ret < 0) {
        char errbuf[1024];
        av_strerror(ret, errbuf, sizeof (errbuf));
        qDebug() << "av_image_alloc inData error:" << errbuf;
        goto end;
    }

    // 创建输出缓冲区
    ret = av_image_alloc(outData, outStrides, out.width, out.height, out.format, 1);
    if (ret < 0) {
        char errbuf[1024];
        av_strerror(ret, errbuf, sizeof (errbuf));
        qDebug() << "av_image_alloc outData error:" << errbuf;
        goto end;
    }

    // 创建转换上下文
    ctx = sws_getContext(in.width, in.height, in.format,
                         out.width, out.height, out.format,
                         SWS_BILINEAR, nullptr, nullptr, nullptr);
    if (!ctx) {
        qDebug() << "sws_getContext error";
        goto end;
    }

    // 计算一帧图像大小
    inFrameSize = av_image_get_buffer_size(in.format, in.width, in.height, 1);
    outFrameSize = av_image_get_buffer_size(out.format, out.width, out.height, 1);

    // 拷贝输入像素数据到 inData[0]
    memcpy(inData[0], in.pixels, inFrameSize);

    // 每一帧的转换
    sws_scale(ctx, inData, inStrides, 0, in.height, outData, outStrides);

    // 拷贝像素数据到 outData[0]
    out.pixels = (char *)malloc(outFrameSize);
    memcpy(out.pixels, outData[0], outFrameSize);

end:
    av_freep(&inData[0]);
    av_freep(&outData[0]);
    sws_freeContext(ctx);
}

你可能感兴趣的:(FFmpeg像素格式转换)