BMP是英文Bitmap(位图)的简写,也被称为DIB(与设备无关的位图),是一种独立于显示器的位图数字图像文件格式,它是Windows操作系统中的标准图像文件格式,能够被多种Windows应用程序所支持。
通过本文,我们将学习BMP的存储结构以及如何通过编程语言C++对BMP格式存储的图像进行基本的读写操作(本文描述复制copy操作,针对24位图和32位图)。
一.BMP文件结构
为了顺利实现BMP图像copy,我们必须先了解BMP文件是如何进行存储的,其内部的编码结构是怎样的。
BMP文件图像由若干大小固定(文件头)和大小可变的结构体按一定的顺序够成,由于该文件格式几经演进,这些结构体的版本也很多。
位图文件由以下结构体依次构成:
(来自WIKI百科)
(翻译版)
其中可选表示有的BMP文件可能不存在对应的结构体,我们本篇文章所讨论的BMP图像只考虑必选的结构体。接下来我们将按顺序介绍Bitmap file header(位图文件头),DIB header(DIB头),Color table(调色板),Pixel Array(像素数组)这个重要的结构体。
Ⅰ.位图文件头(Bitmap file header):
这部分数据块位于文件开头,大小为14个字节,用于进行文件的识别与部分基本信息的存储。典型的应用程序会首先读取这部分数据以确保其为BMP文件且没有损坏。这部分所有的整数值都以小端序存放(即最低有效位前置)。
在C++的windows.h库中,有相应的BITMAPFILEHEADER结构体。(当然,你也可以选择自己编写一个位图文件头的结构体,但要注意成员变量的大小和顺序)
①bfType:2字节
用于标识BMP和DIB文件的魔数,一般为0x42 0x4D即ASCⅡ码对应的B M,BMP文件对应的bfType为0x4D42(B为low字节,M为high字节所以bfType=0x4D42,而不是0x424D)
由此我们可以编写程序来判断读入的是否为BMP文件
②bfSize:4字节
用于表示BMP文件的大小(单位为字节)
顺带地,我们介绍一下BMP文件的存储算法:
BMP文件通常是不进行压缩操作的,因此它们通常比一幅图像的压缩图像文件格式大很多。
根据颜色深度的不同,BMP图像上的一个像素可以用一个或多个字节(一个字节包含了8个数据位)来进行表示,一个像素对应的字节数为 位深度/8 。图片浏览器等基于字节的ASCⅡ码值来计算像素的颜色,然后从调色板中读出相应的值。
位深度为n说明可以有种颜色,则n位
种颜色的包含调色板的位图近似字节数可以用下面的公式计算:
BMP的文件大小≈
width,height单位都是像素。
其中14字节对应的是位图文件头的大小,40字节对应的是DIB文件头的大小,对应的是彩色调色板的大小(只在位深度小于8时存在)。
(注意:调色板仅仅定义了图像所用的颜色,所以实际彩色调色板将小于)
若位图文件不包含调色板,如我们接下来要讨论的24位和32位位图,那么位图的近似字节数公式可以修改为:
BMP的文件大小≈
,width,height单位都是像素。
③bfReserved1:2字节
会被保留,实际值因创建程序而异。
④bfReserved2:2字节
会被保留,实际值因创建程序而异。
⑤bfOffBits:4字节
位图数据(像素数组)的地址偏移,也就是像素数组起始地址。
Ⅱ.DIB头(DIB Header):
这部分内容告诉应用程序BMP图像的详细信息,在屏幕上显示图像会使用到这部分信息。由于微软对DIB格式进行了多次的修改,这部分内容对应的字节数有不同的版本,我们现在通常使用的系统对应了40字节。
下表为不同版本的DIB头:
在C++的windows.h库中,有相应的BITMAPINFOHEADER结构体。(当然,你也可以选择自己编写一个位图文件头的结构体,但要注意成员变量的大小和顺序)
①biSize:4字节
标明该DIB头文件的大小(通常固定为40字节)
②biWidth:4字节
位图宽度,单位为像素(有符号整数)
③biHeight:4字节
位图高度,单位为像素(有符号整数)
④biPlanes:2字节
色彩平面数;只有1为有效值
⑤biBitCount:2字节
每个像素所占用的位数,即图像的色深,典型值为1、4、8、16、24、32
⑥biCompression:4字节
所使用的压缩方式,详情见下表
⑦biSizeImage:4字节
图像大小,即指原始位图数据的大小。
⑧biXPlesPerMeter:4字节
图像的横向分辨率,单位为像素每米(有符号整数)
⑨biYpelsPerMeter:4字节
图像的纵向分辨率,单位为像素每米(有符号整数)
⑩biClrUsed:4字节
调色版的颜色数,为0时表示颜色数就是默认的个
⑪biClrImportant:4字节
重要颜色数,为0时表明所有颜色都是重要的,一般BMP图像不使用本项
Ⅲ.调色板(Color table):
这部分只在BMP图像的位深度小于8时存在,它定义了图像所使用的颜色。BMP图像像素区的像素是一个接一个存储的,每个像素使用一个或者多个字节的值表示,所以调色板的目的就是要告诉应用程序这些值所对应的实际颜色。、
典型的BMP图像文件使用RGB彩色模型。在这种模型中,每种颜色都是由强度不同(从0到最大强度)的红色(R),绿色(G),蓝色(B)组成,也就是说,每种颜色可以用红色、绿色、蓝色的值所定义。
在C++的windows.h库中,有相应的RGBQUAD结构体。(当然,你也可以选择自己编写一个位图文件头的结构体,但要注意成员变量的大小和顺序)
每个条目用来描述一种颜色,包含了四个字节,其中三个表示蓝色、绿色、红色,第四个字节没有使用(大多是应用程序将它设置为0);对于每个字节,数值0表示该颜色分量在当前的颜色中没有使用,而数值255表示这种颜色分量使用的最大强度。
(24位图像素对应3个字节,32位图像素对应4个字节)
Ⅳ.像素数组(Pixel Array):
这个部分逐个像素表示图像,每个像素使用一个或者多个字节表示。
通常,像素是从下到上,从左到右保存的。(注:如果使用的不是BITMAPCOREHEADER,那么未压缩的Windows位图还可以从上到下进行存储,此时图像高度为负值。)
每一行的末尾需要填充若干个字节的数据(不一定为0)使得该行的长度为4字节的倍数(扫描行)。像素数组读入内存后,每一行的起始地址必须为4的倍数,这个限制仅针对内存中的像素数组,针对存储时,仅要求每一行的大小为4字节的倍数,对文件的偏移没有限制。
例如:对于24位BMP图像,其一个像素对应3个字节,如果他的宽度为1个像素,一行就有3个字节,那么我们需要在该行末尾填充1字节;如果宽度为2个像素,一行就有6个字节,那么我们需要在该行末尾填充2个字节;宽度为4个像素时,一行就有12个字节,那么我们就不需要填充。
二.C++实现BMP格式图像读入
注:本文使用Visual Studio 2022进行代码编写,导入了<iostream>,<windows.h>,<fstream>库,设定对齐边界为2。
接下来我们先定义初始BMP文件的文件头变量,DIB头变量,像素数组。
BITMAPFILEHEADER结构体
BITMAPINFOHEADER结构体
两种RGB像素结构体(分别对应24位与32位)
定义全局变量is32_flag用于判断是否为32位BMP图像,如果是32位BMP图像,在DIB头之后还存在一段长度为84字节的信息,我们通过自定义Plus结构体来处理这段信息。
Plus结构体
再定义former_width用于表示原图宽度,former_height用于表示原图高度
定义bmp_in_address用于存储原图像地址(相对路径/绝对路径)
使用者需传入原图像地址,之后我们调用bmp_read函数来进行bmp文件的读入,bmp_read函数返回值为bool类型,如果返回值为true说明正确打开文件,否则说明打开文件遇到了问题,返回false,终止程序。
接下来我们就可以开始读入BMP图像了,首先打开对应地址的文件,判断对应文件是否存在:
然后我们读入头文件,并判断该文件类型是否为BMP格式:
接着我们读入DIB头,并输出头文件和DIB头里面的关键信息(用于debug):
接下来我们需要判断是否有读入Plus结构体的必要,即对BMP图像位深度进行判断:
然后我们文件操作的读指针就来到了像素区,先记录原图像的宽度和高度:
前面提到,BMP文件有补齐一行字节数为4的倍数的操作,所以我们需要计算扫描一行所需要的字节数,并计算出补齐所花的字节数,这是一个简单的数学处理:
然后我们就可以开始对像素数组内存储的信息进行读取了,分为24位和32位两种情况:
这样就完成了BMP文件的常规读取操作,最后不要忘记关闭文件,返回true表示成功读入文件。
运行一下程序,所用的图像为:
得到输出:
符合预期,说明读入成功
附上bmp_read函数的整体代码:
bool bmp_read(string s)
{
//跟随地址以二进制读入bmp文件
ifstream former_bmp_info(s, ios::in | ios::binary);
if (!former_bmp_info)//若无法打开对应文件
{
cout << "Open File Failed!" << endl;
former_bmp_info.close();
return false;
}
else
{
cout << "Open File Successfully!" << endl << endl;
}
former_bmp_info.read((char*)&former_bmp_FILE_Head, sizeof(BITMAPFILEHEADER));//读文件头
//判断是否正确读入bmp文件
if (former_bmp_FILE_Head.bfType != 0x4D42)
{
cout << "该文件非BMP格式!" << endl;
return false;
}
else
{
cout << "该文件为BMP格式!" << endl << endl;
}
former_bmp_info.read((char*)&former_bmp_DIB_Head, sizeof(BITMAPINFOHEADER));//读DIB头
cout << "位图文件头:" << endl;
cout << "DIB头占用字节数:" << "14 bytes" << endl;
cout << "位图文件类型: 0x" << hex << former_bmp_FILE_Head.bfType << endl;//16进制
cout << "位图文件大小: " << dec << former_bmp_FILE_Head.bfSize << " bytes" << endl;
cout << "偏移字节数: " << former_bmp_FILE_Head.bfOffBits << endl;
cout << endl;
cout << "DIB头占用字节数:" << former_bmp_DIB_Head.biSize << " bytes" << endl;
cout << "位图宽度: " << former_bmp_DIB_Head.biWidth << endl;
cout << "位图高度: " << former_bmp_DIB_Head.biHeight << endl;
cout << "位图压缩类型: " << former_bmp_DIB_Head.biCompression << endl;
cout << "位图像素位数: " << former_bmp_DIB_Head.biBitCount << endl;
cout << "位图数据总占用字节数: " << former_bmp_DIB_Head.biSizeImage << " bytes" << endl;
cout << endl;
if (former_bmp_DIB_Head.biBitCount == 32)
{
is32_flag = true;
cout << "输入的图片为32位" << endl;
former_bmp_info.read((char*)&former_bmp_Plus, sizeof(Plus));//32位图像会多一个Plus部分
}
former_width = former_bmp_DIB_Head.biWidth;
former_height = former_bmp_DIB_Head.biHeight;
//计算扫描一行所需要的字节数
int former_line_byte = (former_bmp_DIB_Head.biBitCount * former_width / 8 + 3) / 4 * 4;
int need_zeros = former_line_byte - former_bmp_DIB_Head.biBitCount * former_width / 8;
if (is32_flag)
{
former_rgb_data2 = new RGB_data2[former_width * former_height];
for (int i = 0; i < former_height; i++)
{
former_bmp_info.read((char*)former_rgb_data2 + i * former_width * former_bmp_DIB_Head.biBitCount / 8, former_bmp_DIB_Head.biBitCount / 8 * former_width);
former_bmp_info.seekg(need_zeros, ios::cur);//忽略掉补位
}
}
else
{
former_rgb_data1 = new RGB_data[former_width * former_height];
for (int i = 0; i < former_height; i++)
{
former_bmp_info.read((char*)former_rgb_data1 + i * former_width * former_bmp_DIB_Head.biBitCount / 8, former_bmp_DIB_Head.biBitCount / 8 * former_width);
former_bmp_info.seekg(need_zeros, ios::cur);//忽略掉补位
}
}
former_bmp_info.close();
return true;
}
三、C++实现BMP格式图像复制
接下来再讲如何在成功读入BMP格式图像的基础上对BMP格式图像进行复制。
与读入操作类似的,我们需要一个地址(相对路径/绝对路径)来输出我们复制后的文件,定义为bmp_out_address
使用者需传入输出的图像地址,之后我们调用bmp_copy函数来进行bmp文件的复制输出,bmp_copy函数返回值为bool类型,如果返回值为true说明正确输出文件,否则说明输出文件遇到了问题,返回false,终止程序。
接下来我们就可以开始复制BMP图像了:
首先直接进行赋值操作,由于是复制,可以直接把原先的文件头和DIB头传给后来的文件头和DIB头,然后创建输出文件流,判断能否正确打开文件:
接着我们读入文件头和DIB头:
然后我们判断是否需要读入Plus结构体:
然后我们考虑扫描行的字节数以及补齐操作:
然后我们就可以开始进行像素数组的复制了,分为24位和32位两种情况:
这样就完成了BMP文件的复制操作,最后记得关闭文件,返回true表示成功复制文件。
运行一下程序,所用的24位图像为:
得到如下输出:
检查文件目录,发现复制成功:
运行一下程序,所用的32位图像为:
得到如下输出:
检查文件目录,发现复制成功:
附上bmp_copy函数的整体代码:
bool bmp_copy(string s)
{
BITMAPFILEHEADER copy_bmp_FILE_Head = former_bmp_FILE_Head;
BITMAPINFOHEADER copy_bmp_DIB_Head = former_bmp_DIB_Head;
ofstream bmp_out(s, ios::out | ios::binary);
if (!bmp_out)
{
cout << "Creat File Failed!" << endl;
bmp_out.close();
return false;
}
else
{
cout << "Creat File Successfully!" << endl;
}
bmp_out.write((char*)©_bmp_FILE_Head, sizeof(BITMAPFILEHEADER));
bmp_out.write((char*)©_bmp_DIB_Head, sizeof(BITMAPINFOHEADER));
if(is32_flag)bmp_out.write((char*)&former_bmp_Plus, sizeof(Plus));
int copy_line_byte = (copy_bmp_DIB_Head.biBitCount * copy_bmp_DIB_Head.biWidth / 8 + 3) / 4 * 4;
int need_zeros = copy_line_byte - copy_bmp_DIB_Head.biBitCount * copy_bmp_DIB_Head.biWidth / 8;
string temp_zero = "00000";//用0补齐,当然你也可以自定义补齐语句,这没有大碍
if (is32_flag)
{
RGB_data2* copy_rgb_data = new RGB_data2[former_width * former_height];
for (int i = 0; i < former_height; i++)
{
for (int j = 0; j < former_width; j++)
{
*(copy_rgb_data + i * former_width + j) = *(former_rgb_data2 + i * former_width + j);
}
}
//写入bmp文件
for (int i = 0; i < former_height; i++) {
bmp_out.write((char*)copy_rgb_data + i * former_width * copy_bmp_DIB_Head.biBitCount / 8, copy_bmp_DIB_Head.biBitCount / 8 * former_width);
bmp_out.write((char*)&temp_zero, need_zeros);
}
delete[] copy_rgb_data;
}
else
{
RGB_data* copy_rgb_data = new RGB_data[former_width * former_height];
for (int i = 0; i < former_height; i++)
{
for (int j = 0; j < former_width; j++)
{
*(copy_rgb_data + i * former_width + j) = *(former_rgb_data1 + i * former_width + j);
}
}
//写入bmp文件
for (int i = 0; i < former_height; i++) {
bmp_out.write((char*)copy_rgb_data + i * former_width * copy_bmp_DIB_Head.biBitCount / 8, copy_bmp_DIB_Head.biBitCount / 8 * former_width);
bmp_out.write((char*)&temp_zero, need_zeros);
}
delete[] copy_rgb_data;
}
bmp_out.close();
return true;
}
至此,BMP文件的读入和复制输出操作已经讲解完毕,关于更复杂的旋转操作会在后续文章中给出具体的代码实现,感谢浏览!
最后附上源代码:
#include <iostream>
#include<windows.h>
#include<fstream>
#pragma pack(2)//设置对齐边界为2
using namespace std;
struct RGB_data
{
BYTE blue;
BYTE red;
BYTE green;
};
struct RGB_data2
{
BYTE blue;
BYTE red;
BYTE green;
BYTE rgbReserved;
};
struct Plus//32位图会额外有一段
{
DWORD bV5RedMask;
DWORD bV5GreenMask;
DWORD bV5BlueMask;
DWORD bV5AlphaMask;
DWORD bV5CSType;
CIEXYZTRIPLE bV5Endpoints;
DWORD bV5GammaRed;
DWORD bV5GammaGreen;
DWORD bV5GammaBlue;
DWORD bV5Intent;
DWORD bV5ProfileData;
DWORD bV5ProfileSize;
DWORD bV5Reserved;
};
bool is32_flag = false;
string bmp_in_address;
string bmp_out_address;
BITMAPFILEHEADER former_bmp_FILE_Head;
BITMAPINFOHEADER former_bmp_DIB_Head;
RGB_data* former_rgb_data1;//用于24位BMP图像
RGB_data2* former_rgb_data2;//用于32位BMP图像
Plus former_bmp_Plus;
int former_width;//原图宽度
int former_height;//原图高度
bool bmp_read(string s)
{
//跟随地址以二进制读入bmp文件
ifstream former_bmp_info(s, ios::in | ios::binary);
if (!former_bmp_info)//若无法打开对应文件
{
cout << "Open File Failed!" << endl;
former_bmp_info.close();
return false;
}
else
{
cout << "Open File Successfully!" << endl << endl;
}
former_bmp_info.read((char*)&former_bmp_FILE_Head, sizeof(BITMAPFILEHEADER));//读文件头
//判断是否正确读入bmp文件
if (former_bmp_FILE_Head.bfType != 0x4D42)
{
cout << "该文件非BMP格式!" << endl;
return false;
}
else
{
cout << "该文件为BMP格式!" << endl << endl;
}
former_bmp_info.read((char*)&former_bmp_DIB_Head, sizeof(BITMAPINFOHEADER));//读DIB头
cout << "位图文件头:" << endl;
cout << "DIB头占用字节数:" << "14 bytes" << endl;
cout << "位图文件类型: 0x" << hex << former_bmp_FILE_Head.bfType << endl;//16进制
cout << "位图文件大小: " << dec << former_bmp_FILE_Head.bfSize << " bytes" << endl;
cout << "偏移字节数: " << former_bmp_FILE_Head.bfOffBits << endl;
cout << endl;
cout << "DIB头占用字节数:" << former_bmp_DIB_Head.biSize << " bytes" << endl;
cout << "位图宽度: " << former_bmp_DIB_Head.biWidth << endl;
cout << "位图高度: " << former_bmp_DIB_Head.biHeight << endl;
cout << "位图压缩类型: " << former_bmp_DIB_Head.biCompression << endl;
cout << "位图像素位数: " << former_bmp_DIB_Head.biBitCount << endl;
cout << "位图数据总占用字节数: " << former_bmp_DIB_Head.biSizeImage << " bytes" << endl;
cout << endl;
if (former_bmp_DIB_Head.biBitCount == 32)
{
is32_flag = true;
cout << "输入的图片为32位" << endl;
former_bmp_info.read((char*)&former_bmp_Plus, sizeof(Plus));//32位图像会多一个Plus部分
}
former_width = former_bmp_DIB_Head.biWidth;
former_height = former_bmp_DIB_Head.biHeight;
//计算扫描一行所需要的字节数
int former_line_byte = (former_bmp_DIB_Head.biBitCount * former_width / 8 + 3) / 4 * 4;
int need_zeros = former_line_byte - former_bmp_DIB_Head.biBitCount * former_width / 8;
if (is32_flag)
{
former_rgb_data2 = new RGB_data2[former_width * former_height];
for (int i = 0; i < former_height; i++)
{
former_bmp_info.read((char*)former_rgb_data2 + i * former_width * former_bmp_DIB_Head.biBitCount / 8, former_bmp_DIB_Head.biBitCount / 8 * former_width);
former_bmp_info.seekg(need_zeros, ios::cur);//忽略掉补位
}
}
else
{
former_rgb_data1 = new RGB_data[former_width * former_height];
for (int i = 0; i < former_height; i++)
{
former_bmp_info.read((char*)former_rgb_data1 + i * former_width * former_bmp_DIB_Head.biBitCount / 8, former_bmp_DIB_Head.biBitCount / 8 * former_width);
former_bmp_info.seekg(need_zeros, ios::cur);//忽略掉补位
}
}
former_bmp_info.close();
return true;
}
bool bmp_copy(string s)
{
BITMAPFILEHEADER copy_bmp_FILE_Head = former_bmp_FILE_Head;
BITMAPINFOHEADER copy_bmp_DIB_Head = former_bmp_DIB_Head;
ofstream bmp_out(s, ios::out | ios::binary);
if (!bmp_out)
{
cout << "Creat File Failed!" << endl;
bmp_out.close();
return false;
}
else
{
cout << "Creat File Successfully!" << endl;
}
bmp_out.write((char*)©_bmp_FILE_Head, sizeof(BITMAPFILEHEADER));
bmp_out.write((char*)©_bmp_DIB_Head, sizeof(BITMAPINFOHEADER));
if(is32_flag)bmp_out.write((char*)&former_bmp_Plus, sizeof(Plus));
int copy_line_byte = (copy_bmp_DIB_Head.biBitCount * copy_bmp_DIB_Head.biWidth / 8 + 3) / 4 * 4;
int need_zeros = copy_line_byte - copy_bmp_DIB_Head.biBitCount * copy_bmp_DIB_Head.biWidth / 8;
string temp_zero = "00000";//用0补齐,当然你也可以自定义补齐语句,这没有大碍
if (is32_flag)
{
RGB_data2* copy_rgb_data = new RGB_data2[former_width * former_height];
for (int i = 0; i < former_height; i++)
{
for (int j = 0; j < former_width; j++)
{
*(copy_rgb_data + i * former_width + j) = *(former_rgb_data2 + i * former_width + j);
}
}
//写入bmp文件
for (int i = 0; i < former_height; i++) {
bmp_out.write((char*)copy_rgb_data + i * former_width * copy_bmp_DIB_Head.biBitCount / 8, copy_bmp_DIB_Head.biBitCount / 8 * former_width);
bmp_out.write((char*)&temp_zero, need_zeros);
}
delete[] copy_rgb_data;
}
else
{
RGB_data* copy_rgb_data = new RGB_data[former_width * former_height];
for (int i = 0; i < former_height; i++)
{
for (int j = 0; j < former_width; j++)
{
*(copy_rgb_data + i * former_width + j) = *(former_rgb_data1 + i * former_width + j);
}
}
//写入bmp文件
for (int i = 0; i < former_height; i++) {
bmp_out.write((char*)copy_rgb_data + i * former_width * copy_bmp_DIB_Head.biBitCount / 8, copy_bmp_DIB_Head.biBitCount / 8 * former_width);
bmp_out.write((char*)&temp_zero, need_zeros);
}
delete[] copy_rgb_data;
}
bmp_out.close();
return true;
}
int main()
{
cout << "请输入你要查看的BMP图像的地址:" << endl;
cin >> bmp_in_address;
cout << endl;
if (!bmp_read(bmp_in_address))return 0;
cout << endl;
cout << "请输入你要输出的BMP图像的地址:" << endl;
cin >> bmp_out_address;
if (!bmp_copy(bmp_out_address))cout << "复制文件失败" << endl;
else cout << "复制文件成功" << endl;
return 0;
}