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
23BmpFileHeader 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;
}
讲完了一个像素,再说bmp如何存储一堆像素,嗯,对于图像的每一行来说,就是简简单单地把该行所有的像素的数据从左道右一个紧挨一个地排在一起,就是这样。头24bit是第一个像素数据,接下来的24bit就是第二个。。。。。 但是在存储列的时候情况稍微复杂了一点点,首先,列存储的顺序是自下而上的,也就是说,我们从bmp文件中读取到的第一行的数据是实际图像中最下方一行像素的(这对图像灰化也不重要,代码里也没有体现)。第二,bmp要求每一行的数据以4字节对齐,也就是说如果一行像素所占据的字节数不到4的整数倍,那就用0在末尾补全不足的位数。 >举个例子,如果一幅图像每行有5个像素,共占据位,于是在存储进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
15for (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);//文件就应该读完了
1 | |
注意这些强制类型转换是不可省略的,直接使用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
4for(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
23unsigned 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);
效果
