【总结】关于YUV-RGB格式转换的一些个人理解
这段时间一直在研究YUV的格式问题例如YUV422、YUV420,在网上搜索了很多这方面的资料,发现很多资料讲的东西是重复的,没有比较深入的讲解,所以看了之后印象不是很深,过了一段时间之后又对它们有了困惑。所以就有了一个想法,打算自己写一个c语言小程序,通过对BMP文件的RGB数据读取,然后将得到的RGB数据转换成为YUV格式的文件,然后用YUV的播放器打开,查看是否解析正确。(在这里,BMP文件我选择了24bpp的格式,为了方便。)通过这样的方式,正向和逆向学习来加深YUV文件的认识。
经过了2天的努力,从想法到实践,终于完成了YUV422和YUV420的生成,基本上对YUV的几种常见格式有了一些深入的看法,由于本人的水平有限,如果理解有出入,还请各位网友能够指出。
一、基本思路
先附上主要程序的流程图:
在正式讲解代码前,我们先来了解YUV的分类 :
- packed: 打包模式,即Y、U、V数据是连续排布的
- planar: 平面模式,即Y、U、V是各自分开存放的
- semi-planar: 半平面模式,即Y单独存放, UV一起存放
上面的3种是主要内存排布的分类,至于详细的排布如下(主要是YUV422、YUV420):(我们使用一张分辨率为 height*width的图片为例子)
YUV422 planar:
整个YUV文件中,Y、U、V是一个大数组,而其中我们可以看成是三个小数组,分别是Y数组(长度为Height*Width)、U数组(长度为Height*Width/2)、V数组(长度为Height*Width/2)。
YUV422 Semi planar:
YUV420 planar :
整个YUV文件中,Y、U、V是一个大数组,而其中我们可以看成是三个小数组,分别是Y数组(长度为Height*Width)、U数组(长度为Height*Width/4)、V数组(长度为Height*Width/4)。
YUV420 semi planar:
420planar和420semi-planar的区别:
通过上面的示例,我们就可以知道Y、U、V数据在内存中的排布。那么我们怎么将RGB数据转换成为YUV数据呢?先别急,我们需要获取RGB数据,那么问题来了,怎么样得到RGB数据呢?我们使用的方法是读取BMP文件中的RGB数据(BMP是一种未经过压缩的图片格式)。所以接下来,我们就需要通过读取BMP文件,来得到RGB数据。关于BMP的文件格式,本文不讲解,网上有很多讲解的实例,我们会在后面的代码贴出我们的获取RGB的方式。至于后面RGB转换成为YUV的部分,我们也通过代码进行讲解。整个工程中使用了一些常用的C语言的技巧,包括文件操作、函数指针+回调、指针运算、字节对齐+编译器指令+倒序读取BMP等,同时后面会将工程放在github上面。
二、实验代码
先来看一些宏定义,这些宏定义主要是为了方便读代码和灵活性:
1 // PIX_SIZE 表示的是图片的像素大小,例如一张分辨率为320*240的图片,它的(PIX_SIZE)=320*240 2 // 下面的宏表示 3种YUV格式 所占用的内存空间大小 3 #define YUV444MEMORY_SIZE(PIX_SIZE) ( (PIX_SIZE)*3 ) 4 #define YUV422MEMORY_SIZE(PIX_SIZE) ( (PIX_SIZE)*2 ) 5 #define YUV420MEMORY_SIZE(PIX_SIZE) ( (PIX_SIZE)*3/2 ) 6 7 /**< 下面的宏是基于planar格式的 */ 8 // yuv420中的U分量起始的偏移量 9 #define YUV420MEMORY_U_OFFSET(PIX_SIZE) ( (PIX_SIZE) ) 10 // yuv420中的V分量起始的偏移量 11 #define YUV420MEMORY_V_OFFSET(PIX_SIZE) ( YUV420MEMORY_U_OFFSET(PIX_SIZE) + (PIX_SIZE)*1/4 ) 12 13 // yuv422中的U分量起始的偏移量 14 #define YUV422MEMORY_U_OFFSET(PIX_SIZE) ( (PIX_SIZE) ) 15 // yuv422中的V分量起始的偏移量 16 #define YUV422MEMORY_V_OFFSET(PIX_SIZE) ( YUV422MEMORY_U_OFFSET(PIX_SIZE) + (PIX_SIZE)*1/2 ) 17 18 // yuv444中的U分量起始的偏移量 19 #define YUV444MEMORY_U_OFFSET(PIX_SIZE) ( (PIX_SIZE) ) 20 // yuv444中的V分量起始的偏移量 21 #define YUV444MEMORY_V_OFFSET(PIX_SIZE) ( YUV444MEMORY_U_OFFSET(PIX_SIZE) + (PIX_SIZE) )
再看看关于BMP的相关定义及对外的接口函数:
1 #ifndef _BMP_H 2 #define _BMP_H 3 4 #include "common.h" 5 6 #pragma pack(1) 7 /**< BMP文件头 */ 8 typedef struct BMPHEADER_S { 9 WORD bfType; /* 说明文件的类型 */ 10 DWORD bfSize; /* 说明文件的大小,用字节为单位*/ 11 /* 注意此处的字节序问题*/ 12 WORD bfReserved1; /* 保留,设置为0 */ 13 WORD bfReserved2; /* 保留,设置为0 */ 14 DWORD bfOffBits; /* 说明从BMPHEADER_S结构开始到实际的图像数据之间的字节偏移量 */ 15 }BMPHEADER_S; 16 /**< BMP文件信息头 */ 17 typedef struct BMPINFOHEADER_S { 18 DWORD biSize; /* 说明结构体所需字节数*/ 19 DWORD biWidth; /* 以像素为单位说明图像的宽度*/ 20 DWORD biHeight; /* 以像素为单位说明图像的高速*/ 21 WORD biPlanes; /* 说明位面数,必须为1 */ 22 WORD biBitCount; /* 说明位数/像素,1、2、4、8、24 */ 23 DWORD biCompression; /* 说明图像是否压缩及压缩类型BI_RGB,BI_RLE8,BI_RLE4,BI_BITFIELDS */ 24 DWORD biSizeImage; /* 以字节为单位说明图像大小,必须是4的整数倍*/ 25 DWORD biXPelsPerMeter; /* 目标设备的水平分辨率,像素/米 */ 26 DWORD biYPelsPerMeter; /* 目标设备的垂直分辨率,像素/米 */ 27 DWORD biClrUsed; /* 说明图像实际用到的颜色数,如果为0则颜色数为2的biBitCount次方 */ 28 DWORD biClrImportant; /* 说明对图像显示有重要影响的颜色索引的数目,如果是0,表示都重要。*/ 29 }BMPINFOHEADER_S; 30 31 /**< 调色板信息 */ 32 typedef struct RGBQUAD_S { 33 BYTE rgbBlue; /* 指定蓝色分量*/ 34 BYTE rgbGreen; /* 指定绿色分量*/ 35 BYTE rgbRed; /* 指定红色分量*/ 36 BYTE rgbReserved; /* 保留,指定为0*/ 37 }RGBQUAD_S; 38 39 #pragma pack() 40 41 bool ReadBMPHeader(FILE *fpBMP, BMPHEADER_S *psBMPHeader); 42 bool ReadBMPInfoHeader(FILE *fpBMP, BMPINFOHEADER_S *psBMPInfoHeader); 43 unsigned long GetBMPPixSize( BMPINFOHEADER_S sBMPInfoHeader); 44 bool ReadRGBFromBMP(FILE *fpBMP, BMPHEADER_S sBMPHeader, BMPINFOHEADER_S sBMPInfoHeader , unsigned char *pucRGBRead); 45 46 #endif // _BMP_H
还有YUV的数据类型和相关函数定义:
1 typedef enum YUV_E{ 2 YUV444 = 0 , 3 YUV422 , 4 YUV420 5 }YUV_E; 6 7 /**< RGB24转换YUV格式的函数指针 */ 8 typedef int (*FP_RGB24ToYUVFormat)(unsigned char *pucRGBBuffer,int width,int height,unsigned char *pucYUVBuffer); 9 10 int sample_yuv420_split(const char *url, int w, int h,int num); 11 int RGB24_TO_YUV444(unsigned char *pucRGBBuffer,int width,int height,unsigned char *pucYUVBuffer); 12 int RGB24_TO_YUV420(unsigned char *pucRGBBuffer,int width,int height,unsigned char *pucYUVBuffer); 13 int RGB24_TO_YUV422(unsigned char *pucRGBBuffer,int width,int height,unsigned char *pucYUVBuffer); 14 15 bool RGB24_SaveYUVFile(YUV_E eYUV , const char *bmpUrl);
下面,我们通过一个RGB24转换YUV420的函数进行示例讲解:
1 int RGB24_TO_YUV420(unsigned char *pucRGBBuffer,int width,int height,unsigned char *pucYUVBuffer) 2 { 3 unsigned char y, u, v; /**< 一个像素点对应的YUV分量 */ 4 unsigned char *pucY, *pucU, *pucV; /**< Y、U、V数组对应的指针 */ 5 unsigned char r, g, b; /**< 一个像素点对应的RGB分类 */ 6 unsigned char *pucRGB; /**< RGB24对应的指针 */ 7 8 int iY = 0 ; 9 int iX = 0 ; 10 //存储空间大小 Y : width*height ; U : width/2 * height/2 ; V : width/2 * height/2 11 pucY = pucYUVBuffer; 12 pucU = pucYUVBuffer + width*height; /**< U数组在YUV数组的偏移量 */ 13 pucV = pucU + (width*height*1/4); /**< V数组在YUV数组的偏移量 */ 14 15 for (iY = 0; iY < height;iY++){ 16 pucRGB = pucRGBBuffer + width*iY*3 ; 17 18 for ( iX = 0;iX < width ; iX++){ 19 /**< 读取像素点的RGB分量 */ 20 r = *(pucRGB++); 21 g = *(pucRGB++); 22 b = *(pucRGB++); 23 /**< 通过公式将RGB转换成为YUV */ 24 y = (unsigned char)( ( 66 * r + 129 * g + 25 * b + 128) >> 8) + 16 ; 25 u = (unsigned char)( ( -38 * r - 74 * g + 112 * b + 128) >> 8) + 128 ; 26 v = (unsigned char)( ( 112 * r - 94 * g - 18 * b + 128) >> 8) + 128 ; 27 /**< Y分量直接写入Y数组 */ 28 *(pucY++) = clip_value(y,0,255); 29 30 // 采样: 水平一半,垂直一半,实验证明这两种方式都可以 31 if ( iY%2==0 && iX%2 == 0){ 32 /**< 偶数行偶数列时将U分量写入U数组,即一个4*4方块的左上角 */ 33 /* 34 * 0 1 2 3 35 * ———————————————————— 36 *0 | U | | U | | 37 * ------------------- 38 *1 | | | | | 39 * ———————————————————— 40 */ 41 42 *(pucU++) = clip_value(u,0,255); 43 } 44 else{ /**< else表示奇数行的情况 */ 45 /**< 奇数行偶数列时将U分量写入U数组,即一个4*4方块的左下角*/ 46 /* 47 * 0 1 2 3 48 * ———————————————————— 49 *0 | | | | | 50 * ------------------- 51 *1 | V | | V | | 52 * ———————————————————— 53 */ 54 if ( iX%2 == 0 ){ 55 *(pucV++) = clip_value(v,0,255); 56 } 57 } 58 } 59 } 60 return 1; 61 }
为了方便理解,我画了一张图:
上面的这张图以及比较清晰的展示了RGB转换成为YUV的过程,先是从RGB数组中读取一个像素的R、G、B分量,然后通过公式转换成为对应的Y、U、V分量,直接将Y分量写入对应的Y数组;当满足偶数行偶数列时(即一个4*4方格)取一个U分量,写入U数组;当满足奇数行偶数列时(4*4方格的左下角)取V分量,写入V数组。经过二重循环,就可以完成整个RGB到YUV420的转换。至于RGB转换YUV的公式可以去网上找。
通过上面的例子,如果是RGB24转换成为YUV422,我觉得就应该好理解了,和转换成为YUV420的过程类似,但是什么时候取U分量和什么时候取V分量不同而已,具体的可以看最后的工程来验证你的想法。 附上github地址:
https://github.com/qibaowei-guet/RGB24-To-YUV.git (注意:源工程是通过codeblock创建的)
三、总结
实验效果:(通过7yuv软件打开,地址:http://datahammer.de/)
只显示Y分量:
YUV分量全部显示:
原始的BMP文件:
通过比对,可以看到YUV文件的显得比较淡,这主要是因为我们选取的转换公式的原因,公式的细节不深究。毕竟我们不是专门研究这些公式的专家。
最后,因为YUV格式很多,名称也很多,也不是每一种格式我们在工作的时候都会用到,所以,我们只需要掌握一些基本的就够了。个人觉得,如果对这些格式的YUV内存排布如果认识不深,那么在做一些转换的时候,会出现很多的问题,虽然现在也有一些现成的第三方库给我们调用,方便我们进行各种格式的转换,但是基本的原理还是需要懂得的,否则到真正出现问题还是不能很快解决的。
posted on 2018-04-02 11:29 qiabaowei 阅读(1238) 评论(0) 编辑 收藏 举报