RGB和YUV文件的组成
(1)RGB
RGB文件保存了没有经过任何压缩处理的原始图像,R,G,B表示一个像素红绿蓝三个颜色通道。没有文件头可供解析宽高等图像信息。
存储方式:文件数据按照B,G,R的顺序依次排列,也就是每相邻的3个字节保存了一个像素的信息。存储顺序为按行从左向右存储。
数据量:常用量化比特数为8bit,也就是像素的一个通道占用1字节,每3个字节表示了一个像素的颜色值。例如一个分辨率为640×480的RGB文件,那么占用空间为640×480×3=921,600B=900KB。 (1k=1024B)
(2)YUV
YUV文件的由亮度通道Y和色度通道U、V来表示一个像素的颜色,同样也没有文件头。
存储方式:三个通道按照YUV的顺序按行从左向右分别存储。例如一个分辨率为640×480的YUV文件,前面连续的640×480个字节存储了Y信息,中间320×240个字节存储了U信息,最后320×240个字节存储了V信息。
数据量:实验中亮度信息Y为全分辨率尺寸;色度信息使用4:2:0采样格式进行压缩,即色差信号U,V的取样频率为亮度信号取样频率的四分之一,在水平方向和垂直方向上的取样点数均为Y的一半。如上同样尺寸的图像,占用空间为640×480×1.5=460,800B=450KB。
转换公式
首先由三个颜色通道RGB计算出亮度信号Y和两个色差信号R-Y,B-Y的值:
Y=0.299R+0.587G+0.114B
R−Y=0.701R−0.587G−0.114B
B−Y=−0.299R−0.587G+0.886B
对于模拟信号,这样的色差信号超过了规定的电平范围,因此需要将色差信号的幅度进行压缩。压缩后的的色差信号用U、V表示,与R-Y,G-Y的关系如下:
V=0.877(R−Y),U=0.493(G−Y)
对于数字信号,为了减少数据量也需要对色差信号进行压缩。压缩过的色差信号用Cb,Cr表示,为了使Y,Cb,Cr的幅度相同,转换公式与上面模拟信号的转换系数略有差异:
Cr=0.713(R−Y)=0.500R−0.4187G−0.0813B
Cb=0.564(B−Y)=−0.1684R−0.3316G+0.500B
为了使得量化后的色差信号幅度没有负值,还要加128的偏置,最终结果为:
Cr=0.500R−0.4187G−0.0813B+128
Cb=−0.1684R−0.3316G+0.500B+128
为了方便起见,本实验对于色差信号仍以U、V来表示。V对应数字分量的Cr,U对应数字分量的Cb。则RGB转YUV的公式如下
Y=0.299R+0.587G+0.114B+0
U=−0.1684R−0.3316G+0.5B+128
V=0.5R−0.417G−0.0813B+128
码电平分配
在对亮度信号Y进行8比特均匀量化时,为了防止信号变动造成过载,总共占有220个量化级。在256级上端留20级,下端留16级作为信号超越动态范围的保护带。色差信号U、V经过归一化处理后,总共占225个量化级。在256级上端留15级,下端留16级作为信号超越动态范围的保护带。
因此在用上述公式对亮度信号Y和色差信号U、V计算完毕后,对于那些超过动态范围的颜色值要进行相应的处理。
yuv2rgb.cpp部分
转换色彩空间用的函数YUV2RGB的流程由以下几步构成
(1)使用查找表
与RGB2YUV函数类似,使用static型数组在第一次调用函数时计算出所有要算的数值并保存下来,以便加快计算速度。根据第一部分中的公式,带有小数系数的只有U、V分量,而U、V的值已经限定为了225个量化级,也就是从16到240,因此for循环中不必从0计算到255。
//--------yuv2rgb.cpp--------
//头文件,查找表数组的声明略过
#define u_int8_t unsigned __int8
#define u_int unsigned __int32
int YUV2RGB(int width, int height, void* bmp, void* y, void* u, void* v)
...
//初始化查找表
static u_int initFlag = 0;
if (initFlag == 0)
{
InitLookupTable();
initFlag = 1;
}
//查找表函数
void InitLookupTable()
{
u_int i;
for (i = 16; i < 241; i++) YUVRGB14020[i] = (float)1.4020*i;
for (i = 16; i < 241; i++) YUVRGB03441[i] = (float)0.3441*i;
for (i = 16; i < 241; i++) YUVRGB07139[i] = (float)0.7139*i;
for (i = 16; i < 241; i++) YUVRGB17718[i] = (float)1.7718*i;
for (i = 16; i < 241; i++) YUVRGB00013[i] = (float)0.0013*i;
}
(2)创建缓冲区
y、u、v及bmp四个缓冲区均为函数参数,由于U、V尺寸为Y的1/4,宽高都减半,若要计算出每个像素RGB的值,需要将它们上采样至Y的尺寸,因此还需要创建两个上采样后的缓冲区up_uBuf和up_vBuf。
u_int8_t* yBuf = (u_int8_t*)y;
u_int8_t* uBuf = (u_int8_t*)u;
u_int8_t* vBuf = (u_int8_t*)v;
u_int8_t* rgbBuf = (u_int8_t*)bmp;
u_int8_t* up_uBuf = (u_int8_t*)malloc(width*height);
u_int8_t* up_vBuf = (u_int8_t*)malloc(width*height);
if (up_uBuf == NULL || up_vBuf == NULL)
{
return 1;
}
(3)色度分量的上采样
关键
在YUV2RGB的上采样部分,对于全分辨率的色差缓冲区(以U为例)up_uBuf使用了两个指针pu1,pu2指向相邻的偶数行和奇数行;对于1/4分辨率的色差缓冲区uBuf使用了一个指针psu指向每一行。外层循环每+1说明一行已处理完毕,对于psu需要移到下一行,使用+n*width/2实现;对于pu1和pu2需要移动两行,使用+2*n*width和+(2*n+1)*width实现。
原图像的部分色彩信息已被丢弃,可以有多种插值方法进行恢复。这里仅将其颜色数值简单扩展。内层循环中,将1/4分辨率的色差区域的一个值赋给2×2区域中的所有值,也就是pu1,pu1+1,pu2,pu2+1所指向的四个像素为同一颜色值,之后指针进行更新。
u_int8_t *psu, *psv, *pu1, *pu2, *pv1, *pv2;
u_int m,n;
for (n = 0; n < height / 2; n++)
{
psu = uBuf + n*width/2;
psv = vBuf + n*width/2;
pu1 = up_uBuf + 2 * n * width;
pu2 = up_uBuf + (2 * n + 1)*width;
pv1 = up_vBuf + 2 * n * width;
pv2 = up_vBuf + (2 * n + 1)*width;
for (m = 0; m < width/2; m++)
{
*pu1 = *pu2 = *(pu1 + 1) = *(pu2 + 1) = *psu;
*pv1 = *pv2 = *(pv1 + 1) = *(pv2 + 1) = *psv;
psu++;
psv++;
pu1 += 2;
pu2 += 2;
pv1 += 2;
pv2 += 2;
}
}
(4)YUV按照公式转换成RGB
在计算RGB值的过程中,由于误差等原因,可能计算出的值超过了0到255。这时如果直接强制类型转换只能保留低8位的值,那些大于255的值由于进位,高位已经变成0,转换后会得到一个很小的接近0的数,也就是变得接近黑色;小于0的负数由于补码的原因,高位都是1,转换后会得到一个很大的接近255的数,也就是变得接近白色。
这些错误的计算在三个通道中混合在一起,在图像中的某些位置会表现出杂乱的色点。因此要先用浮点型数据rgb_tmp保留计算结果,再对那些超出范围的数值进行处理,处理完毕后再强制类型转换,就不会出现颜色值溢出的问题。
u_int8_t *rgb, *up_u, *up_v;
rgb = rgbBuf;
up_u = up_uBuf;
up_v = up_vBuf;
u_int i;
for (i = 0; i < width*height; i++)
{
//计算RGB数值
double rgb_tmp[3] = { 0 };
*rgb_tmp = (*yBuf + YUVRGB17718[*up_u] - YUVRGB00013[*up_v]-226.6286);//B
*(rgb_tmp + 1) = (*yBuf - YUVRGB03441[*up_u] - YUVRGB07139[*up_v]+135.4193);//G
*(rgb_tmp + 2) = (*yBuf + YUVRGB14020[*up_v]-179.4497);//R
//对超出范围值的处理
if (*rgb_tmp < 0) *rgb_tmp = 0;
if (*rgb_tmp > 255) *rgb_tmp = 255;
if (*(rgb_tmp + 1) < 0) *(rgb_tmp + 1) = 0;
if (*(rgb_tmp + 1) > 255) *(rgb_tmp + 1) = 255;
if (*(rgb_tmp + 2) < 0) *(rgb_tmp + 2) = 0;
if (*(rgb_tmp + 2) > 255) *(rgb_tmp + 2) = 255;
//量化成整数
*rgb = (u_int8_t)*rgb_tmp;
*(rgb + 1) = (u_int8_t)*(rgb_tmp + 1);
*(rgb + 2) = (u_int8_t)*(rgb_tmp + 2);
//下一个像素
rgb += 3;
yBuf++;
up_u++;
up_v++;
}
mainYUV2rgb.cpp部分
while (fread(yBuf, 1, frameHeight*frameWidth, yuvFile) &&
fread(uBuf, 1, frameHeight*frameWidth / 4, yuvFile) &&
fread(vBuf, 1, frameHeight*frameWidth / 4, yuvFile))
这里重点内容是通过每 一祯读取文件中的像素。开始的尝试的时候把宽高设置错误,导致读的祯中对应像素的yuv没有一一对应。比如视频是176 144的但是设置宽高的时候设置成177 144就会少读视频的总祯数
这之后在进行rgb2yuv转换,就会提示错误
#include
#include
#include
#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
int main(int argc, char** argv)
{
char* yuvFileName = argv[1];
char* rgbFileName = argv[2];
u_int frameWidth = atoi(argv[3]);
u_int frameHeight = atoi(argv[4]);
u_int32_t frames = 0;
//打开文件
FILE* yuvFile = fopen(yuvFileName, "rb");
if (yuvFile == NULL)
{
printf("Cannot open the YUV file.\n");
exit(1);
}
else
{
printf("The YUV file is %s\n",yuvFileName);
}
FILE* rgbFile = fopen(rgbFileName, "wb");
if (rgbFile == NULL)
{
printf("Cannot open the RGB file.\n");
exit(1);
}
else
{
printf("The RGB file is %s\n", rgbFileName);
}
//创建buf
u_int8_t* yBuf = (u_int8_t*)malloc(frameHeight*frameWidth);
u_int8_t* uBuf = (u_int8_t*)malloc(frameHeight*frameWidth / 4);
u_int8_t* vBuf = (u_int8_t*)malloc(frameHeight*frameWidth / 4);
u_int8_t* rgbBuf = (u_int8_t*)malloc(frameHeight*frameWidth * 3);
if (yBuf == NULL || uBuf == NULL || vBuf == NULL || rgbBuf == NULL)
{
printf("Not enough memory\n");
exit(1);
}
//读取文件并开始操作
while (fread(yBuf, 1, frameHeight*frameWidth, yuvFile) &&
fread(uBuf, 1, frameHeight*frameWidth / 4, yuvFile) &&
fread(vBuf, 1, frameHeight*frameWidth / 4, yuvFile))
/*当读入成功时fread函数返回实际读取的数据项个数,当整帧的YUV都能读
入时,进入while循环。由于文件指针自动向后移动到下一个未读字节,直到把视频文件每一祯全部读完,整段视频被读完fread函数返回0,则结束循环。*/
{
if (YUV2RGB(frameWidth, frameHeight, rgbBuf, yBuf, uBuf, vBuf))
{
printf("error\n");
return 0;
}
fwrite(rgbBuf, 1, frameHeight*frameWidth * 3, rgbFile);
printf("\r...%d", ++frames);
}
printf("\n%u %ux%u video frames written\n", frames, frameWidth, frameHeight);
//清理内存
free(rgbBuf);
free(yBuf); free(uBuf); free(vBuf);
fclose(rgbFile);
fclose(yuvFile);
system("pause");
return 0;
}
补充说明:
1.设置main()中的argv[]参数
char* rgbFileName = argv[1];
char* yuvFileName = argv[2];
u_int frameWidth = atoi(argv[3]);
u_int frameHeight = atoi(argv[4]);
通过在项目属性中进行设置,命令参数中分别填入输入文件,输出文件,宽,高并以空格间隔
在工作目录中填入存放输入输出文件的地址
这里对应:
rgbFileName->bridgeRE.rgb
yuvFileName->bridgeRE.yuv
frameWidthf->176
rameHeight->144
2.
if (yBuf == NULL || uBuf == NULL || vBuf == NULL || rgbBuf == NULL)
{
printf("Not enough memory\n");
exit(1);
[一般情况下退出值是0表示正常(exit(0)),其它情况都是不正常的]
3.
while (fread(yBuf, 1, frameHeight * frameWidth, yuvFile)
fread()函数用于从文件流中读取数据,其原型为:
’ size_t fread(void buffer, size_t size, size_t count, FILE stream);’
【参数】buffer为接收数据的地址,size为一个单元的大小,count为单元个数,stream为文件流。
fread()函数每次从stream中最多读取count个单元,每个单元大小为size个字节,将读取的数据放到buffer;文件流的位置指针后移 size * count 字节
分析
可以明显的看到在原图中白色的天空变成了偏蓝色,也就是相对应位置的色度值减小了。这是因为下采样时对四个像素点求平均值所导致的