23_FFmpeg像素格式转换
简介
前面使用 SDL 显示了一张YUV图片以及YUV视频。接下来使用Qt中的
QImage
来实现一个简单的 YUV 播放器,查看QImage
支持的像素格式,你会发现QImage
仅支持显示RGB像素格式数据,并不支持直接显示YUV像素格式数据,但是YUV和RGB之间是可以相互转换的,我们将YUV像素格式数据转换成RGB像素格式数据就可以使用QImage
显示了。
YUV转RGB常见有三种方式:
- 使用 FFmpeg 提供的库 libswscale :
优点:同一个函数实现了像素格式转换和分辨率缩放以及前后图像滤波处理;
缺点:速度慢。 - 使用 Google 提供的 libyuv:
优点:兼容性好功能全面;速度快,仅次于 OpenGL shader;
缺点:暂无。 - 使用 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
发现源码当中调用了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 像素格式数据举例:
如何让inData[0]、inData[1]、inData[2]指向Y、U、V平面数据呢?
1、分别指向各自堆空间
每一帧图片的YUV是紧挨在一起的,如果YUV分别创建各自的堆空间,到时候还需要将它们分别拷贝到各自的堆空间中,比较麻烦。
2、指向同一个堆空间
YUV在同一个堆空间里面,而这个堆空间的大小正好是一帧的大小
// 每一帧的 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中存放的是每个平面每一行的大小也相当于是linesizes,以当前输入数据举例(视频宽高: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
640x480,rgb24
------- 640个RGB ------
RGB RGB .... RGB RGB |
RGB RGB .... RGB RGB |
RGB RGB .... RGB RGB
RGB RGB .... RGB RGB 480行
RGB RGB .... RGB RGB
RGB RGB .... RGB RGB |
RGB RGB .... RGB RGB |
RGB RGB .... RGB RGB |
RGR只有一个平面
一个平面的行大小640 * 3 = 1920
在QT中我们通过debug运行后可以看到inStrides和outStrides数据内容:
我们也可以参考前面用到的开辟输入输出缓冲区函数av_image_alloc
,调用函数时我们把inStrides
传给了参数linesizes
,linesizes
就很好理解了是每一帧平面一行的大小。
// ffmpeg-4.3.2/libavutil/imgutils.h
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 中引入库:
win32{
FFMPEG_HOME = D:/SoftwareInstall/ffmpeg-4.3.2
}
macx{
FFMPEG_HOME = /usr/local/ffmpeg
}
INCLUDEPATH += $${FFMPEG_HOME}/include
LIBS += -L$${FFMPEG_HOME}/lib \
-lavutil \
-lswscale
ffmpegutils.h:
#ifndef FFMPEGUTILS_H
#define FFMPEGUTILS_H
#define __STDC_CONSTANT_MACROS
extern "C"{
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
typedef struct {
const char *filename;
int width;
int height;
AVPixelFormat format;
} RawVideoFile;
class FFmpegUtils
{
public:
FFmpegUtils();
static void convertRawVideo(RawVideoFile &in, RawVideoFile &out);
};
#endif // FFMPEGUTILS_H
ffmpegutils.cpp
#include "ffmpegutils.h"
#include <QFile>
#include <QDebug>
FFmpegUtils::FFmpegUtils(){
}
void FFmpegUtils::convertRawVideo(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:
inFile.close();
outFile.close();
av_freep(&inData[0]);
av_freep(&outData[0]);
sws_freeContext(ctx);
}
main.cpp
#include <QApplication>
#include <QDebug>
#include "ffmpegutils.h"
#ifdef Q_OS_WIN
#define INFILENAME "../test/out_640x480.yuv"
#define OUTFILENAME "../test/out.rgb"
#else
#define INFILENAME "/Users/zuojie/QtProjects/audio-video-dev/test/out_640x480.yuv"
#define OUTFILENAME "/Users/zuojie/QtProjects/audio-video-dev/test/out.rgb"
#endif
int main(int argc, char *argv[]){
RawVideoFile in = {
INFILENAME,
640, 480, AV_PIX_FMT_YUV420P
};
RawVideoFile out = {
OUTFILENAME,
640, 480, AV_PIX_FMT_RGB24
};
FFmpegUtils::convertRawVideo(in, out);
QApplication a(argc, argv);
MainWindow w;
w.show();
int ret = a.exec();
return ret;
}
程序运行后,回在指定文件夹中生成out.rgb文件,我们可以使用ffplay去播放改文件
ffplay -video_size 640x480 -pixel_format rgb24 out.rgb
上面方法是一个YUV文件直接转另外一个RGB文件,现在我们想要一帧YUV转一帧RGB,可以直接在上面的FFmpegUtils类中新增static void convertRawVideo(RawVideoFrame &in, RawVideoFrame &out);
方法
现在ffmpegutils.h
文件中新增struct和一个方法
typedef struct {
char *pixels;
int width;
int height;
AVPixelFormat format;
} RawVideoFrame;
static void convertRawVideo(RawVideoFrame &in, RawVideoFrame &out);
然后在ffmpegutils.cpp
文件中实现此方法
void FFmpegUtils::convertRawVideo(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);
}