BMP文件读写
1. 问题描述
-
分析图像格式BMP
–可借助Matlab体会图像的读写和显示。
-
利用C语言编写程序,实现图像的输入和输出和显示。
–自行编写BMP文件的读写。
–调用开源库实现其他若干常见图像和视频文件格式的输入和输出。
–设计功能较完整的界面。
2. 技术背景
- opencv
- Visual Studio 2019
3. 解决方案
3.1 BMP文件结构
BMP文件由4部分组成:
- 位图文件头(bitmap-file header)
- 位图信息头(bitmap-information header)
- 调色板
- 位图数据
BMP文件整体结构如下图所示:

3.1.1 位图文件头
位图文件头的数据结构如下:
typedef struct tagBITMAPFILEHEADER {
//WORD bfType; //文件类型
DWORD bfSize; //文件大小
WORD bfReserved1; //保留字
WORD bfReserved2; //保留字
DWORD bfOffBits; //文件头到实际图像数据之间的偏移量
}BITMAPFILEHEADER;
结构体中的成员说明如下:
名称 | 占用空间 | 说明 |
---|---|---|
bfType | 2字节 | 说明文件类型,该值必须为0x4D42,也就是字符“BM” |
bfSize | 4字节 | 说明位图文件大小,以字节为单位 |
bfReserved1/2 | 4字节 | 保留字,必须设置为0 |
bfOffBits | 4字节 | 说明从文件头开始到实际的图像数据之间的偏移量 |
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;
结构体中的成员说明如下:
名称 | 占用空间 | 说明 |
---|---|---|
biSize | 4字节 | 位图信息头的大小 |
biWidth | 4字节 | 位图的宽度,单位是像素 |
biHeight | 4字节 | 位图的高度,单位是像素 |
biPlanes | 2字节 | 颜色平面数,固定值1 |
biBitCount | 2字节 | 每个像素的位数 |
biCompression | 4字节 | 压缩方式,0为不压缩 |
biSizeImage | 4字节 | 位图全部像素占用的字节数 |
biXPelsPerMeter | 4字节 | 水平方向分辨率(像素/米) |
biYPelsPerMeter | 4字节 | 垂直方向分辨率(像素/米) |
biClrUsed | 4字节 | 使用的颜色数 |
biClrImportant | 4字节 | 重要颜色数 |
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图像为例
4.1 文件头验证
用UltraEdit打开lena图像后,可以看到图像的具体数据。下图中依次框出了文件头中的文件类型、位图大小、保留值、偏移量。
根据每个名称的字节数,依次读取数据,并转换为10进制,算出实际值,如下表所示:
名称 | 占用空间 | 说明 | 实际数据 |
---|---|---|---|
bfType | 2字节 | 说明文件类型,该值必须为0x4D42,也就是字符“BM” | 0x4D42 (BM) |
bfSize | 4字节 | 说明位图文件大小,以字节为单位 | 0x41EE6 (270054) |
bfReserved1/2 | 4字节 | 保留字,必须设置为0 | 0 |
bfOffBits | 4字节 | 说明从文件头开始到实际的图像数据之间的偏移量 | 0x36 (54) |
运行代码后,文件头输出信息如下:
经过对比,实际读入的文件头信息与表3信息一致。
4.2 信息头验证
下图中依次框出了信息头中每个成员的具体数值。
根据每个名称的字节数,依次读取数据,并转换为10进制,算出实际值,如下表所示:
名称 | 占用空间 | 说明 | 实际数据 |
---|---|---|---|
biSize | 4字节 | 位图信息头的大小 | 0x28 (40) |
biWidth | 4字节 | 位图的宽度,单位是像素 | 0x190 (400) |
biHeight | 4字节 | 位图的高度,单位是像素 | 0xE1 (255) |
biPlanes | 2字节 | 固定值1 | 1 |
biBitCount | 2字节 | 每个像素的位数 | 0x18 (24) |
biCompression | 4字节 | 压缩方式,0为不压缩 | 0 |
biSizeImage | 4字节 | 位图全部像素占用的字节数 | 0x41EB0 (270000) |
biXPelsPerMeter | 4字节 | 水平方向分辨率(像素/米) | 0xB12 (2834) |
biYPelsPerMeter | 4字节 | 垂直方向分辨率(像素/米) | 0xB12 (2834) |
biClrUsed | 4字节 | 使用的颜色数 | 0 |
biClrImportant | 4字节 | 重要颜色数 | 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)]
可以看到,写入的图像和原图像一致。