BMP图像旋转

这应该算是我的第一篇技术型文章吧。仅仅是试个水,看看贴上代码的效果,其实并没有什么技术含量。刚好昨晚我和旋转图像的大作业搏斗了一个晚上,说来也挺搞笑的,我废寝忘食地研究了好几个小时,就为了把一张破图片顺时针旋转90度……之前不是有新闻说吗,某地区负责健康码维护的技术人员花了老大的劲把二维码图片从1M压缩到100K。我对他们也有点感同身受了,或许压缩图片的操作真的隐藏着很多技术上的问题。有没有可能是,他们之前维护系统时一直使用1M的图片,然后很多别的功能都基于这种规格的图片开发出来,产生了极高的耦合。然后他们不得不花大量的时间进行解耦,为了让领导能听懂,就只提了一下压缩图片的事。以上纯属瞎猜。

BMP文件格式有很多坑,其中一个就是文件对齐。在24位BMP图像的像素矩阵中,一个像素用3个字节存储,存储内容是该像素的颜色信息,即rgbBluergbGreenrgbRed。然而,为了让像素矩阵的大小刚好是4的倍数,这个矩阵最后还会加上那么几列!旋转90度后,这个列数可能会变!变了的话又会影响到前面信息头中的数据。

还有就是24位BMP和32位BMP的信息头大小是不一样的!这些不去了解根本不知道。有些时候,bug的产生,并不是因为我们不够谨慎,而是因为还存在未知。很多人调侃自己是“面向百度编程”,“CV工程师”,我不认为这是一件可耻的事情。计算机领域是一个人为构建起来的学科,前人的经验至关重要。善于搜索和查阅先辈遗留下来的知识也是CS技能树中不可或缺的一部分。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// BmpClass.h
#pragma once
#pragma pack(2)

typedef unsigned char Byte; // 1字节
typedef unsigned short Word; // 2字节
typedef unsigned long Dword; // 4字节
typedef long Long; // 4字节

// BMP文件头,12字节
struct BitMapFileHeader
{
//Word bfType; // 位图文件类型 "BM"
Dword bfSize; // 位图文件大小,以字节为单位
Word bfReserved1; // 位图文件保留字
Word bfReserved2; // 位图文件保留字
Dword bfOffBits; // 位图数据起始位置,以相对位图文件头的偏移量表示
};

// DIB信息头,40字节
struct BitMapInfoHeader
{
Dword biSize; // 本结构占用字节数
Long biWidth; // 位图宽度,单位像素
Long biHeight; // 位图高度
Word biPlanes; // 1
Word biBitCount; // 每个像素所需的位数,必须是1、4、8、24之一
Dword biCompression; // 位图压缩类型,必须是0、1、2之一
Dword biSizeImage; // 位图的大小,单位字节
Long biXPelsPerMeter; // 位图水平分辨率,每米像素数
Long biYPelsPerMeter; // 位图垂直分辨率
Dword biClrUsed; // 实际使用颜色数,24位为0
Dword biClrImportant; // 重要颜色数
};

// DIBV5信息头,124字节
struct BitMapInfoHeader32bit
{
Dword biSize; // 本结构占用字节数
Long biWidth; // 位图宽度,单位像素
Long biHeight; // 位图高度
Word biPlanes; // 1
Word biBitCount; // 每个像素所需的位数,必须是1、4、8、24之一
Dword biCompression; // 位图压缩类型,必须是0、1、2之一
Dword biSizeImage; // 位图的大小,单位字节
Long biXPelsPerMeter; // 位图水平分辨率,每米像素数
Long biYPelsPerMeter; // 位图垂直分辨率
Dword biClrUsed; // 实际使用颜色数,24位为0
Dword biClrImportant; // 重要颜色数
Long extras[21]; // 补足124字节
};

// 颜色类型,4字节
struct RgbQuad
{
Byte rgbRed; // 0~255
Byte rgbGreen;
Byte rgbBlue;
Byte rgbReserved; // 0
};

struct PixelInfo
{
Byte blue;
Byte green;
Byte red;
};

struct Image
{
int width;
int height;
Byte* imageData;
};
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
// Bmp.cpp
#pragma pack(2)
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <fstream>
#include "BmpClass.h"
//#define channel 3
using namespace std;
typedef PixelInfo* PMatrix;


BitMapFileHeader fileHeader; // 文件头
BitMapInfoHeader infoHeader; // 信息头
BitMapInfoHeader32bit infoHeader32bit;//
int channel; // 通道数
//RgbQuad palette[256]; // 调色板
//PMatrix matrix, rotateMatrix;


void PrintLog()
{
clog << "位图文件大小: " << fileHeader.bfSize << endl;
clog << "位图宽度: " << infoHeader.biWidth << endl;
clog << "位图高度: " << infoHeader.biHeight << endl;
clog << "每个像素所需的位数: " << infoHeader.biBitCount << endl;
clog << "像素矩阵的大小:" << infoHeader.biSizeImage << endl;
//clog << "实际使用颜色数: " << infoHeader.biClrUsed << endl;
clog << "DIB信息头大小:" << infoHeader.biSize << endl;

}

Image* LoadBMPImage(string path)
{
int height, width, padding = 0;
Image* image = new Image();
Word bfType;
Byte rgbColor;

ifstream inFile(path.c_str(), ios::in | ios::binary);
if (!inFile)
{
delete image;
return NULL;
}

// 读取文件类型
inFile.read((char*)&bfType, sizeof(Word));

if (bfType != 0x4D42)
{
cerr << "Loading...It's not a BMP File!" << endl;
inFile.close();
delete image;
return NULL;
}
else clog << "Loading...It's a BMP File!" << endl;

// 读取文件头与信息头
inFile.read((char*)&fileHeader, sizeof(BitMapFileHeader));
inFile.read((char*)&infoHeader, sizeof(BitMapInfoHeader));

PrintLog();


width = infoHeader.biWidth;
height = infoHeader.biHeight;
image->width = width;
image->height = height;


if (infoHeader.biBitCount == 24)
{
clog << "Loading...It's a 24-bit Image!" << endl;
channel = 3;
padding = (channel * width) % 4;
if (padding)
padding = 4 - padding;
}
else if (infoHeader.biBitCount == 32)
{
clog << "Loading...It's a 32-bit Image!" << endl;
channel = 4;
padding = 0;

inFile.seekg(sizeof(BitMapFileHeader) + sizeof(Word), ios::beg);
inFile.read((char*)&infoHeader32bit, sizeof(BitMapInfoHeader32bit));
}
else // 不符合位数要求
{
cerr << "Loading...It's neither a 24-bit nor a 32-bit BMP File!" << endl;
inFile.close();
delete image;
return NULL;
}

image->imageData = new Byte[sizeof(Byte) * width * height * channel];

inFile.seekg(fileHeader.bfOffBits, ios::beg); // 定位读指针到像素矩阵区

for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
for (int k = 0; k < channel; k++)
{
inFile.read((char*)&rgbColor, sizeof(Byte));
image->imageData[(height - i - 1) * channel * width + j * channel + k] = rgbColor;
// 还原成习惯数组表示(从左到右从上到下)
}
}
if (padding) // 如果需要对齐
{
for (int j = 0; j < padding; j++)
inFile.read((char*)&rgbColor, sizeof(Byte));
}
}

inFile.close();

return image;
}

void RotateAndSaveBMPImage(string path, Image* image)
{
Word bfType = 0x4D42;

ofstream outFile(path.c_str(), ios::out | ios::binary);
if (!outFile)
{
cerr << "Ready to rotate...New file Open error!" << endl;
return;
}

// 调整像素矩阵

clog << "Rotating...Transforming pixel matrix!" << endl;

Byte rgbColor;
int height = image->height;
int width = image->width;
int senpai;
int padding = (channel * height) % 4;

if (channel == 4)
padding = 0;
else if (channel == 3 && padding)
padding = 4 - padding;

outFile.seekp(fileHeader.bfOffBits, ios::beg); // 定位写指针到像素矩阵区

for (int j = width - 1; j >= 0; j--)
{
for (int i = height - 1; i >= 0; i--)
{
for (int k = 0; k < channel; k++)
{
rgbColor = image->imageData[i * channel * width + j * channel + k];
outFile.write((char*)&rgbColor, sizeof(Byte));
//clog << i << " " << j << " " << rgbColor << endl;

//senpai = 1919810 / 114514;
//outFile.write((char*)&senpai, sizeof(Byte));
}
}
if (padding)
{
for (int i = 0; i < padding; i++)
{
senpai = 1919810 / 114514;
outFile.write((char*)&senpai, sizeof(Byte));
}
}
}


// 调整信息头

clog << "Rotating...Modifying InfoHeader!" << endl;

if (channel == 3)
{
infoHeader.biWidth = height;
infoHeader.biHeight = width;
infoHeader.biSizeImage = width * (height * channel + padding); // 重点!!!
}
else if (channel == 4)
{
infoHeader32bit.biWidth = height;
infoHeader32bit.biHeight = width;
infoHeader32bit.biSizeImage = width * (height * channel); // 重点!!!
}

//clog << width * (height * channel + padding) << endl;

outFile.seekp(0, ios::beg);
outFile.write((char*)&bfType, sizeof(Word));
outFile.write((char*)&fileHeader, sizeof(BitMapFileHeader));

if (channel == 3)
outFile.write((char*)&infoHeader, sizeof(BitMapInfoHeader));
else if (channel == 4)
outFile.write((char*)&infoHeader32bit, sizeof(BitMapInfoHeader32bit));

outFile.close();
}

/*bool CopyBMPImage(string inPath, string outPath)
{
ifstream inFile(inPath.c_str(), ios::binary | ios::in);
if (!inFile)
{
cerr << "Copying...Source File Open Error." << endl;
return false;
}

ofstream outFile(outPath.c_str(), ios::binary | ios::out);
if (!outFile)
{
cerr << "Copying...New File Open Error." << endl;
inFile.close();
return false;
}
char c;
while (inFile.get(c))
outFile.put(c);
outFile.close();
inFile.close();
return true;
}*/

void PrintInstruction()
{
clog << "====================================================" << endl;
clog << "可以旋转24位和32位BMP图像" << endl;
clog << "请输入指令:rotatebmp src.bmp dest.bmp" << endl;
clog << "====================================================" << endl;
return;
}

int main()
{
string op, src, dest;

PrintInstruction();

while (cin >> op)
{
if (op == "rotatebmp")
{

clog << "RotateBMP Initialized." << endl;

cin >> src >> dest;

Image* image = LoadBMPImage(src);

if (image != NULL)
{
clog << "Load Successfully!" << endl;

RotateAndSaveBMPImage(dest, image);

clog << "Rotate Successfully!" << endl;
}
else
{
cerr << "Load Failed!" << endl;
}
}

PrintInstruction();

}

return 0;
}

/*
rotatebmp 32.bmp 7.bmp
*/

发现一件怪事,VS上能正常运行的代码,在VSCode上就会segmentation fault。是编译器的问题吗?

破案了,但没有完全破。经另一位饱受困扰的同学提醒,我发现在VSCode中#pragma宏定义要放在#include头文件后面才能正常运行。但我也不知道是什么原因。这周末疯狂写码,再这样下去怕是要寄。所以我也懒得管了,还是身体要紧、好好休息。