上一篇介绍了YUV格式,并给出了一个YUYV422转RGB24的例子。其实,FFmpeg有一个函数专门进行图像格式转换的。本文就介绍怎么用FFmpeg转换,因为在转换时还要用到AVFrame这个结构体,所以这里也会介绍AVFrame。在FFmpeg中,AVFrame是一个比较重要的结构体。
AVFrame,顾名思义,这个结构体应该是保存视频帧的信息的。像一帧图像也是可以保存在AVFrame结构中。事实上,我们可以直接从一个YUV文件中,把一张YUV图像数据读到AVFrame中。本文后面的例子也是这样做的。
为了弄懂AVFrame是怎么存放一张YUV图像的(当然AVFrame可以存放其他格式图像的),现在先看一下AVFrame结构体的主要成员。
typedef struct AVFrame { #define AV_NUM_DATA_POINTERS 8 uint8_t * data [AV_NUM_DATA_POINTERS]; //指向图像数据 int linesize [AV_NUM_DATA_POINTERS]; //行的长度 int width; //图像的宽 int height; //图像的高 int format; //图像格式 …… }AVFrame;
注意到data成员是一个指针数组。其指向的内容就是图像的实际数据。
可以用av_frame_alloc(void)函数来分配一个AVFrame结构体。这个函数只是分配AVFrame结构体,但data指向的内存并没有分配,需要我们指定。这个内存的大小就是一张特定格式图像所需的大小,如前一篇博文中说到的,对于YUYV422格式,所需的大小是width * height * 2。所以AVFrame结构体的整个初始化过程如下:
AVFrame* frame = av_frame_alloc(); //这里FFmpeg会帮我们计算这个格式的图片,需要多少字节来存储 //相当于前一篇博文例子中的width * height * 2 int bytes_num = avpicture_get_size(AV_PIX_FMT_YUV420P, width, height); //AV_PIX_FMT_YUV420P是FFmpeg定义的标明YUV420P图像格式的宏定义 //申请空间来存放图片数据。包含源数据和目标数据 uint8_t* buff = (uint8_t*)av_malloc(bytes_num); //前面的av_frame_alloc函数,只是为这个AVFrame结构体分配了内存, //而该类型的指针指向的内存还没分配。这里把av_malloc得到的内存和AVFrame关联起来。 //当然,其还会设置AVFrame的其他成员 avpicture_fill((AVPicture*)frame, buff, AV_PIX_FMT_ YUV420P,width, height);
看到这里,可能有些读者会疑问:data成员是一个指针数组(即数组里面的每一个元素都是一个指针),一个buff怎么够用(多对一的关系)。其实,这就是FFmpeg设计的一个巧妙之处。还记得前一篇博文说到的 图像物理存储有 planar和packed两种模式吗?这个data指针数组就是为了planar设计的。对于planar模式的YUV。data[0]指向Y分量的开始位置、data[1]指向U分量的开始位置、data[2]指向V分量的开始位置。对于packed模式YUV,data[0]指向数据的开始位置,而data[1]和data[2]都为NULL。
在上面的代码中,运行avpicture_fill后,data[0]将指向buff的开始位置,即data[0]等于buff。data[1]指向buff数组的某一个位置(该位置为U分量的开始处),data[2]也指向buff数组某一个位置(该位置为V分量的开始处)。
有些网友说到,对于planar模式,需要分开读取和写的。其实,无论是planar还是packed模式,在用acpicture_fill函数处理后,都可以用下面的方法把一张图像的数据读取到AVFrame中,而不需要分别读data[0]、data[1]、data[2]。因为对于图像文件来说,如果是plannar模式的图像格式,其存储必然是先存完一张图像所有的所有Y、再存一张图像的所有U、最后存一张图像的所有V。这刚好和data数组的三个指针的对应的。
fread(frame->data[0], 1, bytes_num, fin);同样对于写图像也是如此。无需分data[0]、data[1]、data[2]。
扯了这么多,还没说FFmpeg是怎么转换图像格式的。现在来说一下。
FFmpeg定义了一个结构体SwsContext,它记录进行图像格式转换时,源图像和目标图像的格式、大小分别是什么。然后用sws_scale函数直接转换即可。过程如下:
SwsContext* sws_ctx = sws_getContext(width, height, AV_PIX_FMT_YUV420P, width, height, AV_PIX_FMT_YUYV422, SWS_BICUBIC, NULL, NULL, NULL); sws_scale(sws_ctx, src_frame->data, src_frame->linesize, 0, height, //源图像的高 dst_frame->data, dst_frame->linesize);
#ifdef __cplusplus #define __STDC_CONSTANT_MACROS #ifdef _STDINT_H #undef _STDINT_H #endif # include <stdint.h> #endif extern "C" { #include<libavcodec/avcodec.h> #include<libavformat/avformat.h> #include<libavutil/log.h> #include<libswscale/swscale.h> } #include<stdio.h> #include <windows.h> //for saveAsBitmap bool saveAsBitmap(AVFrame *pFrameRGB, int width, int height, int iFrame) { FILE *pFile = NULL; BITMAPFILEHEADER bmpheader; BITMAPINFO bmpinfo; char fileName[32]; int bpp = 24; // open file sprintf(fileName, "frame%d.bmp", iFrame); pFile = fopen(fileName, "wb"); if (!pFile) return false; bmpheader.bfType = ('M' <<8)|'B'; bmpheader.bfReserved1 = 0; bmpheader.bfReserved2 = 0; bmpheader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); bmpheader.bfSize = bmpheader.bfOffBits + width*height*bpp/8; bmpinfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmpinfo.bmiHeader.biWidth = width; bmpinfo.bmiHeader.biHeight = -height; //reverse the image bmpinfo.bmiHeader.biPlanes = 1; bmpinfo.bmiHeader.biBitCount = bpp; bmpinfo.bmiHeader.biCompression = BI_RGB; bmpinfo.bmiHeader.biSizeImage = 0; bmpinfo.bmiHeader.biXPelsPerMeter = 100; bmpinfo.bmiHeader.biYPelsPerMeter = 100; bmpinfo.bmiHeader.biClrUsed = 0; bmpinfo.bmiHeader.biClrImportant = 0; fwrite(&bmpheader, sizeof(BITMAPFILEHEADER), 1, pFile); fwrite(&bmpinfo.bmiHeader, sizeof(BITMAPINFOHEADER), 1, pFile); uint8_t *buffer = pFrameRGB->data[0]; for (int h=0; h<height; h++) { for (int w=0; w<width; w++) { fwrite(buffer+2, 1, 1, pFile); fwrite(buffer+1, 1, 1, pFile); fwrite(buffer, 1, 1, pFile); buffer += 3; } } fclose(pFile); return true; } int main(int argc, char** argv) { const char* filename = argc > 1 ? argv[1] : "flower_cif.yuv"; FILE* fin = fopen(filename, "rb"); if( fin == NULL ) { printf("can't open the file\n"); return -1; } int width = 352; int height = 288; AVPixelFormat src_fmt = AV_PIX_FMT_YUV420P; AVPixelFormat dst_fmt = AV_PIX_FMT_YUYV422; AVFrame* src_frame = av_frame_alloc(); AVFrame* dst_frame = av_frame_alloc(); if( src_frame == NULL || dst_frame == NULL ) { printf("av_frame_alloc fail\n"); return -1; } //这里FFmpeg会帮我们计算这个格式的图片,需要多少字节来存储 //相当于前面例子中的width * height * 2 int src_bytes_num = avpicture_get_size(src_fmt, width, height); int dst_bytes_num = avpicture_get_size(dst_fmt, width, height); //申请空间来存放图片数据。包含源数据和目标数据 uint8_t* src_buff = (uint8_t*)av_malloc(src_bytes_num); uint8_t* dst_buff = (uint8_t*)av_malloc(dst_bytes_num); //前面的av_frame_alloc函数,只是为这个AVFrame结构体分配了内存, //而该类型的指针指向的内存还没分配。这里把av_malloc得到的内存和AVFrame关联起来。 //当然,其还会设置AVFrame的其他成员 avpicture_fill((AVPicture*)src_frame, src_buff, src_fmt, width, height); avpicture_fill((AVPicture*)dst_frame, dst_buff, dst_fmt, width, height); //这里主要说明linesize这个成员的含义。不想看可以忽略 //YUV格式中有一个很重要的等量关系,那就是有多少个像素就有多少个y。 //linesize正如其名,一条线(即一行)的大小。对于yuv420p。data[0]存放的是y,对应地linesize[0]就 //指明一行有多少个y。对于352*288的图像,一行有352个像素。根据刚才的等量关系。那么linesize[0]就 //应该为352.即一行有352个y。对于linesize[1],因为data[1]存放的是u。而一行352个像素在yuv420p格式中, //其只需352/2,即176个。所以linesize[1]的大小为176。同理linesize[2]也为176。 //而对于yuyv422格式。data[0]这一行要负责存放y、u、v这三个分量。而y:u:v = 2:1:1的关系。根据前面所说的 //等量关系,y等于352(相对于352*288大小的图像来说),u和v都等于352/2 。所以u+v等于352。所以linesize[0] //等于352*2. printf("%d %d %d\n", src_frame->linesize[0], src_frame->linesize[1], src_frame->linesize[2]); printf("%d %d %d \n", dst_frame->linesize[0], dst_frame->linesize[1], dst_frame->linesize[2]); //对转换进行配置。这里要设置转换源的大小、格式和转换目标的大小、格式 //设置后,下面就可以直接使用sws_scale函数,进行转换 SwsContext* sws_ctx = sws_getContext(width, height, src_fmt, width, height, dst_fmt, SWS_BICUBIC, //SWS_BILINEAR, NULL, NULL, NULL); if( sws_ctx == NULL) { printf("sws_getContext fail "); return -1; } FILE* fout = fopen("yuyv422.yuv", "wb"); int count = 0; while( 1 ) { int ret = fread(src_frame->data[0], 1, src_bytes_num, fin); if( ret != src_bytes_num ) { printf("don't read enough data %d\n", ret); break; } sws_scale(sws_ctx, src_frame->data, src_frame->linesize, 0, height, dst_frame->data, dst_frame->linesize); ret = fwrite(dst_frame->data[0], 1, dst_bytes_num, fout); if( ret != dst_bytes_num ) printf("don't write enough data %d \n", ret); //如果要保存为BMP格式,要把目标图像的格式设置为RGB24。 //只需把前面的AVPixelFormat dst_fmt = AV_PIX_FMT_YUYV422; //改成AVPixelFormat dst_fmt = AV_PIX_FMT_RGB24;即可 saveAsBitmap(dst_frame, width, height, count++); } av_free(src_frame); av_free(dst_frame); av_free(src_buff); av_free(dst_buff); sws_freeContext(sws_ctx); fclose(fin); fclose(fout); return 0; }
例子中,还可以把图像保存成bmp图片。那个函数来自:http://blog.csdn.net/ajaxhe/article/details/7340508。在那个博客的另外一篇博文中,还说到怎么保存成一个jpeg文件。不过需要用到libjpeg这个库。
例子中用到的YUV420P格式的文件,可以到这里下载。