Bmp图像读取和灰化

本文实现对Bmp图像中24色彩位深的图像的读写,以及对读取出的图像进行灰化操作

Bmp图像

Bmp是一种比较简单的图形格式,其开头是文件头BmpFileHeader,包括校验码,文件大小等信息,后面跟着信息头BmpInfoHeader,记录了图像的长宽,位深度等信息,之后就是真正的图像数据。

文件头和信息头的格式千言万语不如直接放代码,其中很多字段没有注释,因为它们其实并不重要(对于24色彩位深图像来说),关注那些有注释的就好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#define COLOR_BIT_COUNT 24 //只支持24位深度
#define HEADER_SIZE 54 //BmpFileHeader和BmpInfoHeader加起来的标准大小,可以用static_assert检查一下

#pragma pack(push) //重要,调整结构体对齐策略,使其与标准的完全一致,不写的话编译器可能坏事
#pragma pack(1)
struct BmpFileHeader {
unsigned short bf_type;
unsigned int bf_size;
unsigned int bf_reserved;
unsigned int bf_off_bits;

BmpFileHeader(unsigned int size) {
this->bf_type = 0x4D42; //固定校验值'BM'
this->bf_size = size;//整个文件(包含头和图像数据的总大小)
this->bf_reserved = 0;
this->bf_off_bits = HEADER_SIZE;//偏移量,这里假设BmpInfoHeader紧跟在BmpFileHeader后面,图像数据紧跟在BmpInfoHeader后面,一般的图像文件中都是这样的
}
};
#pragma pack(pop)

#pragma pack(push)
#pragma pack(1)
struct BmpInfoHeader {
unsigned int bi_size;
unsigned int bi_width;//宽,一行的像素数
unsigned int bi_height;//高,一列的像素数
unsigned short bi_plants;
unsigned short bi_bit_count;//位深度,必须是24
unsigned int bi_compression;
unsigned int bi_sized_images;
int bi_x_per_meter;
int bi_y_per_meter;
unsigned int bi_cir_used;
unsigned int bi_cir_important;

BmpInfoHeader(unsigned int width, unsigned int height) {
this->bi_size = 40;
this->bi_width = width;
this->bi_height = height;
this->bi_plants = 1;
this->bi_bit_count = COLOR_BIT_COUNT;//24色彩位深
this->bi_compression = 0;
this->bi_sized_images = 0;
this->bi_x_per_meter = 0;
this->bi_y_per_meter = 0;
this->bi_cir_used = 0;
this->bi_cir_important = 0;
}

};
#pragma pack(pop)

读取文件信息头

知道了信息格式,简单地使用C库的fread就可以把文件头信息读出来了,检查格式的代码比读取的代码还要多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BmpFileHeader file_head(0);//随便初始化,反正会被后面读入的数据覆盖
BmpInfoHeader info_head(0, 0);

FILE* fp;
fopen_s(&fp, name.c_str(), "rb");//打开文件,name是文件路径
if (fp == nullptr) {
printf("Fail to open file!\n");
return false;
}

fread(&file_head, sizeof(BmpFileHeader), 1, fp);//读取BmpFileHeader

if (file_head.bf_type != 0x4D42){//检查校验值
printf("文件格式错误!\n");
return false;
}

fread(&info_head, sizeof(BmpInfoHeader), 1, fp);//读取BmpInfoHeader

if (info_head.bi_bit_count != COLOR_BIT_COUNT){
printf("仅支持24位深图像!\n");
return false;
}
### 读取图像像素 图像的真正的像素信息一般来说紧跟在头信息后面,也就是说只要继续fread就可以把图像读出来了。但是为了解析每一个像素的颜色信息,我们需要进一步了解bmp存储像素颜色的方式。 在bmp文件中,24位深图像使用24位(3个字节)存储一个像素,其中每8位(1个字节)储存其中一个颜色分量,由暗到亮取值范围为0-255。颜色分量的存储顺序为BGR,即第一个8位代表该像素中蓝色的亮度,第二个8位代表绿色的亮度,第三个8位代表红色的亮度。(虽然在图像灰化这个任务中颜色分量的存储顺序并不重要,但是还是讲清楚吧) 举例如下: >白色 0xffffff >黑色 0x000000 >纯红色 0x0000ff

讲完了一个像素,再说bmp如何存储一堆像素,嗯,对于图像的每一行来说,就是简简单单地把该行所有的像素的数据从左道右一个紧挨一个地排在一起,就是这样。头24bit是第一个像素数据,接下来的24bit就是第二个。。。。。 但是在存储列的时候情况稍微复杂了一点点,首先,列存储的顺序是自下而上的,也就是说,我们从bmp文件中读取到的第一行的数据是实际图像中最下方一行像素的(这对图像灰化也不重要,代码里也没有体现)。第二,bmp要求每一行的数据以4字节对齐,也就是说如果一行像素所占据的字节数不到4的整数倍,那就用0在末尾补全不足的位数。 >举个例子,如果一幅图像每行有5个像素,共占据5×3=155 \times 3 = 15位,于是在存储进bmp文件时需要在该行的末尾再补一个1个字节(填0)达到4的倍数16字节,再开始下一行的存储。(读取时则需要跳过这些填补的字节)

综上,我们先通过图像的宽度计算出每行末尾需要填补(跳过)多少个字节

1
2
//unsigned int width; 图像宽度 
unsigned int tail_size = ((width * 3 + 3) / 4) * 4 - width * 3;//天花板除

由此,我们已经可以写出准确读取图像中每一个像素的代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (int i = i; i < (int)height; i--) {
for (int j = 0; j < (int)width; j++) {
unsigned char color[3];
fread((void*)color, sizeof(char), 3, fp);

//color[0] : 蓝色分量
//color[1] : 绿色分量
//color[2] : 红色分量
what_ever_we_want_to_do(color);
}

fseek(fp, tail_size, SEEK_CUR);//skip padding
}

close(fp);//文件就应该读完了
### 图像的灰化操作 首先我们需要明确这样一个事实:我们转化出了“灰色图像”,实际上也是要用24位深的bmp格式存储的。也就是说,每个像素还是有着蓝绿红三个颜色通道,但是呈现的结果却是灰色(白色,黑色)。 一个灰色的像素,实际上只存储了一个颜色信息,其实就是说,它的rgb三个分量是始终相同的,全为0就是黑色,全为255就是白色,取在0-255中间就是灰色。 把一个彩色的像素转化为灰色,在bgr颜色空间下是就极其简单的取平均值

1
2
3
//虽然unsigned short就可以,但unsigned int用习惯了
unsigned char gray = (unsigned char)(((unsigned int)color[0] + (unsigned int)color[1] + (unsigned int)color[2]) / 3);

注意这些强制类型转换是不可省略的,直接使用unsigned char类型相加会导致溢出,所以,需要先转化到更大的类型上再进行加法。

至于这个除法除不尽怎么办,嗯,这个小细节真的不重要,取近似,四舍五入,向上向下取整都不会有太大区别。

最后,我们把颜色的三个分量都用计算出来的灰度值替换,再写回另一个文件里就可以了。 为了代码清楚明了,我们将读图像和写图像的流程分开。即,先把图像读到一个vector中保存,进行处理,再把它写入目标文件,而不是把读,处理和写放在同一个循环中(这样其实节省内存,但可拓展性和稳健性不好)。

于是我们在读图像时直接将图像数据读进vector里存起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//定义一个Pixel结构代表一个像素数据
union Pixel{ //联合体
public:
struct{
unsigned char b, g, r;
};//匿名struct
unsigned char v[3];

Pixel(unsigned char b,unsigned char g,unsigned char r)
:b(b),g(g),r(r)
{}
}

std::vector<Pixel> framebuffer(width * height);//存储需要的空间可以直接计算出来哦

for (int i = i; i < (int)height; i--) {
for (int j = 0; j < (int)width; j++) {
fread((void*)&framebuffer[i * height + j], sizeof(char), 3, fp);//i * height + j:计算j行i列像素存储在framebuffer中的下标
}
fseek(fp, tail_size, SEEK_CUR);//文件指针后移,skip padding
}

close(fp);//文件就应该读完了

接着,进行灰化处理

1
2
3
4
for(Pixel& p : framebuffer){//注意这里的取引用符号&不要漏了
unsigned char gray = (unsigned char)(((unsigned int)p.r + (unsigned int)p.g + (unsigned int)p.b) / 3);
p = {gray, gray, gray};//这样赋值
}

最后,写入bmp文件,和读图像的流程一摸一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int bitmap_size = (width * 3 + tail_size) * height;//图像数据占据的字节数
BmpFileHeader file_head = BmpFileHeader(bitmap_size + HEADER_SIZE);//bitmap_size + HEADER_SIZE是整个文件的大小
BmpInfoHeader info_head = BmpInfoHeader(width, height);

FILE* fp;
fopen_s(&fp, name.c_str(), "wb");//name是要写入的文件路径
if (fp == nullptr) {
printf("Fail to write file!\n");
return false;
}

fwrite((void*)&file_head, sizeof(file_head), 1, fp);//写入BmpFileHeader
fwrite((void*)&info_head, sizeof(info_head), 1, fp);//写入BmpInfoHeader

//写入图像数据
for (int i = i; i < (int)height; i--) {
for (int j = 0; j < (int)width; j++) {
fwrite((void*)framebuffer[i * height + j], sizeof(char), 3, fp);
}
fwrite((void*)"\0\0\0", sizeof(char), tail_size, fp);//填充0
}

fclose(fp);

效果

灰化前 灰化后


Bmp图像读取和灰化
https://9-extra.github.io/2023/04/04/Bmp图像读取和灰化/
作者
9_Extra
发布于
2023年4月4日
许可协议