bmp 图像解码原理
1. BMP 图像数据结构图
下图是从 Wikipedia 上找到的 BITMAPV5HEADER
数据结构图,画的非常全面。
上篇博客中说过,windows BMP 格式各个版本都只是在前一版本结构末尾追加字段,因此 BITMAPV5HEADER
的结构图完全可以覆盖前面各个版本的 windows BMP 格式数据结构。后面我们可以对照这个结构图来解剖一张 BMP 图片以加深对于其数据结构分布的理解。
原图传送门:BMP file format
接下来我们可以找几张图片验证一下上面的结构图。找了一圈,手头的 BMP 图基本都是 Windows V1 BMP 格式的,那就先用两张 Windows V1 BMP 格式的图做一下讲解,掌握基本原理之后,对照上面的数据结构图,理解其他版本的 BMP 数据结构也不难。可以使用 Hex Editor Neo
或 Beyond Compare
的工具,以十六进制的方式打开 BMP 图片来查看其源数据。
2. 徒手拆解 BMP 图像(位深 8 bit,带调色板)
如下图所示为 图像处理标准测试图集
中找到一张 Masuda1.bmp
图;
这张图像的比特深度为 8 比特,且正好是一张彩色图像,因此可以查看一下他的调色板;
2.1 位图文件头(Bitmap file header)
注意,bmp 图片文件头中数据是以小端序
存储的,因此我们阅读时,需要做一下调整。
例如,文件类型
占两个字节,我们将 前两个字节
数据(0x42 和 0x4d)取出,从后往前读,那么文件类型就是 0x4d42
。
序号 | 字节数 | 变量名 | 字段名 | 数据(HEX) | 数据(DEC) | 描述 |
---|---|---|---|---|---|---|
1 | 2 | bfType | 文件类型 | 0x4D42 | 19778 | 字符显示就是‘BM’ |
2 | 4 | bfSize | 文件大小 | 0x0003C436 | 246,838 | 检查文件信息,验证一致 |
3 | 2 | bfReserved1 | 保留字段 1 | 0x0000 | 0 | 保留字段,必须设置为 0 |
4 | 2 | bfReserved2 | 保留字段 2 | 0x0000 | 0 | 保留字段,必须设置为 0 |
5 | 4 | bfOffBits | 数据偏移量 | 0x00000436 | 1,078 | 即偏移 1,078 字节开始为图像数据 |
2.2 DIB 头(Device Independent Bitmap Header)
序号 | 字节数 | 变量名 | 字段名 | 数据(HEX) | 数据(DEC) | 描述 |
---|---|---|---|---|---|---|
6 | 4 | biSize | DIB 头大小 | 0x00000028 | 40 | 即 DIB 头大小为 40 字节 |
7 | 4 | biWidth | 图像宽度 | 0x00000200 | 512 | 图像的宽度为 512 像素 |
8 | 4 | biHeight | 图像高度 | 0x000001E0 | 480 | 图像的高度为 480 像素 |
9 | 2 | biPlanes | 颜色平面数 | 0x0001 | 1 | 目标设备说明颜色平面数,总被设置为 1 |
10 | 2 | biBitCount | 每个像素的位数 | 0x0008 | 8 | 每个像素的比特深度为 8 比特 |
11 | 4 | biCompression | 压缩类型 | 0x00000000 | 0(BI_RGB) | 表示不压缩 |
12 | 4 | biSizeImages | 图像数据大小 | 0x0003C000 | 245,760 | 图像大小 = 文件大小 - 偏移量 |
13 | 4 | biXPelsPerMeter | 水平分辨率 | 0x00000000 | 0 | 表示未知或不适用 |
14 | 4 | biYPelsPerMeter | 垂直分辨率 | 0x00000000 | 0 | 表示未知或不适用 |
15 | 4 | biClrUsed | 调色板大小 | 0x00000100 | 256 | 表示调色板中有 256 种颜色 |
16 | 4 | biClrImportant | 重要颜色数 | 0x00000100 | 256 | 表示对图像显示有重要影响的颜色数目 |
此处注意,除 图像宽度(biWidth)
、图像高度(biHeight)
、水平分辨率(biXPelsPerMeter)
、垂直分辨率(biYPelsPerMeter)
外,其他变量均为无符号整数,而以上四个则是 整型数(int)
,即可以为负的:
-
图像宽度
(biWidth)
如果 biWidth 值为正数
,即图像是从左向右排列
的,那么每行第一个像素就位于最左侧。
如果 biWidth 值为负数
,即图像是从右向左排列
的,那么每行第一个像素就位于最右侧。 -
图像高度
(biHeight)
如果 biHeight 值为正数
,即图像是从下到上排布
的,那么第一个像素就位于文件的左下角。
如果 biHeight 值为负数
,即图像是从上到下排布
的,那么第一个像素就位于文件的左上角。 -
水平分辨率
(biXPelsPerMeter)
当 biXPelsPerMeter 为正数
时,表示水平分辨率为每英寸的像素数
。
当 biXPelsPerMeter 为负数
时,表示水平分辨率为每米的像素数
。 -
垂直分辨率
(biYPelsPerMeter)
当 biYPelsPerMeter 为正数
时,表示垂直分辨率为每英寸的像素数
。
当 biYPelsPerMeter 为负数
时,表示垂直分辨率为每米的像素数
。
2.3 调色板(Color Table)
图像的 比特深度(Bit Depth)
取值有这么几种:1、2、4、8、16、24、32;而 调色板(color table)
则是比特深度小于等于 8 的图像文件所特有的,相对应的调色板大小是 2、4、16 和 256,调色板以 4 字节为单位,每 4 个字节存放一个颜色值,图像的数据是指向调色板的索引。
调色板是颜色的索引
,这里使用的是 8 位色图,共有 256 种颜色。每种颜色由 RGB 三原色再加上 Alpha 值(透明度)组成,需要 4 个字节来表示。256 种颜色并不能涵盖所有的颜色,因此需要一个索引,即用 1 个字节的索引指向 4 个字节表示的颜色。
所以 调色板就像一个二维数组 table[N][4]
,其中 N 是颜色的数量(这里是 256)。因此,调色板的大小就是 256 * 4 = 1024 字节。在调色板之前,有 14 字节的 bmp 文件头,40 字节的位图信息头,加上 1024 字节的调色板,一共 1078 字节。真正的图像数据前面有1078字节,这和 bmp 文件头中的数据偏移量相符。
这张图的调色板从第 0x00003006 比特开始,每 4 个字节为一个颜色,256 个颜色,共计 1024 个字节。每个颜色又分为 B G R A(注意顺序)
四个通道,因此我们可以列一张表直接将调色板解析出来。此处我们可以拿前四个颜色举个例子,解析如下:
序号 | 颜色数据(HEX) | B 通道(蓝色) | G 通道(绿色) | R 通道(红色) | A 通道(透明度) |
---|---|---|---|---|---|
1 | 0xB2B5E500 | 0xB2 [178] | 0xB5 [181] | 0xE5 [229] | 0x00 [0] |
2 | 0x9CB6E600 | 0x9C [156] | 0xB6 [182] | 0xE6 [230] | 0x00 [0] |
3 | 0xA5B1E600 | 0xA5 [165] | 0xB1 [177] | 0xE6 [230] | 0x00 [0] |
4 | 0x9CADED00 | 0x9C [156] | 0xAD [173] | 0xED [237] | 0x00 [0] |
… | … | … | … | … | … |
2.4 图像数据(Image Data)
这是一张带有调色板的 8 bit 图,因此每个像素数据占一个字节,其内容是调色板的色号索引,我们可以举个例子,更直观的去认识调色板的工作原理;我们可以在图像二进制文件中找到第一个像素点和最后一个像素点的数据,手动将其解析出对应的颜色,与图像相应位置的颜色作比对。
如下图所示,我们找到第一个像素点的数据为 0x8D
,即该像素点的颜色应为调色板中第 0x8D 号颜色
。可以计算出这个四字节颜色分量相对于文件 0 位置的偏移为 offset = 0x8D * 4 + 0x36 = 0x26A
,我们在文件中找到该颜色,根据 B G R A
的顺序,并借助画图软件还原出来,可以得到该颜色如右下角的方块所示。
由于这张图的 biWidth
和 biHeight
均为正数,所以 第一个像素点
应该位于文件的 左下角
,我们找到该像素点进行比对,结果一致。
如下图所示,我们找到最后一个像素点的数据为 0x4D
,即该像素点的颜色应为调色板中第 0x4D 号颜色
。可以计算出这个四字节颜色分量相对于文件 0 位置的偏移为 offset = 0x4D * 4 + 0x36 = 0x16A
,我们在文件中找到该颜色,根据 B G R A
的顺序,并借助画图软件还原出来,可以得到该颜色如右下角的方块所示。
由于这张图的 biWidth
和 biHeight
均为正数,所以 最后一个像素点
应该位于文件的 右上角
,我们找到该像素点进行比对,结果一致。
2.5 小结
解析完整张图片后,我们可以再计算一下文件的大小:
数据块 | BMP 文件头 | DIB 头 | 调色板 | 图像数据 | 填充字节 | 文件整体(总计) |
---|---|---|---|---|---|---|
字节数 | 14 | 40 | 1,024 | 245,760 | 0 | 246,838 |
3. 徒手拆解 BMP 图像(位深 24 bit,不带调色板)
如下图所示为 图像处理标准测试图集
中找到一张 LenaRGB.bmp
图;
下面这张图像的比特深度为 24 比特,我们可以看一下不带调色板的图像其像素数据是如何解析的;
3.1 位图文件头(Bitmap file header)
序号 | 字节数 | 变量名 | 字段名 | 数据(HEX) | 数据(DEC) | 描述 |
---|---|---|---|---|---|---|
1 | 2 | bfType | 文件类型 | 0x4D42 | 19778 | 字符显示就是‘BM’ |
2 | 4 | bfSize | 文件大小 | 0x000C0038 | 786,488 | 检查文件信息,验证一致 |
3 | 2 | bfReserved1 | 保留字段 1 | 0x0000 | 0 | 保留字段,必须设置为 0 |
4 | 2 | bfReserved2 | 保留字段 2 | 0x0000 | 0 | 保留字段,必须设置为 0 |
5 | 4 | bfOffBits | 数据偏移量 | 0x00000036 | 54 | 即偏移 54 字节开始为图像数据 |
3.2 DIB 头(Device Independent Bitmap Header)
序号 | 字节数 | 变量名 | 字段名 | 数据(HEX) | 数据(DEC) | 描述 |
---|---|---|---|---|---|---|
6 | 4 | biSize | DIB 头大小 | 0x00000028 | 40 | 即 DIB 头大小为 40 字节 |
7 | 4 | biWidth | 图像宽度 | 0x00000200 | 512 | 图像的宽度为 512 像素 |
8 | 4 | biHeight | 图像高度 | 0x00000200 | 512 | 图像的高度为 512 像素 |
9 | 2 | biPlanes | 颜色平面数 | 0x0001 | 1 | 目标设备说明颜色平面数,总被设置为 1 |
10 | 2 | biBitCount | 每个像素的位数 | 0x0018 | 24 | 每个像素的比特深度为 24 比特 |
11 | 4 | biCompression | 压缩类型 | 0x00000000 | 0(BI_RGB) | 表示不压缩 |
12 | 4 | biSizeImages | 图像数据大小 | 0x0003C000 | 245,760 | 图像大小 = 文件大小 - 偏移量 |
13 | 4 | biXPelsPerMeter | 水平分辨率 | 0x0000B881 | 47,233 | 水平方向每英尺像素数为 47,233 个像素 |
14 | 4 | biYPelsPerMeter | 垂直分辨率 | 0x0000B881 | 47,233 | 垂直方向每英尺像素数为 47,233 个像素 |
15 | 4 | biClrUsed | 调色板大小 | 0x00000000 | 0 | 无调色板 |
16 | 4 | biClrImportant | 重要颜色数 | 0x00000000 | 0 | 无重要颜色 |
3.3 图像数据(Image Data)
这张图像的 bit 深度为 24,表示每个像素占 3 字节,由三个 8 bit RGB 分量组成,其排布顺序应该是按照 B G R
顺序排列的。全图共计 512 x 512 = 262144 个像素,因此图像数据占据了 262144 x 3 = 786432 字节。其起始位置位于偏移量为 54 字节的位置。如下图所示,为图像数据的起始部分。
以下是头部像素点的数据解析:
序号 | 颜色数据(HEX) | B 通道(蓝色) | G 通道(绿色) | R 通道(红色) |
---|---|---|---|---|
1 | 0x391652 | 0x39 [57] | 0x16 [22] | 0x52 [82] |
2 | 0x391652 | 0x39 [57] | 0x16 [22] | 0x52 [82] |
3 | 0x3E2060 | 0x3E [62] | 0x20 [32] | 0x60 [96] |
4 | 0x3E1C5D | 0x3E [62] | 0x1C [28] | 0x5D [93] |
… | … | … | … | … |
为了更直观的理解其原理,我们还是选取第一个和最后一个像素点进行手动解析,然后与原图中的像素点颜色进行对比。
第一个像素点
,在原图中位置位于图像 左下角
,其数据偏移为 0x00000036
如下图所示:
最后一个像素点
,在原图中位置位于图像 右上角
,其数据偏移为 [(512 x 512) - 1] x 3 + 54 = 786483
即 0x00C0033
如下图所示:
对比之后,以上两个像素点均与原图中一致。
3.4 填充字节
为保证文件的兼容性和 CPU 读写效率,BMP 文件格式通常要求像素数据的每一行字节数必须是 4 的倍数,但是某些 BMP 文件并不完全遵循这个规则。这通常是因为它们使用了不同的压缩方式或文件格式。比如,8 位深度的 BMP 图像使用的是索引颜色,而不是直接存储像素的 RGB 值。例如上面提到的 Masuda1.bmp
,文件数据总大小为 246,838 字节
,并不是 4 的倍数。在这种情况下,每个像素只需要占用一个字节,因此不需要填充字节来调整行的大小。
另一方面,24 位深度的 BMP 文件中,每个像素需要占用 3 个字节来存储 RGB 值。如果每一行的字节数不是 4 的倍数,则需要添加填充字节来调整行的大小以确保像素数据能够正确存储。填充字节的数量可以通过以下公式计算:
padding = (4 - (width * bpp) % 4) % 4
其中,width
表示该行像素的宽度(单位为像素),bpp
表示每个像素的位数(单位为 bit)。例如,一张 24 bit 深度的 BMP 图像,宽度为 100 像素,则每行像素数据占用的字节数为:
bytes_per_row = (100 * 24) / 8 = 300
因为 300 不是 4 的倍数,所以需要添加填充字节。根据公式,填充字节的数量为:
padding = (4 - (300 % 4)) % 4 = 2
因此,每行像素数据占用的总字节数为 302,其中包括 300 个像素数据字节和 2 个填充字节。
现在看上面我们解析的 LenaRGB.bmp
图,按照原本的像素数据排列,虽然一行数据有 512 个像素,每个像素 3 字节存储,每行图像占有 1356 个字节是 4 的倍数,不需要填充字节,但是,文件总大小是 786,486 个字节
并非 4 的倍数,显然不符合每行四字节,所以在文件最后,我们会看到有两字节的 0 数据进行填充:
3.5 小结
解析完整张图片后,我们可以再计算一下文件的大小:
数据块 | BMP 文件头 | DIB 头 | 调色板 | 图像数据 | 填充字节 | 文件整体(总计) |
---|---|---|---|---|---|---|
字节数 | 14 | 40 | 0 | 768,432 | 2 | 786,488 |