问题:现有一张bmp图片,要求将它读取到程序中并进行灰度化、水平翻转、模糊、茶色滤镜四种效果的一种,并输出新图片,如下所示:
命令行输入:
其中:
参数1:-b/g/s/r,先后表示blur(模糊),grey(灰度化),sepia(褐色),row reverse(水平翻转)
参数2:源文件名
参数3:新文件名
当我第一次接触到这个问题时,是无从下手的。但在查阅了不少资料之后,整整一天,我成功地只用C++实现了打开、修饰、保存bmp文件的功能!
目录
1.bmp文件的基本信息
(1).bmp文件的种类
打开Windows自带的画图软件,发现bmp的存储格式有好几种。
- 单色位图:只有黑白两种颜色,每个像素占1位(1/8字节)
- 16色位图:每个像素占4位(1/2字节)
- 256色位图:每个像素占8位(1字节)
- 24位位图(真彩色):每个像素占24位(3字节),每个字节存储R/G/B三种中的一种颜色数值(0~255)
每个像素占的位数被称为位深度(biBitCount,在后面会用到),可以在图片的属性->详细信息中查看。
(2).bmp文件结构(重点)
bmp文件数据由4部分组成:
- 文件头
- 文件信息头
- 调色板(24位位图无)
- 图像颜色信息
在此只讨论24位位图即真彩色的问题,至于其他的bmp文件种类不做讨论。
先放出图片:
1>文件头
bfType | 如果是bmp文件,值为“BM”,对应十进制为19778 |
bfSize | 文件总大小 |
bfReserved1 | 保留字1,一般为0 |
bfReserved2 | 保留字2,一般为0 |
bfOffBits | 文件起始位置距真正的图像信息的距离 |
2>信息头
biSize | 信息头大小,24位图中为40 |
biWidth | 图像宽度(px),即水平方向的像素个数 |
biHeight | 图像高度(px),即垂直方向的像素个数 |
biPlanes | 一般为1 |
biBitCount | 位深度,重要,决定了bmp的类型 |
biCompression | 是否压缩,一般为0 |
biSizeImages | 图像颜色信息占用的实际字节数,包括了对齐所需的0 |
biXPelsPerMeter | 水平分辨率 |
biYPelsPerMeter | 垂直分辨率 |
biClrUsed | 一般为0 |
biClrImportant | 一般为0 |
注意:我们可能会发现 biWidth*3*biHeight与biSizeImages并不一样,这是为什么呢?接下来会解释。
3>调色板(不作讨论)
4>图像颜色信息
- 像素的存储顺序是从下到上,从左到右,在文件中以类似一维数组的方法线性存储。
- 每个像素的颜色信息每3个字节一组,按BGR的顺序存放。
- 其中每个字节只存一个颜色值。颜色值范围是0~255,用无符号char型存储。
三个图就能说明问题:
但是这些数据真的如此紧密地排列吗?
对于宽为4的倍数的图片(如:1024px),确实如此。每一行的像素数据存完后,紧挨着存储下一行像素的数据,行与行的数据之间没有空隙。
但对于宽度不是4的倍数的图片(如:474px),每一行的像素数据存储完后,会自动空出几个字节,直到这一行的字节数为4的倍数为止。
直接呈上图片:
biWidth=4时:很好,不用补任何0,因为4*3=12已经是4的倍数
biWidth=5时:糟糕,5*3=15不是4的倍数,要补一个0才能是4的倍数16
所以,在读取宽度不是4的倍数的图片时,一行的数据读完后,要跳过几个字节才能读到下一行的数据。跳过字节的个数,我取名为offset。它的计算方法如下:
offset = (fileInFoHeader.biWidth * 3) % 4;
if (offset != 0) {
offset = 4 - offset;
}
现在我可以解释2>中末尾提到的问题了。
例子:现在有一张宽度为474px,高度为842px的图片。
不考虑offset时:
474*3*842=1197324(Byte)
考虑时:
(474*3)%4=2
offset=4-2=2(Byte)
每行字节数:474*3+2=1424(Byte)
图像数据总字节数:1424*842=1199008(Byte)
谁对谁错?看看图就知道了。
它们的差值:1199008-1197324= 1684(Byte),而1684=842*2。因为每一行末尾有2字节的空隙,那么,842行的空隙积累起来,正好就是1684字节。
debug的结果说明,图像数据占用的实际字节数是考虑了偏移的,这些数据在存的时候就已经有空隙,因此我们写文件的时候也要刻意的写入空隙,不然系统无法读取我们生成的新图片。这一点在后面很关键!
2.实现思路
首先要把bmp文件读进来。由以上的分析,应该把bmp的文件头、信息头、图像数据分开读取。
然后要生成新bmp文件。应该要依次写入文件头、信息头、图像数据。
最后实现图像处理功能。这些用于图像处理的函数封装在一个单独的头文件中,使用时传入函数指针即可。
3.定义相关类、结构体
定义文件头、信息头结构体(因为它们不需要任何函数),里面存放与文件相关的属性。各个属性的大小参考一开始时的bmp文件结构图,2字节一般定义成unsigned short,4字节一般定义成unsigned int.
定义bmp类(因为它需要定义函数),里面最重要的是一个存放”颜色“结构体对象的数组,用于接收读出的图像颜色数据。还有一个int型的offset,一个文件头结构体对象,一个数据头结构体对象。定义读文件和写文件两个函数。
很自然地,需要一个”颜色“结构体对象,它有三个属性B/G/R。
注意:结构体中,所有属性的定义顺序必须和文件存储的信息顺序一致,否则在读文件时得到的数据会混乱!也就是:必须根据bmp文件结构来!
文件头BmpFileHeader
#pragma pack(2)//注意这里
struct BmpFileHeader {
unsigned short bfType;
unsigned int bfSize;
unsigned short bfReserved1;
unsigned short bfReserved2;
unsigned int bfOffBits;
};
尤其要注意#pragma pack(2),没有这一行,这个结构体占用的空间大小就不是所有属性大小之和,换句话说,它清除了属性与属性之间内存的”空洞“。明白这个,对读文件操作极其重要!