C++ 实现BMP位图读写

BMP文件读写

1. 问题描述

  • 分析图像格式BMP

    –可借助Matlab体会图像的读写和显示。

  • 利用C语言编写程序,实现图像的输入和输出和显示。

    –自行编写BMP文件的读写。

    –调用开源库实现其他若干常见图像和视频文件格式的输入和输出。

    –设计功能较完整的界面。

2. 技术背景

  • opencv
  • Visual Studio 2019

3. 解决方案

3.1 BMP文件结构

BMP文件由4部分组成:

  1. 位图文件头(bitmap-file header)
  2. 位图信息头(bitmap-information header)
  3. 调色板
  4. 位图数据

BMP文件整体结构如下图所示:

3.1.1 位图文件头

位图文件头的数据结构如下:

typedef struct tagBITMAPFILEHEADER {
	//WORD bfType;			//文件类型
	DWORD bfSize;			//文件大小
	WORD bfReserved1;		//保留字
	WORD bfReserved2;		//保留字
	DWORD bfOffBits;		//文件头到实际图像数据之间的偏移量
}BITMAPFILEHEADER;

结构体中的成员说明如下:

表1 BMP文件头信息说明
名称占用空间说明
bfType2字节说明文件类型,该值必须为0x4D42,也就是字符“BM”
bfSize4字节说明位图文件大小,以字节为单位
bfReserved1/24字节保留字,必须设置为0
bfOffBits4字节说明从文件头开始到实际的图像数据之间的偏移量
3.1.2 位图信息头

位图信息头的数据结构如下:

typedef struct tagBITMAPINFOHEADER {
	DWORD biSize;			//位图信息头大小
	LONG biWidth;			//位图宽度
	LONG biHeight;			//位图高度
	WORD biPlanes;			//颜色平面数
	WORD biBitCount;		//每个像素的位数
	DWORD biCompression;	//压缩方式
	DWORD biSizeImage;		//位图全部像素占用的字节数
	LONG biXPelsPerMeter;	//水平方向分辨率
	LONG biYPelsPerMeter;	//垂直方向分辨率
	DWORD biClrUsed;		//使用的颜色数
	DWORD biClrImportant;	//重要颜色数
}BITMAPINFOHEADER;

结构体中的成员说明如下:

表2 BMP文件头信息说明
名称占用空间说明
biSize4字节位图信息头的大小
biWidth4字节位图的宽度,单位是像素
biHeight4字节位图的高度,单位是像素
biPlanes2字节颜色平面数,固定值1
biBitCount2字节每个像素的位数
biCompression4字节压缩方式,0为不压缩
biSizeImage4字节位图全部像素占用的字节数
biXPelsPerMeter4字节水平方向分辨率(像素/米)
biYPelsPerMeter4字节垂直方向分辨率(像素/米)
biClrUsed4字节使用的颜色数
biClrImportant4字节重要颜色数
3.1.3 调色板

调色板是一张映射表,标识颜色索引号与其代表的颜色的对应关系。

typedef struct tagRGBQUAD {
	BYTE rgbBlue;
	BYTE rgbGreen;
	BYTE rgbRed;
	BYTE rgbReserved;
}RGBQUAD;
3.1.4 位图数据

windows在早期开发BMP文件系统时,依照笛卡尔坐标系设计了BMP位图的数据存储结构,即图像的原点是上左下角。但是现在的图像基本上是以图像左上角为原点存储图像数据,这就导致如果按照现在的方式读取BMP位图显示的图像在垂直方向上和原图是相反的。所以在读取BMP位图数据时,应该把第一行位图数据放在存储矩阵的最后一行。

typedef struct tagIMAGEDATA {
	BYTE red;
	BYTE green;
	BYTE blue;
}IMAGEDATA;

3.2 BMP文件读写

在C++中,读写文件需要用文件流对象(fstream),文件流对象有两个子类,分别是文件输入流对象(ifstream)和文件输出流对象(ofstream)。

ifstream in(path,ios::binary);

ofstream out(path, ios::binary);

上面的代码分别定义了一个文件输入流对象和文件输出流对象,而且都是以二进制方式读写。

3.2.1 读BMP文件

C++中,读取文件数据可以用文件输入流ifstrem类中的read函数。

函数原型ifstream& read(char* s, streamsize n)

其中,s为一个char型数组,用来保存读取得到的数据值,n为读取的数据长度。

下面的代码定义了一个文件输入流对象,连接到path所指的文件。通过read函数把path所指的文件的前sizeof(bfType)个字节的数据保存到bfType变量中,然后文件指针向后移动**sizeof(bfType)**个字节。

ifstream in(path,ios::binary);

WORD bfType;
in.read((char*)(&bfType), sizeof(bfType));

上面有提到,windows在早期开发BMP文件系统时,依照笛卡尔坐标系设计了BMP位图的数据存储结构,即图像的原点是再左下角。但是opencv中显示图像时,原点时图像的左上角,所以两种存储格式在行数上是相反的,在列数上是相同的。下面的代码就是把BMP位图中数据依照opencv图像存储格式读了出来,使得opencv显示的图像和原图一致。

if (channels == 1) {

	Mat dst = Mat(rows, cols, CV_8UC1);

	for (int i = rows - 1; i >= 0; i--) {
		for (int j = 0; j < cols; j++) {
			in.read((char*)(&dst.at<uchar>(i, j)), sizeof(uchar));
		}
	}

	in.close();
	return dst;
}

完整代码如下:

//*********************BMP图像读取函数**********************************
//参数:
//		path:BMP图像路径
//返回值:
//		BMP图像数据
//*********************************************************************
Mat CV2::imreadBMP(const char* path) {

	ifstream in(path,ios::binary);

	WORD bfType;
	in.read((char*)(&bfType), sizeof(bfType));
	
	//检查是否为BMP文件
	if (bfType != 0x4d42) {
		throw "the file is not a bmp file!";
	}

	//读取文件头数据
	BITMAPFILEHEADER strHead;
	in.read((char*)(&strHead), sizeof(strHead));
	showBmpFileHead(strHead);

	//读取信息头数据
	BITMAPINFOHEADER strInfo;
	in.read((char*)(&strInfo), sizeof(strInfo));
	showBmpInfoHead(strInfo);

	//读取调色板数据
	RGBQUAD strPla[256];
	for (unsigned int nCounti = 0; nCounti < strInfo.biClrUsed; nCounti++) {
		//存储的时候,一般去掉保留字rgbReserved
		in.read((char*)(&strPla[nCounti].rgbBlue), sizeof(BYTE));
		in.read((char*)(&strPla[nCounti].rgbGreen),  sizeof(BYTE));
		in.read((char*)(&strPla[nCounti].rgbRed),  sizeof(BYTE));
		//cout << "strPla[nCounti].rgbBlue" << strPla[nCounti].rgbBlue << endl;
		//cout << "strPla[nCounti].rgbGreen" << strPla[nCounti].rgbGreen << endl;
		//cout << "strPla[nCounti].rgbRed" << strPla[nCounti].rgbRed << endl;
	}

	int rows = strInfo.biHeight;
	int cols = strInfo.biWidth;

	//得到通道数
	int channels = strInfo.biBitCount / 8;

	//读取位图数据,并保存在Mat(dst)数据结构中
	if (channels == 1) {

		Mat dst = Mat(rows, cols, CV_8UC1);

		for (int i = rows - 1; i >= 0; i--) {
			for (int j = 0; j < cols; j++) {
				in.read((char*)(&dst.at<uchar>(i, j)), sizeof(uchar));
			}
		}

		in.close();
		return dst;
	}

	if (channels == 3) {

		Mat dst = Mat(rows, cols, CV_8UC3);

		for (int i = rows - 1; i >= 0; i--) {
			for (int j = 0; j < cols; j++) {
				in.read((char*)(&dst.at<Vec3b>(i, j)), sizeof(Vec3b));
			}
		}
		in.close();
		return dst;
	}
	
	return Mat();
}
3.2.1 写BMP文件

写BMP文件与读BMP文件类似,下面的代码定义了一个输出流对象out,然后在头文件中的bfType中写入了0x4d42。

ofstream out(path, ios::binary);

WORD bfType = 0x4d42;
out.write((char*)(&bfType), sizeof(bfType));

在写文件时,一些参数是根据经验值预设的,opencv中存储像素的数据结构是MAT,下面的代码根据输入参数image的属性值,初始化了BMP位图的文件头与信息头。

//写入文件头
BITMAPFILEHEADER strHead;

int rows = image.rows;
int cols = image.cols;

strHead.bfOffBits = 54;
strHead.bfReserved1 = 0;
strHead.bfReserved2 = 0;
strHead.bfSize = image.channels() * rows * cols + strHead.bfOffBits;

out.write((char*)(&strHead), sizeof(strHead));

//写入信息头
BITMAPINFOHEADER strInfo;

strInfo.biSize = 40;
strInfo.biWidth = cols;
strInfo.biHeight = rows;
strInfo.biPlanes = 1;
strInfo.biBitCount = image.channels() * 8;
strInfo.biCompression = 0;
strInfo.biSizeImage = image.channels() * rows * cols;
strInfo.biXPelsPerMeter = 2834;
strInfo.biYPelsPerMeter = 2834;
strInfo.biClrUsed = 0;
strInfo.biClrImportant = 0;

完整代码如下:

//*********************BMP图像写入函数**********************************
//参数:
//		path:保存BMP图像路径,image:图像数据
//返回值:
//		
//*********************************************************************
void CV2::imwriteBMP(const char* path, Mat image) {

	ofstream out(path, ios::binary);

	WORD bfType = 0x4d42;
	out.write((char*)(&bfType), sizeof(bfType));

	//写入文件头
	BITMAPFILEHEADER strHead;

	int rows = image.rows;
	int cols = image.cols;

	strHead.bfOffBits = 54;
	strHead.bfReserved1 = 0;
	strHead.bfReserved2 = 0;
	strHead.bfSize = image.channels() * rows * cols + strHead.bfOffBits;
	
	out.write((char*)(&strHead), sizeof(strHead));
	
	//写入信息头
	BITMAPINFOHEADER strInfo;

	strInfo.biSize = 40;
	strInfo.biWidth = cols;
	strInfo.biHeight = rows;
	strInfo.biPlanes = 1;
	strInfo.biBitCount = image.channels() * 8;
	strInfo.biCompression = 0;
	strInfo.biSizeImage = image.channels() * rows * cols;
	strInfo.biXPelsPerMeter = 2834;
	strInfo.biYPelsPerMeter = 2834;
	strInfo.biClrUsed = 0;
	strInfo.biClrImportant = 0;

	out.write((char*)(&strInfo), sizeof(strInfo));

	int channels = image.channels();
	if (channels == 1) {

		for (int i = rows - 1; i >= 0; i--) {
			for (int j = cols - 1; j >= 0; j--) {
				out.write((char*)(&image.at<uchar>(i, j)), sizeof(uchar));
			}
		}

		out.close();
	}

	if (channels == 3) {

		for (int i = rows - 1; i >= 0; i--) {
			for (int j = 0; j < cols; j++) {
				out.write((char*)(&image.at<Vec3b>(i, j)), sizeof(Vec3b));
			}
		}
		out.close();
	}
}

4.实施示例

以lena图像为例

lena.bmp

4.1 文件头验证

UltraEdit打开lena图像后,可以看到图像的具体数据。下图中依次框出了文件头中的文件类型、位图大小、保留值、偏移量。

lena.bmp

根据每个名称的字节数,依次读取数据,并转换为10进制,算出实际值,如下表所示:

表3 lena图像文件头数据
名称占用空间说明实际数据
bfType2字节说明文件类型,该值必须为0x4D42,也就是字符“BM”0x4D42 (BM)
bfSize4字节说明位图文件大小,以字节为单位0x41EE6 (270054)
bfReserved1/24字节保留字,必须设置为00
bfOffBits4字节说明从文件头开始到实际的图像数据之间的偏移量0x36 (54)

运行代码后,文件头输出信息如下:

经过对比,实际读入的文件头信息与表3信息一致。

4.2 信息头验证

下图中依次框出了信息头中每个成员的具体数值。

lena.bmp

根据每个名称的字节数,依次读取数据,并转换为10进制,算出实际值,如下表所示:

表4 lena图像信息头数据
名称占用空间说明实际数据
biSize4字节位图信息头的大小0x28 (40)
biWidth4字节位图的宽度,单位是像素0x190 (400)
biHeight4字节位图的高度,单位是像素0xE1 (255)
biPlanes2字节固定值11
biBitCount2字节每个像素的位数0x18 (24)
biCompression4字节压缩方式,0为不压缩0
biSizeImage4字节位图全部像素占用的字节数0x41EB0 (270000)
biXPelsPerMeter4字节水平方向分辨率(像素/米)0xB12 (2834)
biYPelsPerMeter4字节垂直方向分辨率(像素/米)0xB12 (2834)
biClrUsed4字节使用的颜色数0
biClrImportant4字节重要颜色数0

运行代码后,文件头输出信息如下:

经过对比,实际读入的信息头信息与表4信息一致。

4.3 图像读取效果

下面的代码调用了自己写的函数imreadBMP,将读取的图像数据保存在mybmp变量中,然后调用opencv库函数imshow显示图像。

Mat mybmp = cv2.imreadBMP("lena.bmp");
imshow("mybmp", mybmp);

可以看到,读取的图像和原始图像一致。

4.4 图像写入效果

下面的代码调用了自己写的函数imwriteBMP,将上面读取的图像数据写入了myfun.bmp文件中。

cv2.imwriteBMP("myfun.bmp", mybmp);

可以看到,写入的图像和原图像一致。
经过对比,实际读入的信息头信息与表4信息一致。

4.3 图像读取效果

下面的代码调用了自己写的函数imreadBMP,将读取的图像数据保存在mybmp变量中,然后调用opencv库函数imshow显示图像。

Mat mybmp = cv2.imreadBMP("lena.bmp");
imshow("mybmp", mybmp);

[外链图片转存中…(img-w3C7xP0F-1629094703854)]

可以看到,读取的图像和原始图像一致。

4.4 图像写入效果

下面的代码调用了自己写的函数imwriteBMP,将上面读取的图像数据写入了myfun.bmp文件中。

cv2.imwriteBMP("myfun.bmp", mybmp);

[外链图片转存中…(img-iMUyfr4p-1629094703856)]

可以看到,写入的图像和原图像一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值