目录

一、位图Bitmap基本介绍

二、bmp图片介绍

三、实现代码

3.1 BitMap位图类创建

        3.1.1 定义像素结构体

        3.1.2 定义位图类

3.2 BMP图片的保存

        3.2.1 BMP图片格式定义

        3.2.2 函数的实现


一、位图Bitmap基本介绍

位图(Bitmap)是一种图像文件格式,用于存储数字图像。在计算机图形学中,位图是基于像素的图像表示方式,每个像素具有特定的颜色值。

位图是一种典型的栅格图形,由一个像素矩阵组成。每个像素在这个矩阵中占据一个固定的位置,并包含特定的颜色信息。位图数据可以表示为一个二维数组,其中每个元素代表一个像素。在内存中,这通常是一个线性数组,使用行优先或列优先的顺序存储。

在位图中,每个像素的颜色通常由红色、绿色和蓝色(RGB)三原色的组合来表示。颜色深度(比如24位)决定了每个颜色通道(红、绿、蓝)的位数。例如,在24位颜色深度中,每个颜色通道有8位,允许256种不同的强度,从而可以表示大约1670万种不同的颜色。

位图可以是压缩的或未压缩的。未压缩的位图直接以原始形式存储像素数据,这使得它们易于读取和编辑,但文件大小较大。压缩的位图使用各种算法(如RLE、JPEG、PNG压缩)来减小文件大小,但可能会牺牲一定的图像质量。

二、bmp图片介绍

BMP(就是位图图像文件)格式是一种无损压缩的栅格图像文件格式,非常适合存储详细的高质量图像。

文件结构

BMP 文件通常包含以下几个部分:

组件

描述

文件头(File Header)

包含文件类型、大小、保留字和图像数据偏移等信息

信息头(Image Header)

包含图像宽、高、颜色深度、压缩类型等信息

颜色表(Color Table,可选)

在使用索引颜色时包含颜色索引

像素数据(Pixel Data)

存储图像的实际像素颜色数据

  • 文件头:通常为14字节,包含识别文件类型的标识("BM")、整个文件的大小、以及像素数据开始的位置。
  • 信息头:长度可以变化,但通常为40字节(BITMAPINFOHEADER)。包含图像的尺寸信息、颜色深度(每像素位数)、是否使用压缩等信息。
  • 颜色表:这部分仅在位图使用索引颜色(而非直接颜色)时出现。它包含一个颜色索引,定义图像中所有可能的颜色。
  • 像素数据:包含图像的实际像素值。在不同的颜色深度下,每个像素可能占用不同的字节数。例如,在24位颜色深度下,每个像素由3个字节(红、绿、蓝)表示。

颜色表示

  • 颜色深度:BMP 格式支持不同的颜色深度,从 1-bit(黑白)到 4-bit(16色)、8-bit(256色)直至 24-bit(真彩色)甚至32-bit(增加了透明通道)。
  • 颜色存储:在24-bit BMP中,每个像素由3个字节表示,分别为蓝色、绿色和红色(BGR格式)。32-bit BMP添加了一个透明度(alpha)通道。

本文章只讨论最基础,最简单的8-bit BMP。

三、实现代码

3.1 BitMap位图类创建

3.1.1 定义像素结构体

        下面这段代码定义的是存放在位图数组中每一位(即:像素)的信息,也就是RGB数据。使用的是一个无符号8位整数的类型(uint8_t),范围从0-255,这允许每个通道有 256 种不同的强度级别(从 0(无光)到 255(最亮)),因此这个是一个即可以满足8-bit BMP而且可以尽可能的减少资源使用的选择。

//定义像素结构体
typedef struct Pixel
{
	//每一个像素点的RGB通道值
	uint8_t red;
	uint8_t green;
	uint8_t blue;
    
    //初始化
	Pixel() :red(0), green(0), blue(0) {}
	Pixel(uint8_t r, uint8_t g, uint8_t b) :red(r), green(g), blue(b) {}
}Pixel;

3.1.2 定义位图类

        这里使用的是opencv进行的图片读取,在构造函数中将.jpg格式的图片中的像素信息保存在pixels中。(这里我将pixels放在public中,为的是后续简单话操作,实际应用的时候最好修改它的可见性,包括width和height)。需要注意的是Mat类的数组索引是 行索引-列索引。因此在初始化pixels的时候特意的将行和列进行了调换,便于后续操作的理解。

//定义位图类
class Bitmap
{
public:
	int width, height;					//图片的宽度和高度
	vector<vector<Pixel>> pixels;		//用于保存图片的像素矩阵

public:
	Bitmap(int w, int h);
	Bitmap(const cv::Mat& img);			//将读取的.jpg图片保存pixels中
	void setPixel(int x, int y, const Pixel& color);	//设置像素 
	Pixel getPixel(int x, int y) const;					//读取像素
};

Bitmap::Bitmap(int w, int h)
	:width(w), height(h), pixels(h, vector<Pixel>(w))
{

}

Bitmap::Bitmap(const cv::Mat& img)
	:width(img.cols), height(img.rows), pixels(img.cols, vector<Pixel>(img.rows))	//notice the initation of this vector
{
	for (int y = 0; y < img.rows; y++)
	{
		for (int x = 0; x < img.cols; x++)
		{
			//Mat的索引方式是[行索引][列索引]
			auto color = img.at<cv::Vec3b>(y, x);
			//std::cout << "Red: " << static_cast<int>(color[2]) << ", Green: " << static_cast<int>(color[1]) << ", Blue: " << static_cast<int>(color[0]) << std::endl;

			// OpenCV 默认使用 BGR 格式
			pixels[x][y] = Pixel(static_cast<int>(color[2]), static_cast<int>(color[1]), static_cast<int>(color[0]));
		}
	}
}


void Bitmap::setPixel(int x, int y, const Pixel& color)
{
	if (x >= 0 && x <= width && y >= 0 && y <= height)
	{
		pixels[x][y] = color;
	}
}

Pixel Bitmap::getPixel(int x, int y)const
{
	if (x >= 0 && x <= width && y >= 0 && y <= height)
	{
		return pixels[x][y];
	}
	//如果坐标超出了图片的边界,则返回一个纯黑色的图片
	return Pixel();
}

现在就可以使用Bitmap类讲.jpg格式的图片转化为bitmap类型了!

int main()
{

	Mat img = cv::imread("demo.jpg");
	cv::imshow("demo", img);
	
	if (img.empty())
	{
		cerr << "Error loading image" << endl;
	}

	Bitmap bitmap(img);
	Pixel pixel = bitmap.getPixel(12, 12);
	return 0;
}

但是仅仅将其转化为bitmap类还是不够的, 转化后的保存才是关键。

3.2 BMP图片的保存

3.2.1 BMP图片格式定义

        bmp的格式定义是严格按照要求进行的,正因如此,我们需要关闭结构体中自动内存对齐的机制。

3.2.1.1 结构体内存对齐

*结构体内存对齐

结构体内部对齐(Structure Padding)是编译器在编译过程中进行的一种优化操作,其目的是为了提高程序的访问效率。这种对齐通常涉及在结构体的成员之间插入填充字节(padding),以保证每个成员都按照其自然对齐界限放置。

*为什么需要结构体内部对齐?

现代计算机系统中,内存是以字节块的形式访问的,这些字节块通常是 2、4、8 字节等。当一个数据类型的自然对齐界限(例如,4字节整数的自然对齐界限是 4 字节)与其存储位置对齐时,处理器可以最高效地访问这个数据。如果数据没有对齐,处理器可能需要进行额外的内存访问来读取或写入不对齐的数据,从而降低效率。

*内存对对齐的原理

编译器会自动在结构体成员之间添加填充字节,以确保每个成员的地址对齐到其自然对齐界限。例如,假设有一个结构体包含一个 char 和一个 int

struct Example {
    char a; // 占用 1 字节
    int b;  // 占用 4 字节
};

在这个结构体中,char 类型的自然对齐界限是 1 字节,而 int 类型是 4 字节。为了确保 int 类型的成员 b 在 4 字节边界上对齐,编译器会在 char a 后面插入 3 个填充字节。

*内存对齐的控制

在某些情况下,想要控制结构体的内部对齐方式,为了减少内存占用或满足特定的内存布局要求。在 C++ 中,可以使用 #pragma pack 指令来控制结构体的对齐方式:

#pragma pack(push, 1) // 设置对齐界限为 1 字节
struct Example {
    char a;
    int b;
};
#pragma pack(pop) // 恢复到之前的对齐设置

虽然减少填充可以降低内存占用,但可能会降低数据访问的效率。在考虑更改结构体的默认对齐方式时,需要权衡内存效率和访问效率之间的关系。在一些特定的应用场景(如与硬件设备的低级通信或网络协议的实现)中,控制结构体的对齐是非常重要的。

3.2.1.2 禁用内存对齐进行bmp格式定义

在定义位图文件头(如 BitmapFileHeaderBitmapInfoHeader)时,禁用结构体内存对齐是为了确保结构体的布局与位图文件格式的规范严格一致。位图文件格式是一种非常具体的、按照字节顺序排列的文件格式,任何额外的填充字节都会破坏文件结构,导致文件无法被正确解析。

为什么内存对齐会影响位图文件头?

位图文件格式(尤其是 .bmp 格式)有一个精确的字节布局,每个字段都有固定的大小和顺序。例如,文件头可能包含一个2字节的类型字段,紧接着是4字节的文件大小字段,等等。如果编译器对结构体成员进行了内存对齐,它可能会在这些字段之间插入填充字节以满足硬件的对齐要求,这会导致实际的文件头布局与位图规范不匹配。

#pragma pack(push, 1) // 禁用结构体内存对齐以满足位图的格式规范
//定义位图文件头
typedef struct BitmapFileHeader {
	uint16_t file_type{ 0x4D42 };      // "BM" 字符
	uint32_t file_size{ 0 };           //指定文件的大小,包括位图头文件的14字节
	uint16_t reserved1{ 0 };           //保留字,不用考虑
	uint16_t reserved2{ 0 };           //同上
	uint32_t offset_data{ 0 };         //从头文件到实际位图数据的偏移字节数
}BitmapFileHeader;


//定义信息头结构体
typedef struct BitmapInfoHeader {
	uint32_t size{ 0 };                //该结构体的长度
	int32_t width{ 0 };                //图像的宽度
	int32_t height{ 0 };               //图像的高度
	uint16_t planes{ 1 };              //位平面数,必须是1,不用考虑
	uint16_t bit_count{ 0 };           //颜色位数:1位2色;4位16色;8为256色;16,24,32为全彩
	uint32_t compression{ 0 };         //是否压缩,有效值:BI_RGB,BI_RLE8,BI_RLE4,BI_BITIELDS
	uint32_t size_image{ 0 };          //实际的位图数据占用的字节数
	int32_t x_pixels_per_meter{ 0 };   //目标设备的水平分辨率,单位是每米的像素数
	int32_t y_pixels_per_meter{ 0 };   //目标设备的垂直分辨率,单位同上
	uint32_t colors_used{ 0 };         //实际使用的颜色数
	uint32_t colors_important{ 0 };    //图像中重要的颜色数
}BitmapInfoHeader;
#pragma pack(pop)                      //取消禁用结构体对齐

3.2.2 函数的实现

为了方便起见,我讲这个保存函数放在了Bitmap类中,需要稍微的修改一个class的定义

//定义位图类
class Bitmap
{
public:
	int width, height;					
	vector<vector<Pixel>> pixels;		
public:
	Bitmap(int w, int h);
	Bitmap(const cv::Mat& img);							 
	void setPixel(int x, int y, const Pixel& color);	
	Pixel getPixel(int x, int y) const;					

	//定义位图写入功能
	//这个函数接收一个 Bitmap 对象,并将其内容保存为 .bmp 文件
	void writeBitmapToFile(const std::string& file_path);

};

函数的实现为:

void Bitmap::writeBitmapToFile(const std::string& file_path)
{
	BitmapFileHeader file_header;		//文件头
	BitmapInfoHeader info_header;		//信息头

	//define the values of information header
	info_header.size = sizeof(BitmapInfoHeader);
	info_header.width = width;
	info_header.height = height;
	info_header.bit_count = 24; // 每个像素24位,即RGB
	info_header.compression = 0; // 不压缩
	info_header.size_image = width * height * 3; // 图像数据大小
	


	file_header.file_size = sizeof(BitmapFileHeader) + sizeof(BitmapInfoHeader) + info_header.size_image;
	file_header.offset_data = sizeof(BitmapFileHeader) + sizeof(BitmapInfoHeader);

	std::ofstream file;
	file.open(file_path, std::ios::out | std::ios::binary);
	if (!file) {
		std::cerr << "Unable to open file: " << file_path << std::endl;
		return;
	}

	// 写入文件头和信息头
	file.write(reinterpret_cast<const char*>(&file_header), sizeof(file_header));
	file.write(reinterpret_cast<const char*>(&info_header), sizeof(info_header));
	
	// 写入像素数据
	for (int y = 0; y < height; ++y) 
	{
		for (int x = 0; x < width; ++x) 
		{
			Pixel pixel = getPixel(x, y);
			//cout << "A" << endl;
			uint8_t color[3] = { pixel.blue, pixel.green, pixel.red };
			//cout << "B" << endl;
			//cout << color[0] << " " << color[1] << " " << color[2] << endl;
			file.write(reinterpret_cast<const char*>(color), 3);
		}
	}
	
	file.close();
}

这样我们就能在转化为bitmap类型后进行保存了

int main()
{

	Mat img = cv::imread("demo.jpg");
	cv::imshow("demo", img);
	
	if (img.empty())
	{
		cerr << "Error loading image" << endl;
	}

	Bitmap bitmap(img);
	Pixel pixel = bitmap.getPixel(12, 12);
	//cout << "red: " << pixel.red << "green: " << pixel.green << "blue: " << pixel.blue << endl;
	bitmap.writeBitmapToFile("img.bmp");
	cin.get();

	return 0;
}

#完整代码

#include<iostream>
#include<vector>
#include<opencv2/opencv.hpp>

#include<cstdint>
#include<fstream>

using namespace cv;
using namespace std;

//定义像素结构体
typedef struct Pixel
{
	//每一个像素点的RGB通道值
	uint8_t red;
	uint8_t green;
	uint8_t blue;

	Pixel() :red(0), green(0), blue(0) {}
	Pixel(uint8_t r, uint8_t g, uint8_t b) :red(r), green(g), blue(b) {}
}Pixel;

#pragma pack(push, 1) // 禁用结构体内存对齐以满足位图的格式规范
//定义位图文件头
typedef struct BitmapFileHeader {
	uint16_t file_type{ 0x4D42 }; // "BM" 字符
	uint32_t file_size{ 0 };
	uint16_t reserved1{ 0 };
	uint16_t reserved2{ 0 };
	uint32_t offset_data{ 0 };
}BitmapFileHeader;


//信息头结构体
typedef struct BitmapInfoHeader {
	uint32_t size{ 0 };
	int32_t width{ 0 };
	int32_t height{ 0 };
	uint16_t planes{ 1 };
	uint16_t bit_count{ 0 };
	uint32_t compression{ 0 };
	uint32_t size_image{ 0 };
	int32_t x_pixels_per_meter{ 0 };
	int32_t y_pixels_per_meter{ 0 };
	uint32_t colors_used{ 0 };
	uint32_t colors_important{ 0 };
}BitmapInfoHeader;
#pragma pack(pop)



//定义位图类
class Bitmap
{
public:
	int width, height;					//the width and height of the picture which you want to dispose
	vector<vector<Pixel>> pixels;		//the array which is used to save the information of the picture
public:
	Bitmap(int w, int h);
	Bitmap(const cv::Mat& img);							 //transform ".jpg" type to Bitmap object
	void setPixel(int x, int y, const Pixel& color);	//set the color of the picture inside the array 
	Pixel getPixel(int x, int y) const;					//get the color in the specified positoin 

	//定义位图写入功能
	//这个函数接收一个 Bitmap 对象,并将其内容保存为 .bmp 文件:
	void writeBitmapToFile(const std::string& file_path);

};

Bitmap::Bitmap(int w, int h)
	:width(w), height(h), pixels(h, vector<Pixel>(w))
{

}

Bitmap::Bitmap(const cv::Mat& img)
	:width(img.cols), height(img.rows), pixels(img.cols, vector<Pixel>(img.rows))	//notice the initation of this vector
{
	for (int y = 0; y < img.rows; y++)
	{
		for (int x = 0; x < img.cols; x++)
		{
			//Mat的索引方式是[行索引][列索引]
			auto color = img.at<cv::Vec3b>(y, x);
			//std::cout << "Red: " << static_cast<int>(color[2]) << ", Green: " << static_cast<int>(color[1]) << ", Blue: " << static_cast<int>(color[0]) << std::endl;

			// OpenCV 默认使用 BGR 格式
			pixels[x][y] = Pixel(static_cast<int>(color[2]), static_cast<int>(color[1]), static_cast<int>(color[0]));
		}
	}
}


void Bitmap::setPixel(int x, int y, const Pixel& color)
{
	if (x >= 0 && x <= width && y >= 0 && y <= height)
	{
		pixels[x][y] = color;
	}
}

Pixel Bitmap::getPixel(int x, int y)const
{
	if (x >= 0 && x <= width && y >= 0 && y <= height)
	{
		return pixels[x][y];
	}
	//如果坐标超出了图片的边界,则返回一个纯黑色的图片
	return Pixel();
}

void Bitmap::writeBitmapToFile(const std::string& file_path)
{
	BitmapFileHeader file_header;		//文件头
	BitmapInfoHeader info_header;		//信息头

	//define the values of information header
	info_header.size = sizeof(BitmapInfoHeader);
	info_header.width = width;
	info_header.height = height;
	info_header.bit_count = 24; // 每个像素24位,即RGB
	info_header.compression = 0; // 不压缩
	info_header.size_image = width * height * 3; // 图像数据大小
	


	file_header.file_size = sizeof(BitmapFileHeader) + sizeof(BitmapInfoHeader) + info_header.size_image;
	file_header.offset_data = sizeof(BitmapFileHeader) + sizeof(BitmapInfoHeader);

	std::ofstream file;
	file.open(file_path, std::ios::out | std::ios::binary);
	if (!file) {
		std::cerr << "Unable to open file: " << file_path << std::endl;
		return;
	}

	// 写入文件头和信息头
	file.write(reinterpret_cast<const char*>(&file_header), sizeof(file_header));
	file.write(reinterpret_cast<const char*>(&info_header), sizeof(info_header));
	
	// 写入像素数据
	for (int y = 0; y < height; ++y) 
	{
		for (int x = 0; x < width; ++x) 
		{
			Pixel pixel = getPixel(x, y);
			//cout << "A" << endl;
			uint8_t color[3] = { pixel.blue, pixel.green, pixel.red };
			//cout << "B" << endl;
			//cout << color[0] << " " << color[1] << " " << color[2] << endl;
			file.write(reinterpret_cast<const char*>(color), 3);
		}
	}
	
	file.close();
}


int main()
{

	Mat img = cv::imread("demo.jpg");
	cv::imshow("demo", img);
	
	if (img.empty())
	{
		cerr << "Error loading image" << endl;
	}

	Bitmap bitmap(img);
	Pixel pixel = bitmap.getPixel(12, 12);
	//cout << "red: " << pixel.red << "green: " << pixel.green << "blue: " << pixel.blue << endl;
	bitmap.writeBitmapToFile("img.bmp");
	cin.get();

	return 0;
}