BMP 文件格式
File Format, Image File Format
目录
BMP 是什么
BMP 是一种图像文件格式,取自位图(Bit MaP)的缩写,扩展名为.bmp
,是微软搞出来的。这种格式对图像几乎是不经过压缩的,所以文件大小会非常地大,不适合用来在因特网上传输信息。
自己设计一个图片文件格式
文件存在电脑里都是一些二进制数,例如有个文件hello.txt
,在里面写了helloworld
,这些字符串存在电脑里就是:
$ od -tax hello.txt
0000000 h e l l o w o r l d
6c6c6568 726f776f 0000646c
0000012
可以看到h
对应0x68
,e
对应0x65
,l
对应0x6c
,o
对应0x6f
,w
对应0x77
,r
对应0x72
,r
对应0x6c
,d
对应0x64
这些文本的编码方式是 UTF-8,而 UTF-8 是和 ASCII编码兼容的,即前 128 个字符都是用单字节来表示,所以这里的英文字符对应 ASCII 编码。。
和在文本文件里写字符串不同,图片文件写的颜色,所以图片就是颜色的集合,如何用二进制数来表示颜色呢?
最简单的办法就是用 3 个字节表示一个颜色,分别对应红绿蓝三原色,例如0xff0000
表示红色、0x00ff00
表示绿色、0x000000
表示黑色。
那么一张 4x4 从黑色到白色的图片就可以表示为:
000000 111111 222222 333333
444444 555555 666666 777777
888888 999999 aaaaaa bbbbbb
cccccc dddddd eeeeee ffffff
最后可以再给这个文件追加两个0x00
字节,表示文件结束。这样一来,这个文件的大小就是 4 * 4 * 3 + 2 = 50 个字节。假如有张图片的大小为 1080x1920,那这个文件的大小就变成了 1080 * 1920 * 3 + 2 = 6220802 字节 = 6 MB!也就是说看一张超清的图片需要 6 MB 的流量!很显然,我刚刚设计的这种图片文件格式非常地不合理。
BMP 文件格式
其实 BMP 文件格式和上面的那种格式差不多,不过 BMP 使用到了一个称为 调色板 的概念,类似索引,利用“索引”,我们可以省下一部分空间,不过文件大小还是有点大。
BMP 文件可以分为 4 个区域:
- 文件头
- 位图信息头
- 调色板
- 像素数据区
下面分别来说说各个部分的含义。
文件头
首先的 14 字节是文件头,存储了文件的一些通用信息。其中包括:
字段 | 字节数 | 说明 |
---|---|---|
文件签名,即 magic number | 2 | 相当于一个文件的标识符,通常是0x42 0x4d ,即 ASCII 码的 “BM” 字符 |
文件大小 | 4 | 也就是说 BMP 文件的大小最大为 $2^{8*4} = 2^{32} = 4\text{(GB)}$ |
保留 | 4 | 以后可能有用 |
像素数据区的偏移量 | 4 | 可以看做是一个指针,表示最后的像素数据区在整个文件中的字节偏移量 |
位图信息头
随后是位图信息头,常见的为 40 字节:
字段 | 字节数 | 说明 |
---|---|---|
位图信息头的总大小 | 4 | 例如,如果位图信息头占 40 个字节,则这里应该是0x00000028 |
图像宽度 | 4 | 单位为像素 |
图像高度 | 4 | 单位为像素 |
色彩平面数 | 2 | 只能是 1 |
色深 | 2 | 表示每个像素的位数,即在像素数据区中,一个颜色记录占多少位,常见的值有 1、4、8、16、24 和 32 |
压缩方法 | 4 | 通常为 0,即不压缩 |
像素数据区的大小 | 4 | 表示最后的像素数据区的字节大小 |
图像的横像分辨率 | 4 | 单位为像素/米,常见为0x00000b12 ,即 2834 像素/米 |
图像的纵向分辨率 | 4 | 和图像的横像分辨率一样 |
调色板中颜色的个数 | 4 | 也就是“索引”的数量,值为 0 时表示颜色个数为 $2^{\text{色深}}$ 个 |
重要的颜色数量 | 4 | 通常为 0,即所有的颜色都重要 |
调色板
随后是调色板,调色板中包含的是颜色记录,一条颜色记录可以表示一种颜色,每条颜色记录占 4 个字节。例如0xff000000
表示蓝色、0xffff0000
表示青色。所以调色板的大小为 $\text{颜色数} * 4 \text{(B)}$,如果用到的颜色只有一种的话,那就只用占 1 * 4 个字节。
调色板的颜色记录数是可变的,可以根据 $\text{文件头中的位图数据偏移量} - 52$ 算出调色板的字节数,然后除以 4 得到颜色记录数。
像素数据区
例如调色板的数据为0xff000000
、0x00ff0000
、0x0000ff00
,而像素数据为0x00
、0x02
、0x00
、0x01
则表示为 蓝色、红色、蓝色、绿色,颜色索引的位数由色深指定。
整个文件如下:

文件的最后的 2 个表示文件结束的字节0x0000
,也可以不加。
动手制作一个 BMP 文件
知道了文件的格式,我们便可以新建一个空的文件,然后再一个字节一个字节地将数据写入文件里:
with open('a.bmp', mode='wb') as f:
# 定义文件头,一共 14 字节
FILE_HEADER = bytes([
0x42, 0x4d, # ASCII 的 "BM"
0x38, 0x05, 0x00, 0x00, # 文件的大小:4 字节
0x00, 0x00, # 保留的字节
0x00, 0x00, # 保留的字节
0x36, 0x04, 0x00, 0x00 # 像素数据区的偏移量
])
f.write(FILE_HEADER)
# 位图信息头,一共 40 字节
FILE_INFO = bytes([
0x28, 0x00, 0x00, 0x00, # 位图信息头大小:40 个字节
0x10, 0x00, 0x00, 0x00, # 图像的宽度,单位为像素:16 像素
0x10, 0x00, 0x00, 0x00, # 图像的高度:16 像素
0x01, 0x00, # 色彩平面数:只能是 1
0x08, 0x00, # 每个像素的位数:8位
0x00, 0x00, 0x00, 0x00, # 是否压缩:不压缩
0x00, 0x01, 0x00, 0x00, # 像素数据区占的字节数
0x12, 0x0b, 0x00, 0x00, # 图像的横向分辨率
0x12, 0x0b, 0x00, 0x00, # 图像的纵向分辨率
0x00, 0x00, 0x00, 0x00, # 调试板的颜色数:为 0 时表示颜色数为 2^{色深} 个
0x00, 0x00, 0x00, 0x00 # 重要颜色的数量:为 0 时说明所有颜色都重要
])
f.write(FILE_INFO)
# 调色板,从全黑到全白,共 256 个颜色
PALETTE_SIZE = 256
for i in range(PALETTE_SIZE):
f.write(bytes([i, i, i, 0x00]))
# 像素数据区
# 注意:数据是从图像左下角往右上角写的。
# 也就是说左下角为黑色,右上角为白色。
IMAGEDATA_SIZE = 256
for i in range(IMAGEDATA_SIZE):
f.write(bytes([i]))
f.write(bytes([0x00, 0x00])) # 结束标志
f.flush()
f.close()
最后的结果就是一个 16x16 的位图图像文件:

需要注意的是:像素数据区的从图像的左下角开始,往右写的,所以最后左下角是黑色,因为我们一开始写入的是0x00
。
位图文件格式还是很容易理解,不过这种文件并不适合用来在互联网上传输,因为图像的颜色信息几乎是没有压缩过的,所以只要文件尺寸大一些,文件大小会很大。
例如一张 256x256 的灰度图就需要 54 + (4 * 256 + 512 * 512 * 1) = 263222 (B) ≈ 257 (KB)! 这还只是一张 256x256 的黑白照片,而一张 400x400 的 JPEG 格彩色图片文件也不过 25730(B) ≈ 25(KB), 具体的大小要看 JPEG 的压缩率。
所以网络上大多数都是 JPEG、PNG 这类格式的图片,更省流量。
JPEG 使用到了小波变换来滤掉图片的高频部分,然后再进行编码,所以文件相对就会小很多,而 BMP 格式的文件更加适合用来做研究,因为 BMP 文件几乎是没有压缩过的。