基于 FFMPEG 的像素格式变换(swscale,致敬雷霄骅)

Posted liyuanbhu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 FFMPEG 的像素格式变换(swscale,致敬雷霄骅)相关的知识,希望对你有一定的参考价值。

基于 FFMPEG 的像素格式变换(swscale,致敬雷霄骅)

前几天写了几篇关于ffmpeg 编程转封装的入门文章,下一步本来是要写转码或者编码的。但是发现无论是转码还是编码,都会遇到图像像素格式的变换。我们通常能在软件界面上显示的图像,都是 RGB 格式的(RGB24 或者 RGB32)。但是视频文件中的图像基本都是 YUV格式的(YUV420p 或者 YUV422p)。为了能继续我后面的软件开发,就需要先补充些 YUV 格式的知识。还有 YUV和 RGB 直接相互转换的方法。

关于 YUV 和 RGB 的转换我一起写过一篇博客:

https://blog.csdn.net/liyuanbhu/article/details/68951683

读完这篇博客应该就能转换了。不过 ffmpeg 里面既然有相应的功能,我们还是尽量用 ffmpeg 提供的功能,毕竟 ffmpeg 里面的代码应该比我自己写的要更优化。

这篇博客参考了雷博士的博客:

https://blog.csdn.net/leixiaohua1020/article/details/14215391

雷博士这篇博客写于好几年之前了,现在里面用到的有些函数已经不适用了。我的博客里会给出替代方案。另外,我这篇博客的体系也与雷博的有些差别。我尽量把代码做到了最精简。

ffmpeg 里面 libswscale 是用来做像素格式转换的,同时也能做图像的大小的放缩。为了方便,我的代码中还用到了 Qt。因为 Qt 中的 QImage 非常的方便。但是 QImage 也有不足的地方,就是不支持 YUV 格式。

图像放缩

libswscale 使用比较简单,基本流程就是三个函数。

sws_getContext()
sws_scale()
sws_freeContext()

sws_getContext() 函数和 sws_freeContext() 很简单。其中 sws_getContext 最后三个参数一般用不到,都可以输入 nullptr。flags 是插值类型,通常用 SWS_BICUBIC 会有比较好的效果。如果对速度要求比较高,可以用 SWS_FAST_BILINEAR。这两个函数的声明如下:

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);
void sws_freeContext(struct SwsContext *swsContext);

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[]);

其中 srcSlice 是输入图像的数据指针,dst 是输出图像的数据指针。srcStride 和 dstStride 是每行有多少个字节。相当于QImage 的 bytesPerLine()。srcSliceY 可以直接填 0。srcSliceH 是输入图像的行数。

我们仔细看看两个图像数据指针:

const uint8_t *const srcSlice[];
uint8_t *const dst[];

和我们想的有些不同,通常大家可能觉得这两个指针会是这样的:

const uint8_t * srcSlice;
uint8_t dst;

结果这两个指针都是指针数组。为什么会这样呢?因为我们的图像数据有可能是多个内存块。或者说图像数据存在多个数组中。这时候用一个指针就不够用了。我们知道,图像中的数据的排布方式有两大类:planar 和 packed。

packed 比较常见,比如 RGB24 的图像,像素数据的排布就是 R、G、B、R、G、B、R、G、B… 。

planar 通常用在视频数据中,比如 YUV422p。像素数据的排布是 Y Y Y Y Y Y… U U U… V V V…

一幅图像中的 Y U V 数据分别在三个数组中。

先写一个QImage 图像缩放的代码,QImage 的数据都是 packed。不用考虑图像分块的问题。下面是代码:

bool scale(const QImage &inImage, QImage &outImage, double scaleX, double scaleY)

    int srcW = inImage.width();
    int srcH = inImage.height();
    int desW = srcW * scaleX;
    int desH = srcH * scaleY;
    if(outImage.size() != QSize(desW, desH) || outImage.format() != inImage.format())
    
        outImage = QImage(QSize(desW, desH), inImage.format());
    
    AVPixelFormat srcFormat = toAVPixelFormat(inImage.format());

    uint8_t *in_data[1];
    int in_linesize[1];
    in_data[0] = (uint8_t *) inImage.bits();
    in_linesize[0] = inImage.bytesPerLine();
    //av_image_fill_arrays(in_data, in_linesize, inImage.bits(), AV_PIX_FMT_YUYV422, srcW, srcH, 1);

    uint8_t *out_data[1];
    int out_linesize[1];
    out_data[0] = outImage.bits();
    out_linesize[0] = outImage.bytesPerLine();

    SwsContext * pContext = sws_getContext(srcW, srcH, srcFormat,
                                           desW, desH, srcFormat, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
    if(!pContext) return false;
    sws_scale(pContext, in_data, in_linesize, 0, srcH,
              out_data, out_linesize);
    sws_freeContext(pContext);
    return true;

分几部分讲讲:

uint8_t *in_data[1];
int in_linesize[1];
in_data[0] = (uint8_t *) inImage.bits();
in_linesize[0] = inImage.bytesPerLine();

由于 QImage 数据只有 1块,用一个指针就够了。所以我们的指针数组长度是 1。in_linesize[] 数组的长度也是 1 就够了。代码里还涉及到 QImage::Format 到 AVPixelFormat 格式的映射。我写了如下这样的一个函数。

enum AVPixelFormat toAVPixelFormat(QImage::Format format)

    switch (format) 
    case QImage::Format_Invalid:
    case QImage::Format_MonoLSB:
        return AV_PIX_FMT_NONE;
    case QImage::Format_Mono:
        return AV_PIX_FMT_MONOBLACK;
    case QImage::Format_Indexed8:
        return AV_PIX_FMT_PAL8;
    case QImage::Format_Alpha8:
    case QImage::Format_Grayscale8:
        return AV_PIX_FMT_GRAY8;
    case QImage::Format_Grayscale16:
        return AV_PIX_FMT_GRAY16LE;
    case QImage::Format_RGB32:
    case QImage::Format_ARGB32:
    case QImage::Format_ARGB32_Premultiplied:
        return AV_PIX_FMT_BGRA;
    case QImage::Format_RGB16:
    case QImage::Format_ARGB8565_Premultiplied:
        return AV_PIX_FMT_RGB565LE;
    case QImage::Format_RGB666:
    case QImage::Format_ARGB6666_Premultiplied:
        return AV_PIX_FMT_NONE;
    case QImage::Format_RGB555:
    case QImage::Format_ARGB8555_Premultiplied:
        return AV_PIX_FMT_BGR555LE;
    case QImage::Format_RGB888:
        return AV_PIX_FMT_RGB24;
    case QImage::Format_RGB444:
    case QImage::Format_ARGB4444_Premultiplied:
        return AV_PIX_FMT_RGB444LE;
    case QImage::Format_RGBX8888:
    case QImage::Format_RGBA8888:
    case QImage::Format_RGBA8888_Premultiplied:
        return AV_PIX_FMT_RGBA;
    case QImage::Format_BGR30:
    case QImage::Format_A2BGR30_Premultiplied:
    case QImage::Format_RGB30:
    case QImage::Format_A2RGB30_Premultiplied:
        return AV_PIX_FMT_NONE;
    case QImage::Format_RGBX64:
    case QImage::Format_RGBA64:
    case QImage::Format_RGBA64_Premultiplied:
        return AV_PIX_FMT_RGBA64LE;
    case QImage::Format_BGR888:
        return AV_PIX_FMT_BGR24;
    default:
        return AV_PIX_FMT_NONE;
    
    return AV_PIX_FMT_NONE;

这个 toAVPixelFormat() 函数没有充分测试,有可能有些格式的映射是错的。不过常见的 QImage::Format_Grayscale8、QImage::Format_RGB32、QImage::Format_RGB888 应该是没什么问题的。

下面的例子是 YUV422p 到 RGB32 的转换代码。

QImage YUV422pToQImageRGB32(const uchar *y, const uchar *u, const uchar *v, int width, int height)

    QImage image(QSize(width, height), QImage::Format_RGB32);
    SwsContext * pContext = sws_getContext(width, height, AV_PIX_FMT_YUV422P,
                                           width, height, AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);

    const uint8_t *in_data[4];
    int in_linesize[4];
    in_data[0] = y;
    in_data[1] = u;
    in_data[2] = v;
    in_linesize[0] = width;
    in_linesize[2] = width / 2;
    in_linesize[3] = width / 2;

    uint8_t *out_data[1];
    int out_linesize[1];
    out_data[0] = image.bits();
    out_linesize[0] = image.bytesPerLine();

    sws_scale(pContext, in_data, in_linesize, 0, height, out_data, out_linesize);
    sws_freeContext(pContext);

    return image;

有的时候YUV422P 的数据是连在一起的,只有一个数据的头指针。

QImage YUV422pToQImageRGB32(const uchar *yuv, int width, int height)

    QImage image(QSize(width, height), QImage::Format_RGB32);
    SwsContext * pContext = sws_getContext(width, height, AV_PIX_FMT_YUV422P,
                                           width, height, AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);

    const uint8_t *in_data[4];
    int in_linesize[4];
    in_data[0] = yuv;
    in_data[1] = in_data[0] + width * height;
    in_data[2] = in_data[1] + width * height / 2;
    in_linesize[0] = width;
    in_linesize[2] = width / 2;
    in_linesize[3] = width / 2;

    uint8_t *out_data[1];
    int out_linesize[1];
    out_data[0] = image.bits();
    out_linesize[0] = image.bytesPerLine();

    sws_scale(pContext, in_data, in_linesize, 0, height, out_data, out_linesize);
    sws_freeContext(pContext);

    return image;

如果是 YUV420P 数据。那么代码是这样的:

QImage YUV420pToQImageRGB32(const uchar *yuv, int width, int height)

    QImage image(QSize(width, height), QImage::Format_RGB32);
    SwsContext * pContext = sws_getContext(width, height, AV_PIX_FMT_YUV420P,
                                           width, height, AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);

    const uint8_t *in_data[4];
    int in_linesize[4];
    in_data[0] = yuv;
    in_data[1] = in_data[0] + width * height;
    in_data[2] = in_data[1] + width * height / 4;
    in_linesize[0] = width;
    in_linesize[2] = width / 2;
    in_linesize[3] = width / 2;

    uint8_t *out_data[1];
    int out_linesize[1];
    out_data[0] = image.bits();
    out_linesize[0] = image.bytesPerLine();

    sws_scale(pContext, in_data, in_linesize, 0, height, out_data, out_linesize);
    sws_freeContext(pContext);

    return image;

有些同学可能会问,你是如何确定这么写代码就是对的呢?其实我在写这些代码之前,写了另一个代码,自己生成了一个 YUV420P 数据。

    int width = 1280;
    int height = 960;
    int yuvBufferSize = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height, 1);
    uchar * yuvBuffer = (uint8_t*)av_malloc(yuvBufferSize);

    qDebug() << "width = " << width << ", height = " << height;
    qDebug() << "yuvBufferSize = " << yuvBufferSize;

    uint8_t *out_data[4];
    int out_linesize[4];
    av_image_fill_arrays(out_data, out_linesize, yuvBuffer, AV_PIX_FMT_YUV420P, width, height, 1);

    qDebug() << "out_data[0] = " << out_data[0] << ", size = " << out_data[1] - out_data[0] << ", out_linesize[0] = " << out_linesize[0];
    qDebug() << "out_data[1] = " << out_data[1] << ", size = " << out_data[2] - out_data[1] << ", out_linesize[1] = " << out_linesize[1];
    qDebug() << "out_data[2] = " << out_data[2] << ", out_linesize[2] = " << out_linesize[2];

    av_free(yuvBuffer);

这个代码的输出结果如下:

width =  1280 , height =  960
yuvBufferSize =  1843200
out_data[0] =  0x1f7dda88080 , size =  1228800 , out_linesize[0] =  1280
out_data[1] =  0x1f7ddbb4080 , size =  307200 , out_linesize[1] =  640
out_data[2] =  0x1f7ddbff080 , out_linesize[2] =  640

我们知道 1280 * 960 = 1228800,1843200 = 1228800 * 1.5

所以 YUV420P 格式下, Y 通道占 1228800个字节, UV 加起来占 614400 字节,U、V 每个通道是 Y 通道的 1/4。U、V 每个通道每行的字节数是 Y 每行字节数的一半。

如果程序改成 YUV422P ,结果则是:

width =  1280 , height =  960
yuvBufferSize =  2457600
out_data[0] =  0x23c9bd5c080 , size =  1228800 , out_linesize[0] =  1280
out_data[1] =  0x23c9be88080 , size =  614400 , out_linesize[1] =  640
out_data[2] =  0x23c9bf1e080 , out_linesize[2] =  640

U、V 每个通道字节数是 Y 通道的 1/2。U、V 每个通道每行的字节数还是 Y 每行字节数的一半。

YUV422P 和 YUV420P 的区别在于 YUV420P 的 UV 通道的行数和列数都只有图片行数和列数的一边。YUV422P 则 UV 通道的行数和图片的行数是相同的。

从上面的代码中我们也能看到。当我们不确定如何填 srcStride 和 dstStride 时 ,可以用 av_image_fill_arrays() 来帮我们。

在我们处理视频图像时,通常不会像我这样直接定义 out_data 和out_linesize。我们通常是用 AVFrame。这时可以参考我下面的代码片段:

    AVFrame *pFrameYUV420P = av_frame_alloc();
    pFrameYUV420P->width = 1280;
    pFrameYUV420P->height = 960;
    pFrameYUV420P->format = AV_PIX_FMT_YUV422P;

    int yuvBufferSize = av_image_get_buffer_size((AVPixelFormat) pFrameYUV420P->format,
                                                 pFrameYUV420P->width,
                                                 pFrameYUV420P->height, 1);

    uchar * yuvBuffer = (uint8_t*)av_malloc(yuvBufferSize);

    av_image_fill_arrays(pFrameYUV420P->data,
                         pFrameYUV420P->linesize,
                         yuvBuffer,
                         (AVPixelFormat) pFrameYUV420P->format,
                         pFrameYUV420P->width,
                         pFrameYUV420P->height,
                         1);

    // 这里来填充具体的数据

    av_frame_free(&pFrameYUV420P);//这里会自动释放 yuvBuffer

这个代码还有另一种写法:

    AVFrame *pFrameYUV420P = av_frame_alloc();
    pFrameYUV420P->width = 1280;
    pFrameYUV420P->height = 960;
    pFrameYUV420P->format = AV_PIX_FMT_YUV422P;

    av_image_alloc(pFrameYUV420P->data,
                   pFrameYUV420P->linesize,
                   pFrameYUV420P->width,
                   pFrameYUV420P->height,
                   (AVPixelFormat) pFrameYUV420P->format,
                   1);

    // 这里来填充具体的数据

    av_frame_free(&pFrameYUV420P);

下面给个 QImage 转 YUV420P 的例子:

void QImageToYUV420P(QImage &image, uchar * yuv)

    int srcW = image.width();
    int srcH = image.height();
    int desW = srcW;
    int desH = srcH;

    AVPixelFormat srcFormat = toAVPixelFormat(image.format());

    struct SwsContext *pContext = sws_getContext(srcW, srcH, srcFormat ,
                                                        desW, desH, AV_PIX_FMT_YUV420P,
                                                        SWS_BICUBIC, nullptr, nullptr, nullptr);

    uint8_t *in_data[1];
    int in_linesize[1];
    av_image_fill_arrays(in_data, in_linesize, image.bits(), srcFormat, srcW, srcH, 1);

    uint8_t *out_data[4];
    int out_linesize[4];
    av_image_fill_arrays(out_data, out_linesize, yuv, AV_PIX_FMT_YUV420P, srcW, srcH, 1);

    sws_scale(pContext, in_data, in_linesize, 0, srcH,
              out_data, out_linesize);
    sws_freeContext(pContext);

void testYUV420p()

    QImage image;
    image.load("D:/test.jpg");

    int width = image.width();
    int height = image.height();

    int yuvBufferSize = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height, 1);
    uchar * yuvBuffer = (uint8_t*)av_malloc(yuvBufferSize);

   QImageToYUV420P(image, yuvBuffer);

   QFile file("D:/image.yuv");
   file.open(QFile::WriteOnly);
   file.write((const char *)yuvBuffer, yuvBufferSize);
   file.close();

    av_free(yuvBuffer);

原始图像如下:

转码后的图像:

这里用到了一个 YUV 播放器,下载地址如下:

https://sourceforge.net/projects/raw-yuvplayer/
好了,就写这么多吧。看过这篇博客的同学应该掌握了用sws_scale() 做像素格式转换的方法了。

以上是关于基于 FFMPEG 的像素格式变换(swscale,致敬雷霄骅)的主要内容,如果未能解决你的问题,请参考以下文章

FFmpeg 播放器视频渲染优化

FFmpeg(10)-基于FFmpeg进行像素格式转换(sws_getCachedContext(), sws_scale())

FFmpeg解码H264及swscale缩放详解

基于FFmpeg的视频播放器之五:使用SDL2渲染yuv420p

FFmpeg基础: YUV像素格式介绍和使用

FFmpegffmpeg 命令查询二 ( 比特流过滤器 | 可用协议 | 过滤器 | 像素格式 | 标准声道布局 | 音频采样格式 | 颜色名称 )