一.实验原理
BMP(全称Bitmap)是Windows操作系统中标准图像文件格式:设备相关位图(DDB)和设备无关位图(DIB),使用非常广。它采用位映射存储格式,除了图像深度可选以外,在绝大多数应用中不采用其他任何压缩。
BMP当中数据的色彩空间是RGB。
典型的BMP图像由以下四部分组成:
1. 位图头文件数据结构,它包含BMP图像文件的类型、显示内容等信息
2. 位图信息数据结构,它包含有BMP图像的宽、高、压缩方法,以及定义颜色等信息。
3. 调色板,可选。真彩色图(24位)不需要调色板
4.位图数据,根据BMP位图使用的位数不同而不同,在24位图中直接使用RGB,而其他小于24位的需要在调色板中颜色索引值。
BITMAPFILEHEADER数据结构:
typedef struct tagBITMAPFILEHEADER {
WORD bfType;/* 说明文件的类型 */
DWORD bfSize;/* 说明文件的大小,用字节为单位 */
WORD bfReserved1; /* 保留,设置为0 */
WORD bfReserved2; /* 保留,设置为0 */
DWORD bfOffBits; /* 说明从BITMAPFILEHEADER结构开始到实际的图像数据之间的字节偏移量 */
} BITMAPFILEHEADER;
BITMAPINFOHEADER数据结构
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;
调色板数据结构
typedef struct tagRGBQUAD {
BYTE rgbBlue; /*指定蓝色分量*/
BYTE rgbGreen; /*指定绿色分量*/
BYTE rgbRed; /*指定红色分量*/
BYTE rgbReserved; /*保留,指定为0*/
} RGBQUAD;
假如此时的BMP位图使用位数为2bit,则该文件中共有 22个 RGBQUAD结构体;
同样的,如果此时BMP使用位数为8bit,则该文件中有28=256个 RGBQUAD结构体,分别对应着值为0-255所指定的R、G、B值。
提取得到的RGB再进行RGB到YUV空间的色彩转换,可得到生成的YUV序列。以bmp位图中使用16bit为例,位运算的图示:
这里采用小尾字节序,即低位字节排放在内存的低端,高位字节排放在内存的高端。
(ps:如有错误望不吝指正)大端字节序与小端字节序
”大端”和”小端”表示多字节值的哪一端存储在该值的起始地址处;小端存储在起始地址处,即是小端字节序;大端存储在起始地址处,即是大端字节序,或者说:
1.小端法(Little-Endian)就是低位字节排放在内存的低地址端(即该值的起始地址),高位字节排放在内存的高地址端;
2.大端法(Big-Endian)就是高位字节排放在内存的低地址端(即该值的起始地址),低位字节排放在内存的高地址端; 举个简单的例子,对于整型数据0x12345678,它在大端法和小端法的系统中,各自的存放方式如下图所示
网络字节序
网络上传输的数据都是字节流,对于一个多字节数值,在进行网络传输的时候,先传递哪个字节?也就是说,当接收端收到第一个字节的时候,它将这个字节作为高位字节还是低位字节处理,是一个比较有意义的问题; UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节(即:高位字节存放在低地址处);由此可见,多字节数值在发送之前,在内存中因该是以大端法存放的; 所以说,网络字节序是大端字节序; 比如,我们经过网络发送整型数值0x12345678时,在80X86平台中,它是以小端发存放的,在发送之前需要使用系统提供的字节序转换函数htonl()将其转换成大端法存放的数值。
htonl()函数:
即host to net long的意思,返回类型是unsigned long。
类似的对应的字节序变化函数还有:
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
二.实验流程
1.程序初始化(打开多个文件,定义变量和缓冲区)
2.读取BMP文件,抽取或生成RGB数据写入缓冲区
3.调用RGB2YUV的函数实现RGB到YUV数据的转换
4.写YUV文件
5.程序收尾工作(关闭文件,释放缓冲区)
实验流程图:
- 生成文件:
实验要求将多个场景Bmp文件生成一个YUV文件,执行Bmp2yuv.exe时需传入多组参数。为方便调试,利用bat脚本文件编辑参数。
-调用时可传入多个参数,传参次序: argv[scene*2+1]为bmp路径名,argv[scene*2+2]为当前场景出现的帧数,最后的参数为输出yuv的路径
.bat编辑如下图
cmd截图如下:
三.实验结果
5个场景24bit位图转YUV结果:
文件名 | 图片 |
---|---|
场景一:30帧 | |
场景二:40帧 | |
场景三:40帧 | |
场景四:50帧 | |
场景五:60帧 |
不同比特位的bmp位图的对比:
biBitCount | 原Bmp | 生成yuv |
---|---|---|
1bit | ||
4bit | ||
8bit | ||
16bit |
四.代码分析
main.cpp
BITMAPFILEHEADER File_header;
BITMAPINFOHEADER Info_header;
void main(int argc, char *argv[])
{
...//定义变量
for (int scene = 0; scene < (argc-1)/2 ; scene++)
{
//实现多个Bmp文件的读入,使用哨兵为scene的for循环
//argc为所传入的参数个数
bmpFileName = argv[scene*2+1];//读入第一个Bmp文件名
yuvFileName = argv[argc-1];//读入储存的yuv文件名
Frames = atoi(argv[scene*2+2]);//读入当前场景的帧数
bmpFile = fopen(bmpFileName, "rb+");
yuvFile = fopen(yuvFileName, "ab+");//因为需要反复打开yuv文件,第二个参数传"ab+"
//即yuvFileName若不存在,则新建一个;若存在,则在当前文件尾追加
....
if (fread(&File_header, sizeof(BITMAPFILEHEADER), 1, bmpFile) != 1)
{//读入BITMAPFILEHEADER内容
printf("read file header error!\n");
exit(0);
}
if (File_header.bfType != 0x4D42)
{
printf("Not bmp file!\n");
exit(0);
}
else
{
printf("this is a %c%c\n", 0x42, 0x4d);
}
if (fread(&Info_header, sizeof(BITMAPINFOHEADER), 1, bmpFile) != 1)
{//读入BITMAPINFOHEADER内容
printf("read info header error!");
exit(0);
}
rgbData = (unsigned char *)malloc(Info_header.biWidth*Info_header.biHeight * 3);
memset(rgbData, 0, Info_header.biWidth*Info_header.biHeight * 3);
y_buf = (unsigned char *)malloc(Info_header.biWidth*Info_header.biHeight);
u_buf = (unsigned char *)malloc(Info_header.biWidth*Info_header.biHeight / 4);
v_buf = (unsigned char *)malloc(Info_header.biWidth*Info_header.biHeight / 4);
//开辟内存空间
ReadRGB(bmpFile, File_header, Info_header, rgbData);//调用readRgb.cpp中的ReadRGB()函数
//将文件名、头文件、信息头文件、储存rgb数据的空间传入函数
RGB2YUV(Info_header.biWidth, Info_header.biHeight, rgbData, y_buf, u_buf, v_buf, 0);
//调用RGB2YUV中的RGB2YUV()函数
for (u_int q = 0; q < Frames; q++)
{
fwrite(y_buf, 1, Info_header.biWidth*Info_header.biHeight, yuvFile);
fwrite(u_buf, 1, Info_header.biWidth*Info_header.biHeight / 4, yuvFile);
fwrite(v_buf, 1, Info_header.biWidth*Info_header.biHeight / 4, yuvFile);
}//写YUV
//清理内存,释放空间
}
}
readrgb.cpp
void ReadRGB(FILE *pFile, BITMAPFILEHEADER & file_h, BITMAPINFOHEADER & info_h, unsigned char * rgbdata)
{
unsigned char mask,* tmpData;//tmpData变量用于储存临时的rgb数据
unsigned long width, height;
unsigned long i;//循环变量i
width = info_h.biWidth / 8 * info_h.biBitCount;
height = info_h.biHeight;
tmpData = (unsigned char *)malloc(height*width);
fseek(pFile, file_h.bfOffBits, 0);//通过File_h结构体的bfOffBits快速找到Bmp中的数据块,并将文件指针偏移到该处
if (fread(tmpData, height*width, 1, pFile) != 1){
printf("read file error!\n\n");//将数据读入临时buffer
exit(0);
}
switch (info_h.biBitCount)//判断bmp文件所用的bit位数
{
case 24://24位是最容易操作的情况,直接将tmpData赋予rgbdata,进行下一步转换
memcpy(rgbdata, tmpData, height*width);
if (tmpData)
free(tmpData);
return;
case ...:
case ...:
}
}
这里用case语句判断biBitCount的值,从而进行不同的读取值操作,24位的操作方式最为容易,直接将数据赋给rgbData即可。其中的“width*height”,为一帧图像中像素所占的总字节数。
case 16:
if (info_h.biCompression == BI_RGB){
for (i = 0; i < height*width;i+=2)
{
*rgbdata = (tmpData[i] & 0x1F) << 3;
*(rgbdata + 1) = ((tmpData[i] & 0xE0) >> 2) + ((tmpData[i+1] & 0x03) <<6);
*(rgbdata + 2) = (tmpData[i + 1] & 0x7c) << 1;
rgbdata += 3;
}
}
if (tmpData)
free(tmpData);
return;
当位图的biBitCount为16bit时,其内部仍有多种可能的储存方式,如:X1 R5 G5 B5 ,A1 R5 G5 B5,R5 G6 B5,X4 R4 G4 B4 ,A4 R4 G4 B4。这里代码所列举的仅是第一种的读取方式。
读取文件头中info_h.biCompression,如果是BI_RGB未压缩的情况,则进行读取操作。因为两个字节储存着三个色彩分量信息,所以对应的,当rgbdata指针步进3字节时,i相应地对应着步进2个字节(i+=2)。相应的“位与”、“位移”操作参照下图。
default://biBitCount小于8的情况处理:
RGBQUAD *pRGB = (RGBQUAD *)malloc(sizeof(RGBQUAD)*(int)pow(float(2), info_h.biBitCount));//给调色板分配一个指针,并开拓相应的内存空间
if (!MakePalette(pFile, file_h, info_h, pRGB))
printf("No palette!\n\n");//初始化调色板
switch (info_h.biBitCount)
{//根据不同的位数设不同的掩码值
case 1:
mask = 0x80;
break;
case 2:
mask = 0xC0;
break;
case 4:
mask = 0xF0;
break;
case 8:
mask = 0xFF;
break;
}
for (i = 0; i < height*width; i++)
{//循环读取每个像素点的RGB值,步长为一个字节
int shiftCnt = 1;
int turn=mask;//将掩码值赋给移位变量
while (turn)
{
unsigned char index = (turn == 0xFF) ? tmpData[i] : ((tmpData[i] & turn) >> (8 - shiftCnt * info_h.biBitCount));
//index为索引值,用于索引调色板数组中的值
//如果biBiCount为8bit,那么这个字节的值,就是索引值,不需要使用掩码
*rgbdata = pRGB[index].rgbBlue;
*(rgbdata + 1) = pRGB[index].rgbGreen;
*(rgbdata + 2) = pRGB[index].rgbRed;
if (info_h.biBitCount == 8)
turn = 0;//如果biBiCount为8bit,则不需要第二次移位
else
turn >>= info_h.biBitCount;
rgbdata += 3;
shiftCnt++;//移位偏移量自增一
}
}
if (tmpData)
free(tmpData);
return;
bool MakePalette(FILE * pFile, BITMAPFILEHEADER &file_h, BITMAPINFOHEADER & info_h, RGBQUAD *pRGB_out)
{
if ((file_h.bfOffBits - sizeof(BITMAPFILEHEADER)-info_h.biSize) == sizeof(RGBQUAD)*pow(float(2), info_h.biBitCount))
//做差确定调色板的空间大小
{
fseek(pFile, sizeof(BITMAPFILEHEADER)+info_h.biSize, 0);//找到调色板的内存位置
fread(pRGB_out, sizeof(RGBQUAD), (unsigned int)pow(float(2), info_h.biBitCount), pFile);
//读入调色板数据
return true;
}
else
return false;
}
若读入的BMP位图的biBiCount为8bit或更小,则需要调用调色板。调色板开拓的空间大小为sizeof(RGBQUAD)∗biBiCount2。因为数据块与信息头之间的空间,即储存着调色板数组,我们只需要在两者之间做差,便能确定调色板中的内存数据。
由于我们至需要一个字节当中个别位数的数据,便要用到位操作,这里引入mask–掩码。举例,对2bit图像来说,其数据块中的第一个字节,对应着第一个像素的索引值为:b’xx zz zz zz,其中只有头两位包含该像素的索引信息,而后六位为其他像素的索引信息。这时候需要与b’11 00 00 00=0xC0相与,再进行6位右移,得到b’00 00 00 xx=index,这便是其对应的索引值,代入pRGB[index]获得其对应的RGB分量值。
当处理像素是第二个像素是,其对应的索引值应为该字节中的第五第六位,即b’zz xx zz zz,这时候掩码值也相应跟着位移,遂引入turn–掩码位移变量,turn可用作循环判断用,当turn为零时——说明该字节中所有像素信息已读取完,可以结束循环,进行下一个字节的读取。
同时,应注意到右移的位数也发生了变换,引入shiftcnt变量,用于不同像素点需位移的长度。每进行一次像素数据读取后,shiftcnt++,因为下一个像素(不是该字节最后一个像素时)需要位移的位数减少了shift*biBitCount个。