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 <libavutil/avutil.h>
}

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 <QDebug>
#include <QFile>

extern "C" {
    #include <libswscale/swscale.h>
    #include <libavutil/imgutils.h>
    #include <libavutil/opt.h>
}

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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容