23_FFmpeg像素格式轉換

簡介

前面使用 SDL 顯示了一張YUV圖片以及YUV視頻。接下來使用Qt中的QImage來實現(xiàn)一個簡單的 YUV 播放器,查看QImage支持的像素格式,你會發(fā)現(xiàn)QImage僅支持顯示RGB像素格式數據,并不支持直接顯示YUV像素格式數據,但是YUV和RGB之間是可以相互轉換的,我們將YUV像素格式數據轉換成RGB像素格式數據就可以使用QImage顯示了。

QImage_Format

YUV轉RGB常見有三種方式:

  1. 使用 FFmpeg 提供的庫 libswscale :
    優(yōu)點:同一個函數實現(xiàn)了像素格式轉換和分辨率縮放以及前后圖像濾波處理;
    缺點:速度慢。
  2. 使用 Google 提供的 libyuv:
    優(yōu)點:兼容性好功能全面;速度快,僅次于 OpenGL shader;
    缺點:暫無。
  3. 使用 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

libswscale 源碼

發(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);

outDataoutStrides是同樣的道理。輸出像素格式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);
}

代碼鏈接

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容