目录
2. 编写将第一步所生成的多个BMP文件转化为YUV文件,最后形成的YUV文件包含200帧
一、 实验项目名称
图像文件的读写和转换
二、 实验目的
- 理解图像文件的基本组成。
- 掌握结构体作为复杂数据对象的用法。进一步熟悉由问题到程序的解决方案,并掌握编程细节:如内存分配、倒序读写、字节序、文件读写过程等
三、实验内容
1. BMP文件的组成结构
BMP(全称Bitmap)是Windows操作系统中的标准图像文件格式,可以分成两类:设备相关位图(DDB)和设备无关位图(DIB),使用广泛。它采用位映射存储格式,除了图像深度可选以外,在绝大多数应用中不采用其他任何压缩,因此,BMP文件所占用的空间很大。BMP文件的图像深度可选lbit、4bit、8bit、16bit及24bit。BMP文件存储数据时,图像的扫描方式是按从左到右、从下到上的顺序。由于BMP文件格式是Windows环境中交换与图有关的数据的一种标准,因此在Windows环境中运行的图形图像软件都支持BMP图像格式。
典型的BMP图像文件由四部分组成:
位图头文件数据结构 | 包含BMP图像文件的类型、显示内容等信息 |
位图信息数据结构 | 包含有BMP图像的宽、高、压缩方法,以及定义颜色等信息 |
调色板 | 可选部分,有些位图需要调色板,有些位图比如真彩色图(24位的BMP)就不需要调色 |
位图数据 | 数据内容根据BMP位图使用的位数不同而不同,在24位图中直接使用RGB,而其他的小于24位的使用调色板中颜色索引值 |
相应的数据结构表示如下:
(1) 位图文件头主要有:
typedef struct tagBITMAPFILEHEADER {
WORD bfType; /* 说明文件的类型*/
DWORD bfSize; /* 说明文件的大小,用字节为单位*/
WORD bfReserved1; /* 保留,设置为0 */
WORD bfReserved2; /* 保留,设置为0 */
DWORD bfOffBits; /* 说明从BITMAPFILEHEADER结构开始到实际的图像数据之间的字节偏移量*/
} BITMAPFILEHEADE
(2) 位图信息头主要有:
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
(3) 调色板
调色板实际上是一个数组,它所包含的元素与位图所具有的颜色数相同,决定于biClrUsed和biBitCount字段。数组中每个元素的类型是一个RGBQUAD结构。真彩色无调色板部分。
typedef struct tagRGBQUAD {
BYTE rgbBlue; /*指定蓝色分量*/
BYTE rgbGreen; /*指定绿色分量*/
BYTE rgbRed; /*指定红色分量*/
BYTE rgbReserved; /*保留,指定为0*/
} RGBQUAD
(4) 位图数据
紧跟在调色板之后的是图像数据字节阵列。对于用到调色板的位图,图像数据就是该像素颜色在调色板中的索引值(逻辑色)。对于真彩色图,图像数据就是实际的R、G、B值。图像的每一扫描行由表示图像像素的连续的字节组成,每一行的字节数取决于图像的颜色数目和用像素表示的图像宽度。规定每一扫描行的字节数必须是4的整倍数,也就是DWORD对齐的。扫描行是由底向上存储的,这就是说,阵列中的第一个字节表示位图左下角的像素,而最后一个字节表示位图右上角的像素。
2. 字节序
不同的计算机系统采用不同的字节序存储数据,同样一个4字节的32位整数,在内存中存储的方式不同。
字节序分为小尾字节序(Little Endian)和大尾字节序(Big Endian)。Intel处理器大多数使用小尾字节序,Motorola处理器大多数使用大尾(Big Endian)字节序。小尾就是低位字节排放在内存的低端,高位字节排放在内存的高端,即所谓的“低位在前,高位在后”。大尾就是高位字节排放在内存的低端,低位字节排放在内存的高端,即所谓的“高位在前,低位在后”。
BMP文件中的数据是小尾字节序,在实现BMP文件头信息的写入和读出时,需要注意整数保存时的字节序。
四、 实验思路
BMP文件需要先转为RGB,进而将RGB转为YUV写入YUV文件
五、 实验步骤
1. 在图像处理软件中生成5个不同场景画面的BMP文件
如图借助ffmpeg截取了5张图片并保存为BMP文件,右上角加上水印
通过二进制编辑器打开No1,bmp进行查看
424D | 0x4D42='BM',表示这是BMP文件 |
36D20F00 | 0x000FD236,共1036854个字节 |
36000000 | 0x00000036,54,从文件开头到具体图像数据的字节偏移量,文件头(14)+位图信息头(40)=54字节(没有调色板) |
D0020000 | 0x000002D0,720,位图宽度为720个像素 |
E0010000 | 0x000001E0,480,位图高度为480个像素 |
0018 | 0x0018,24,表示颜色深度为24(不需要调色板) |
接着来看一下8bit位深的BMP图片
36040000 | 0x00000436,1078,从文件开头到具体图像数据的字节偏移量,文件头(14)+位图信息头(40)+调色板(256*4)=1078字节 |
0008 | 0x0008,8,表示颜色深度为8 |
00000000 | 0,表示不压缩 |
2. 编写将第一步所生成的多个BMP文件转化为YUV文件,最后形成的YUV文件包含200帧
(1) 新建工程,填写命令行参数并指定工作目录
int main(int argc,char *argv[])
/*argc=n+1=8;
argv[0]=命令名 指向该代码生成的exe文件
argv[1]=No1.bmp
argv[2]=No2.bmp
….
argv[5]=No5.bmp
argv[6]=每幅图像出现的帧数
argv[7]=test.yuv*/
将实验BMP图片,每幅图片出现的帧数,结果输出YUV文件写进命令参数,方便程序调用
(2) 程序部分
程序部分分为四大块,分别是主函数“main.cpp”、“readrgb.cpp”、“rgb2yuv.cpp”、“writeyuv.cpp”,看名字就可以大致理解每一部分实现的是什么功能了。
a. 主函数:main.cpp
首先定义了文件指针,缓存区等一些变量,接着使用for循环遍历每个图片,打开bmp图片文件,读取bmp文件文件头和信息头,处理宽度和高度,使其满足一行字节数是4的倍数和行数是偶数的条件,得到处理后的宽度和高度以及处理后的一行的字节数和行数,开辟缓存区,读取BMP文件的RGB,RGB转为YUV,YUV写入输出文件,最后释放缓存。
注意本次结果YUV文件的格式是4:2:0,因此缓存区UV的大小是Y的四分之一
int main(int argc, char* argv[])
{
//定义文件指针,缓存区等一些变量
BITMAPFILEHEADER File_header; //位图文件头
BITMAPINFOHEADER Info_header; //位图信息头
FILE* bmpFile = NULL; //指向输入bmp文件指针
FILE* yuvFile = NULL; //指向输出yuv文件指针
unsigned char* rgbBuf = NULL; //rgb缓存区
unsigned char* yBuf = NULL; //y缓存区
unsigned char* uBuf = NULL; //u缓存区
unsigned char* vBuf = NULL; //v缓存区
unsigned long width, height, w, h; //宽和高的处理变量
yuvFile = fopen(argv[7], "wb"); //二进制方式打开test.yuv文件,只写
//for循环遍历每个bmp文件进行处理
for (int i = 1; i < argc - 2; i++)
{
bmpFile = fopen(argv[i], "rb"); //二进制方式打开第一个No1.bmp文件,只读
//判断文件是否存在
if (bmpFile == NULL) //找不到bmp文件
{
printf("cannot find bmp file\n");
exit(1);
}
if (yuvFile == NULL) //找不到yuv文件
{
printf("cannot find yuv file\n");
exit(1);
}
//读取bmp文件文件头和信息头并判断读取是否正确
if (fread(&File_header, sizeof(BITMAPFILEHEADER), 1, bmpFile) != 1) //读取的文件头数据项数量不为1
{
printf("read file header error!");
exit(0);
}
if (File_header.bfType != 0x4D42) //类型不是bmp
{
printf("Not bmp file!");
exit(0);
}
else
{
printf("this is a bmp file!");
}
if (fread(&Info_header, sizeof(BITMAPINFOHEADER), 1, bmpFile) != 1) //读取的信息头数据项数量不为1
{
printf("read info header error!");
exit(0);
}
//处理宽度和高度
//每行字节数必须是4的整数倍,若不足则要补齐
if ((Info_header.biWidth % 4) == 0) {
w = Info_header.biWidth;
}
else {
w = (Info_header.biWidth * Info_header.biBitCount + 31) / 32 * 4;
//处理后的宽度
}
//高度需要为偶数不足则补为偶数
if ((Info_header.biHeight % 2) == 0) {
h = Info_header.biHeight;
}
else {
h = Info_header.biHeight + 1;
//处理后的高度
}
//得到处理后的一行的字节数width和行数height
width = w * Info_header.biBitCount / 8;
height = h;
//开辟缓存区
rgbBuf = new unsigned char[h * w * 3];
yBuf = new unsigned char[h * w];
uBuf = new unsigned char[h * w / 4];
vBuf = new unsigned char[h * w / 4];
//读取BMP文件的RGB
ReadRGB(bmpFile, File_header, Info_header, rgbBuf, width, height);
//RGB转为YUV
RGB2YUV(w, h, rgbBuf, yBuf, uBuf, vBuf);
//YUV写入输出文件,每幅图像写20次(传入的命令参数)
WriteYUV(yBuf, uBuf, vBuf, w * h, yuvFile, atoi(argv[6]));
free(rgbBuf);
free(yBuf);
free(uBuf);
free(vBuf);
fclose(bmpFile);
}
fclose(yuvFile);
return 0;
}
b. 读取RGB函数:readrgb.cpp
这一部分实现从BMP文件中读取得到RGB。
首先注意扫描行是由底向上存储的,这就是说,阵列中的第一个字节表示位图左下角的像素,而最后一个字节表示位图右上角的像素,因此为了还原图片,数据部分先进行倒序存储。
接着看位图深度:
如果是24bit深度则位图数据部分就是真实的RGB直接输出即可;
如果位图深度是16bit,这里仅考虑了555格式,即biCompression的值为BI_RGB,它没有调色板,16位中最低的5位表示B,中间的5位表示G,高的5位表示R,最高的1位保留为0,这里采取相与移位的方式来从2字节中得到RGB
如果位图深度小于等于8bit,则要用到调色板,原位图数据部分存放的是调色板的索引值,借助掩膜读取到真实RGB值
同时注意RGB排列顺序为BGRBGRBGR……
bool MakePalette(FILE* bmpFile, BITMAPFILEHEADER& File_header, BITMAPINFOHEADER& Info_header, RGBQUAD* pRGB_out);
void ReadRGB(FILE* bmpFile, BITMAPFILEHEADER& File_header, BITMAPINFOHEADER& Info_header, unsigned char* rgbDataOut, unsigned long width, unsigned long height)
{
unsigned long Loop, iLoop, jLoop;
unsigned char mask=0, * Index_Data, * Data; //Index_Data为倒序前数据,Data为倒序后数据
Index_Data = (unsigned char*)malloc(height * width);
Data = (unsigned char*)malloc(height * width);
fseek(bmpFile, File_header.bfOffBits, 0); //bmpFile文件指针从0偏移偏移量个字节到数据部分
//读取BMP有效数据并判断读取是否正确
if(fread(Index_Data, height*width, 1, bmpFile) != 1)
{
printf("readfile error!");
exit(0);
}
//倒序存放
for (iLoop= 0; iLoop < height; iLoop++)
for (jLoop= 0; jLoop < width; jLoop++)
Data[iLoop*width + jLoop] =Index_Data[(height - iLoop - 1)*width+ jLoop];
//颜色深度24位,Data数据即为RGB数据,直接输出
if (Info_header.biBitCount== 24)
{
memcpy(rgbDataOut,Data, height*width);
free(Index_Data);
free(Data);
return;
}
//颜色深度16位,通过移位从2字节中取出RGB信息存到3字节中
else if (Info_header.biBitCount== 16)
{
if (Info_header.biCompression == BI_RGB)
{ //未压缩
for (Loop = 0; Loop < height * width; Loop += 2)
{
*rgbDataOut = (Data[Loop] & 0x1F) << 3;
//取出B,与00011111相与取出低字节的右5位,通过右移3位放到目标字节的高5位
*(rgbDataOut + 1) = ((Data[Loop] & 0xE0) >> 2) + ((Data[Loop + 1] & 0x03) << 6);
//取出G:与11100000相与取出低字节的左3位,与00000011相与取出高字节的右2位,合并后放到目标字节的高5位
*(rgbDataOut + 2) = (Data[Loop + 1] & 0x7C) << 1;
//取出R:与01111100相与取出高字节的中间5位,再放到目标字节的高5位
rgbDataOut += 3;
}
}
}
//颜色深度1,2,4,8位。通过移位取出RGB信息存到3字节中
else if (Info_header.biBitCount <= 8) {
//生成调色板数组,通过下标对应查找得到该数据对应的颜色
RGBQUAD* pRGB = (RGBQUAD*)malloc(sizeof(RGBQUAD) * (unsigned int)pow((float)2, Info_header.biBitCount));
//读取位图调色板数据并判断是否有调色板
if (!MakePalette(bmpFile, File_header, Info_header, pRGB))
printf("Nopalette!");
for (Loop = 0; Loop < width * height; Loop++)
{
//根据位深设置掩膜
switch (Info_header.biBitCount)
{
case 1: //1000 0000,1位
mask = 0x80;
break;
case 2: //1100 0000,2位
mask = 0xC0;
break;
case 4: //1111 0000,4位
mask = 0xF0;
break;
case 8: //1111 1111,8位
mask = 0xFF;
}
int shiftCnt = 1; //控制mask的移位
while (mask)
{
unsigned char index = mask == 0xFF ? Data[Loop] : ((Data[Loop] & mask) >> (8 - shiftCnt * Info_header.biBitCount));
//index为bmp中原始有效数据,是对应调色板数组下标
//查找调色板,取出对应BGR存入rgbDataOut
*rgbDataOut = pRGB[index].rgbBlue;
*(rgbDataOut + 1) = pRGB[index].rgbGreen;
*(rgbDataOut + 2) = pRGB[index].rgbRed;
if (Info_header.biBitCount == 8)
mask = 0; //8位一次性可取完
else
mask >>= Info_header.biBitCount; //1位取8次;2位取4次;4位取2次
if (Loop == width * height - 1)
{
rgbDataOut = rgbDataOut + 3 - width * height * 3;
break;
}
rgbDataOut += 3;
shiftCnt++;
}
}
free(pRGB);
}
free(Index_Data);
free(Data);
}
bool MakePalette(FILE* bmpFile, BITMAPFILEHEADER& File_header, BITMAPINFOHEADER& Info_header, RGBQUAD* pRGB_out) {
//判断是否存在调色板
if ((File_header.bfOffBits - sizeof(BITMAPFILEHEADER) - Info_header.biSize) == sizeof(RGBQUAD) * pow((float)2, Info_header.biBitCount))
{
fseek(bmpFile, sizeof(BITMAPFILEHEADER) + Info_header.biSize, 0);
//获取调色板数据放入pRGB_out并判断是否读取正常
if (fread(pRGB_out, sizeof(RGBQUAD), (unsigned int)pow((float)2, Info_header.biBitCount), bmpFile) != (unsigned int)pow((float)2, Info_header.biBitCount))
{
printf("Fail to read RGBQUAD!\n");
}
return true;
}
else
return false;
}
c. RGB转YUV函数:rgb2yuv.cpp
首先RGB转YUV,这一步用到公式:
Y=0.2990R+0.5870G+0.1140B
U=−0.1684R−0.3316G+0.5000B
V=0.5000R−0.4187G−0.0813B
这是对两个色差信号进行归一化后的,且UV还需要零电平调整要+128。
接着对UV进行采样得到4:2:0格式
最后对YUV进行限量化电平处理。
float RGBYUV02990[256], RGBYUV05870[256], RGBYUV01140[256];
float RGBYUV01684[256], RGBYUV03316[256];
float RGBYUV04187[256], RGBYUV00813[256];
void initLookupTable();
void RGB2YUV(unsigned long w, unsigned long h, unsigned char* rgbData, unsigned char* y, unsigned char* u, unsigned char* v)
{
initLookupTable();//初始化查找表
unsigned char* ytemp = NULL;
unsigned char* utemp = NULL;
unsigned char* vtemp = NULL;
utemp = (unsigned char*)malloc(w * h);
vtemp = (unsigned char*)malloc(w * h);
unsigned long i, nr, ng, nb, nSize;
//对每个像素进行 rgb -> yuv的转换
for (i = 0, nSize = 0; nSize < w * h * 3; nSize += 3)
{
nb = rgbData[nSize];
ng = rgbData[nSize + 1];
nr = rgbData[nSize + 2];
y[i] = (unsigned char)(RGBYUV02990[nr] + RGBYUV05870[ng] + RGBYUV01140[nb]);
utemp[i] = (unsigned char)(-RGBYUV01684[nr] - RGBYUV03316[ng] + nb / 2 + 128);
vtemp[i] = (unsigned char)(nr / 2 - RGBYUV04187[ng] - RGBYUV00813[nb] + 128);
i++;
}
//对u信号及v信号进行采样
int k = 0;
for (i = 0; i < h; i += 2)
for (unsigned long j = 0; j < w; j += 2)
{
u[k] = (utemp[i * w + j] + utemp[(i + 1) * w + j] + utemp[i * w + j + 1] + utemp[(i + 1)* w + j + 1]) / 4;
v[k] = (vtemp[i * w + j] + vtemp[(i + 1) * w + j] + vtemp[i * w + j + 1] + vtemp[(i + 1)* w + j + 1]) / 4;
k++;
}
//对y、u、v 信号进行限量化电平处理
for (i = 0; i < w * h; i++)
{
if (y[i] < 16)
y[i] = 16;
if (y[i] > 235)
y[i] = 235;
}
for (i = 0; i < h * w / 4; i++)
{
if (u[i] < 16)
u[i] = 16;
if (v[i] < 16)
v[i] = 16;
if (u[i] > 240)
u[i] = 240;
if (v[i] > 240)
v[i] = 240;
}
if (utemp)
free(utemp);
if (vtemp)
free(vtemp);
}
//采用部分查找表法提高运行效率
void initLookupTable() {
for (int i = 0; i < 256; i++)
{
RGBYUV02990[i] = (float)0.2990 * i;
RGBYUV05870[i] = (float)0.5870 * i;
RGBYUV01140[i] = (float)0.1140 * i;
RGBYUV01684[i] = (float)0.1684 * i;
RGBYUV03316[i] = (float)0.3316 * i;
RGBYUV04187[i] = (float)0.4187 * i;
RGBYUV00813[i] = (float)0.0813 * i;
}
}
d. 写入YUV输出文件函数:writeyuv.cpp
直接循环写入即可
void WriteYUV(unsigned char* Y, unsigned char* U, unsigned char* V, unsigned long size, FILE* outFile,int number)
{
//将YUV写入文件outFile
for (int j = 0; j < number; j++){
fwrite(Y, 1, size, outFile);
fwrite(U, 1, size / 4, outFile);
fwrite(V, 1, size / 4, outFile);
}
}
六、实验结果
24bitBMP2YUV
对于其他位深图片,可以通过电脑画图软件生成,程序中只需要修改命令行参数即可
8bitBMP2YUV
4bitBMP2YUV