1.实验原理
RGB转YUV的公式
- Y=0.30 * R+0.59 * G+0.11 * B
- U=0.493 * (B-Y)
- V=0.877 * (R-Y)
位深度和存储方式
BMP 文件的图像深度可选 lbit、4bit、8bit、16bit 及 24bit,8位图像可以是索引彩色图像外,也可以是灰阶图像。
如果图像是16位、24位,则图像文件中不保存调色板,则不存在调色板数据给出。如果图像是1位、4位或者8位,则紧跟其后的是位图数据,位图数据是指向调色板的索引序号。
BMP文件通常是不压缩的,所以它们通常比同一幅图像的压缩图像文件格式要大很多。根据颜色深度的不同,图像上的一个像素可以用一个或者多个字节表示,它由n/8所确定(n是位深度,1字节包含8个数据位)。图片浏览器等基于字节的ASCII值计算像素的颜色,然后从调色板中读出相应的值。
n位2n种颜色的位图近似字节数可以用下面的公式计算:BMP文件大小约等于 54+4∗2n+(w∗h∗n)/8,其中高度和宽度都是像素数。需要注意的是上面公式中的54是位图文件的文件头,是彩色调色板的大小。另外需要注意的是这是一个近似值,对于n位的位图图像来说,尽管可能有最多2n中颜色,一个特定的图像可能并不会使用这些所有的颜色。由于彩色调色板仅仅定义了图像所用的颜色,所以实际的彩色调色板将小于计算的值。
bmp文件格式
- bmp位图文件头
typedef struct tagBITMAPFILEHEADER {
WORD bfType; /* 说明文件的类型 */
DWORD bfSize; /* 说明文件的大小,用字节为单位 */
/*注意此处的字节序问题
WORD bfReserved1; /* 保留,设置为0 */
WORD bfReserved2; /* 保留,设置为0 */
DWORD bfOffBits; /* 说明从BITMAPFILEHEADER结构开始到实际的图像数据之间的字节偏移量 */
} BITMAPFILEHEADER;
- bmp位图信息头
typedef struct tagBITMAPINFOHEADER {
DWORD biSize; /* 说明结构体所需字节数 */
LONG biWidth; /* 以像素为单位说明图像的宽度 */
LONG biHeight; /* 以像素为单位说明图像的高速 */
WORD biPlanes; /* 说明位面数,必须为1 */
WORD biBitCount; /* 说明位数/像素,1、2、4、8、24 */
DWORD biCompression; /* 说明图像是否压缩及压缩类型:BI_RGB,BI_RLE8,BI_RLE4,BI_BITFIELDS */
DWORD biSizeImage; /* 以字节为单位说明图像大小 ,必须是4的整数倍*/
LONG biXPelsPerMeter; /* 目标设备的水平分辨率,像素/米 */
LONG biYPelsPerMeter; /*目标设备的垂直分辨率,像素/米 */
DWORD biClrUsed; /* 说明图像实际用到的颜色数,如果为0,则颜色数为2的biBitCount次方 */
DWORD biClrImportant; /*说明对图像显示有重要影响的颜色索引的数目,如果是0,表示都重要。*/
} BITMAPINFOHEADER;
- bmp调色板
调色板实际上是一个数组,它所包含的元素与位图所具有的颜色数相同,决定于biClrUsed和biBitCount字段。数组中每个元素的类型是一个RGBQUAD结构,每个1字节。真彩色无调色板部分。所以深度小于等于8时,位图有调色板,称为索引颜色此时文件不存储RGB而从LUT中寻找对应的颜色。由于位深度不超过8位,颜色表不超过256个数值。
typedef struct tagRGBQUAD {
BYTE rgbBlue; /*指定蓝色分量*/
BYTE rgbGreen; /*指定绿色分量*/
BYTE rgbRed; /*指定红色分量*/
BYTE rgbReserved; /*保留,指定为0*/
} RGBQUAD;
bmp实际数据
紧跟在调色板之后的是图像数据字节阵列。对于用到调色板的位图,图像数据就是该像素颜色在调色板中的索引值(逻辑色)。对于真彩色图,图像数据就是实际的 R、G、B值。图像的每一扫描行由表示图像像素的连续的字节组成,每一行的字节数取决于图像的颜色数目和用像素表示的图像宽度。规定每一扫描行的字节数必须是 4 的整倍数,也就是DWORD 对齐的。扫描行是由底向上存储的,这就是说,阵列中的第一个字节表示位图左下角的像素,而最后一个字节表示位图右上角的像素。
下面通过一张24Bit的BMP图像(512x288)说明文件数据的内容
每行16个数据,存储bmp图像。
可看出0x00-0x0D存储位图文件头,0x0E-0x35存储位图信息头,0x36-0x39存储调色板。
0x00~0x01,标记了此文件为BMP文件,内容是B和M两个字母的ASCII码,0x4D42.
0x02~0x05,以字节为单位的文件大小,可以看出其文件大小是0x0006C036,转换成十六进制即为442422,而文件本身是432KB,二者近似相等。而且文件大小的存储方式是低位在前高位在后。
0x12~0x15,00000200,即文件宽度为512。
0x16~0x19,00000120,即文件高度为288。
0x1c~0x1d,0018,即位数为24bit。
2.实验流程分析
- 程序初始化(打开两个文件、定义变量和缓冲区等)
- 读取BMP文件,抽取或生成RGB数据写入缓冲区
读取BMP文件的具体流程 |
---|
读位图文件头(判断是否可读出;判断是否为BMP文件) |
读位图信息头(判断是否读出) |
判断像素的实际点阵数 |
开辟缓冲区,读数据,倒序存放 |
根据每像素位数的不同,执行不同的操作 |
8bit以下 | 16bit | 24bit |
---|---|---|
构造调色板 | 位与移位取像素数据转换为8bit彩色分量写RGB缓冲区 | 直接取像素数据写RGB缓冲区 |
位与移位取像素数据查调色板写RGB缓冲区 |
3. 调用RGB2YUV的函数实现RGB到YUV数据的转换
4. 写YUV文件
5. 程序收尾工作(关闭文件,释放缓冲区)
3.关键代码分析
头文件和函数与上个实验相同,在此省去
共有5幅图像需要读,所以用 for(j = 0; j < 5; j++)
设置循环
设置命令行参数,即先输入bmp文件名称再输入需要显示的帧数。
bmpFileName = argv[j+1];
bmpFrame = atoi(argv[j+6]);
输入bmp文件后,需要证明bmp文件是否存在,读取bmp文件的文件头和信息头,若文件不存在,则说明文件损坏或者读取有错误,可调试
if (((Info_header.biWidth / 8 *Info_header.biBitCount) % 4) == 0)
width= Info_header.biWidth/ 8 * Info_header.biBitCount;
else
width =(Info_header.biWidth*Info_header.biBitCount + 31) / 32 * 4;
/*bmp的一行所占的字节数需要是4的倍数,若不满足,则要补齐位数使其成为4的整数倍,本实验中图片设置的大小是512x288,512÷8=64,满足是4的倍数,所以不需要补齐,若大小是600x338,则需要补齐,但若希望yuv的大小依然是600x338,则需要舍弃补齐的那一位。*/
if ((Info_header.biHeight % 2) == 0)
height = Info_header.biHeight;
else
height = Info_header.biHeight + 1;
/*同样bmp的字节高度也应该是偶数,否则需要加上一行成为偶数。*/
开空间
bmpBuf = (u_int8_t*)malloc(Info_header.biWidth * Info_header.biHeight * 3);//存放rgb缓存数据
/* get the output buffers for a frame */
yBuf = (u_int8_t*)malloc(frameWidth * frameHeight);
uBuf = (u_int8_t*)malloc((frameWidth * frameHeight) / 4);
vBuf = (u_int8_t*)malloc((frameWidth * frameHeight) / 4);
rgbDataOut = (u_int8_t*)malloc(width * height );//存放bmp的数据,开的空间大小为字节宽度x高度
unsigned char *rgb;
rgb = bmpBuf;//定义一个指针指向rgb缓存数据空间
判断是否有调色板
RGBQUAD *pRGB = (RGBQUAD *)malloc(sizeof(RGBQUAD)*(unsigned int)pow(2, Info_header.biBitCount));
在判断调色板是否存在时,注意因为调色板的大小是2的比特数次方,而bmp图片在位深度为1bit,4bit,8bit时存在调色板,而biBitCount=8时,调色板的大小为256,对于之前(unsigned char)pow(2, Info_header.biBitCount)
的程序会超出范围,所以改为unsigned int
。
下面对不同位深度进行分析
1. 24bit
因为24bit位真色彩,没有调色板也不需要补充字节数,直接倒序读出数据再进行rgb转yuv的程序。
fread(bmpBuf, 1, frameWidth*frameHeight * 3, bmpFile);
bmpBuf是存放rgb缓存数据,所以直接读取。
2. 16bit
位与移位取像素数据转换为8bit,彩色分量写RGB缓冲区
fread(rgbDataOut, 1, width * height, bmpFile);
if (Info_header.biCompression == BI_RGB)
{
for (Loop = 0; Loop < height * width; Loop += 2)
{
*rgb = (rgbDataOut[Loop] & 0x1F) << 3;//B
*(rgb + 1) = ((rgbDataOut[Loop] & 0xE0) >> 2) + ((rgbDataOut[Loop + 1] & 0x03) << 6);//G
*(rgb + 2) = (rgbDataOut[Loop + 1] & 0x7C) << 1;//R
rgb += 3;
}
}
rgbDataOut存放bmp的数据,rgb是指向bmpBuf即rgb缓存数据的指针,16bit不是真色彩,所以将读取的bmp文件数据存入rgbDataOut进行处理,再将处理过的数据存入bmpBuf,最后读出。
将第一个字节rgbDataOut[Loop]与0x1F(0001 1111)与运算且左移3位(1111 1000)得B。将第一个字节rgbDataOut[Loop]与0xE0(1110 0000)与运算后右移两位(0011 1000),再将第二个字节rgbDataOut[Loop+1]与0x03(0000 0011)与运算后左移6位(1100 0000),加起来(1100 0011)得G;第一个字节与0x7C(0111 1100)与运算左移1位(1111 1000)得R。
3. 1、4、8bit
for (Loop = 0; Loop < height*width; Loop++)
{
switch (Info_header.biBitCount)
{
case 1:
mask = 0x80;//1000 0000
break;
case 4:
mask = 0xF0;//1111 0000
break;
case 8:
mask = 0xFF;//1111 1111
break;
}
int shiftCnt = 1;//控制调色板的移位
while (mask)
{
unsigned char index = mask == 0xFF ? rgbDataOut[Loop] : ((rgbDataOut[Loop] & mask) >> (8 - shiftCnt * Info_header.biBitCount));
*rgb = pRGB[index].rgbBlue;
*(rgb + 1) = pRGB[index].rgbGreen;
*(rgb + 2) = pRGB[index].rgbRed;
if (Info_header.biBitCount == 8)
mask = 0;
else
mask >>= Info_header.biBitCount;
/* 如果是8位,开始读下一个字节,如果不是,继续读当前字节*/
rgb += 3;
shiftCnt++;
}
}
由于读取文件的缓冲区rgbdataOut是一个字节一个字节读取的,一个字节里包含了多个像素的信息。因此在读取像素索引值时需要定义一个遮罩,遮住不需要的值,读完当前像素值把该蒙版向后移动相应的位数,显示出下一个像素值的信息。不需要的为0,需要的为1。
因为8bit的图像索引值是一个字节,其它的都没有1个字节,比如8bit第一个像素就在第一个字节,4bit的第一个像素在第一个字节的前4位,1bit的第一个像素在第一个字节的第一个,所以8bit的遮罩为1111 1111,4bit的遮罩为1111 0000,1bit的遮罩为1000 0000,然后不断右移以获得不同像素值。
最后读文件
if (RGB2YUV(frameWidth, frameHeight, bmpBuf, yBuf, uBuf, vBuf, flip))
{
printf("error");
return 0;
}
for (i = 0; i < frameWidth*frameHeight; i++)
{
if (yBuf[i] < 16) yBuf[i] = 16;
if (yBuf[i] > 235) yBuf[i] = 235;
}
for (i = 0; i < frameWidth*frameHeight / 4; i++)
{
if (uBuf[i] < 16) uBuf[i] = 16;
if (uBuf[i] > 240) uBuf[i] = 240;
if (vBuf[i] < 16) vBuf[i] = 16;
if (vBuf[i] > 240) vBuf[i] = 240;
}
for (i = 0; i < bmpFrame; i++)
{
fwrite(yBuf, 1, frameWidth * frameHeight, yuvFile);
fwrite(uBuf, 1, (frameWidth * frameHeight) / 4, yuvFile);
fwrite(vBuf, 1, (frameWidth * frameHeight) / 4, yuvFile);
printf("\r...%d", ++videoFramesWritten);
}
4.实验结果
1.错误分析
-读取文件个数的循环和读取帧数的循环共用了一个变量i,所以只能读出第一幅画面的内容。
-因为调色板的大小是2的比特数次方,而bmp图片在位深度为1bit,4bit,8bit时存在调色板,而biBitCount=8时,调色板的大小为256,对于之前(unsigned char)pow(2, Info_header.biBitCount)
的程序会超出范围,所以改为unsigned int
。
-下为错误的程序。最开始没有理清bmpBuf,rgbDataOut的关系,在写程序的时候,用来计算的数据用的是bmpBuf内的数据,而指针用的是rgbDataOut,此时出现的问题是,将rgn缓存数据赋给了bmp本身的数据。与整个实验的思路相反,将顺序颠倒后,也没有再设置一个指向写入rgb缓存数据的指针,导致在执行完这段循环后,继续使用bmpBuf时,超出了空间范围。
for (Loop = 0; Loop < height * width; Loop += 2)
{
*rgbDataOut= (bmpBuf[Loop] & 0x1F) << 3;//B
*(rgbDataOut+ 1) = ((bmpBuf[Loop] & 0xE0) >> 2) + ((bmpBuf[Loop + 1] & 0x03) << 6);//G
*(rgbDataOut + 2) = (bmpBuf[Loop + 1] & 0x7C) << 1;//R
rgbDataOut+= 3;
}
-在调用函数if(RGB2YUV(frameWidth, frameHeight, bmpBuf, yBuf, uBuf, vBuf, flip))
时,依然是没有理清两个数据空间的关系,将bmpBuf写成rgbDataOut ,但因为调用函数时应用的是rgb缓存数据,所以未读出图像。
-开空间的时候,用到了width和height,但是给width和height赋值的程序写在了开空间程序之后,导致开的空间大小错误。
2.实验结果
图像展示(512x288)
命令行参数的输入
结果展示
将命令行参数输入的参数乱序,得到另一个结果,说明实验的正确性