簡介
前面使用 SDL 顯示了一張YUV圖片以及YUV視頻。接下來使用Qt中的
QImage來實現(xiàn)一個簡單的 YUV 播放器,查看QImage支持的像素格式,你會發(fā)現(xiàn)QImage僅支持顯示RGB像素格式數據,并不支持直接顯示YUV像素格式數據,但是YUV和RGB之間是可以相互轉換的,我們將YUV像素格式數據轉換成RGB像素格式數據就可以使用QImage顯示了。
QImage_Format
YUV轉RGB常見有三種方式:
- 使用 FFmpeg 提供的庫 libswscale :
優(yōu)點:同一個函數實現(xiàn)了像素格式轉換和分辨率縮放以及前后圖像濾波處理;
缺點:速度慢。 - 使用 Google 提供的 libyuv:
優(yōu)點:兼容性好功能全面;速度快,僅次于 OpenGL shader;
缺點:暫無。 - 使用 OpenGL shader:
優(yōu)點:速度快,不增加包體積;
缺點:兼容性一般。
下面主要介紹如何使用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創(chuàng)建; - srcSlice[]:輸入緩沖區(qū),元素指向一幀中每個平面的數據,以yuv420p為例,
{指向每幀中Y平面數據的指針,指向每幀中U平面數據的指針,指向每幀中V平面數據的指針,null}; - srcStride[]:每個平面一行的大小,以yuv420p為例,
{每幀中Y平面一行的長度,每幀中U平面一行的長度,每幀中U平面一行的長度,0}; - srcSliceY:輸入圖像上開始處理區(qū)域的起始位置。
- srcSliceH:處理多少行。如果
srcSliceY = 0,srcSliceH = height,表示一次性處理完整個圖像。這種設置是為了多線程并行,例如可以創(chuàng)建兩個線程,第一個線程處理[0, h/2-1]行,第二個線程處理[h/2, h-1]行,并行處理加快速度。 - dst[]:輸出的圖像數據,和輸入參數
srcSlice[]類似。 - dstStride[]:和輸入參數
srcStride[]類似。
注意:sws_scale 函數不會為傳入的輸入數據和輸出數據創(chuàng)建堆空間。
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

發(fā)現(xiàn)源碼當中調用了sws_alloc_set_opts,所以最后是需要釋放上下文的。當然我們也可以使用如下方式創(chuàng)建轉換上下文,最后同樣需要調用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、創(chuàng)建輸入輸出緩沖區(qū)
首先我們創(chuàng)建需要的局部變量:
// 輸入/輸出緩沖區(qū),元素指向每幀中每一個平面的數據
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];
我們創(chuàng)建好了輸入輸出緩沖區(qū)變量,然后需要為輸入輸出緩沖區(qū)各開辟一塊堆空間(sws_scale函數不會為我們開辟輸入輸出緩沖區(qū)堆空間,可查看源碼),F(xiàn)Fmpeg為我們提供了現(xiàn)成的函數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);
// 最后不要忘記釋放輸入輸出緩沖區(qū)
av_freep(&inData[0]);
av_freep(&outData[0]);
建議inData數組和inStrides數組的大小是4,雖然我們目前的輸入像素格式y(tǒng)uv420p有 Y 、U 和 V 共 3 個平面,但是有可能會有 4 個平面的情況,比如可能會多1個透明度平面。有多少個平面取決于像素格式。
以 yuv420p 像素格式數據舉例:
如何讓inData[0]、inData[1]、inData[2]指向Y、U、V平面數據呢?
1、分別指向各自堆空間
每一幀圖片的YUV是緊挨在一起的,如果YUV分別創(chuàng)建各自的堆空間,到時候還需要將它們分別拷貝到各自的堆空間中,比較麻煩。
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數據內容:

我們也可以參考前面用到的開辟輸入輸出緩沖區(qū)函數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;
// 輸入/輸出緩沖區(qū),元素指向每幀中每一個平面的數據
uint8_t *inData[4], *outData[4];
// 每個平面一行的大小
int inStrides[4], outStrides[4];
// 每一幀圖片的大小
int inFrameSize, outFrameSize;
// 輸入文件
QFile inFile(in.filename);
// 輸出文件
QFile outFile(out.filename);
// 創(chuàng)建輸入緩沖區(qū)
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;
}
// 創(chuàng)建輸出緩沖區(qū)
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;
}
// 創(chuàng)建轉換上下文
// 方式一:
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文件,現(xiàn)在我們想要一幀YUV轉一幀RGB,可以直接在上面的FFmpegUtils類中新增static void convertRawVideo(RawVideoFrame &in, RawVideoFrame &out);方法
現(xiàn)在ffmpegutils.h文件中新增struct和一個方法
typedef struct {
char *pixels;
int width;
int height;
AVPixelFormat format;
} RawVideoFrame;
static void convertRawVideo(RawVideoFrame &in, RawVideoFrame &out);
然后在ffmpegutils.cpp文件中實現(xiàn)此方法
void FFmpegUtils::convertRawVideo(RawVideoFrame &in, RawVideoFrame &out){
int ret = 0;
// 轉換上下文
SwsContext *ctx = nullptr;
// 輸入/輸出緩沖區(qū),元素指向每幀中每一個平面的數據
uint8_t *inData[4], *outData[4];
// 每個平面一行的大小
int inStrides[4], outStrides[4];
// 每一幀圖片的大小
int inFrameSize, outFrameSize;
// 創(chuàng)建輸入緩沖區(qū)
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;
}
// 創(chuàng)建輸出緩沖區(qū)
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;
}
// 創(chuàng)建轉換上下文
// 方式一:
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);
}

