FFmpeg像素格式轉(zhuǎn)換

前面使用 SDL 顯示了一張 YUV 圖片以及 YUV 視頻。接下來(lái)使用 Qt 中的 QImage 來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 YUV 播放器讥耗,查看 QImage 支持的像素格式打厘,你會(huì)發(fā)現(xiàn) QImage 僅支持顯示 RGB 像素格式數(shù)據(jù),并不支持直接顯示 YUV 像素格式數(shù)據(jù)秫逝,但是 YUV 和 RGB 之間是可以相互轉(zhuǎn)換的恕出,我們將 YUV 像素格式數(shù)據(jù)轉(zhuǎn)換成 RGB 像素格式數(shù)據(jù)就可以使用 QImage 顯示了。

YUV 轉(zhuǎn) RGB 常見(jiàn)有三種方式:
1违帆、使用 FFmpeg 提供的庫(kù) libswscale
優(yōu)點(diǎn):同一個(gè)函數(shù)實(shí)現(xiàn)了像素格式轉(zhuǎn)換和分辨率縮放以及前后圖像濾波處理浙巫;
缺點(diǎn):速度慢。
2刷后、使用 Google 提供的 libyuv:
優(yōu)點(diǎn):兼容性好功能全面的畴;速度快,僅次于 OpenGL shader尝胆;
缺點(diǎn):暫無(wú)丧裁。
3、使用 OpenGL shader:
優(yōu)點(diǎn):速度快班巩,不增加包體積渣慕;
缺點(diǎn):兼容性一般嘶炭。

下面主要介紹如何使用 FFmpeg 提供的庫(kù) libswscale 進(jìn)行轉(zhuǎn)換,其他轉(zhuǎn)換方式將會(huì)在后面介紹逊桦。

1眨猎、像素格式轉(zhuǎn)換核心函數(shù) sws_scale

sws_scale函數(shù)主要是用來(lái)做像素格式和分辨率的轉(zhuǎn)換,每次轉(zhuǎn)換一幀數(shù)據(jù):

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

參數(shù)說(shuō)明:
c:轉(zhuǎn)換上下文强经,可以通過(guò)函數(shù) sws_getContext 創(chuàng)建睡陪;
srcSlice[]:輸入緩沖區(qū),元素指向一幀中每個(gè)平面的數(shù)據(jù)匿情,以 yuv420p 為例兰迫,{指向每幀中 Y 平面數(shù)據(jù)的指針,指向每幀中 U 平面數(shù)據(jù)的指針炬称,指向每幀中 V 平面數(shù)據(jù)的指針汁果,null}
srcStride[]:每個(gè)平面一行的大小玲躯,以 yuv420p 為例据德,{每幀中 Y 平面一行的長(zhǎng)度,每幀中 U 平面一行的長(zhǎng)度跷车,每幀中 U 平面一行的長(zhǎng)度棘利,0}
srcSliceY:輸入圖像上開(kāi)始處理區(qū)域的起始位置朽缴。
srcSliceH:處理多少行善玫。如果 srcSliceY = 0,srcSliceH = height密强,表示一次性處理完整個(gè)圖像茅郎。這種設(shè)置是為了多線程并行,例如可以創(chuàng)建兩個(gè)線程誓斥,第一個(gè)線程處理 [0, h/2-1] 行只洒,第二個(gè)線程處理 [h/2, h-1] 行,并行處理加快速度劳坑。
dst[]:輸出的圖像數(shù)據(jù),和輸入?yún)?shù) srcSlice[] 類似成畦。
dstStride[]:和輸入?yún)?shù) srcStride[] 類似距芬。

注意:sws_scale 函數(shù)不會(huì)為傳入的輸入數(shù)據(jù)和輸出數(shù)據(jù)創(chuàng)建堆空間。

2循帐、獲取轉(zhuǎn)換上下文函數(shù)
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);

參數(shù)說(shuō)明:
srcW, srcH, srcFormat:輸入圖像寬高和輸入圖像像素格式(我們這里輸入圖像像素格式是 yuv420p)框仔;
dstW, dstH, dstFormat:輸出圖像寬高和輸出圖像像素格式(我們這里輸出圖像像素格式是 rgb24),不僅可以轉(zhuǎn)換像素格式拄养,也可以分辨率縮放离斩;
flag:指定使用何種算法银舱,例如快速線性、差值和矩陣等等跛梗,不同的算法性能也不同寻馏,快速線性算法性能相對(duì)較高。只針對(duì)尺寸的變換核偿。

/* 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:這兩個(gè)參數(shù)是做過(guò)濾器用的诚欠,目前暫時(shí)沒(méi)有用到,傳 nullptr 即可漾岳;
param:和 flag 算法相關(guān)轰绵,也可以傳 nullptr;

返回值:成功返回轉(zhuǎn)換格式上下文指針尼荆,失敗返回 NULL左腔;

注意:sws_getContext 函數(shù)注釋中有提示我們最后使用完上下文不要忘記調(diào)用函數(shù) sws_freeContext 釋放,一般函數(shù)名中有 create 或者 alloc 等單詞的函數(shù)需要我們釋放捅儒,為什么調(diào)用 sws_getContext 后也需要釋放呢翔悠?此時(shí)我們可以參考一下源碼:
ffmpeg-4.3.2/libswscale/utils.c

libswscale 源碼

發(fā)現(xiàn)源碼當(dāng)中調(diào)用了 sws_alloc_set_opts,所以最后是需要釋放上下文的野芒。當(dāng)然我們也可以使用如下方式創(chuàng)建轉(zhuǎn)換上下文蓄愁,最后同樣需要調(diào)用 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ū)狞悲,元素指向每幀中每一個(gè)平面的數(shù)據(jù)
uint8_t *inData[4], *outData[4];
// 每個(gè)平面一行的大小
int inStrides[4], outStrides[4];
// 每一幀圖像的大小
int inFrameSize, outFrameSize;

// 此處需要注意的是下面寫(xiě)法是錯(cuò)誤的撮抓,*是跟著最右邊的變量名的:
uint8_t *inData[4], outData[4];
// 其等價(jià)于:
uint8_t *inData[4];
uint8_t outData[4];

我們創(chuàng)建好了輸入輸出緩沖區(qū)變量,然后需要為輸入輸出緩沖區(qū)各開(kāi)辟一塊堆空間(sws_scale函數(shù)不會(huì)為我們開(kāi)辟輸入輸出緩沖區(qū)堆空間摇锋,可查看源碼)丹拯,F(xiàn)Fmpeg 為我們提供了現(xiàn)成的函數(shù) 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 數(shù)組和 inStrides 數(shù)組的大小是 4,雖然我們目前的輸入像素格式 yuv420p 有 Y 荸恕、U 和 V 共 3 個(gè)平面乖酬,但是有可能會(huì)有 4 個(gè)平面的情況,比如可能會(huì)多 1 個(gè)透明度平面融求。有多少個(gè)平面取決于像素格式咬像。

yuv420p 像素格式數(shù)據(jù)舉例:

// 每一幀的 Y 平面數(shù)據(jù)、U 平面數(shù)據(jù)和 V 平面數(shù)據(jù)是緊挨在一起的
// inData[0] -> Y 平面數(shù)據(jù)
// inData[1] -> U 平面數(shù)據(jù)
// inData[2] -> V 平面數(shù)據(jù)
inData[0] = (uint8_t *)malloc(inFrameSize);
inData[1] = inData[0] + 每幀中 Y 平面數(shù)據(jù)長(zhǎng)度;
inData[2] = inData[0] + 每幀中 Y 平面數(shù)據(jù)長(zhǎng)度 + 每幀中 U 平面數(shù)據(jù)長(zhǎng)度;

關(guān)于 inStrides 的理解生宛,inStrides 中存放的是每個(gè)平面每一行的大小县昂,以當(dāng)前輸入數(shù)據(jù)舉例(視頻寬高: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

我們也可以參考前面用到的開(kāi)辟輸入輸出緩沖區(qū)函數(shù) av_image_alloc,調(diào)用函數(shù)時(shí) 我們把 inStrides 傳給了參數(shù) 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 個(gè)平面(yuv444 packed 像素格式也只有一個(gè)平面)莱睁。

示例代碼:
在 .pro 中引入庫(kù):

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待讳,默認(rèn)傳入一幀數(shù)據(jù)芒澜,輸出一幀數(shù)據(jù)
    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;
    // 轉(zhuǎn)換上下文
    SwsContext *ctx = nullptr;
    // 輸入/輸出緩沖區(qū),元素指向每幀中每一個(gè)平面的數(shù)據(jù)
    uint8_t *inData[4], *outData[4];
    // 每個(gè)平面一行的大小
    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)建轉(zhuǎn)換上下文
    // 方式一:
    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;
    }

    // 計(jì)算一幀圖像大小
    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) {
        // 每一幀的轉(zhuǎn)換
        sws_scale(ctx, inData, inStrides, 0, in.height, outData, outStrides);
        // 每一幀寫(xiě)入文件
        outFile.write((char *)outData[0], outFrameSize);
    }

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

// pixels -> pixels创淡,默認(rèn)傳入一幀數(shù)據(jù)痴晦,輸出一幀數(shù)據(jù)
void FFmpegUtils::convretRawVideo(RawVideoFrame &in, RawVideoFrame &out)
{
    int ret = 0;
    // 轉(zhuǎn)換上下文
    SwsContext *ctx = nullptr;
    // 輸入/輸出緩沖區(qū),元素指向每幀中每一個(gè)平面的數(shù)據(jù)
    uint8_t *inData[4], *outData[4];
    // 每個(gè)平面一行的大小
    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)建轉(zhuǎn)換上下文
    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;
    }

    // 計(jì)算一幀圖像大小
    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);

    // 拷貝輸入像素?cái)?shù)據(jù)到 inData[0]
    memcpy(inData[0], in.pixels, inFrameSize);

    // 每一幀的轉(zhuǎn)換
    sws_scale(ctx, inData, inStrides, 0, in.height, outData, outStrides);

    // 拷貝像素?cái)?shù)據(jù)到 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);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末辩昆,一起剝皮案震驚了整個(gè)濱河市阅酪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌汁针,老刑警劉巖术辐,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異施无,居然都是意外死亡辉词,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)猾骡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)瑞躺,“玉大人,你說(shuō)我怎么就攤上這事兴想〈鄙冢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵嫂便,是天一觀的道長(zhǎng)捞镰。 經(jīng)常有香客問(wèn)我,道長(zhǎng)毙替,這世上最難降的妖魔是什么岸售? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮厂画,結(jié)果婚禮上凸丸,老公的妹妹穿的比我還像新娘。我一直安慰自己袱院,他們只是感情好屎慢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著坑填,像睡著了一般抛人。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上脐瑰,一...
    開(kāi)封第一講書(shū)人閱讀 51,443評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音廷臼,去河邊找鬼苍在。 笑死绝页,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的寂恬。 我是一名探鬼主播续誉,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼初肉!你這毒婦竟也來(lái)了酷鸦?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤牙咏,失蹤者是張志新(化名)和其女友劉穎臼隔,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體妄壶,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡摔握,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丁寄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氨淌。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖伊磺,靈堂內(nèi)的尸體忽然破棺而出盛正,到底是詐尸還是另有隱情,我是刑警寧澤屑埋,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布豪筝,位于F島的核電站,受9級(jí)特大地震影響雀彼,放射性物質(zhì)發(fā)生泄漏壤蚜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一徊哑、第九天 我趴在偏房一處隱蔽的房頂上張望袜刷。 院中可真熱鬧,春花似錦莺丑、人聲如沸著蟹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)萧豆。三九已至,卻和暖如春昏名,著一層夾襖步出監(jiān)牢的瞬間涮雷,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工轻局, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留洪鸭,地道東北人样刷。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像览爵,于是被迫代替她去往敵國(guó)和親置鼻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容