文章目錄

  • 📖 前言
  • 🎯 為什麼需要格式轉換?
  • 1. YUV 與 RGB 的區別
  • YUV 格式(解碼器輸出)
  • RGB 格式(用於顯示)
  • 2. 像素格式對照表
  • 🔧 三個核心 API
  • API 1️⃣:`sws_getContext` - 創建格式轉換上下文
  • 函數原型
  • 參數説明
  • 縮放算法對比
  • 返回值
  • 基本用法
  • 關鍵要點
  • API 2️⃣:`sws_scale` - 執行格式轉換
  • 函數原型
  • 參數説明
  • 返回值
  • 基本用法
  • 關鍵概念:linesize(每行字節數)
  • API 3️⃣:`sws_freeContext` - 釋放轉換上下文
  • 函數原型
  • 參數説明
  • 基本用法
  • 關鍵要點
  • 🔑 輔助 API
  • API 4️⃣:`av_image_alloc` - 分配圖像緩衝區
  • 函數原型
  • 參數説明
  • 返回值
  • 基本用法
  • 關鍵要點
  • 💾 內存管理要點
  • 1. 核心原則:`linesize` 不需要釋放
  • 2. 本階段資源釋放清單
  • 3. 記憶法則
  • 💻 完整示例代碼(將第一幀保存為PPM圖片)
  • 🎓 常見問題 FAQ
  • Q1: 轉換後的圖片是綠色的?
  • Q2: 圖片有條紋或錯位?
  • Q3: 程序運行後內存一直增長?
  • Q4: `linesize` 需要釋放嗎?
  • Q5: 為什麼有 `free` 和 `freep` 兩種?
  • Q7: RGB24 和 RGBA 選哪個?

📖 前言

回顧前三個階段,我們已經能夠:

  • 階段一:打開文件 → 得到 AVFormatContextAVStream
  • 階段二:查找解碼器 → 配置並打開 AVCodecContext
  • 階段三:讀取並解碼 → 得到 AVFrame(YUV格式)

但是!解碼出來的幀是YUV格式,不能直接顯示。就像你從相機裏拿出了底片(YUV),但需要衝洗成照片(RGB)才能看。

本階段的核心任務

解碼後的YUV幀(AVFrame)→ 格式轉換(SwsContext)→ RGB幀 → 顯示或保存

這就是視頻顯示的關鍵一步


🎯 為什麼需要格式轉換?

1. YUV 與 RGB 的區別

YUV 格式(解碼器輸出)

特點

  • 用於視頻編碼存儲,節省空間
  • Y(亮度)+ U(藍色色度)+ V(紅色色度)分量分開存儲
  • 常見格式:YUV420P(最常用)

優點

  • 數據量小(U和V採樣率是Y的1/4)
  • 適合壓縮和傳輸

缺點

  • 不能直接顯示在屏幕上
  • 需要轉換成RGB才能渲染

RGB 格式(用於顯示)

特點

  • 屏幕顯示的標準格式
  • R(紅)G(綠)B(藍)三個分量組成
  • 常見格式:RGB24RGBABGR24

優點

  • 可以直接顯示
  • 符合人眼感知模型

缺點

  • 數據量大(每像素3-4字節)

2. 像素格式對照表

格式

每像素字節數

適用場景

説明

YUV420P

1.5

視頻編解碼

Y、U、V分開存儲

RGB24

3

通用顯示

按RGB順序存儲

BGR24

3

OpenCV

按BGR順序存儲

RGBA

4

Qt/Web

多了透明度A通道

RGB32

4

Qt

實際是BGRA格式


🔧 三個核心 API

API 1️⃣:sws_getContext - 創建格式轉換上下文

函數原型

SwsContext *sws_getContext(
    int srcW,                      // 源圖像寬度
    int srcH,                      // 源圖像高度
    enum AVPixelFormat srcFormat,  // 源像素格式
    int dstW,                      // 目標圖像寬度
    int dstH,                      // 目標圖像高度
    enum AVPixelFormat dstFormat,  // 目標像素格式
    int flags,                     // 縮放算法標誌
    SwsFilter *srcFilter,          // 源濾鏡(一般傳NULL)
    SwsFilter *dstFilter,          // 目標濾鏡(一般傳NULL)
    const double *param            // 額外參數(一般傳NULL)
);

參數説明

參數

説明

常用值

srcW, srcH

源圖像寬高

codec_ctx->width, codec_ctx->height

srcFormat

源像素格式

codec_ctx->pix_fmt(通常是YUV420P)

dstW, dstH

目標圖像寬高

可以和源一樣,也可以縮放

dstFormat

目標像素格式

AV_PIX_FMT_RGB24AV_PIX_FMT_RGBA

flags

縮放算法

SWS_BILINEAR(推薦)

後3個參數

高級選項

一般都傳 nullptr

縮放算法對比

算法標誌

速度

質量

適用場景

SWS_FAST_BILINEAR

⚡⚡⚡

⭐⭐

實時預覽

SWS_BILINEAR

⚡⚡

⭐⭐⭐

推薦(速度質量平衡)

SWS_BICUBIC


⭐⭐⭐⭐

高質量輸出

SWS_LANCZOS

🐌

⭐⭐⭐⭐⭐

最高質量(慢)

返回值

  • 成功:返回 SwsContext* 指針
  • 失敗:返回 NULL

基本用法

// 創建 YUV → RGB 轉換器  codec_context是解碼器上下文
SwsContext* sws_ctx = sws_getContext(
    codec_context->width,           // 源寬度
    codec_context->height,          // 源高度
    codec_context->pix_fmt,         // 源格式(YUV)
    codec_context->width,           // 目標寬度
    codec_context->height,          // 目標高度
    AV_PIX_FMT_RGB24,              // 目標格式(RGB24)
    SWS_BILINEAR,                   // 雙線性插值
    nullptr, nullptr, nullptr       // 其他參數默認
);

if(!sws_ctx) {
    qDebug() << "格式轉換器創建失敗";
    return -1;
}

關鍵要點

  1. 只需創建一次:轉換器可以重複使用,不要每幀都創建
  2. 支持縮放dstWdstH 可以與源不同,實現圖像縮放
  3. 格式轉換+縮放:一次調用可以同時完成格式轉換和尺寸調整

API 2️⃣:sws_scale - 執行格式轉換

函數原型

int sws_scale(
    SwsContext *c,                  // 轉換上下文
    const uint8_t *const srcSlice[], // 源數據指針數組
    const int srcStride[],          // 源每行字節數數組
    int srcSliceY,                  // 起始行(一般傳0)
    int srcSliceH,                  // 處理行數(一般傳圖像高度)
    uint8_t *const dst[],           // 目標數據指針數組
    const int dstStride[]           // 目標每行字節數數組
);

參數説明

參數

説明

常用值

c

轉換上下文

sws_getContext() 返回的指針

srcSlice

源數據指針

frame->data

srcStride

源每行字節數

frame->linesize

srcSliceY

起始行

0(從第0行開始)

srcSliceH

處理行數

frame->height(處理整個圖像)

dst

目標數據指針

rgb_data

dstStride

目標每行字節數

rgb_linesize

返回值

  • 成功:返回處理的行數(通常等於圖像高度)
  • 失敗:返回負數

基本用法

// 執行 YUV → RGB 轉換
int rows = sws_scale(
    sws_ctx,                // 轉換器
    frame->data,            // 源數據(YUV)
    frame->linesize,        // 源每行字節數
    0,                      // 從第0行開始
    frame->height,          // 處理整個圖像
    rgb_data,               // 目標數據(RGB) 事先分配好的!!!!!!!!
    rgb_linesize            // 目標每行字節數  事先分配好的!!!!!!!!
);

if(rows > 0) {
    qDebug() << "成功轉換" << rows << "行";
}

關鍵概念:linesize(每行字節數)

什麼是 linesize?

linesize 表示圖像每一行佔用的字節數,它可能大於實際圖像寬度乘以像素字節數。

為什麼會大於?

因為內存對齊!為了提高訪問效率,很多系統要求圖像每行按照特定字節數對齊(如16字節、32字節)。

示例:

// 假設圖像寬度 1920,RGB24 格式
int 理論字節數 = 1920 * 3 = 5760;
int 實際linesize = 5760;  // 如果正好對齊
// 或者
int 實際linesize = 5776;  // 補齊到16的倍數

API 3️⃣:sws_freeContext - 釋放轉換上下文

函數原型

void sws_freeContext(SwsContext *swsContext);

參數説明

參數

説明

swsContext

要釋放的轉換上下文

基本用法

// 程序結束時釋放
sws_freeContext(sws_ctx);
sws_ctx = nullptr;  // 好習慣:釋放後置空

關鍵要點

  1. 必須調用:每個 sws_getContext() 都要對應一個 sws_freeContext()
  2. 釋放時機:通常在程序結束時釋放
  3. 不要提前釋放:轉換器要用到最後才釋放

🔑 輔助 API

API 4️⃣:av_image_alloc - 分配圖像緩衝區

函數原型

int av_image_alloc(
    uint8_t *pointers[4],           // 輸出:數據指針數組
    int linesizes[4],               // 輸出:每行字節數數組
    int w, int h,                   // 圖像寬高
    enum AVPixelFormat pix_fmt,     // 像素格式
    int align                       // 對齊字節數
);

參數説明

參數

説明

常用值

pointers

數據指針數組(輸出)

rgb_data

linesizes

每行字節數數組(輸出)

rgb_linesize

w, h

圖像寬高

codec_context->width, height

pix_fmt

像素格式

AV_PIX_FMT_RGB24

align

對齊字節數

1(不對齊)或 16(SSE優化)

返回值

  • 成功:返回分配的總字節數
  • 失敗:返回負數

基本用法

uint8_t* rgb_data[4];
int rgb_linesize[4];

int buffer_size = av_image_alloc(
    rgb_data, 
    rgb_linesize,
    codec_context->width,
    codec_context->height,
    AV_PIX_FMT_RGB24,
    1  // 1字節對齊
);

if(buffer_size < 0) {
    qDebug() << "內存分配失敗";
    return -1;
}

qDebug() << "分配了" << buffer_size << "字節內存";

關鍵要點

  1. 自動計算大小:不需要手動計算緩衝區大小
  2. 處理對齊:自動處理內存對齊問題
  3. 填充數組:自動填充 pointerslinesizes 數組

💾 內存管理要點

1. 核心原則:linesize 不需要釋放

uint8_t* rgb_data[4];      // 指針數組
int rgb_linesize[4];       // 整數數組

av_image_alloc(rgb_data, rgb_linesize, ...);

// ✅ rgb_data[0] 指向動態分配的內存,需要釋放
av_freep(&rgb_data[0]);

// ❌ rgb_linesize[0] 只是整數,不需要釋放
// av_freep(&rgb_linesize[0]);  // 錯誤!

為什麼?

av_image_alloc 內部:

  • 分配了一塊內存,把地址存到 rgb_data[0]需要釋放
  • 把每行字節數(整數)存到 rgb_linesize[0]不需要釋放

2. 本階段資源釋放清單

資源

分配函數

釋放函數

格式轉換器

sws_getContext()

sws_freeContext()

RGB圖像數據

av_image_alloc()

av_freep(&rgb_data[0])

Packet

av_packet_alloc()

av_packet_free()

Frame

av_frame_alloc()

av_frame_free()

解碼器上下文

avcodec_alloc_context3()

avcodec_free_context()

格式上下文

avformat_open_input()

avformat_close_input()


3. 記憶法則

alloc  → 需要 free
open   → 需要 close
get    → 需要 free(如 sws_getContext)

💻 完整示例代碼(將第一幀保存為PPM圖片)

#include "mainwindow.h"
#include<QDebug>
#include <QApplication>
#include<stdio.h>

extern "C"{
#include<libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
}



int main(int argc, char *argv[])
{
    QString path="E:video.mp4";

    AVFormatContext * avfmatcontext=nullptr;
    int result=avformat_open_input(&avfmatcontext,path.toUtf8().data(),nullptr,nullptr);

    if(result<0)
    {
        qDebug()<<"avformat_open_input is error";
        return 0;
    }
    qDebug()<<"avformat_open_input is success";

    result=av_find_best_stream(avfmatcontext,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
    int video_dex=result;
    if(result!=0)
    {
        qDebug()<<"av_find_best_stream is error";
        return 0;
    }
    qDebug()<<"av_find_best_stream is success";

    AVStream* stream=avfmatcontext->streams[result];
    qDebug()<<"this video stream index is "<<stream->index;


    const AVCodec* decoder=avcodec_find_decoder(stream->codecpar->codec_id);
    if(!decoder)
    {
        qDebug()<<"avcodec_find_decoder is error";
        return 0;
    }
    qDebug()<<"avcodec_find_decoder is success ,this decoder name is "<<decoder->name;

    //分配解碼器上下文
    AVCodecContext * codec_context=avcodec_alloc_context3(decoder);

    //複製信息
    result=avcodec_parameters_to_context(codec_context,stream->codecpar);
    if(result!=0)
    {
        qDebug()<<"avcodec_parameters_to_context is error";
        return 0;
    }
    qDebug()<<"avcodec_parameters_to_context is success";


    //打開解碼器
    result=avcodec_open2(codec_context,decoder,nullptr);
    if(result!=0)
    {
        qDebug()<<"avcodec_open2 is error";
        return 0;
    }
    qDebug()<<"avcodec_open2 is success";
    qDebug()<<"視頻分辨率:"<<codec_context->width<<" x "<<codec_context->height;

    AVPacket* packet=av_packet_alloc();
    AVFrame* frame=av_frame_alloc();
    int total_frame=0;

    //創建格式轉換器 後續解碼第一幀的時候傳入參數
    SwsContext* sws_ctx=nullptr;


    //讀取packet
    while(av_read_frame(avfmatcontext,packet)>=0&&total_frame!=1000)
    {
        if(packet->stream_index!=video_dex)continue;
        //發送packet給解碼器
        if(avcodec_send_packet(codec_context,packet)==0)
        {
            //解碼器解碼
            while(avcodec_receive_frame(codec_context,frame)==0&&total_frame!=1000)
            {
                if(total_frame == 0) {
                    //第一幀的時候設置格式轉換器參數
                    sws_ctx=sws_getContext(
                        codec_context->width,
                        codec_context->height,
                        codec_context->pix_fmt,
                        codec_context->width,
                        codec_context->height,
                        AV_PIX_FMT_RGB24,
                        SWS_BILINEAR,
                        nullptr,
                        nullptr,
                        nullptr);

                    if(!sws_ctx)
                    {
                        qDebug()<<"格式轉換器創建失敗";
                        return 0;
                    }
                    qDebug()<<"格式轉換器創建成功";

                    //分配rgb圖像內存
                    uint8_t* rgb_data[4];
                    int rgb_linesize[4];

                    int result_alloc=av_image_alloc
                    (rgb_data,rgb_linesize,codec_context->width,codec_context->height,
                                                      AV_PIX_FMT_RGB24,1);
                    if(result_alloc<0)
                    {
                        qDebug()<<"給rgb分配內存空間失敗";
                        return 0;
                    }
                    qDebug() << "RGB 內存分配成功,大小:" << result_alloc << "字節";


                    //只轉換第一幀
                    int p=sws_scale(sws_ctx,frame->data,frame->linesize,
                    0,frame->height,rgb_data,rgb_linesize);
                    if(p>0)
                    {
                        qDebug() << "成功將一幀 YUV 轉換為 RGB,共轉換" << p << "行";
                        // 保存為 PPM 格式
                        FILE* ppm_file = fopen("E:/frame.ppm", "wb");
                        fprintf(ppm_file, "P6\n%d %d\n255\n", 
                        codec_context->width, codec_context->height);
                        // 寫入 RGB 數據
                        fwrite(rgb_data[0], 1, result_alloc, ppm_file);
                        fclose(ppm_file);
                    }
                    //釋放手動開闢的rgb空間
                    av_freep(&rgb_data[0]);
                }
                total_frame++;
                av_frame_unref(frame);
            }
        }
        av_packet_unref(packet);
    }
    qDebug()<<"總共讀取了"<<total_frame<<"幀";

    // 釋放所有資源
    sws_freeContext(sws_ctx);
    av_packet_free(&packet);
    av_frame_free(&frame);
    avcodec_free_context(&codec_context);
    avformat_close_input(&avfmatcontext);
    
    qDebug() << "程序執行完成";
    return 0;
}

🎓 常見問題 FAQ

Q1: 轉換後的圖片是綠色的?

原因:像素格式不匹配,可能是 RGB 和 BGR 搞反了。

解決

// 如果保存的圖片是綠色,嘗試改成 BGR24
sws_getContext(..., AV_PIX_FMT_BGR24, ...);

Q2: 圖片有條紋或錯位?

原因:沒有正確使用 linesize,而是直接用 width * 3

解決

// ❌ 錯誤
int index = y * width * 3 + x * 3;

// ✅ 正確
int index = y * rgb_linesize[0] + x * 3;

Q3: 程序運行後內存一直增長?

原因:內存泄漏,忘記釋放資源。

檢查清單

// ✅ 每個 alloc 都要有對應的 free
av_packet_alloc()     → av_packet_free()
av_frame_alloc()      → av_frame_free()
sws_getContext()      → sws_freeContext()
av_image_alloc()      → av_freep(&data[0])
avcodec_alloc_context3() → avcodec_free_context()
avformat_open_input() → avformat_close_input()

Q4: linesize 需要釋放嗎?

答案:❌ 不需要

linesize 只是存儲整數的數組,不是動態分配的內存。

av_freep(&rgb_data[0]);     // ✅ 需要
av_freep(&rgb_linesize[0]); // ❌ 不需要

Q5: 為什麼有 freefreep 兩種?

答案

  • av_free(ptr) - 只釋放內存,ptr 還指向原地址(野指針)
  • av_freep(&ptr) - 釋放內存 + 把 ptr 設為 NULL(更安全)

推薦:優先使用 freep,更安全!


Q7: RGB24 和 RGBA 選哪個?

答案:看用途

格式

大小

適用場景

RGB24

3字節/像素

保存圖片、OpenGL

RGBA

4字節/像素

Qt、Web、需要透明度

BGR24

3字節/像素

OpenCV