【总结】关于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) )
YUV相关宏定义

再看看关于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
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);
YUV格式和函数
 

下面,我们通过一个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 }
RGB24_TO_YUV420

 为了方便理解,我画了一张图:

  上面的这张图以及比较清晰的展示了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编辑  收藏  举报

导航

TOP