一、基本原理 1.RGB->YUV彩色空间转换公式及其分析
1)由电视原理可知,亮度和色差信号的构成如下:
Y=0.2990R+0.5870G+0.1140B R-Y=0.7010R-0.5870G-0.1140B B-Y=-0.2990R-0.5870G+0.8860B
2)如果亮度信号Y的动态取值为0~1(黑0、白1),那么根据计算R-Y的动态取值为-0.701~0.701(红0.701、青-0.701),B-Y的动态取值为-0.886~0.886(蓝0.886、黄-0.886),为了将R-Y和B-Y的动态取值宽度也变成1,要进行归一化,把动态取值变为-0.5~0.5,由±0.701 * 0.713=±0.5、±0.886 * 0.564=±0.5,得:
V=0.713(R-Y)= -0.1684R-0.3316G+0.5B U=0.564(B-Y)= 0.5R-0.4187G-0.0813B
3)对亮度和色差信号进行8bit量化,为防止信号变动造成过载:亮度信号码电平0255,上端留20级,下端留16级作为信号超越动态范围的保护带,即Y值大于235的一律变成235,小于16的一律变成16;因为色差信号的动态范围在0.5之间,因此色差信号的零电平对应,码电平的128,上端留15级,下端留16级,即大于240的变成240,小于16的变成16。
V=-0.1684R-0.3316G+0.5B+128 U= 0.5R-0.4187G-0.0813B+128
4)色度取样格式 实验中采用4:2:0的取样格式,即U、V的取样频率在水平和垂直方向均为亮度取样的1/2。
2.YUV->RGB彩色空间转换公式及其分析
1)YUV->RGB是RGB->YUV相反的过程,因此根据转换公式可得:
R=Y+1.4075V G=Y-0.3455U-0.7169V B=Y+1.779U
2)由于色差信号整体抬高了128的码电平,因此需要减去:
R=Y+1.4075(V-128) G=Y-0.3455(U-128)- 0.7169(V-128) B=Y+1.779(U-128)
3.yuv和rgb存储方式分析
1)yuv的存储方式 在内存中yuv(4:2:0)格式的文件,以Y(0帧)U(0帧)V(0帧)、Y(1帧)U(1帧)V(1帧)……的方式存储在连续的内存中,其中U、V的数据量是Y的1/4,那么:一帧的数据量 = 水平像素数 * 垂直像素数 * 量化比特数 * 1.5
2)rgb的存储方式 和yuv以帧为单位存储不同,内存中rgb是以像素为单位存储的,每个像素都对应了1字节的R、1字节的G、1字节的B,一个像素用3个字节表示,所以:一帧的数据量 = 水平像素数 * 垂直像素数 * 量化比特数 * 3
二、实验流程分析
主函数流程 1.程序初始化(打开两个文件、定义变量和缓冲区等) 2.读取YUV文件,抽取YUV数据写入缓冲区 3.调用YUV2RGB的函数实现YUV到RGB数据的转换 4.写RGB文件 5.程序收尾工作(关闭文件,释放缓冲区)
YUV2RGB函数流程 1.从主函数中获得输入的yuv文件指针 2.定义相关中间变量及指针(开辟缓冲区) 3.u、v上采样 4.数据转换算法 5.释放缓冲区
三、关键代码及其分析
1)头文件yuv2rgb.h 在同一文件中只能包含同一头文件一次,但main函数和yuv2rgb都要包含,所以加上编译预处理命令。
#ifndef YUV2RGB_H_ /*编译预处理命令*/ #define YUV2RGB_H_ /*编译预处理命令*/ int YUV2RGB(int x_dim,int y_dim,void *bmp,void *y_out,void *u_out,void *v_out,int flip); void InitLookupTable(); #endif /*编译预处理命令*/2)主函数main.cpp
#include<stdio.h> #include <stdlib.h> #include <malloc.h> #include "yuv2rgb.h" #define u_int8_t unsigned __int8 #define u_int unsigned __int32 #define u_int32_t unsigned __int32 #define FALSE false #define TRUE true /* * yuv2rgb * required arg1 should be the input RAW YUV24 file * required arg2 should be the output RAW RGB12 file */ int main (int argc,char** argv) { /* variables controlable from command line */ u_int frameWidth = 352; /* --width=<uint> */ u_int frameHeight =240; /* --height=<uint> */ bool flip = TRUE; /* --flip */ unsigned int i; /* internal variables */ char* rgbFileName = NULL; char* yuvFileName = NULL; FILE* rgbFile = NULL; FILE* yuvFile = NULL; u_int8_t* rgbBuf = NULL; u_int8_t* yBuf = NULL; u_int8_t* uBuf = NULL; u_int8_t* vBuf = NULL; u_int32_t videoFramesWritten = 0; /*处理视频需要定义视频帧数,通过外部参数传入,图片就将参数写为1*/ long frameNum; /* begin process command line */ /* point to the specified file names */ rgbFileName = argv[1]; yuvFileName = argv[2]; frameWidth = atoi(argv[3]); frameHeight = atoi(argv[4]); frameNum = atoi(argv[5]); /* open YUV file */ yuvFile = fopen(yuvFileName, "rb"); if(yuvFile == NULL) { printf("cannot find yuv file\n"); exit(1); } else { printf("The input yuv file is %s\n", yuvFileName); } /* open rgb file */ rgbFile = fopen(rgbFileName, "ab"); if(rgbFile == NULL) { printf("cannot open rgb file\n"); exit(1); } else { printf("The output rgb file is %s\n", rgbFileName); } /* get an input buffer for a frame */ yBuf = (u_int8_t*)malloc(frameWidth * frameHeight * frameNum );//视频转换要乘以帧数 uBuf = (u_int8_t*)malloc((frameWidth * frameHeight * frameNum)/4); vBuf = (u_int8_t*)malloc((frameWidth * frameHeight * frameNum)/4); /* get the output buffers for a frame */ rgbBuf = (u_int8_t*)malloc(frameWidth * frameHeight * 3); if(rgbBuf == NULL||yBuf == NULL||uBuf == NULL||vBuf ==NULL) { printf("no enough memory\n"); exit(1); } while(fread(yBuf, 1, frameWidth * frameHeight, yuvFile),fread(uBuf, 1,(frameWidth * frameHeight)/4 ,yuvFile), fread(vBuf, 1, (frameWidth * frameHeight)/4, yuvFile))//顺序读取YUV文件,一次读一帧,直至读到空,循环结束 { if(YUV2RGB(frameWidth, frameHeight, rgbBuf, yBuf, uBuf, vBuf, flip)) { printf("error"); } fwrite(rgbBuf, 1, frameWidth * frameHeight * 3, rgbFile); printf("\r...%d", ++videoFramesWritten); } printf("\n%u %ux%u video frames written\n", videoFramesWritten, frameWidth, frameHeight); /* clean up */ free(yBuf); free(uBuf); free(vBuf); free(rgbBuf); fclose(yuvFile); fclose(rgbFile); return (0); }3)转换函数yuv2rgb.cpp
/* This file contains YUV to RGB transformation functions. */ #include "stdio.h" //包含printf函数打印溢出像素信息 #include "stdlib.h" #include "yuv2rgb.h" static float YUVRGB14075[256]; static float YUVRGB03455[256], YUVRGB07169[256]; static float YUVRGB1779[256]; int YUV2RGB(int x_dim, int y_dim, void *bmp, void *y_in, void *u_in, void *v_in, int flip) { static int init_done = 0; int m,n;//用于表示溢出像素的行和列 long i, j, size, k; unsigned char *b; float rgb[3];//作为判断越界的中间变量 unsigned char *y, *u, *v; unsigned char *pu1, *pu2, *pv1, *pv2, *psu, *psv; unsigned char *y_buffer, *u_buffer, *v_buffer; unsigned char *sub_u_buf, *sub_v_buf; if(init_done == 0) { InitLookupTable(); init_done = 1; } //check to see if x_dim and y_dim are divisible by 2 if((x_dim%2)||(y_dim%2)) return 1; size = x_dim * y_dim; //allocate memory y_buffer = (unsigned char*)y_in; sub_u_buf = (unsigned char*)u_in; sub_v_buf = (unsigned char*)v_in; u_buffer = (unsigned char*)malloc(size *sizeof(unsigned char)); v_buffer = (unsigned char*)malloc(size *sizeof(unsigned char)); if(!(u_buffer && v_buffer)) { if(u_buffer) free(u_buffer); if(v_buffer) free(v_buffer); return 2; } b=(unsigned char*)bmp; y = y_buffer; u = u_buffer; v = v_buffer; /*************** uv 上采样****************** 对示例中的下采样程序稍作修改即可: 下采样后的u、v是Y的1/4数据量,为了计算rgb方便,先进行上采样转换到和Y一样的数据量,就是将一个值变成周围像素的四个值,输出的数据量变成输入的四倍。 ********************************************/ for(j =0;j < y_dim/2; j++) { //psu、psv指向sub_u_buf、sub_v_buf输入的u、v数据在第一层循环中依次指向下一行第一个数据 psu = sub_u_buf + j * x_dim/2; psv = sub_v_buf + j * x_dim/2; //pu1、pu2(pv1、pv2)指向输出的u_buffer(v_buffer)偶数行和奇数行,在第一层循环中一次下移两行 pu1 = u_buffer + 2 * j * x_dim; pu2 = u_buffer +( 2 * j +1) * x_dim; pv1 = v_buffer + 2 * j * x_dim; pv2 = v_buffer +( 2 * j +1) * x_dim; for(i = 0;i < x_dim/2; i++ ) { //u转换 *pu1 = *psu; *(pu1+1) = *psu;//为偶数行相邻两元素赋相同的值 *pu2 = *psu; *(pu2+1) = *psu;//为奇数行相邻两元素赋相同的值 //v转换 *pv1 = *psv; *(pv1+1) = *psv;//为偶数行相邻两元素赋相同的值 *pv2 = *psv; *(pv2+1) = *psv;//为奇数行相邻两元素赋相同的值 //移动指针 psu ++;//取下一个值 psv ++; pu1 += 2;//跳过已赋值的一个 pu2 += 2; pv1 += 2; pv2 += 2; } } /*********** convert YUV to RGB ************/ for(i=0;i<size;i++) { /******************************** debug by zml 1、直接用了unsigned char强制类型转换float,超出0~255范围的数以补码的形式直接截取后8位,导致一些点颜色不对 2、在这里对u、v减了128导致整个画面偏紫色(错误图在实验结果分析中) g=b+1; r=b+2; *r = (unsigned char)((*y) + YUVRGB14075[(*v)-128]); *g = (unsigned char)((*y) - YUVRGB03455[(*u)-128]-YUVRGB07169[(*v)-128]); *b = (unsigned char)((*y) + YUVRGB1779[(*u)-128]); ************************************************/ rgb[2]=((*y) + YUVRGB14075[(*v)]);//计算R,存储是bgr的顺序,方便起见数组倒过来 rgb[1]=((*y) - YUVRGB03455[(*u)]-RYUVRGB07169[(*v)]); //计算G rgb[0]=((*y) + YUVRGB1779[(*u)]);//计算B for(k=0;k<3;k++) { if((rgb[k]>=0)&&(rgb[k]<=255))//判断计算值是否越界 //不越界,float强制类型转换为unsigned char *(b++) = rgb[k]; else { //越界则赋值为上下限 *(b++) = (rgb[k] < 0)?0:255; m=(int)(i/256); n= i%256; printf("第%d行,第%d个像素溢出,%d值为%f\n",m,n,k,rgb[k]);//打印溢出像素值及位置 } } y++; u++; v++; } //释放空间 free(u_buffer); free(v_buffer); return 0; } void InitLookupTable() { int i; for(i=0;i<256;i++) { /**** debug by zml 一开始在转换循环里减了128 ****/ YUVRGB14075[i] = (float)1.4075 * (i-128); YUVRGB03455[i] = (float)0.3455 * (i-128); YUVRGB07169[i] = (float)0.7169 * (i-128); YUVRGB1779[i] = (float)1.779 * (i-128); } }四、实验结果及分析
左边是yuv格式的原图像,右边是转成rgb格式再转回yuv的图像。
Figure 1 示例图片256*256错误图: 1、直接用了unsigned char强制类型转换float,超出0~255范围的数以补码的形式直接截取后8位,导致一些点颜色不对 2、在这里对u、v减了128导致整个画面偏紫色
根据打印的信息可知,若没有判断计算出的RGB值是否在0~255内,并为其赋值为上下限,以下这些点会出错(ctrl+F5“开始执行,不调试”能够使窗口不一闪而过):
错误信息较多,仅截屏两张作为示例 从打印的信息可以看出,错误集中在图片下方;错误大部分对应参数rgb[k=2],即r(红色):与出现的错误图符合。 Figure 2自选视频cif格式352*288 24帧 Figure 3自选视频cif格式352*288 300帧 Figure 4自选视频cif格式352*288 250帧五、结论 通过这次实验,对yuv和rgb图像格式的转换原理有了更直观的认识,了解了它们是如何存储的,深刻地理解了色度取样是如何进行的。在仿写代码和不断查错的过程中,增加了编程的知识,比如怎么在vs中配置命令参数,怎么解决“fatal error LNK1123: 转换到 COFF 期间失败”这样的错误等,除了基本的算法,还有很多细节的问题需要考虑,比如说越界、强制类型转换带来的问题。