引子
OpenCV中有自己的用于處理圖片和視頻的類VideoCapture刹缝,可以很方便的讀入文件和顯示。
現(xiàn)在視頻數(shù)據(jù)流是ffmpeg解碼h264文件得到的颈将,由于要依賴該數(shù)據(jù)源進(jìn)行相應(yīng)的后續(xù)處理梢夯,所以需要將ffmpeg中得到的數(shù)據(jù)緩存轉(zhuǎn)換成可以被OpenCV處理的Mat類對象。
ffmpeg介紹
FFmpeg是一個(gè)開源免費(fèi)跨平臺的視頻和音頻流方案晴圾,屬于自由軟件颂砸,采用LGPL或GPL許可證(依據(jù)你選擇的組件)。它提供了錄制死姚、轉(zhuǎn)換以及流化音視頻的完整解決方案人乓。它包含了非常先進(jìn)的音頻/視頻編解碼庫libavcodec,為了保證高可移植性和編解碼質(zhì)量都毒,libavcodec里很多codec都是從頭開發(fā)的色罚。
FFmpeg是一套可以用來記錄、轉(zhuǎn)換數(shù)字音頻账劲、視頻戳护,并能將其轉(zhuǎn)化為流的開源計(jì)算機(jī)程序。它包括了目前領(lǐng)先的音/視頻編碼庫libavcodec涤垫。 FFmpeg是在Linux下開發(fā)出來的姑尺,但它可以在包括Windows在內(nèi)的大多數(shù)操作系統(tǒng)中編譯。
FFmpeg的組成結(jié)構(gòu)
FFmpeg主要由一下幾個(gè)部分組成:
- libavcodec:一個(gè)包含了所有FFmpeg音視頻編解碼器的庫蝠猬。 為了保證最優(yōu)性能和高可復(fù)用性,大多數(shù)編解碼器從頭開發(fā)的切蟋。
- libavformat:一個(gè)包含了所有的普通音視格式的解析器和 產(chǎn)生器的庫。
- 三個(gè)實(shí)例程序榆芦,這三個(gè)實(shí)例較為復(fù)雜柄粹,基本可以作為API使用手冊:
ffmpeg:命令行的視頻格式轉(zhuǎn)換程序喘鸟。
ffplay:視頻播放程序。(需要SDL支持)
ffserver:多媒體服務(wù)器
了解完組成結(jié)構(gòu)后驻右,你會發(fā)現(xiàn)什黑,如果你在尋找一種視頻格式轉(zhuǎn)換的方式,那FFmpeg絕對是你的第一選擇堪夭,libavcodec 則又是重 中之重愕把。如果遇上API不會使用的情況,可以參考ffmpeg.c森爽、ffplay.c恨豁、 ffserver.c、apiexample.c(解碼)和output_example.c(編碼)爬迟。
ffmpeg使用說明
ffmpeg庫的接口都是c函數(shù)橘蜜,其頭文件也沒有extern "C"的聲明,所以在cpp文件里調(diào)用ffmpeg函數(shù)要注意了付呕。
一般來說计福,一個(gè)用C寫成的庫如果想被C/C++同時(shí)可以使用,那在頭文件應(yīng)該加上
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
} // endof extern "C"
#endif
如果在.cpp里調(diào)用av_register_all()在鏈接時(shí)將找到不符號徽职,因?yàn)?cpp要求的符號名
和ffmpeg庫提供的符號名不一致象颖。
可以這么解決:
extern "C"
{
#include <libavutil/avutil.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}
使用ffmpeg SDK解碼流數(shù)據(jù)過程
以H264視頻流為例,講解解碼流數(shù)據(jù)的步驟姆钉。
準(zhǔn)備變量
定義AVCodec力麸,AVCodec *變量為解碼器指針。
定義AVCodecContext育韩,使用該變量可以將其定義為ffmpeg解碼類的類成員。
定義AVFrame闺鲸,AVFrame描述一個(gè)多媒體幀筋讨。解碼后的數(shù)據(jù)將被放在其中。
定義AVFormatContext變量摸恍,AVFormatContext用于保存視頻流的有效信息悉罕。
AVCodec *pCodec;
AVCodecContext * pCodecCtx;
AVFrame * pAvFrame;
AVFormatContext *pFormatCtx;
初始化解碼器
第一件事情就是初始化libavformat/libavcodec:
ffmpeg注冊復(fù)用器,編碼器等的函數(shù)av_register_all()立镶。
av_register_all();
這一步注冊庫中含有的所有可用的文件格式和編碼器壁袄,這樣當(dāng)打開一個(gè)文件時(shí),它們才能夠自動(dòng)選擇相應(yīng)的文件格式和編碼器媚媒。要注意你只需調(diào)用一次 av_register_all()嗜逻,所以,盡可能的在你的初始代碼中使用它缭召。這里注冊了所有的文件格式和編解碼器的庫栈顷,所以它們將被自動(dòng)的使用在被打開的合適格式的文件上逆日。注意你只需要調(diào)用 av_register_all()一次,因此我們在主函數(shù)main()中來調(diào)用它萄凤。如果你喜歡室抽,也可以只注冊特定的格式和編解碼器,但是通常你沒有必要這樣做靡努。
打開視頻文件坪圾,取出包含在文件中的流信息
// 打開視頻文件
if(av_open_input_file(&pFormatCtx, filename, NULL, 0, NULL)!=0)
handle_error(); // 不能打開此文件
這個(gè)函數(shù)讀取文件的頭部并且把信息保存到我們給的AVFormatContext結(jié)構(gòu)體中。
最后三個(gè)參數(shù)描述了文件格式惑朦,緩沖區(qū)大惺扌埂(size)和格式參數(shù);我們通過簡單地指明NULL或0告訴 libavformat 去自動(dòng)探測文件格式并且使用默認(rèn)的緩沖區(qū)大小行嗤。
// 取出流信息
if(av_find_stream_info(pFormatCtx)<0)
handle_error(); // 不能夠找到流信息
查找文件的流信息,avformat_open_input函數(shù)只是檢測了文件的頭部已日,接著要檢查在文件中的流的信息。
這一步會用有效的信息把 AVFormatContext 的流域(streams field)填滿栅屏。作為一個(gè)可調(diào)試的診斷飘千,我們會將這些信息全盤輸出到標(biāo)準(zhǔn)錯(cuò)誤輸出中,不過你在一個(gè)應(yīng)用程序的產(chǎn)品中并不用這么做栈雳。
我們僅僅處理視頻流护奈,而不是音頻流。為了讓這件事情更容易理解哥纫,我們只簡單使用我們發(fā)現(xiàn)的第一種視頻流霉旗。
//遍歷文件的各個(gè)流,找到第一個(gè)視頻流蛀骇,并記錄該流的編碼信息
videoindex = -1;
for(i=0; i<pFormatCtx->nb_streams; i++)
{
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO)
{
videoindex=i;
break;
}
}
if(videoindex==-1)
{
printf("Didn't find a video stream.\n");
return;
}
pCodecCtx=pFormatCtx->streams[videoindex]->codec;
我們已經(jīng)得到了一個(gè)指向視頻流的稱之為上下文的指針厌秒。接下來,我們需要找到真正的編碼器打開它擅憔。
尋找視頻流的解碼器
在庫里面查找支持該格式的解碼器
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec == NULL)
handle_error(); // 找不到解碼器
打開解碼器
if(avcodec_open(pCodecCtx, pCodec)<0)
handle_error();
給視頻幀分配空間鸵闪,以便存儲解碼后的圖片數(shù)據(jù)
pAvFrame = avcodec_alloc_frame();
解碼視頻幀就像我前面提到過的,視頻文件包含數(shù)個(gè)音頻和視頻流暑诸,并且他們各個(gè)獨(dú)自被分開存儲在固定大小的包里蚌讼。我們要做的就是使用libavformat依次讀取這些包,過濾掉所有那些視頻流中我們不感興趣的部分个榕,并把它們交給libavcodec進(jìn)行解碼處理篡石。
進(jìn)行解碼
通過該api讀入一幀
result = av_read_frame(pFormatCtx, packet);
通過下面的api進(jìn)行解碼一幀數(shù)據(jù),將有效的圖像數(shù)據(jù)存儲到pAvFrame成員變量中
ret = avcodec_decode_video2(pCodecCtx, pAvFrame, &got_picture, packet);
下面是ffmpeg解碼的API:
將YUV420p顏色編碼轉(zhuǎn)換成BGR顏色編碼
首先得到圖片轉(zhuǎn)換上下文img_convert_ctx,這里注意的是西采,opencv的RGB編碼順序?yàn)锽GR凰萨,所以選用AV_PIX_FMT_BGR24的編碼方式。
//根據(jù)編碼信息設(shè)置渲染格式
if(img_convert_ctx == NULL){
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,
AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
}
再得到為BGR格式幀分配內(nèi)存
AVFrame *pFrameRGB = NULL;
uint8_t *out_bufferRGB = NULL;
pFrameRGB = avcodec_alloc_frame();
//給pFrameRGB幀加上分配的內(nèi)存;
int size = avpicture_get_size(AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height);
out_bufferRGB = new uint8_t[size];
avpicture_fill((AVPicture *)pFrameRGB, out_bufferRGB, AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height);
最后進(jìn)行轉(zhuǎn)換
sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);
OpenCV Mat數(shù)據(jù)復(fù)制
cv::Mat對象中有data指針,指向內(nèi)存中存放矩陣數(shù)據(jù)的一塊內(nèi)存 (uchar data)沟蔑。*
所以湿诊,要將ffmpeg解碼之后得到的RGB色彩的幀數(shù)據(jù)復(fù)制給該指針,這樣就實(shí)現(xiàn)了ffmpeg解碼數(shù)據(jù)到opencv中Mat格式的轉(zhuǎn)換瘦材,進(jìn)而就可以對Mat對象進(jìn)行相應(yīng)的處理厅须。
代碼示例
ffmpegDecode.h文件
#ifndef __FFMPEG_DECODE_H__
#define __FFMPEG_DECODE_H__
#include <opencv2/core/core.hpp>
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
//圖像轉(zhuǎn)換結(jié)構(gòu)需要引入的頭文件
#include "libswscale/swscale.h"
};
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avformat.lib ")
#pragma comment(lib, "avutil.lib ")
#pragma comment(lib, "avdevice.lib ")
#pragma comment(lib, "avfilter.lib ")
#pragma comment(lib, "postproc.lib ")
#pragma comment(lib, "swresample.lib")
#pragma comment(lib, "swscale.lib ")
class ffmpegDecode
{
public:
ffmpegDecode(char * file = NULL);
~ffmpegDecode();
cv::Mat getDecodedFrame();
cv::Mat getLastFrame();
int readOneFrame();
int getFrameInterval();
private:
AVFrame *pAvFrame;
AVFormatContext *pFormatCtx;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
int i;
int videoindex;
char *filepath;
int ret, got_picture;
SwsContext *img_convert_ctx;
int y_size;
AVPacket *packet;
cv::Mat *pCvMat;
void init();
void openDecode();
void prepare();
void get(AVCodecContext *pCodecCtx, SwsContext *img_convert_ctx,AVFrame *pFrame);
};
#endif
ffmpegDecode.cpp文件
#include "ffmpegDecode.h"
ffmpegDecode :: ~ffmpegDecode()
{
pCvMat->release();
//釋放本次讀取的幀內(nèi)存
av_free_packet(packet);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
}
ffmpegDecode :: ffmpegDecode(char * file)
{
pAvFrame = NULL/**pFrameRGB = NULL*/;
pFormatCtx = NULL;
pCodecCtx = NULL;
pCodec = NULL;
pCvMat = new cv::Mat();
i=0;
videoindex=0;
ret = 0;
got_picture = 0;
img_convert_ctx = NULL;
y_size = 0;
packet = NULL;
if (NULL == file)
{
filepath = "opencv.h264";
}
else
{
filepath = file;
}
init();
openDecode();
prepare();
return;
}
void ffmpegDecode :: init()
{
//ffmpeg注冊復(fù)用器,編碼器等的函數(shù)av_register_all()食棕。
//該函數(shù)在所有基于ffmpeg的應(yīng)用程序中幾乎都是第一個(gè)被調(diào)用的朗和。只有調(diào)用了該函數(shù),才能使用復(fù)用器簿晓,編碼器等眶拉。
//這里注冊了所有的文件格式和編解碼器的庫,所以它們將被自動(dòng)的使用在被打開的合適格式的文件上憔儿。注意你只需要調(diào)用 av_register_all()一次忆植,因此我們在主函數(shù)main()中來調(diào)用它。如果你喜歡谒臼,也可以只注冊特定的格式和編解碼器朝刊,但是通常你沒有必要這樣做。
av_register_all();
//pFormatCtx = avformat_alloc_context();
//打開視頻文件,通過參數(shù)filepath來獲得文件名蜈缤。這個(gè)函數(shù)讀取文件的頭部并且把信息保存到我們給的AVFormatContext結(jié)構(gòu)體中拾氓。
//最后2個(gè)參數(shù)用來指定特殊的文件格式,緩沖大小和格式參數(shù)底哥,但如果把它們設(shè)置為空NULL或者0咙鞍,libavformat將自動(dòng)檢測這些參數(shù)。
if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0)
{
printf("無法打開文件\n");
return;
}
//查找文件的流信息,avformat_open_input函數(shù)只是檢測了文件的頭部趾徽,接著要檢查在文件中的流的信息
if(av_find_stream_info(pFormatCtx)<0)
{
printf("Couldn't find stream information.\n");
return;
}
return;
}
void ffmpegDecode :: openDecode()
{
//遍歷文件的各個(gè)流续滋,找到第一個(gè)視頻流,并記錄該流的編碼信息
videoindex = -1;
for(i=0; i<pFormatCtx->nb_streams; i++)
{
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO)
{
videoindex=i;
break;
}
}
if(videoindex==-1)
{
printf("Didn't find a video stream.\n");
return;
}
pCodecCtx=pFormatCtx->streams[videoindex]->codec;
//在庫里面查找支持該格式的解碼器
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL)
{
printf("Codec not found.\n");
return;
}
//打開解碼器
if(avcodec_open2(pCodecCtx, pCodec,NULL) < 0)
{
printf("Could not open codec.\n");
return;
}
}
void ffmpegDecode :: prepare()
{
//分配一個(gè)幀指針孵奶,指向解碼后的原始幀
pAvFrame=avcodec_alloc_frame();
y_size = pCodecCtx->width * pCodecCtx->height;
//分配幀內(nèi)存
packet=(AVPacket *)av_malloc(sizeof(AVPacket));
av_new_packet(packet, y_size);
//輸出一下信息-----------------------------
printf("文件信息-----------------------------------------\n");
av_dump_format(pFormatCtx,0,filepath,0);
//av_dump_format只是個(gè)調(diào)試函數(shù)吃粒,輸出文件的音、視頻流的基本信息了拒课,幀率、分辨率事示、音頻采樣等等
printf("-------------------------------------------------\n");
}
int ffmpegDecode :: readOneFrame()
{
int result = 0;
result = av_read_frame(pFormatCtx, packet);
return result;
}
cv::Mat ffmpegDecode :: getDecodedFrame()
{
if(packet->stream_index==videoindex)
{
//解碼一個(gè)幀
ret = avcodec_decode_video2(pCodecCtx, pAvFrame, &got_picture, packet);
if(ret < 0)
{
printf("解碼錯(cuò)誤\n");
return cv::Mat();
}
if(got_picture)
{
//根據(jù)編碼信息設(shè)置渲染格式
if(img_convert_ctx == NULL){
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,
AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
}
//----------------------opencv
if (pCvMat->empty())
{
pCvMat->create(cv::Size(pCodecCtx->width, pCodecCtx->height),CV_8UC3);
}
if(img_convert_ctx != NULL)
{
get(pCodecCtx, img_convert_ctx, pAvFrame);
}
}
}
av_free_packet(packet);
return *pCvMat;
}
cv::Mat ffmpegDecode :: getLastFrame()
{
ret = avcodec_decode_video2(pCodecCtx, pAvFrame, &got_picture, packet);
if(got_picture)
{
//根據(jù)編碼信息設(shè)置渲染格式
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);
if(img_convert_ctx != NULL)
{
get(pCodecCtx, img_convert_ctx,pAvFrame);
}
}
return *pCvMat;
}
void ffmpegDecode :: get(AVCodecContext * pCodecCtx, SwsContext * img_convert_ctx, AVFrame * pFrame)
{
if (pCvMat->empty())
{
pCvMat->create(cv::Size(pCodecCtx->width, pCodecCtx->height),CV_8UC3);
}
AVFrame *pFrameRGB = NULL;
uint8_t *out_bufferRGB = NULL;
pFrameRGB = avcodec_alloc_frame();
//給pFrameRGB幀加上分配的內(nèi)存;
int size = avpicture_get_size(AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height);
out_bufferRGB = new uint8_t[size];
avpicture_fill((AVPicture *)pFrameRGB, out_bufferRGB, AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height);
//YUV to RGB
sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);
memcpy(pCvMat->data,out_bufferRGB,size);
delete[] out_bufferRGB;
av_free(pFrameRGB);
}
轉(zhuǎn)載請注明作者Jason Ding及其出處
Github主頁(http://jasonding1354.github.io/)
CSDN博客(http://blog.csdn.net/jasonding1354)
簡書主頁(http://www.reibang.com/users/2bd9b48f6ea8/latest_articles)