分类: 嵌入式
2016-12-23 09:23:36
原文地址:【转】BMP格式详解 作者:zimang
BMP格式详解
前言
记得本科时候讲《计算机体系结构》的老师(很遗憾忘了他姓名)评价过中外教材的差别,他说按照老外的体系结构教材,你就真的能够做出一个CPU来 (虽然只能做出很老很老的CPU),但国内的教材就很难教到这个程度。
几个月前我从零开始写了一个简单的bmp解码库,现在用一篇文章把其中的关键内容记录下来,希望能够达到让别人照着文章就可以开发出任何语言绑定的
bmp解码库的程度,以便日后我可以放心地忘却,因为我的脑子里总是不能同时记住太多的东西。
BMP简介
BMP是一种与硬件设备无关的图像文件格式,使用非常广。BMP是Windows环境中交换与图有关的数据的一种标准,在Windows环境中运行 的图形图像软件都支持BMP图像格式。
相对来讲,BMP格式比较简单,它只包含两个重要参数:编码格式(Encoding)和像素位数(bpp, bit-per-pixel)。到目前为止,BMP格式所支持的所有像素位数与编码格式的组合如下:
序号 |
像素位数(bpp) |
编码格式(Encoding) |
1 |
1 |
bit |
2 |
4 |
bgr(blue-green-red) |
3 |
4 |
rle(run-length encode) |
4 |
8 |
bgr |
5 |
8 |
rle |
6 |
grayscale |
bgr |
7 |
grayscale |
rle |
8 |
16 |
bgr |
9 |
16 |
bitfields-555 |
10 |
16 |
bitfields-565 |
11 |
16 |
bitfields-customized |
12 |
24 |
bgr |
13 |
32 |
bgr |
14 |
32 |
bitfields-888 |
15 |
32 |
bitfields-customized |
其中24bpp称为真彩(true-color)图像,应用最为广泛。16bpp的bmp图像拥有存储空间小,解析速度快,仿真彩效果好等特点,经
常出现在游戏软件中。grayscale(灰度)图像其实是8bpp的一种情况。
文件结构
典型的BMP图像文件由四部分组成:
1:文件头,它包含BMP图像文件的类型、内容尺寸和起始偏移量等信息;
2:图像参数,它包含
图像的宽、高、压缩方法,以及颜色定义等信息;
3:调色板,可选部分,bpp较小的位图需要调色板;有些位图,比如24bpp(真彩色)图就不需
要调色板;
4:位图数据,这部分的内容因位图实际像素位数和编码格式而不同,在真彩位图中直接使用RGB真彩色值;而有调色板的位图则使用调色板
中颜色索引值。
数据类型
先定义几个数据类型以便于描述。
byte —— 8 bits
word —— 16 bits
int/uint/dword
—— 32 bits
文件头
BMP的文件头共14个字节。
字节顺序 |
数据结构 |
描述 |
1,2 |
word |
高8位为字母’B’,低8位为字母’M’ |
3,4,5,6 |
uint |
文件尺寸 |
7,8 |
word |
保留字1 |
9,10 |
word |
保留字2 |
11,12,13,14 |
uint |
位图数据部分相对于文件首的起始偏移量 |
数据部分偏移量的存在,说明图像数据部分并不一定要紧随图像参数或调色板之后放置,BMP图片的制作者其实可以在调色板之后、数据部分之前填充任何
内容,只要正确地设置偏移量即可。
图像参数信息
这一个数据块共40字节或56字节。前40字节的内容如下:
字节顺序 |
数据结构 |
描述 |
15,16,17,18 |
uint |
当前结构体的大小,通常是40或56 |
19,20,21,22 |
int |
图像宽度(像素) |
23,24,25,26 |
int |
图像高度(像素) |
27,28 |
word |
这个字的值永远是1 |
29,30 |
word |
每像素占用的位数,即bpp |
31,32,33,34 |
uint |
压缩方式 |
35,36,37,38 |
uint |
图像的尺寸(字节数) |
39,40,41,42 |
int |
水平分辨率,pixels-per-meter |
43,44,45,46 |
int |
垂直分辨率,pixels-per-meter |
47,48,49,50 |
uint |
引用色彩数 |
51,52,53,54 |
uint |
关键色彩数 |
水平分辨率和垂直分辨率我从来没用过。看上去应该是与设备相关的参数。
如果你是一个有优化癖的程序员,你一定会问,图像的宽和高为什么是int型而不是uint型呢?因为想象中负数宽和高似乎没有意义。比较滑稽的是, 在BMP格式中,负数的高是有意义的。为了与高搭配,因此图像的宽也定义为int型。负数高的意义我们将在图像数据块中讨论。
第31-34字节存储着一个uint型参数,它代表图像数据的压缩方式。该参数的取值范围是0、1、2或3等等。这些取值的含义分别是:
0 —— RGB方式
1 —— 8bpp的run-length-encoding方式
2 ——
4bpp的run-length-encoding方式
3 —— bit-fields方式
只有压缩方式选项被置为bit-fields时,当前结构体的大小为56字节,否则为40字节。
调色板
当bpp小于等于8时,BMP使用调色板记录色彩信息,调色板中每条数据(即每种色彩值)都是一个uint型数据。当调色板存在时,图像数据块中存 储的只是各个像素的色彩在调色板中的索引值,必须通过在调色板中查表,才能获知各个像素的真实颜色。若引入调色板,则调色板数据块紧随在图像参数数据块之 后。
当bpp == 1时,调色板合法索引值只有0和1。因此调色板中只有2个色彩值,分别表示索引值为0和1时的色彩信息。
当bpp == 4或bpp == 8时,合法索引值范围扩大为[0,15]和[0,255]。但图像中不一定使用到了全部16种或256种颜色。第47-50字节存储的uint型数据指出 图像中实际应用的色彩数,也即调色板中的色彩值数目。当然,它不应超出调色板的合法索引值的范围。
当bpp == 4或bpp ==
8时,可以采用Run-Length-Encoding方式压缩图像的存储空间,即压缩方式选项的值为1或2(当选项值为0时,不压缩)。这种编码格式所
考虑的情况是,若4bpp或8bpp位图的尺寸较大时,由于色彩总数非常有限,所以图像中必然会有很多颜色重复的像素。因此BMP图像格式的设计者决定采
取一个简单的措施来挽回一些被浪费掉的存储空间。这个简单的措施就是RLE压缩方法。
RLE, Run-Length-Encoding
如果你是一个有优化癖的程序员,当你发现一张8bpp、宽300像素的位图中有一行像素只有两种颜色:前100个像素是红色,后200个像素是蓝 色,然而你的位图却忠实地用100字节来存储前半行重复的红色,又用200字节来存储后半行重复的蓝色,那么你一定会抓狂到大骂BMP格式的设计者是白 痴。
为了避免被骂,BMP格式的设计者想出了这样的办法:先用一个字节来存储重复色彩的数量,再用一个字节来存储这个色彩的值,即用两个字节代表一段颜 色重复的像素。并且,他们给重复色彩的数量起了个名字,叫做Run-Length,可能是因为只有在运行时我们才能知道这段重复色彩的长度。由于 runlength为0时没有意义,因此设计者把runlength=0当做每行的终止符。于是,同样存储一行300个像素,原先需要300字节,现在只 用5个字节就搞定!
字节 |
1 |
2 |
3 |
4 |
5 |
内容 |
100 |
red |
200 |
blue |
0 |
由于你是一个有着严重优化癖的程序员,所以你对这样粗制滥造的优化方法并不满足,因为你很快发现,如果一张位图中没有连续重复的像素(例如红蓝像素 点阵),那么用刚才发明的这个办法存储300个像素,居然要用601字节!当然,这种情况下最好的办法是不用压缩算法。可是,如果既有重复像素,又有点阵 的情况呢?比如前150像素是重复的绿色,后150像素是红蓝相间的像素点阵。
为了满足你变态的优化癖,BMP格式的设计者只好继续发展这个算法。首先,他们保留了“用一个字节来存储重复色彩的数量,再用一个字节来存储这个色 彩的值”的设计思路,然后修改了runlength为0的含义。设计者规定,当遇到runlength==0时,我们要继续读取下一个字节,若该字节值为 n,意味着后面的n个像素将采用“逐字翻译”的方式来解析,也就是说,这n个像素的前面没有runlength这个字节。用这种方法压缩“前150像素是 重复的绿色,后150像素是红蓝相间的像素点阵”的300个像素,只需要154个字节。
字节 |
1 |
2 |
3 |
4 |
后150个字节 |
内容 |
150 |
green |
0 |
150 |
red,blue,…,red,blue |
这个近似完美的结果中有个小问题:设计BMP格式的天才们把runlength==0的含义修改后,我们就没有行终结符了。不过天才终归是天才,他 们发现“逐字翻译”的像素数必须大于2才有意义(想想这是为什么?),因此runlength==0之后的那个字节的值为0、1或2时,目前还没有意义。 于是天才们规定,当这个值为0时,表示行结束符;当这个值为1时,表示文件结束符;当这个值为2时,似乎仍然没有什么意义;只有当该值大于等于3时,才是 “逐字翻译”。完整的压缩结果是:
字节 |
1 |
2 |
3 |
4 |
后150个字节 |
155 |
156 |
内容 |
150 |
green |
0 |
150 |
red,blue,…,red,blue |
0 |
0 |
这就是传说中的Run-Length-Encoding for 8bpp。4bpp的RLE跟8bpp时没有本质差别。
RGB与Bit-Fields
当图像中引用的色彩超过256种时,我们就需要16bpp或更高bpp的位图。调色板不适合bpp较大的位图,因此16bpp以上的位图都不使用调 色板。不使用调色板的位图图像有两种编码格式:RGB和Bit-Fields(下称BF)。
RGB编码格式是一种均分的思想,使Red、Green、Blue三个颜色分量所包含的信息容量尽可能一样大。
16bpp-RGB:在每个像素所占的16bits中,低5位表示Blue分量;中5为表示Green分量;高5位表示Red分量;最高1位无意义 (后来有些应用程序将其视为透明度Alpha分量,但这并不是标准)。所以从低到高的顺序实际上是B-G-R,这也是我在BMP简介的表格里,把RGB的 编码方式都写成BGR的原因。
24bpp-RGB:24bpp的位图又称为真彩位图,它通常只有这一种编码格式,在24bits中,低8位表示Blue分量;中8为表示 Green分量;高8位表示Red分量。
32bpp-RGB:在32bits中,低24位的编码方式与24bpp位图相同,最高8位用来表示透明度Alpha分量。32bpp的位图尺寸太 大,一般只有在图像处理的中间过程中使用。对于需要半透过效果的图像,更好的选择是PNG格式。
BF编码格式与RGB不同,它利用位域操作,人为地确定RGB三分量所包含的信息容量。在图像参数信息模块的介绍中提及,当压缩方式选项置为BF 时,图像参数结构体将比平时多出16字节。这16字节实际上是4个dword的位域掩码。按照先后顺序,它们分别是R、G、B、A四个分量的位域掩码。当 然如果没有Alpha分量,则Alpha掩码没有实际意义。
位域掩码的作用是:指出像素色彩值中RGB分量,就像子网掩码指出子网网段一样。
16bpp-BF-565:这是BF编码格式最著名和最普遍的应用。它的Red、Green和Blue分量的位域掩码分别是0xF800、 0x07E0和0x001F。
我们平时所能够见到的位图中使用BF编码格式的并不多,因为它看上去比较麻烦,而效果也不见得比RGB要好(你能用肉眼分辨出16bpp-RGB和 16bpp-BF-565之间的区别吗?)。
BF编码格式的重要应用在于游戏软件。游戏软件通常包含数量庞大的小尺寸图片。如果一张图片中几乎没有Blue分量,那么使用16bpp-RGB格 式显然会浪费掉B分量所占的5位。此时若采用16bpp-BF-772格式,只给B分量2位,那么R与G分量都拥有7位的容量,几乎接近真彩图像。因此存 储空间小、仿真彩能力强的特点使BF编码格式仍然有着独特的用武之地。
32bpp-BF-xxx:我一直不明白为什么会存在32bpp的位图。如果说32bpp-RGB格式的存在是因为在图像处理过程中存储起来比较高
效(不用压缩),那么32bpp-BF又是为什么存在呢?
图像数据块
图像数据块从文件头中起始偏移量字段所指出的位置开始,其中存放着位图图像的数据,数据格式由图像参数信息块中的压缩方式选项的取值决定。操作图像 数据块时,有一些注意事项:
当压缩方式为RGB时,图像数据块以“行”为单位双字对齐。例如一张宽度为5像素的8bpp的图像,其实际使用的存储空间是每行8个字节。又如一幅 4bpp、宽度为5像素的位图图像,其实际使用的存储空间是每行4个字节。
当bpp < 8时,每个字节将存放多个像素的色彩索引,则先出现的像素存放在高位中。例如某4bpp图像第一行像素的顺序是red, green, blue, yellow, …则图像数据块中第一个字节的高4位值为red,低4位值为green;第二字节高4位值为blue,低4位值为yellow。1bpp时的情况以此类 推。
还记得前面提到,图像参数里,高度有可能是负值吗?这看上去很逗,但事实是,你见过的大多数位图,其图像参数里的高度都是负值。BMP格式设计者规 定,当高度为正值时,图像数据块中记录的第一行像素数据是图像的最后一行;而数据块中最后一行数据才是实际图像的第一行,也就是说,数据块中的行记录与实 际图像反序。而当高度为负值时,数据块中的行记录与实际图像才是同序的。
如果你觉得这太奇怪了,我很理解。不过我们必须怀着无比崇敬之情接受这个看似滑稽的规定。这是因为在那个年代里,那些设计BMP格式的天才首先都是 数学家,让天才的数学家们习惯左上角为原点,并且y轴方向向下的二维直角坐标系的格局显然是很困难的,既然他们手上又有设计BMP的大权,于是……唉,这 就是历史。