Venus' Blog

Archive

About

BMP 文件格式

2019 / 01 / 24

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,为了省点纸,二进制都是显示成十六进制。

和在文本文件里写字符串不同,图片文件写的颜色,所以图片就是颜色的集合,如何用二进制数来表示颜色呢?

最简单的办法就是用 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=504 \times 4 \times 3 + 2 = 50 个字节。假如有张图片的大小为 1080x1920,那这个文件的大小就变成了 1080×1920×3+2=62208021080 \times 1920 \times 3 + 2 = 6220802 个字节,有 6 MB!。也就是说看一张超清的图片需要 6 MB 的流量!很显然,我刚刚设计的这种图片文件格式非常地不合理。

BMP 文件格式

其实 BMP 文件格式和上面的那种格式差不多,不过 BMP 使用到了一个称为 调色板 的概念,类似索引,利用“索引”,我们可以省下一部分空间,不过文件大小还是有点大。

文件头

首先,BMP 文件开头的 14 个字节是 文件头,存储了文件的一些通用信息。其中包括:

  1. 占 2 个字节的签名,通常是 0x42 0x4d,即 ASCII 码的 “BM” 字符,
  2. 然后是占 4 个字节的文件大小,单位为字节,也就是说 BMP 文件的大小最大为 2(8×4)(B)=232(B)=4(GB)2^{(8\times4)} (B) = 2^{32} (B) = 4 (GB)
  3. 随后的 4 个字节保留,通常是 0x00
  4. 最后的 4 个字节表示的是位图数据的偏移量,可以看做是一个指针,指向用来表示颜色的数据在整个文件的字节偏移量。

位图信息头

随后是 位图信息头,也是固定大小的,常见的为 40 个字节:

  1. 刚开始的 4 个字节指明位图信息头的大小,单位为字节。例如,如果位图信息头占 40 个字节,则这里应该是 0x00000028
  2. 随后的 4 个字节说明图像宽度,单位为像素
  3. 然后是占 4 个字节的图像高度,单位为像素
  4. 然后是占 2 个字节的色彩平面数,只可以是 1
  5. 然后是每个像素的位数,称为图像的色深,占 2 个字节,常见的值有 8、24、32,即在最后的像素数据中,一个像素占多少位
  6. 然后是占 4 个字节的压缩方法,通常为 0,即不压缩
  7. 随后的 4 个字节表示颜色数据的大小,单位为字节
  8. 随后的 4 个字节表示图像的横像分辨率,单位为像素每米,常见为 0x00000b12,即 2834 像素/米
  9. 然后是图像的纵向分辨率,占 4 个字节
  10. 随后的 4 个字节表示调色板的颜色数,也就是“索引”的数量,值为 0 时表示颜色数为 22^{色深} 个。
  11. 最后的 4 个字节表示重要的颜色数量,通常为 0,即所有的颜色都重要。

到目前为止,文件的大小一共是 14+40=54B14+40 = 54 B,随后是调色板,用来定义该图像文件中所有使用到的颜色。

调色板

调色板 中包含着许多条颜色记录,一条颜色记录可以表示一种颜色,占 4 个字节,分别对应颜色的 BGR 分量和 alpha 透明度。例如 0xff000000 表示蓝色、0xffff0000 表示青色。所以调色板的大小为 ×4(B)颜色数 \times 4 (B),如果用到的颜色只有一种的话,那就只用占 1×41 \times 4 个字节。

调色板的长度是可变的,不过可以根据 52文件头中的位图数据偏移量 - 52 算出调色板的长度。

像素数据

最后就是实际的像素数据了。每个像素使用一个字节或多个字节表示,位图信息头中的色深指明了一个像素的位数,常见的有 8、24 和 32。像素里的数据指的是相对于调色板的偏移量。例如调色板的数据为 0xff000000、0x00ff0000、0x0000ff00,而像素数据为 0x00、0x02、0x00、0x01 则表示为 蓝色、红色、蓝色、绿色。

整个文件如下:

../images/bitmapfileformat.png

文件的最后的 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 的位图图像文件:

../images/create_bitmap_file_result.png

需要注意的是:像素数据区的从图像的左下角开始,往右写的,所以最后左下角是黑色,因为我们一开始写入的是 0x00

位图文件格式还是很容易理解,不过这种文件并不适合用来在互联网上传输,因为图像的颜色信息几乎是没有压缩过的,所以只要文件尺寸大一些,文件大小会很大。

例如一张 256x256 的灰度图就需要 54+(4×256+512×512×1)=263222(B)257(KB)54 + (4 \times 256 + 512 \times 512 \times 1) = 263222 (B) \approx 257 (KB)!这还只是一张 256x256 的黑白照片,而一张 400x400 的 JPEG 格彩色图片文件也不过 25730(B)25(KB)25730(B) \approx 25(KB),具体的大小要看 JPEG 的压缩率。

所以网络上大多数都是 JPEG、PNG 这类格式的图片,更省流量。

JPEG 使用到了小波变换来滤掉图片的高频部分,然后再进行编码,所以文件相对就会小很多,而 BMP 格式的文件更加适合用来做研究,因为 BMP 文件几乎是没有压缩过的。