文章目錄
- 📖 前言
- 🎯 為什麼需要格式轉換?
- 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 選哪個?
📖 前言
回顧前三個階段,我們已經能夠:
- 階段一:打開文件 → 得到
AVFormatContext和AVStream - 階段二:查找解碼器 → 配置並打開
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(藍)三個分量組成
- 常見格式:
RGB24、RGBA、BGR24
優點:
- 可以直接顯示
- 符合人眼感知模型
缺點:
- 數據量大(每像素3-4字節)
2. 像素格式對照表
|
格式
|
每像素字節數
|
適用場景
|
説明
|
|
|
1.5
|
視頻編解碼
|
Y、U、V分開存儲
|
|
|
3
|
通用顯示
|
按RGB順序存儲
|
|
|
3
|
OpenCV
|
按BGR順序存儲
|
|
|
4
|
Qt/Web
|
多了透明度A通道
|
|
|
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)
);
參數説明
|
參數
|
説明
|
常用值
|
|
|
源圖像寬高
|
|
|
|
源像素格式
|
|
|
|
目標圖像寬高
|
可以和源一樣,也可以縮放
|
|
|
目標像素格式
|
|
|
|
縮放算法
|
|
|
後3個參數
|
高級選項
|
一般都傳 |
縮放算法對比
|
算法標誌
|
速度
|
質量
|
適用場景
|
|
|
⚡⚡⚡
|
⭐⭐
|
實時預覽
|
|
|
⚡⚡
|
⭐⭐⭐
|
推薦(速度質量平衡) |
|
|
⚡
|
⭐⭐⭐⭐
|
高質量輸出
|
|
|
🐌
|
⭐⭐⭐⭐⭐
|
最高質量(慢)
|
返回值
- 成功:返回
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;
}
關鍵要點
- 只需創建一次:轉換器可以重複使用,不要每幀都創建
- 支持縮放:
dstW和dstH可以與源不同,實現圖像縮放 - 格式轉換+縮放:一次調用可以同時完成格式轉換和尺寸調整
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[] // 目標每行字節數數組
);
參數説明
|
參數
|
説明
|
常用值
|
|
|
轉換上下文
|
|
|
|
源數據指針
|
|
|
|
源每行字節數
|
|
|
|
起始行
|
|
|
|
處理行數
|
|
|
|
目標數據指針
|
|
|
|
目標每行字節數
|
|
返回值
- 成功:返回處理的行數(通常等於圖像高度)
- 失敗:返回負數
基本用法
// 執行 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);
參數説明
|
參數
|
説明
|
|
|
要釋放的轉換上下文
|
基本用法
// 程序結束時釋放
sws_freeContext(sws_ctx);
sws_ctx = nullptr; // 好習慣:釋放後置空
關鍵要點
- 必須調用:每個
sws_getContext()都要對應一個sws_freeContext() - 釋放時機:通常在程序結束時釋放
- 不要提前釋放:轉換器要用到最後才釋放
🔑 輔助 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 // 對齊字節數
);
參數説明
|
參數
|
説明
|
常用值
|
|
|
數據指針數組(輸出)
|
|
|
|
每行字節數數組(輸出)
|
|
|
|
圖像寬高
|
|
|
|
像素格式
|
|
|
|
對齊字節數
|
|
返回值
- 成功:返回分配的總字節數
- 失敗:返回負數
基本用法
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 << "字節內存";
關鍵要點
- 自動計算大小:不需要手動計算緩衝區大小
- 處理對齊:自動處理內存對齊問題
- 填充數組:自動填充
pointers和linesizes數組
💾 內存管理要點
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. 本階段資源釋放清單
|
資源
|
分配函數
|
釋放函數
|
|
格式轉換器
|
|
|
|
RGB圖像數據
|
|
|
|
Packet
|
|
|
|
Frame
|
|
|
|
解碼器上下文
|
|
|
|
格式上下文
|
|
|
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: 為什麼有 free 和 freep 兩種?
答案:
av_free(ptr)- 只釋放內存,ptr還指向原地址(野指針)av_freep(&ptr)- 釋放內存 + 把ptr設為NULL(更安全)
推薦:優先使用 freep,更安全!
Q7: RGB24 和 RGBA 選哪個?
答案:看用途
|
格式
|
大小
|
適用場景
|
|
RGB24
|
3字節/像素
|
保存圖片、OpenGL
|
|
RGBA
|
4字節/像素
|
Qt、Web、需要透明度
|
|
BGR24
|
3字節/像素
|
OpenCV
|