解读BMP图片

从底层带你了解BMP格式

Posted by lijiahao on March 2, 2021

最近在提取《基连的野望 阿克西斯V》的MRG文件时接触了BMP图片,于是从底层对BMP图片进行研究,学习BMP图片的显示和成像原理。

BMP图片简介

BMP图片是一种栅格图片,BMP图片的每一个像素都由一个比特位(bit)或多个比特位(bits)表示,因此BMP全称是bitmap 或 a map of bits and pixels。

BMP是Windows操作系统中的标准图像文件格式,也是众多早期游戏广泛应用的图片格式,从游戏贴图到游戏字幕都可以是BMP图片。

BMP是一种没有经过压缩的图片格式,也就是说,BMP图片的每一个像素有自己的比特位(或一组比特位),常见的PNG或JPEG则不同,这些是经过压缩技术对图片的信息进行压缩,以达到减小图片体积的目的。因此多数情况下,显示相同的图像,BMP图片的大小总是最大的。

BMP图片组成

本文使用以下BMP图为例进行说明,图片另存为可将图片保存到本地。 example 使用十六进制打开图片,看到部分图片信息如下: example 看似杂乱无章的显示,其实bmp图片由以下四部分组成: example

  • 文件头(bit map file header): 共14字节
  • 位图信息头(bitmap information): 共40字节
  • 调色板(color palette): 可选
  • 位图数据(pixel data): 可选

下面将对每部分进行详细解读。

注:本文涉及16进制读取均使用小端模式(little-endian)。

第一部分:文件头(bit map file header)

此部分一共包含14个字节,例子中的图片对应的内容为: example

一共5个字段,每个字段如下:

字段名 大小 描述
FileType(文件类型) 2 bytes 文件类型,BMP两字节是0x4D42,ASCII字符对应为“BM”
FileSize(文件大小) 4 bytes 文件大小,对应的值单位为字节。例子中的值为0xC016,十进制为49174字节,对应为49KB。
Reserved(保留位) 2 bytes 2位保留位预留给图片渲染应用,初始化必须为0
Reserved(保留位) 2 bytes 同上
PixelDataOffset(从头到位图数据的偏移) 4 bytes 表示从文件头到位图数据的偏移,例子图片的值是0x00000436,十进制是1078,这个值在第三部分调色板会进行验证

第二部分:位图信息头(bitmap information)

此部分一共包含40个字节,例子中的图片对应的内容为: example

一共11个字段,每个字段如下:

字段名 大小 描述
HeaderSize 4 bytes 信息头大小,即0x28,十进制为40
ImageWidth 4 bytes 图片宽度,单位像素。例子图片为0x0120,十进制为288,即288px,与实际符合
ImageHeight 4 bytes 图片高度,单位像素。例子图片为0x00A7,十进制为167,即167px,与实际符合
Planes 2 bytes 目标设备说明颜色平面数,总被设置为1
BitsPerPixel 2 bytes 定义颜色的每个像素有多少位,也称为颜色深度,简称bpp,值有1、2、4、8、16、24、32。例子图片值为0x0008,表示每个像素用8位表示,一共有2^8 = 256个颜色
Compression 4 bytes 图像的压缩类型,最常用的就是0(BI_RGB),表示不压缩
ImageSize 4 bytes 位图数据的大小,当用BI_RGB格式时,可以设置为0
XpixelsPerMeter 4 bytes 水平分辨率,单位是像素/米,有符号整数
YpixelsPerMeter 4 bytes 垂直分辨率,单位是像素/米,有符号整数
TotalColors 4 bytes 位图使用的调色板中的颜色索引数,为0说明使用所有
ImportantColors 4 bytes 对图像显示有重要影响的颜色索引数,为0说明都重要

第三部分:调色板(color palette)

调色板就是颜色的索引,但调色板是可选的。由于每个颜色都有RGB三原色,也就是要3个字节表示(例如白色表示为#FFFFFF),当bpp≤8时,2^bpp 所计算出来的颜色就无法表示全部的颜色,因此对于bpp≤8的图片就必须使用调色板,下表展示各种位图的调色板情况:

BitsPerPixel(bpp) 调色板 颜色数量 描述
1 2 调色板中最多只能存在2种颜色
4 8 调色板中最多只能存在8种颜色
8 256 调色板中最多只能存在256种颜色
16 65536 可直接使用RGB三原色,每种原色用5bit表示
24 16M (16777215) 可直接使用RGB三原色,每种原色用8bit表示
32 4B (4294967296) 可直接使用RGBA,每种原色用8bit表示,加8bit Alpha

例子图片中bpp为8,因此每个像素只能有256钟颜色,为了增加图片表现形式,就需要一个索引,使用一个字节表示索引指向4个字节(RGB加Alpha值)。如果把这4个字节表示为一个Color类型,那么调色板就是Color的数组。由于Color类型也是一个数组,调色板就像一个二维数组palette[N][4],其中N是颜色的数量,这里就是256。因此,这个例子中的调色板的大小就是256x4=1024字节,在调色板之前,有14字节的bmp文件头,40字节的位图信息头,加上1024字节的调色板,一共1078字节,也就是说真正的图像数据前面有1078字节,这和bmp文件头中的bfOffBits相符。

第四部分:位图数据(pixel data)

最后就是位图数据,该区域存放的是每个像素的颜色值,以行的形式展示,这个取决于图片的颜色深度(bpp)。同时BMP图片扫描引擎的最小单位是4字节,因此在扫描每一行时,要求每一行的数据长度必须是4的倍数,不足的行将进行比特填充(bit padding)

BMP扫描

当图片引擎读取了一张BMP图片,从文件头信息读取数据后,引擎就知道该如何扫描BMP数据,进而把图像展示在用户面前。BMP图像扫描使用从左下角开始到右上角,以行为主序进行扫描。下图以一个4px X 4px的图像为例:

example

构建24位BMP图

经过以上图片组成的详细解说了,相信读到这里的你已经以上众多字段忘的差不多了,没关系,接下来我们将通过自己编写十六进制,运用上述原理自己构建一个5px X 5px的24位bmp图像。

首先初始化文件头(第一部分)和位图信息头(第二部分),这两部分一共占据14+40=54个字节。初始化数据如下:

字段 大小
FileType 2 bytes 0x42 0x4D
FileSize 4 bytes 0x00 0x00 0x00 0x00
Reserved 2 bytes 0x00 0x00
Reserved 2 bytes 0x00 0x00
PixelDataOffset 4 bytes 0x36 0x00 0x00 0x00
HeaderSize 4 bytes 0x28 0x00 0x00 0x00
ImageWidth 4 bytes 0x05 0x00 0x00 0x00
ImageHeight 4 bytes 0x05 0x00 0x00 0x00
Planes 2 bytes 0x01 0x00
BitsPerPixel 2 bytes 0x18 0x00
Compression 4 bytes 0x00 0x00 0x00 0x00
ImageSize 4 bytes 0x00 0x00 0x00 0x00
XpixelsPerMeter 4 bytes 0x00 0x00 0x00 0x00
YpixelsPerMeter 4 bytes 0x00 0x00 0x00 0x00
TotalColors 4 bytes 0x00 0x00 0x00 0x00
ImportantColors 4 bytes 0x00 0x00 0x00 0x00

初始化数据中多数字段都设置为0,有一些非0数据这里做下解释:

  • FileType: 固定值0x42 0x4D,表示BM。
  • PixelDataOffset: 从头到位图数据的偏移,一共是54bytes,使用十六进制表示即为0x36。
  • HeaderSize: 文件头大小,同40bytes,十六进制表示为0x28。
  • ImageWidth: 图片宽度,5px,十六进制表示为0x05
  • ImageHeight: 图片高度,5px,十六进制表示为0x05
  • Planes:固定值1
  • BitsPerPixel:bpp,24位图,值为24,十六进制表示为0x18

将上述数据随便写入一个文本编辑器中,如下:

42 4D
00 00 00 00
00 00
00 00
36 00 00 00

28 00 00 00
05 00 00 00
05 00 00 00
01 00
18 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00

新建一个test.bmp文件,使用十六进制编辑器打开图片,将以上内容复制到图片中,此时一个24位的bmp图片的基本头已经形成,但此时还无法打开文件,因为还缺少了和颜色相关的位图数据,我们把他补上。

24位图,即每个颜色都可以使用24比特位表示,例如#000000的颜色:

十六进制表示:
00 00 00
-- -- --
R  G  B
二进制表示:
00000000 00000000 00000000
-------- -------- --------
R         G         B

由于是5x5的图片,因此每一行需要5x3bytes,但扫描行的原则需要每行字节数为4的倍数,因此最后还需加上1个字节进行填充(0x00),故完整的位图数据如下:

00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00
00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00
00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00
00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00
00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00

拼上文件头和文件信息头,完整的图片数据如下:

42 4D
00 00 00 00
00 00
00 00
36 00 00 00

28 00 00 00
05 00 00 00
05 00 00 00
01 00
18 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00

00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00
00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00
00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00
00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00
00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00

将数据写入test.bmp中,打开图片,就可以看到一个5x5的黑色图: example

之后我们来改变下颜色:

FF FF FF   00 00 00   00 00 00   00 00 00   00 FF FF   00
00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00
00 00 00   00 00 00   00 FF 00   00 00 00   00 00 00   00
00 00 00   00 00 00   00 00 00   00 00 00   00 00 00   00
00 00 FF   00 00 00   00 00 00   00 00 00   FF 00 00   00

example

可以验证BMP图片扫描顺序为左下到右上

构建16位BMP图

16位和24位的区别主要在于位图数据的表现形式不同。16位图是一个像素只有16位表示,即一种颜色只能用16位表示,因此一种三原色只能用5位表示(最高位为0):

[ 0  XXXXX XXXXX XXXXX ]
     ----- ----- -----
       R     G     B

因此24位的颜色转为16位颜色如下:

RED      0111110000000000 => 01111100 00000000 => 0x7C 0x00
GREEN    0000001111100000 => 00000011 11100000 => 0x03 0xE0
BLUE     0000000000011111 => 00000000 00011111 => 0x00 0x1F
WHITE    0111111111111111 => 01111111 11111111 => 0x7F 0xFF
YELLOW   0111111111100000 => 01111111 11100000 => 0x7F 0xE0

即:每种颜色只能使用2个字节表示,对于5x5的图片,每行字节数需为4的倍数,因此每行最后还需填充2个字节:

42 4D
00 00 00 00
00 00
00 00
36 00 00 00

28 00 00 00
05 00 00 00
05 00 00 00
01 00
10 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00

FF 7F   00 00   00 00   00 00   E0 7F       00 00
00 00   00 00   00 00   00 00   00 00       00 00
00 00   00 00   E0 03   00 00   00 00       00 00
00 00   00 00   00 00   00 00   00 00       00 00
00 7C   00 00   00 00   00 00   1F 00       00 00

example

最后test.bmp图片显示依旧和24位一致,只是16位图片文件更小了。

总结

本文针对bmp图像进行文件结构解析,并逐一解释内部信息的含义,最后通过十六进制编写的方法实现了24位图和16位图以加强对bmp图像的理解,对bmp图像的描述本文可能有错误或遗漏之处,欢迎反馈和指点。

(完)


原创不易,如果觉得这篇文章对你有帮助,不如赏杯咖啡吧
微信
支付宝