浅谈图像格式 .bmp
位图(Bitmap)格式其实并不能说是一种很常见的格式(从我们日常的使用频率上来讲,远不如 .jpg .png .gif 等),因为其数据没有经过压缩,或最多只采用行程长度编码(RLE,run-length encoding)来进行轻度的无损数据压缩。以至于,LaTeX 并不能像插入 .jpg 甚至于矢量图那样便捷地插入 BMP 图片,知乎的专栏封面上传也不支持 BMP。
但是,.bmp 仍然发挥着很重要的角色,而且也确实有拿来聊一聊,进而学习一些更深入的知识的意义。正是因为它没有进行数据压缩,其内部存储的色彩信息(灰度图,RGB 或 ARGB)直接以二进制的形式暴露在外,也十分方便借助计算机软件进行简单或深入的分析。那么,今天,我将带领大家从二进制文件的角度,探索 .bmp 格式的奥秘。
文件头
位图格式的文件头长度可变,而且其中参数繁多。但是我们日常生活中遇到的 .bmp 格式图片的文件头长度绝大多数都是 54 字节,其中包括 14 字节的 Bitmap 文件头以及 40 字节的 DIB (Device Independent Bitmap) 数据头,或称位图信息数据头(BItmap Information Header)。其他形式的在此暂不讨论。
以下两张图表大家不必认真观看,因为在后续章节中我们将会针对实际例子,借助十六进制数据和表格进行具体分析。
位图文件头 Bitmap File Header (14 bytes)
这是一张汉化版的英文维基百科相关词条(BMP file format - Wikipedia)中的表格。
位图信息数据头 DIB Header (54 bytes)
对于压缩方式,虽然 Bitmap 格式提供简单的压缩功能,但是绝大多数情况下,并没有采用任何压缩手段。
原始位图数据 Raw Bitmap Data
接下来才是重头戏——原始位图数据。拿最常见的 24BPP RGB (24 比特每像素,红绿蓝三通道) 位图来说,每种颜色需要 8 比特,或者说 1 字节,来存储。在二进制文件中,通常情况下,RGB 按照蓝、绿、红的顺序依次表示图片中的像素点,而 RGBA 则按照蓝、绿、红、透明的顺序(从左下开始,横向逐行向上扫描)。特殊时候,也会出现顺序与上述情况不同的特例,这时色彩顺序会写在 DIB Header 的 Bit Fields 中,以不同色彩通道的 Mask 的形式进行规定。由于 BI_BITFIELDS 也是一种压缩方式,而通常 BMP 不采用任何压缩方式,所以绝大多数时候,我们都是按照前面说的顺序进行排序。
关于 Bit Fields,在此贴一张维基百科的图片,便于理解。
这里与别处不同,采用的是 big endian(事实上,整个 BMP 文件中,只有此处使用 big endian。big endian 指的是先出现的数字在高位)。因此,如果 Mask 呈现如下表中的数据:
则由于 Blue 对应数字最大,因此顺序为蓝、绿、红、透明。
数据按照像素行进行包装,便于读取。但是这并不是全部,因为其中还可能会有补零(zero-padding)。这涉及到计算机的数据结构对齐(data structure alignment)的问题。
主流的 CPU 每次从内存中读取并处理数据块(chunk),且通常为 32 比特(4 字节)。因此,为了提升读取效率,位图每行的数据(字节)都需要是 4 的倍数。不可避免地,有些时候每行的结尾就会出现补零(其实补其他任意数字也是可以的,但常见都是补 0)。
对于每行补零的个数,是有计算方法的。公式如下(知乎的 LaTeX 公式不能很好地支持中文,因此无法进行翻译,抱歉):
即,每行的字节数等于:每像素比特数乘以图片宽度加 31 的和除以 32,并向下取整,最后乘以 4。
得到了每行的字节数,进而就能够得到原始位图数据,或者说存储图片的所有像素的色彩信息的数据,的大小了:
即,原始位图数据大小等于:每行的字节数乘以图像高度(也就是总行数)
再加上之前说的文件头的数据大小(通常为 54 字节),就能够得到文件大小了。
实践:文件大小
说完了理论,我们来看一看实际效果吧。
这里有一张 10x10 的 BMP:
在 Windows 画图的保存选项中,选择 24BPP RGB(同时我们也可以看到, .bmp 和 .dib 是等效的):
从上到下颜色依次为(每种颜色宽度 10 像素,高度 2 像素):
- 蓝(0,0,255)
- 青(0,255,255)
- 绿(0,255,0)
- 黄(0,255,255)
- 红(255,0,0)
首先,我们来计算一下每行的字节数:
所以,原始位图数据的大小为:
前面已经说过,文件头的大小通常为 54 字节。因此,整个文件的大小应该为:
右键查看文件属性:
看来我们的计算是完全正确的,的确是 374 字节。
实践:头文件内容
我们通过查看二进制文件来进一步分析上面这张图。(借助软件 Sublime Text 进行简易的二进制查询)
为了防止在寻找对应数字的时候出现迷惑,我们先来大致理一理基础的进制转换。图中,每个数字都是十六进制,也就是 4 比特。每字节有 8 比特,也就是 2 个十六进制数字。每四个数字分为一组,占 2 字节。因此每行共 16 字节。
假如我们想要找第 0x22 (十进制为 34)个字节,方法如下:
首先,找到第三行。因为每行 16 字节,所以在十六进制下的 22,直接看左边第一位数字对应的就是第三行(行号从 0 开始)。然后,开始寻找第三个字节(同样地,列号也从 0 开始)。因此答案是 40。
再试一次。假如我们想要找第 0xBC 个字节,方法如下:
首先,找到第 12 (B=11)行,然后找第 13 个字节。 不难找到吧?
相信看到这里,大家都已经具备阅读十六进制的能力了。我们现在步入正题。
首先,位图头文件有 14 字节,因此我们先看这一部分(为方便观察,标注了位置数字,并两个数字为一组,为一字节)。
还记得它们分别是什么意思吗?我把图片再贴一遍。
位置 00,尺寸 2,内容 42 4D,也就是 ASCII 表的“BM”两个字母。
位置 02,尺寸 4,内容 76 01 00 00,表示文件大小。因为是 little endian(靠前的数字对应低数位),所以实际十六进制数字为 00000176。对应十进制数字为:
这与我们先前的计算以及实际答案完全相同。
位置 06,尺寸 2,内容 00 00,预留字段,通常为 0。
位置 08,尺寸 2,内容 00 00,同上。
位置 0A,尺寸 4,内容 36 00 00 00,表示位图数据的开始位置。同样为 little endian,对应十进制数字为 54(这也是文件头的总长度)。
然后,再来看 DIB 数据头的 40 字节内容。
位置 0E,尺寸 4,内容 28 00 00 00,意思是 DIB header 大小。对应十进制数字为 40 (little endian)。
位置 12,尺寸 4,内容 0A 00 00 00,代表图像宽度。对应十进制 10。
位置 16,尺寸 4,内容 0A 00 00 00,代表图像高度,10。
位置 1A,尺寸 2,内容 01 00,表示色彩平面的数量,必须为 1。
位置 1C,尺寸 2,内容 18 00,表示每像素用多少比特来表示。对应十进制 24。
位置 1E,尺寸 4,内容 00 00 00 00,表示采用何种压缩方式。此处为不压缩。
位置 22,尺寸 4,内容 40 01 00 00,表示原始位图数据的大小。对应十进制 320。还记得 little endian 怎么计算吗?
剩下的内容我们可以不看,因为几乎用不到,它们也“如我们所愿”,都是 0。
实践:原始位图数据
分析完文件头,剩下的内容就是我们期待已久的,以数组形式排列的所有像素的色彩信息。
大家还记得前面提到过的数据结构对齐吗?如果每像素 24 比特(3 字节),那么总共 10x10 大小的图片,原始位图数据的大小不应该是 300 字节吗?但是实际却是 320 字节。由此可见,每行都有额外的补零。
之前我们已经计算过了,每行字节数为 32 字节。这是一个除不尽 3 的数字。
所以,每行有 10 个像素信息(30 字节),外加 2 字节的补零。也就是说,每 32 字节(在下图中正好为两行)中都会有 2 字节的补零。实际是不是这样呢?我们从 0x36 (54)位,也就是原始位图数据的起始位置开始看,每两行都会有用绿色方框括住的 2 字节补零。
然后,从图片的最下面开始看,最先出现的是红色,也就是(255,0,0)。前面已经说过,位图文件中,每像素三种颜色的数据的出现顺序依次为蓝,绿,红。因此红色对应的数据应当为 00 00 FF。不难看出,前 20 个像素点(请注意跳过补零)内容都是 00 00 FF。
下一个颜色为黄色,即 00 FF FF,再之后是绿色,即 00 FF 00……
是不是看起来清楚多了?
最后还有一点想说的,就是并不是所有位图文件都需要补零。我们再来看另外一个例子。这里有一张 1920x1080 尺寸的 BMP(也就是本篇文章的封面图)。经计算:
而 5760 是 3 的倍数,因此对于这张图片,就不需要补零了。也就是说,有 1920x1080 个像素点,原始位图数据的大小就正好为
字节。再加上 54 字节的头文件,文件的大小应该为 6,220,854 字节。实际上呢?
岂不美哉?一丁点字节都不浪费。
今天我们就先讲到这里。下次有机会的话,我将为大家展示如何借助 BMP 文件的格式做一些简单有趣的图像处理。