1. BMP图片格式的核心概念与设计逻辑
1.1 从一个实际需求说起:为什么今天还要聊BMP
很多人第一次接触BMP,都是在Windows画图里随手另存为的时候。那个体积大得离谱、打开速度也不算快的文件,往往被当作“临时格式”用完就删。但如果你做过嵌入式显示、工控上位机、医疗影像存档,或者写过MFC界面的图像加载模块,就会发现BMP其实是一个绕不开的存在。它结构简单、无压缩、字节序明确,几乎任何一门语言都能在半小时内手写一个解析器。这种“笨但可靠”的特性,恰恰是它在特定场景下活到今天的原因。
BMP全称Bitmap,中文叫位图。它本质上就是一块像素数据的容器,把每个像素的颜色值按行排列,再加上一个描述“这块数据有多大、怎么解释”的文件头。没有复杂的熵编码,没有调色板索引的歧义(有调色板的情况也写得很直白),更没有块结构带来的解析负担。你拿到一个BMP文件,用十六进制编辑器打开,前几十个字节就能把它的尺寸、位深、数据偏移全部读出来。这种透明性,是JPEG、PNG这些格式给不了的。
这篇文章面向的读者很明确:需要自己动手解析或生成BMP的开发者,尤其是做Windows桌面开发、嵌入式GUI、图像处理底层库、以及需要把图像数据喂给硬件显示控制器的工程师。我会从文件结构讲起,把每个字段的含义、字节序、对齐规则都拆开说清楚,然后给出可运行的解析和生成代码,最后分享一些实际项目中踩过的坑。如果你只是想找个库调一下,那这篇文章可能偏底层;但如果你想真正搞懂BMP里每一个字节在干什么,那接下来的内容应该够用。
1.2 BMP格式的家族谱系:你遇到的到底是哪一种
BMP并不是只有一种固定结构。从Windows 1.0时代到现在,它演化出了多个版本,常见的有BITMAPCOREHEADER、BITMAPINFOHEADER、BITMAPV4HEADER、BITMAPV5HEADER。你平时用画图保存出来的,绝大多数是BITMAPINFOHEADER,头部固定40字节。这个版本支持1位、4位、8位、16位、24位、32位像素,也是兼容性最好的。
BITMAPCOREHEADER是早期OS/2和Windows 1.0用的,头部只有12字节,宽度和高度都是16位无符号整数,位深也只支持1、4、8、24。现在基本见不到了,但如果你解析老文件时发现头部大小是12,就得按这个结构来读,否则后面全乱。
BITMAPV4HEADER和V5HEADER分别在Windows 95和Windows 98时代引入,头部大小分别是108字节和124字节。它们增加了颜色空间信息、gamma值、ICC配置文件相关字段,主要服务于色彩管理。V5还多了对透明度掩码的支持。实际项目中,如果你只关心像素显示,用INFOHEADER就够了;但如果要做色彩校正或者处理带Alpha通道的图,就得留意V4/V5里的那些扩展字段。
判断版本的方法很简单:读文件头第15到18字节(从0开始计数是14到17),这是一个32位小端整数,表示DIB头的大小。12就是CORE,40就是INFO,108就是V4,124就是V5。知道这个,你就能决定后面按哪个结构去解析。
1.3 为什么BMP坚持不压缩:设计取舍背后的逻辑
BMP诞生于1980年代末,那时候CPU算力有限,内存也贵,但显示适配器的帧缓冲是直接映射到内存的。BMP的设计目标就是让文件内容能尽可能直接地拷贝到显存里,不需要解码。所以它选择了无压缩的逐行存储,每行像素按从左到右、从下到上的顺序排列(默认情况下高度为正时是倒序的,后面会细说)。
这种设计带来的好处是:加载一张BMP,理论上只需要一次内存拷贝,不需要任何解码计算。对于早期的图形界面、游戏、以及后来的工控HMI,这意味着极低的CPU占用和确定的加载时间。坏处也很明显:文件体积大。一张1920x1080的24位BMP,光像素数据就是192010803=6,220,800字节,接近6MB。如果换成PNG,可能只有几百KB。
但BMP也支持RLE压缩,主要是4位和8位色深下的RLE4和RLE8。这种压缩是行程编码,对有大片同色区域的图像有效,但对照片类图像几乎没用,甚至可能变大。实际项目中,我很少见到用RLE的BMP,因为兼容性反而会出问题——有些解析器不支持RLE,遇到就报错。所以如果你要生成BMP给别人用,最稳妥的还是用无压缩的24位或32位。
2. BMP文件结构的逐字节拆解
2.1 文件头:那14个字节里藏着什么
BMP文件最开头是BITMAPFILEHEADER,固定14字节。虽然叫“文件头”,但它其实只负责告诉解析器“这是一个BMP文件”以及“像素数据从哪里开始”。具体字段如下:
| 偏移 | 长度 | 字段名 | 说明 |
|---|---|---|---|
| 0 | 2 | bfType | 固定为0x4D42,即ASCII的'BM'。小端存储时字节序列是42 4D |
| 2 | 4 | bfSize | 整个文件的大小,单位字节 |
| 6 | 2 | bfReserved1 | 保留,必须为0 |
| 8 | 2 | bfReserved2 | 保留,必须为0 |
| 10 | 4 | bfOffBits | 从文件开头到像素数据的偏移量 |
这里最容易踩坑的是bfType的字节序。你在十六进制编辑器里看到的是42 4D,但按小端读成16位整数是0x4D42。很多新手写解析器时直接比较前两个字节是否为'B'和'M',这没问题;但如果用整数比较,就得注意大小端。
bfSize字段有时候不可靠。我遇到过一些老工具生成的BMP,bfSize写的是0或者错误的值。所以解析时不要完全依赖它,最好用文件实际大小来校验。bfOffBits则非常重要,它告诉你从哪里开始读像素。对于标准的INFOHEADER+调色板+像素数据的结构,这个值通常是14+40+调色板大小。但如果你遇到V4/V5头,或者有ICC配置文件嵌入,这个偏移就会更大。所以永远以bfOffBits为准,不要自己算。
2.2 DIB头:描述图像属性的核心区域
紧跟在文件头后面的是DIB头,最常见的是BITMAPINFOHEADER,40字节。这个结构决定了后面像素数据怎么解释。字段如下:
| 偏移 | 长度 | 字段名 | 说明 |
|---|---|---|---|
| 0 | 4 | biSize | 本结构的大小,40 |
| 4 | 4 | biWidth | 图像宽度,单位像素 |
| 8 | 4 | biHeight | 图像高度,正数表示倒序,负数表示正序 |
| 12 | 2 | biPlanes | 固定为1 |
| 14 | 2 | biBitCount | 每像素位数,1/4/8/16/24/32 |
| 16 | 4 | biCompression | 压缩方式,0=BI_RGB无压缩,1=BI_RLE8,2=BI_RLE4,3=BI_BITFIELDS |
| 20 | 4 | biSizeImage | 像素数据大小,无压缩时可设为0 |
| 24 | 4 | biXPelsPerMeter | 水平分辨率,像素/米 |
| 28 | 4 | biYPelsPerMeter | 垂直分辨率,像素/米 |
| 32 | 4 | biClrUsed | 调色板中实际使用的颜色数,0表示使用全部 |
| 36 | 4 | biClrImportant | 重要颜色数,0表示都重要 |
biHeight的正负是整个BMP里最反直觉的设计之一。当biHeight为正数时,像素数据是从下到上存储的,也就是说文件里第一行像素实际上是图像的最后一行。当biHeight为负数时,才是从上到下。Windows画图保存的BMP默认是正数,所以你在内存里直接按行读出来,图像是上下颠倒的。很多初学者写加载器时忘了翻转,结果图片倒着显示,排查半天才发现是这个原因。
biBitCount决定了像素的编码方式。1位表示每个像素用1个bit,8个像素打包成1字节,颜色由调色板索引决定。4位是每字节两个像素。8位是每字节一个像素,也是调色板索引。24位是每像素3字节,顺序是B、G、R。32位是每像素4字节,顺序是B、G、R、A(Alpha通道不一定有效,很多工具写0)。
biCompression为0时是无压缩,这是最常用的。为3时表示BITFIELDS,此时调色板位置会被三个或四个掩码替代,用来指定16位或32位像素中R、G、B、A各占哪些位。这个在游戏纹理和高动态范围图像里常见,但普通BMP很少用。
2.3 调色板:索引颜色的查找表
当biBitCount小于等于8时,像素数据里存的是调色板索引,不是实际颜色。调色板紧跟在DIB头后面,每个条目4字节,顺序是B、G、R、保留字节(通常为0)。条目数量由biClrUsed决定,如果为0,则默认是2的biBitCount次方。比如8位就是256个条目,4位是16个,1位是2个。
这里有个细节:调色板条目的顺序是BGR,不是RGB。很多新手按RGB读,结果颜色偏了。另外,保留字节虽然叫保留,但有些工具会用它存Alpha值,不过标准BMP里这个字节应该为0。
如果biClrUsed不为0且小于2的biBitCount次方,那么调色板只存实际使用的颜色数,但像素数据里的索引仍然可能超出这个范围。解析时要做边界检查,否则会读到调色板外面的内存。我见过一个案例,某设备生成的BMP里biClrUsed写的是0,但实际只用了16个颜色,解析器按256个条目去读,结果把后面的像素数据当成了调色板,颜色全乱。
2.4 像素数据:行对齐是最大的坑
像素数据从bfOffBits开始,按行存储。每行的大小不是简单的宽度乘以每像素字节数,而是要按4字节对齐。也就是说,每行占用的字节数必须是4的倍数,不足的要补0。这个规则叫“行填充”或“行对齐”。
计算公式是:行字节数 = ((biWidth * biBitCount + 31) / 32) * 4。用整数运算就是:((biWidth * biBitCount + 31) >> 5) << 2。
举个例子,一张3x3的24位BMP,每行像素数据是33=9字节,但按4字节对齐后,每行实际占12字节,多出来的3字节是填充。所以整个像素数据是312=36字节,而不是27字节。如果你解析时按9字节一行去读,第二行就会错位,图像会斜着花掉。
对于1位、4位、8位的图像,行对齐同样适用。比如一张5x5的8位图,每行5字节,对齐后是8字节,填充3字节。1位图更复杂,因为像素是位打包的,行对齐是按字节对齐后再补到4字节边界。
还有一个容易忽略的点:当biHeight为负数时,像素数据是从上到下存储的,但行对齐规则不变。解析时先按绝对值算出行数,然后根据符号决定是否翻转。
3. 手写BMP解析器的完整实操
3.1 环境准备与工具选型
我平时解析BMP主要用Python和C++。Python适合快速验证和脚本处理,C++适合嵌入到实际项目里。这里我用Python来演示,因为它的struct模块处理二进制非常方便,而且代码可以直接复制运行。你需要Python 3.6以上,不需要额外安装库,标准库就够了。
如果你要用C++,建议用std::ifstream以二进制模式打开文件,然后用memcpy把字节拷到结构体里。注意结构体要对齐,否则读出来的字段会错位。Windows下可以用#pragma pack(push,1)来取消对齐。Linux下可以用__attribute__((packed))。
调试工具方面,我强烈建议装一个十六进制编辑器,比如HxD或者010 Editor。010 Editor有BMP模板,可以直接把文件结构树状展示出来,排查问题时非常直观。另外,Windows自带的画图可以另存为不同位深的BMP,用来生成测试样本很方便。
3.2 读取文件头与DIB头
先定义一个函数,接收文件路径,返回解析后的图像数据。第一步是读前14字节,解析文件头。
import struct def parse_bmp(filepath): with open(filepath, 'rb') as f: data = f.read() # 文件头 bfType = data[0:2] if bfType != b'BM': raise ValueError('不是BMP文件') bfSize = struct.unpack('<I', data[2:6])[0] bfOffBits = struct.unpack('<I', data[10:14])[0] # DIB头大小 dib_size = struct.unpack('<I', data[14:18])[0] if dib_size == 40: # BITMAPINFOHEADER width = struct.unpack('<i', data[18:22])[0] height = struct.unpack('<i', data[22:26])[0] planes = struct.unpack('<H', data[26:28])[0] bit_count = struct.unpack('<H', data[28:30])[0] compression = struct.unpack('<I', data[30:34])[0] image_size = struct.unpack('<I', data[34:38])[0] clr_used = struct.unpack('<I', data[46:50])[0] else: raise ValueError(f'暂不支持的DIB头大小: {dib_size}') return { 'width': width, 'height': height, 'bit_count': bit_count, 'compression': compression, 'bfOffBits': bfOffBits, 'clr_used': clr_used, 'data': data }这段代码里,struct.unpack的格式字符串'<I'表示小端无符号32位整数,'<i'表示小端有符号32位整数,'<H'表示小端无符号16位整数。宽度用有符号是因为有些情况下宽度可能为负(虽然很少见),高度用有符号是因为要区分正负。
注意bfOffBits是从文件开头算的,所以后面取像素数据时直接用data[bfOffBits:]就行。
3.3 处理调色板与像素数据
如果bit_count小于等于8,需要先读调色板。调色板条目数由clr_used决定,如果为0则用2的bit_count次方。
def read_palette(data, dib_size, bit_count, clr_used): if bit_count > 8: return None palette_offset = 14 + dib_size if clr_used == 0: clr_used = 1 << bit_count palette = [] for i in range(clr_used): offset = palette_offset + i * 4 b, g, r, _ = data[offset:offset+4] palette.append((r, g, b)) return palette像素数据的读取要处理行对齐。先算出行字节数,然后逐行读取。
def read_pixels(data, bfOffBits, width, height, bit_count): abs_height = abs(height) row_size = ((width * bit_count + 31) // 32) * 4 pixels = [] for y in range(abs_height): row_start = bfOffBits + y * row_size row_data = data[row_start:row_start + row_size] row_pixels = [] if bit_count == 24: for x in range(width): b = row_data[x*3] g = row_data[x*3+1] r = row_data[x*3+2] row_pixels.append((r, g, b)) elif bit_count == 32: for x in range(width): b = row_data[x*4] g = row_data[x*4+1] r = row_data[x*4+2] a = row_data[x*4+3] row_pixels.append((r, g, b, a)) elif bit_count == 8: for x in range(width): idx = row_data[x] row_pixels.append(idx) elif bit_count == 4: for x in range(width): byte_idx = x // 2 if x % 2 == 0: idx = row_data[byte_idx] >> 4 else: idx = row_data[byte_idx] & 0x0F row_pixels.append(idx) elif bit_count == 1: for x in range(width): byte_idx = x // 8 bit_idx = 7 - (x % 8) idx = (row_data[byte_idx] >> bit_idx) & 1 row_pixels.append(idx) pixels.append(row_pixels) # 如果height为正,需要翻转 if height > 0: pixels.reverse() return pixels这段代码里,1位图的位顺序是从高位到低位,也就是每字节的最高位对应最左边的像素。这个顺序是BMP规范定的,不能搞反。4位图是先高4位后低4位,也是从左到右。
3.4 生成BMP文件:从像素数组到文件
生成BMP比解析简单,因为你可以控制所有字段。下面是一个生成24位BMP的函数。
def write_bmp(filepath, width, height, pixels): # pixels是二维列表,每个元素是(r,g,b) row_size = ((width * 24 + 31) // 32) * 4 pixel_data_size = row_size * height file_size = 14 + 40 + pixel_data_size with open(filepath, 'wb') as f: # 文件头 f.write(b'BM') f.write(struct.pack('<I', file_size)) f.write(struct.pack('<H', 0)) f.write(struct.pack('<H', 0)) f.write(struct.pack('<I', 14 + 40)) # DIB头 f.write(struct.pack('<I', 40)) f.write(struct.pack('<i', width)) f.write(struct.pack('<i', height)) f.write(struct.pack('<H', 1)) f.write(struct.pack('<H', 24)) f.write(struct.pack('<I', 0)) f.write(struct.pack('<I', pixel_data_size)) f.write(struct.pack('<i', 2835)) # 72 DPI f.write(struct.pack('<i', 2835)) f.write(struct.pack('<I', 0)) f.write(struct.pack('<I', 0)) # 像素数据,从下到上 for y in range(height - 1, -1, -1): row = pixels[y] row_bytes = bytearray() for (r, g, b) in row: row_bytes.append(b) row_bytes.append(g) row_bytes.append(r) # 补填充 padding = row_size - len(row_bytes) row_bytes.extend(b'\x00' * padding) f.write(row_bytes)这里height传正数,像素数据从下到上写,所以循环是倒序的。如果你想要从上到下的顺序,可以把height写成负数,然后循环正序。但要注意,很多软件对负height的支持不好,所以建议还是用正height加倒序写入。
生成8位BMP时,需要先写调色板,再写像素索引。调色板每个条目4字节,BGR顺序。像素数据每行也要对齐。
4. 实际项目中常见的坑与排查技巧
4.1 图像倒置、花屏、颜色错乱:三大高频问题
图像倒置是最常见的。原因就是biHeight为正时像素从下到上存储,而很多解析器默认从上到下读。解决方法就是在读完后翻转行顺序。如果你在内存里直接操作,可以在读取时就从最后一行开始读,这样就不用额外翻转。
花屏通常是行对齐没处理。比如一张宽度不是4的倍数的24位图,每行末尾有填充字节,如果你按紧密排列去读,第二行就会偏移。排查方法是打印每行的起始偏移,看看是否等于bfOffBits + y * row_size。如果不等,就是对齐算错了。
颜色错乱多半是BGR和RGB搞反了。BMP像素数据里是B、G、R的顺序,调色板也是B、G、R。如果你按RGB读,红色和蓝色会互换。另外,32位图的Alpha通道很多工具写0,如果你直接当透明度用,图像会全透明。处理时要么忽略Alpha,要么判断是否全0再决定。
4.2 不同位深的兼容性处理
1位和4位图现在很少见,但工控和嵌入式领域还有。处理1位图时,位顺序是从高位到低位,这个和很多人的直觉相反。4位图是先高4位后低4位。8位图最简单,每字节一个索引。
16位图有两种:RGB555和RGB565。默认情况下,如果没有BITFIELDS掩码,16位是RGB555,即高5位红、中5位绿、低5位蓝,最高位忽略。如果有掩码,就按掩码来。32位图默认是BGRA,但Alpha不一定有效。
跨位深转换时,要注意精度损失。比如24位转8位,需要做颜色量化,简单的做法是用调色板,但效果一般。如果只是显示,可以直接截断低位。
4.3 大文件与内存映射的优化思路
一张4K的32位BMP,3840x2160x4大约是33MB。如果同时加载多张,内存压力不小。优化思路有几个:一是用内存映射文件,只在访问时读入对应页;二是流式解析,不一次性把像素全读进内存,而是按需读取;三是如果只是显示,可以转成更紧凑的格式再缓存。
Python里可以用mmap模块做内存映射。C++里可以用CreateFileMapping或者mmap。但要注意,BMP的行对齐和倒序存储会让随机访问变得麻烦,所以内存映射更适合顺序扫描的场景。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 图像上下颠倒 | biHeight为正 | 检查biHeight符号 | 翻转行顺序 |
| 图像斜向花屏 | 行对齐错误 | 计算row_size并对比实际偏移 | 按4字节对齐读取 |
| 红蓝互换 | BGR/RGB搞反 | 检查像素读取顺序 | 交换R和B |
| 颜色全黑 | 调色板未读或索引越界 | 检查biClrUsed和调色板偏移 | 正确读取调色板 |
| 图像右侧有杂色 | 填充字节被当成像素 | 检查每行是否多读了填充 | 按width截断 |
| 32位图全透明 | Alpha通道为0 | 检查Alpha值 | 忽略Alpha或设为255 |
| 文件打不开 | bfType不是BM | 检查前两字节 | 确认文件格式 |
| 解析越界 | bfOffBits错误 | 对比文件实际大小 | 以bfOffBits为准 |
5. BMP在现代开发中的实际应用场景
5.1 嵌入式LCD与工控HMI的显示缓冲
在嵌入式领域,很多LCD控制器要求帧缓冲是连续的像素数组,格式往往是RGB565或RGB888。BMP因为无压缩、行对齐明确,非常适合直接转换成帧缓冲格式。我做过一个项目,STM32驱动ILI9341屏幕,图片资源就是BMP转成的C数组。转换工具自己写,读BMP,按RGB565重新打包,生成头文件。这样烧录到Flash里,显示时直接DMA搬运,CPU占用几乎为零。
工控HMI也类似。很多组态软件支持BMP作为背景图,因为解析快,不需要额外的解码库。在资源受限的WinCE或Linux嵌入式系统上,BMP是首选。
5.2 Windows桌面开发中的资源嵌入
MFC和Win32 API里,LoadImage可以直接加载BMP资源。如果你把BMP作为资源编译进exe,用LoadBitmap就能拿到HBITMAP句柄。这种方式在对话框背景、工具栏图标、启动画面里很常见。虽然现在PNG更流行,但BMP不需要额外的解码器,系统原生支持,所以在一些老项目维护中还是主流。
用MFC显示BMP的典型流程是:CImage加载,然后Draw到DC。或者用GDI+的Bitmap类。如果要做透明效果,32位BMP的Alpha通道需要手动处理,因为GDI不支持Alpha混合,得用GDI+或者Direct2D。
5.3 图像处理算法验证的中间格式
做图像算法时,我习惯用BMP作为中间格式。因为无压缩,读写不会引入额外误差,而且可以用十六进制编辑器直接看像素值。比如做边缘检测,输入输出都用BMP,方便对比每个像素的变化。OpenCV虽然支持BMP,但默认的imread可能会做颜色空间转换,所以我会用imread的IMREAD_UNCHANGED标志,保持原始数据。
另外,BMP的简单结构也适合用来测试自己写的图像库。解析一张BMP,验证像素值是否正确,比调试JPEG解码器容易得多。
5.4 跨平台数据交换的“笨办法”
有时候需要在不同系统之间传图像,但对方系统可能没有常见的图像库。这时候BMP反而是个好选择,因为它的规范足够简单,任何语言都能手写解析。我遇到过在一个老式工业设备上,只能用C语言,没有libpng、没有libjpeg,但需要显示一张公司Logo。最后就是把Logo转成24位BMP,然后用几十行代码解析显示。虽然文件大了点,但省去了移植库的麻烦。
当然,如果网络传输带宽有限,BMP的体积是硬伤。这时候可以先压缩再传,接收端解压后再解析BMP。或者用RLE压缩的BMP,但兼容性要测试好。
6. 进阶话题:BITFIELDS与色彩管理
6.1 BITFIELDS掩码的解析与生成
当biCompression为3时,DIB头后面会跟三个或四个32位掩码,分别指定红、绿、蓝、alpha在像素值中占哪些位。这个机制允许非标准的位分配,比如RGB565、ARGB1555等。解析时,先读掩码,然后对每个像素做位运算提取分量。
def apply_bitfields(pixel, masks): r_mask, g_mask, b_mask = masks[0], masks[1], masks[2] r = (pixel & r_mask) >> (r_mask.bit_length() - 1 - (r_mask & -r_mask).bit_length() + 1) # 更简单的做法是算移位和缩放 def extract(value, mask): if mask == 0: return 0 shift = (mask & -mask).bit_length() - 1 max_val = mask >> shift return ((value & mask) >> shift) * 255 // max_val return (extract(pixel, r_mask), extract(pixel, g_mask), extract(pixel, b_mask))生成BITFIELDS BMP时,要在DIB头后面写掩码,然后bfOffBits要相应调整。注意,有些解析器不支持BITFIELDS,所以如果要做通用交换,还是用标准24位最保险。
6.2 V4/V5头中的色彩空间信息
BITMAPV4HEADER在INFOHEADER基础上增加了颜色空间类型、端点、gamma值等。这些字段主要用于色彩管理,普通显示用不到。V5又增加了ICC配置文件相关的偏移和大小。如果你要生成用于印刷或专业影像的BMP,这些字段就重要了。但大多数场景下,填0或者标准sRGB值就行。
解析V4/V5时,头部大小是108或124,后面的调色板和像素数据偏移要按这个大小算。不要硬编码40,否则会读错。
6.3 实际项目中的取舍建议
如果你的BMP只是内部使用,比如嵌入式资源、算法中间结果,那就用24位无压缩INFOHEADER,兼容性最好,代码最简单。如果需要透明度,用32位,但要注意Alpha通道的处理。如果要做色彩管理,再考虑V4/V5。BITFIELDS除非硬件要求,否则尽量避开。
文件大小方面,如果实在嫌大,可以用RLE8,但一定要测试目标平台的兼容性。我个人的经验是,RLE的BMP在Windows画图里能打开,但在一些第三方库和嵌入式解析器里会出问题。所以除非有明确需求,否则不推荐。
最后再分享一个小技巧:如果你需要快速查看BMP的头部信息,可以用Python的struct写个一行命令,或者用file命令。Linux下file命令能直接告诉你BMP的尺寸和位深,调试时很方便。Windows下可以用PowerShell读前几个字节,但不如装个十六进制编辑器直观。