手把手 bmp 格式编解码(二)—— 徒手拆解 bmp 图像

1. BMP 图像数据结构图

  下图是从 Wikipedia 上找到的 BITMAPV5HEADER 数据结构图,画的非常全面。

  上篇博客中说过,windows BMP 格式各个版本都只是在前一版本结构末尾追加字段,因此 BITMAPV5HEADER 的结构图完全可以覆盖前面各个版本的 windows BMP 格式数据结构。后面我们可以对照这个结构图来解剖一张 BMP 图片以加深对于其数据结构分布的理解。

  原图传送门:BMP file format

BITMAPV5HEADER
  接下来我们可以找几张图片验证一下上面的结构图。找了一圈,手头的 BMP 图基本都是 Windows V1 BMP 格式的,那就先用两张 Windows V1 BMP 格式的图做一下讲解,掌握基本原理之后,对照上面的数据结构图,理解其他版本的 BMP 数据结构也不难。可以使用 Hex Editor NeoBeyond Compare 的工具,以十六进制的方式打开 BMP 图片来查看其源数据。

2. 徒手拆解 BMP 图像(位深 8 bit,带调色板)

  如下图所示为 图像处理标准测试图集 中找到一张 Masuda1.bmp 图;

  这张图像的比特深度为 8 比特,且正好是一张彩色图像,因此可以查看一下他的调色板;

Masuda1.bmp

2.1 位图文件头(Bitmap file header)

  注意,bmp 图片文件头中数据是以小端序存储的,因此我们阅读时,需要做一下调整。

  例如,文件类型占两个字节,我们将 前两个字节 数据(0x42 和 0x4d)取出,从后往前读,那么文件类型就是 0x4d42

Masuda1_file_header

序号字节数变量名字段名数据(HEX)数据(DEC)描述
12bfType文件类型0x4D4219778字符显示就是‘BM’
24bfSize文件大小0x0003C436246,838‬检查文件信息,验证一致
32bfReserved1保留字段 10x00000保留字段,必须设置为 0
42bfReserved2保留字段 20x00000保留字段,必须设置为 0
54bfOffBits数据偏移量0x000004361,078即偏移 1,078 字节开始为图像数据

2.2 DIB 头(Device Independent Bitmap Header)

Masuda1_file_DIB_header

序号字节数变量名字段名数据(HEX)数据(DEC)描述
64biSizeDIB 头大小0x0000002840即 DIB 头大小为 40 字节
74biWidth图像宽度0x00000200512图像的宽度为 512 像素
84biHeight图像高度0x000001E0480图像的高度为 480 像素
92biPlanes颜色平面数0x00011目标设备说明颜色平面数,总被设置为 1
102biBitCount每个像素的位数0x00088每个像素的比特深度为 8 比特
114biCompression压缩类型0x000000000(BI_RGB)表示不压缩
124biSizeImages图像数据大小0x0003C000245,760图像大小 = 文件大小 - 偏移量
134biXPelsPerMeter水平分辨率0x000000000表示未知或不适用
144biYPelsPerMeter垂直分辨率0x000000000表示未知或不适用
154biClrUsed调色板大小0x00000100256表示调色板中有 256 种颜色
164biClrImportant重要颜色数0x00000100256表示对图像显示有重要影响的颜色数目

  此处注意,除 图像宽度(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 文件头中的数据偏移量相符。

Masuda1_color_table
  这张图的调色板从第 0x00003006 比特开始,每 4 个字节为一个颜色,256 个颜色,共计 1024 个字节。每个颜色又分为 B G R A(注意顺序) 四个通道,因此我们可以列一张表直接将调色板解析出来。此处我们可以拿前四个颜色举个例子,解析如下:

序号颜色数据(HEX)B 通道(蓝色)G 通道(绿色)R 通道(红色)A 通道(透明度)
10xB2B5E5000xB2 [178]0xB5 [181]0xE5 [229]0x00 [0]
20x9CB6E6000x9C [156]0xB6 [182]0xE6 [230]0x00 [0]
30xA5B1E6000xA5 [165]0xB1 [177]0xE6 [230]0x00 [0]
40x9CADED000x9C [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 的顺序,并借助画图软件还原出来,可以得到该颜色如右下角的方块所示。

  由于这张图的 biWidthbiHeight 均为正数,所以 第一个像素点 应该位于文件的 左下角,我们找到该像素点进行比对,结果一致。

Masuda1_FirstPixel

  如下图所示,我们找到最后一个像素点的数据为 0x4D,即该像素点的颜色应为调色板中第 0x4D 号颜色。可以计算出这个四字节颜色分量相对于文件 0 位置的偏移为 offset = 0x4D * 4 + 0x36 = 0x16A,我们在文件中找到该颜色,根据 B G R A 的顺序,并借助画图软件还原出来,可以得到该颜色如右下角的方块所示。

  由于这张图的 biWidthbiHeight 均为正数,所以 最后一个像素点 应该位于文件的 右上角,我们找到该像素点进行比对,结果一致。

Masuda1_LastPixel

2.5 小结

  解析完整张图片后,我们可以再计算一下文件的大小:

数据块BMP 文件头DIB 头调色板图像数据填充字节文件整体(总计)
字节数14401,024245,7600246,838

3. 徒手拆解 BMP 图像(位深 24 bit,不带调色板)

  如下图所示为 图像处理标准测试图集 中找到一张 LenaRGB.bmp 图;

  下面这张图像的比特深度为 24 比特,我们可以看一下不带调色板的图像其像素数据是如何解析的;

LenaRGB.bmp

3.1 位图文件头(Bitmap file header)

LenaRGB_file_header

序号字节数变量名字段名数据(HEX)数据(DEC)描述
12bfType文件类型0x4D4219778字符显示就是‘BM’
24bfSize文件大小0x000C0038786,488检查文件信息,验证一致
32bfReserved1保留字段 10x00000保留字段,必须设置为 0
42bfReserved2保留字段 20x00000保留字段,必须设置为 0
54bfOffBits数据偏移量0x0000003654即偏移 54 字节开始为图像数据

3.2 DIB 头(Device Independent Bitmap Header)

LenaRGB_file_DIB_header

序号字节数变量名字段名数据(HEX)数据(DEC)描述
64biSizeDIB 头大小0x0000002840即 DIB 头大小为 40 字节
74biWidth图像宽度0x00000200512图像的宽度为 512 像素
84biHeight图像高度0x00000200512图像的高度为 512 像素
92biPlanes颜色平面数0x00011目标设备说明颜色平面数,总被设置为 1
102biBitCount每个像素的位数0x001824每个像素的比特深度为 24 比特
114biCompression压缩类型0x000000000(BI_RGB)表示不压缩
124biSizeImages图像数据大小0x0003C000245,760图像大小 = 文件大小 - 偏移量
134biXPelsPerMeter水平分辨率0x0000B88147,233水平方向每英尺像素数为 47,233 个像素
144biYPelsPerMeter垂直分辨率0x0000B88147,233垂直方向每英尺像素数为 47,233 个像素
154biClrUsed调色板大小0x000000000无调色板
164biClrImportant重要颜色数0x000000000无重要颜色

3.3 图像数据(Image Data)

  这张图像的 bit 深度为 24,表示每个像素占 3 字节,由三个 8 bit RGB 分量组成,其排布顺序应该是按照 B G R 顺序排列的。全图共计 512 x 512 = 262144 个像素,因此图像数据占据了 262144 x 3 = 786432 字节。其起始位置位于偏移量为 54 字节的位置。如下图所示,为图像数据的起始部分。

LenaRGB_Image_Data
  以下是头部像素点的数据解析:

序号颜色数据(HEX)B 通道(蓝色)G 通道(绿色)R 通道(红色)
10x3916520x39 [57]0x16 [22]0x52 [82]
20x3916520x39 [57]0x16 [22]0x52 [82]
30x3E20600x3E [62]0x20 [32]0x60 [96]
40x3E1C5D0x3E [62]0x1C [28]0x5D [93]

  为了更直观的理解其原理,我们还是选取第一个和最后一个像素点进行手动解析,然后与原图中的像素点颜色进行对比。
  第一个像素点,在原图中位置位于图像 左下角,其数据偏移为 0x00000036 如下图所示:

LenaRGB_First_Pixel
  最后一个像素点,在原图中位置位于图像 右上角,其数据偏移为 [(512 x 512) - 1] x 3 + 54 = 7864830x00C0033 如下图所示:

在这里插入图片描述
  对比之后,以上两个像素点均与原图中一致。

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 头调色板图像数据填充字节文件整体(总计)
字节数14400768,4322786,488
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值