图像文件读写与转换
文章目录
实验基本原理
-
BMP图像由四部分组成
BMP文件格式 位图文件头BITMAPFILEHEADER 包含BMP文件的类型、显示内容等信息 位图信息头BITMAPINFOHEADER 包含BMP图像的宽、高、压缩方法、定义颜色等信息 调色板Palette 调色板实际上是一个数组,它所包含的元素与位图所具有的颜色数相同,真彩图无调色板部分 实际的位图数据ImageData 这部分的内容根据BMP位图使用的位数不同而不同
单色:1bit表示1个像素
16色:4个bit表示1个像素
256色:8个bit表示1个像素
24位:24个bit表示1个像素(BGR)
32位:32个bit表示1个像素 -
位图头文件数据结构: (WORD–2字节,DWORD–4字节)
//定义bmp文件结构体 typedef struct tagBITMAPFILEHEADER { WORD bfType; //说明文件的类型 DWORD bfSize; // 说明文件的大小,用字节为单位 WORD bfReserved1; //保留,设置为0 WORD bfReserved2; // 保留,设置为0 DWORD bfOffBits; //说明从BITMAPFILEHEADER结构开始到实际的图像数据之间的字节偏移量 } BITMAPFILEHEADER;
-
位图信息数据结构:
//定义位图信息 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;
-
位图数据:存放实际的数据
-
RGB图像存储:444格式,按照BGRBGR…方式存储
-
YUV图像存储:420格式,这种取样格式表示色差信号U和V的取样点数在水平和垂直方向上均为Y的1/2,因此Y分量的大小为 h e i g h t × w i d t h height \times width height×width而U和V分量的大小为 h e i g h t × w i d t h × 1 4 height\times width \times \frac{1}{4} height×width×41,存储时先存储Y分量,再存储U、V分量
-
RGB与YUV之间的转换公式:
Y = 0.2990 R + 0.5870 G + 0.1140 B U = 0.493 ( B − Y ) = − 0.1684 R − 0.3316 G + 0.5000 B V = 0.877 ( R − Y ) = 0.5000 R − 0.4187 G − 0.0813 B Y=0.2990R+0.5870G+0.1140B\\ U=0.493(B-Y)=−0.1684R−0.3316G+0.5000B\\ V=0.877(R-Y)=0.5000R−0.4187G−0.0813B Y=0.2990R+0.5870G+0.1140BU=0.493(B−Y)=−0.1684R−0.3316G+0.5000BV=0.877(R−Y)=0.5000R−0.4187G−0.0813B
在实际转换过程中,U和V的转换公式还需要再加128,使得零电平对应128的码电平另外需要注意,Y的电平范围是16-235,U和V的电平范围是16-240
实验流程分析
1.程序初始化(打开两个文件,定义变量和缓冲区等)
2.读取BMP文件,抽取或生成RGB数据写入缓冲区
- 读位文件头,判断是否可读出以及是否是BMP文件
- 读位图信息头,判断是否可以读出
- 判断像素的实际点阵数
- 开辟缓冲区,读数据,倒序存放
- 最后根据每像素位数的不同,执行不同的操作
- 24/32bit:直接取像素数据写RGB缓冲区
- 16bit:位与移位取像素数据转换为8bit/彩色分量写RGB缓冲区
- 8bit及以下:构造调色板,位与移位取像素数据查调色板写RGB缓冲区。
3.调用RGB2YUV的函数实现RGB到YUV数据的转换
4.写YUV文件
5.程序收尾工作(关闭文件,释放缓冲区)
注意:
-
BMP数据存放的时候是低位在前高位在后
-
16位图像使用2字节保存颜色值,常见格式有555和565格式;24位图像使用3字节保存颜色值,每一个字节代表一种颜色,按红绿蓝排列,这两种形式均无调色板
-
8位及以下使用调色板,对于用到调色板的位图,图像数据就是该像素颜色在调色板中的索引值(逻辑色)。
-
BMP图片是由底向上,由左到右存储的,这就是说,存储阵列中的第一个字节表示位图左下角的像素,而最后一个字节表示位图右上角的像素。规定每一扫描行的字节数必须是4的整倍数。
图像数据分析
本次实验共输入10张图片,每张图片循环20次写入YUV文件
-
打开一张实验所用的bmp图片,观察
-
4D 42:表示这是一张BMP图像
-
36 D2 0F 00:0x000FD236,共1036854字节
-
36 00 00 00:0x00000036,字节偏移量为54,也就是从第54字节开始是实际图像数据
-
28 00 00 00:0x00000028,结构体所需字节数40
-
D0 02 00 00:0x000002D0,图像宽度720像素
-
E0 01 00 00:0x000001E0,图像高度480像素
-
18 00:0x0018,24位图
关键代码分析
1.设置命令行参数
设置带参数的主函数传递的参数。main函数本身具有两个参数,其中argc表示main函数参数的个数,其值为传入参数个数+1,argv表示传入main的参数列表,其中argv[0]表示程序生成的.exe文件,后面依次为传入的参数。
此次实验设置的命令行参数分别为10张图片的重复帧数
main.cpp
中读取并存储每张图片循环次数
//设置命令行参数: 20 20 20 20 20 20 20 20 20 20
const int picnum = 10; //图片张数
int bmpnum[picnum] = { 0 };
for (int i = 0; i < picnum; i++) {
bmpnum[i] = atoi(argv[i + 1]); //存储每张图片循环次数
}
2.程序准备
(1)定义缓冲区
在main.cpp
中定义缓冲buffer,用来存储数据,完成读写操作
//定义缓冲区
FILE* bmpfile = NULL;
FILE* yuvfile = NULL;
unsigned char* rgbBuf = NULL;
unsigned char* yBuf = NULL, * uBuf = NULL, * vBuf = NULL;
(2)定义输入输出
在main.cpp
中定义输入输出文件
const char* bmpfilename[10] = { "outno001.bmp", "outno002.bmp", "outno003.bmp", "outno004.bmp", "outno005.bmp", "outno006.bmp", "outno007.bmp", "outno008.bmp", "outno009.bmp", "outno010.bmp" };
const char* yuvname = "outyuv.yuv";
(3)打开文件
打开yuv文件
//打开yuv文件
if ((yuvfile = fopen(yuvname, "wb")) == NULL) {
cout << "can't open yuvfile!" << endl;
exit(0);
}
循环打开bmp图片并进行相应的转换和读写操作
for (int i = 0; i < picnum; i++) {
//打开bmp文件
if ((bmpfile = fopen(bmpfilename[i], "rb")) == NULL) {
cout << "can't open bmpfile!" << endl;
exit(0);
}
3.文件读取
每次循环时读入一张bmp图片的文件头数据和文件信息头数据,同时判断是否是一张bmp图片
头文件引入#include <windows.h>
,直接使用已经写好的结构体
BITMAPFILEHEADER File_header;
BITMAPINFOHEADER Info_header;
//读取BMP文件头
if (fread(&File_header, sizeof(BITMAPFILEHEADER), 1, bmpfile) != 1) {
cout << "read fileheader error!" << endl;
exit(0);
}
//判断是否是bmp
if (File_header.bfType != 0x4D42) {
cout << "not bmp file!" << endl;
exit(0);
}
else {
cout << "this is a bmp!" << endl;
}
//读取bmp文件信息头
if (fread(&Info_header, sizeof(BITMAPINFOHEADER), 1, bmpfile) != 1) {
cout << "read infoheader error!" << endl;
exit(0);
}
处理图像的宽和高,由于每一扫描行的字节数必须是4的整倍数,因此需要对图像的宽和高做一定的处理
//处理图像宽高
int width, height;
//每行字节数必须是4的整数倍
if ((Info_header.biWidth % 4) == 0) {
width = Info_header.biWidth;
}
else {
width = (Info_header.biWidth * Info_header.biBitCount + 31) / 32 * 4;
//加31再除以32后下取整,保证计算结果是离这个数最近的而且是比它大的32的倍数,即为4字节的整数倍。乘以4和行数,得到4字节整数倍的图像大小。
}
//高是偶数
if ((Info_header.biHeight % 2) == 0) {
height = Info_header.biHeight;
}
else {
height = Info_header.biHeight + 1;
}
为之前定义的缓冲区开辟空间,其中u_tmp
和v_tmp
分别表示采样之前的u和v数据
//开辟空间
rgbBuf = new unsigned char[width * height * 3];
yBuf = new unsigned char[width * height];
uBuf = new unsigned char[(width * height) / 4];
vBuf = new unsigned char[(width * height) / 4];
unsigned char* u_tmp = new unsigned char[width * height];
unsigned char* v_tmp = new unsigned char[width * height];
4.抽取RGB数据
从bmp中抽取RGB数据,需要注意bmp图片存储时是倒序存储的,因此读取数据后需要将倒序转换成正序
//BMP2RGB
bmp2rgb(width, height, File_header, Info_header, bmpfile, rgbBuf);
传入图片的宽、高、文件头数据、文件信息数据、bmp文件和存储rgb数据的buffer,并将文件转换成正序存储。
判断不同的位深进行不同的处理:
- 位深=24的时候可以直接将data部分的数据存储到rgb中
- 位深=16的时候位与移位取像素数据转换为8bit/彩色分量写RGB缓冲区
- 位深<=8的时候,就会用到调色板和掩膜。由于读取文件的缓冲区data是一个字节一个字节读取的,但是只有8bit量化的索引值才足够一个字节,而1、2、4bit中一个字节会包含多个像素信息,因此需要一个掩膜遮住不需要的索引值,读完当前像素值把该掩膜向后移动相应的位数以显示出下一个像素值的信息。掩膜的具体作用就是做与操作,不需要的位与0,需要的位与1,由此可以计算出如下的掩膜值
这一部分的完整代码:
void bmp2rgb(int width, int height, BITMAPFILEHEADER& File_header, BITMAPINFOHEADER& Info_header, FILE* file, unsigned char* rgb) {
unsigned char mask, * tmp_data, * data;
unsigned long loop;
WORD bitcount = Info_header.biBitCount;
DWORD offbits = File_header.bfOffBits;
int w = width * bitcount / 8 ;
int h = height;
tmp_data = new unsigned char[w * h];
data = new unsigned char[w * h];
fseek(file, offbits, 0); //file指向以0为基准,偏移offbits(指针偏移量)个字节的位置
if (fread(tmp_data, w * h, 1, file) != 1) {
cout << "read imgdata error!" << endl;
exit(0);
}
//倒序转正序
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
data[i * w + j] = tmp_data[(h - 1 - i) * w + j];
}
}
if (bitcount == 24) {
memcpy(rgb, data, w * h); //从data所指的地址开始将w*h大小的数据给rgb
}
if (bitcount == 16) {
for (loop = 0; loop < w * h; loop += 2)
{
*rgb = (data[loop] & 0x1F) << 3;
*(rgb + 1) = ((data[loop] & 0xE0) >> 2) + ((data[loop + 1] & 0x03) << 6);
*(rgb + 2) = (data[loop + 1] & 0x7C) << 1;
rgb += 3;
}
}
RGBQUAD* prgb = (RGBQUAD*)malloc(sizeof(RGBQUAD) * (unsigned int)pow(2, (float)bitcount));
if (!MakePalette(file, File_header, Info_header, prgb)) //判断是否有调色板数据
cout << "No palette!" << endl;
if (bitcount <= 8) {
for (loop = 0; loop < w * h; loop++)
{
//根据位深设置掩膜
switch (bitcount) {
case 1:
mask = 0x80; //1000 0000
break;
case 2:
mask = 0xC0; //1100 0000
break;
case 4:
mask = 0xF0; //1111 0000
break;
case 8:
mask = 0xFF; //1111 1111
}
int shiftCnt = 1;
while (mask) //循环mask次:1bit每字节循环8次,2bit4次,4bit2次
{
unsigned char index = mask == 0xFF ? data[loop] : ((data[loop] & mask) >> (8 - shiftCnt * bitcount));
*rgb = prgb[index].rgbBlue;
*(rgb + 1) = prgb[index].rgbGreen;
*(rgb + 2) = prgb[index].rgbRed;
if (bitcount == 8)
mask = 0;
else
mask >>= bitcount;
rgb += 3;
shiftCnt++;
}
}
}
//delete[] tmp_data;
//delete[] data;
}
调色板
这一部分的除了做是否有调色板数据之外还做了指针移动和文件读取!
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(2, info_h.biBitCount))
{
fseek(pFile, sizeof(BITMAPFILEHEADER) + info_h.biSize, 0);
fread(pRGB_out, sizeof(RGBQUAD), (unsigned int)pow(2, info_h.biBitCount), pFile);
return true;
}
else
return false;
}
5.RGB2YUV
调用rgb2yuv
函数完成转换
rgb2yuv(width * height, rgbBuf, yBuf, u_tmp, v_tmp);
void rgb2yuv(int file_size, unsigned char* rgbBuf, unsigned char* yBuf, unsigned char* uBuf, unsigned char* vBuf) {
Init_table(); //初始化查找表
unsigned char* r, * g, * b;
unsigned char* y, * u, * v;
float y_tmp, u_tmp, v_tmp; //临时变量,防止计算和转换过程中yuv值溢出
y = yBuf;
u = uBuf;
v = vBuf;
b = rgbBuf; //b指向rgb文件
for (int i = 0; i < file_size; i++) {
g = b + 1;
r = b + 2;
unsigned char r_tmp, g_tmp, b_tmp;
r_tmp = *r;
g_tmp = *g;
b_tmp = *b;
y_tmp = RGB2YUV02990[r_tmp] + RGB2YUV05870[g_tmp] + RGB2YUV01140[b_tmp];
u_tmp = -RGB2YUV01684[r_tmp] - RGB2YUV03316[g_tmp] + (b_tmp) / 2 + 128;
v_tmp = (r_tmp) / 2 - RGB2YUV04187[g_tmp] - RGB2YUV00813[b_tmp] + 128; //uv分量+128,将零电平变为128码电平
if (y_tmp < 16) y_tmp = 16;
if (y_tmp > 235) y_tmp = 235;
if (u_tmp < 16) u_tmp = 16;
if (u_tmp > 240) u_tmp = 240;
if (v_tmp < 16) v_tmp = 16;
if (v_tmp > 240) v_tmp = 240; //防止计算出的yuv溢出
// printf("No.%d",i);
*y = (unsigned char)y_tmp;
*u = (unsigned char)u_tmp;
*v = (unsigned char)v_tmp;
b += 3;
y++;
u++;
v++;
}
yBuf = y;
uBuf = u;
vBuf = v;
}
定义查找表,采用部分查找表法可以提高运行效率
//查找表
void Init_table() {
for (int i = 0; i < 256; i++) {
RGB2YUV02990[i] = (float)0.2990 * i;
RGB2YUV05870[i] = (float)0.5870 * i;
RGB2YUV01140[i] = (float)0.1140 * i;
RGB2YUV01684[i] = (float)0.1684 * i;
RGB2YUV03316[i] = (float)0.3316 * i;
RGB2YUV04187[i] = (float)0.4187 * i;
RGB2YUV00813[i] = (float)0.0813 * i;
}
}
通过上述转换后yuv是444格式,因此需要对uv分量进行下采样,这里取平均值作为采样后的数据
//下采样
DownSample_YUV(height, width, uBuf, vBuf, u_tmp, v_tmp);
void DownSample_YUV(int height, int width, unsigned char* uBuf, unsigned char* vBuf, unsigned char* uBuf_tmp, unsigned char* vBuf_tmp) {
//YUV文件色度格式为420,对UV分量进行采样,计算相邻4个像素的平均值作为对应的值存入UV
int k = 0;
unsigned char* ub = uBuf, * vb = vBuf;
unsigned char* utmp = uBuf_tmp, * vtmp = vBuf_tmp;
float u, v;
for (int i = 0; i < height * width / 4; i++) {
u = (float)(*(utmp + k) + *(utmp + k + 1) + *(utmp + k + width) + *(utmp + k + width + 1)) / 4.0;
v = (float)(*(vtmp + k) + *(vtmp + k + 1) + *(vtmp + k + width) + *(vtmp + k + width + 1)) / 4.0;
*ub = (unsigned char)u;
*vb = (unsigned char)v;
ub++;
vb++;
if ((i + 1) % (width / 2) == 0) {
k += 2 + width;
}
else {
k += 2;
}
}
uBuf = ub;
vBuf = vb;
}
6.循环写入文件
转换后的文件按照命令行参数设置的循环数量循环写入yuv文件
//循环写入文件
for (int k = 0; k < bmpnum[i]; k++) {
fwrite(yBuf, 1, width * height, yuvfile);
fwrite(uBuf, 1, (width * height) / 4, yuvfile);
fwrite(vBuf, 1, (width * height) / 4, yuvfile);
}
实验结束后释放空间,关闭文件
delete[] rgbBuf;
delete[] yBuf;
delete[] uBuf;
delete[] vBuf;
delete[] u_tmp;
delete[] v_tmp;
fclose(bmpfile);
fclose(yuvfile);
实验结果
24bit
8bit
4bit
实验总结
-
实验开始的是发现没有办法用命令行的方式给main函数传入参数,最后发现debug的平台和项目用的平台不一样,都改成相同的x64就可以了
-
开始的时候忘记YUV文件的采样格式是4:2:0,因此在rgb2yuv函数中总是会出现指针指向位置不对的错误,后来采用了先将rgb文件转换成444格式的yuv文件再进行采样的方式,最终得到结果
-
实验开始的时候得到的结果如下图所示:
经检查发现在
bmp2rgb
这一步的时候修改了图像的宽和高为int w = width * bitcount / 8 ; int h = height;
但是在后面循环的过程中又错误的使用了
width
和height
-
在进行位深<=8bit的实验过程中,得到的所有实验结果都是灰色的
经检查发现在写入文件之前所有的buffer数据都是127,后又发现在进行判断是否有调色板数据的时候进行了指针的移动和文件的读取,由于编写代码的时候认为这里只是单纯做了判断,没有细读里面其实有指针移动和文件读取,因此其实开始只是单纯做了调色板的定义,导致整个调色板里面是空值,后面的rgb文件并没有读入数据,所以出错
-
本次实验过程比较曲折,开始的是因为当时分析图片格式的时候做的PNG,所以就想尝试做一下PNG2YUV的转换,但是在编写修改两周之后依旧没能完成,因此重新写了bmp2yuv