ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

BMP位深度与文件头解析:用Python实现不同格式的精准复制

BMP位深度与文件头解析:用Python实现不同格式的精准复制 简介BMP位深度直接决定图像颜色数目与文件体积也是图像读写时不可忽略的核心参数。围绕这一主题面向C#图像处理初学者的示例包集中整理了1位、8位、16位、24位、32位等常见位深位图的复制实现帮助读者清晰理解黑白二值、256色灰度、65536色高彩、1677万色真彩以及带Alpha通道图像在存储与解析时的差异。压缩包共17个文件整体体积仅130KB包含6个不同位深的BMP样例、Visual Studio工程配置、源码与头文件、资源脚本及日志文件工程结构一目了然适合打开后对照代码与图片逐项调试。目前已有213人学习下载对入门级开发者非常有参考意义。通过例程可以掌握不同位深度BMP的文件头解析、像素数据排列与复制逻辑例如8位图需要处理调色板24位图按BGR三通道排列32位图额外携带透明度信息同时资料也澄清了“256位BMP”并非标准格式的常见误解帮助避开命名与设计陷阱。无论是完成图像处理课程实验还是编写图片格式转换、批量复制等工具这套样例都能提供直接而扎实的支撑。1. BMP 图片复制没那么简单位深度不同拷贝的就不是同一种数据拿到 bmp1位、8位、16位、24位、32位、256位图片复制.rar 这类素材包第一反应常常是解压之后cp到另一个目录。但 BMP 不是把像素一排就完事的裸数据它由文件头、信息头、调色板颜色表/掩码和像素区四段拼接而成1 位和 8 位图片依赖调色板索引16 位可能带着颜色掩码32 位携带 alpha 通道行尾还强制按 4 字节对齐。把 BMP 当普通二进制文件复制文件还能打开可一旦要批量整理调色板、去 RLE 压缩、做格式归一化就必须把复制理解成“解析 bmp头文件、读像素区、再生成一个新的 bmp图像”。这篇按位深度把复制逻辑拆开讲脚本可以直接拿去跑批参数和坑也一并给出。2. bmp头文件解析四个字段决定你从哪个位置开始拷贝2.1 文件头和信息头里真正要读的四个数字复制 BMP 之前不用关心图片长什么样先把前 54 字节当成一对结构体读出来。BITMAPFILEHEADER 固定 14 字节BITMAPINFOHEADER 固定 40 字节覆盖了绝大多数素材包。读 bmp头文件时我一般只取四个数字bfOffBits决定像素区起点biBitCount决定每个像素占几位biCompression决定后面有没有掩码或 RLEbiClrUsed决定调色板被实际用了多少条。这四个数字一旦读错复制出来的 bmp图像轻则颜色错乱重则打开直接报“文件格式不支持”。相对文件起始的偏移字段名长度作用复制时必须做的事0bfType2BM 标识不是 BM 就拒绝处理10bfOffBits4像素数据开始字节从这里读取像素区14biSize4信息头长度调色板位置的计算基准18biWidth4像素宽参与 stride 计算22biHeight4像素高负号表示自顶向下保持原行序不翻转28biBitCount2位深度 1/8/16/24/32复制逻辑的分支条件30biCompression40 未压缩、3 掩码、1 RLE8决定 extra 区怎么保留34biSizeImage4像素区字节数为 0 时按 stride 重算46biClrUsed4调色板有效条目数校验调色板规模这些字段不只是用来读取还直接影响你从bfOffBits偏移处能读到多少字节。常见做法是先用biSizeImage做像素长度但这个字段在 mspaint、Photoshop 导出的未压缩图里经常是 0这时就得靠文件尾部反推后面第 4 章会单独说。只写死54偏移的脚本第一次碰 8 位或 16 位图就会错位。2.2 按位深度拆像素区1位、8位靠调色板16位、24位、32位直接存颜色位深度决定的是像素区的存储间距也决定颜色表放在哪里。1 位和 8 位 BMP 的像素值是调色板索引。1 位只有 0 和 1 两个索引对应调色板前两条 RGBQUAD即 8 字节8 位最多 256 个索引对应 1024 字节调色板标题里写的“256 位图片”在实际文件里就是这种 8 位 256 色调色板图。调色板紧跟在信息头后面bfOffBits指向调色板之后的第一个像素字节。复制时如果只拷贝从bfOffBits开始的像素数据、漏掉前面的调色板新图索引不变但查表结果是错的。16 位 BMP 每像素 2 字节常见两种布局未压缩的 RGB555最高 1 位不用以及 BI_BITFIELDS 掩码模式下的 RGB555 或 RGB565。后者在信息头后再跟 12 字节掩码复制时把掩码丢掉R/G/B 通道会互相串位。24 位 BMP 每像素 3 字节按 B、G、R 顺序存储没有颜色表32 位 BMP 每像素 4 字节多出来的高 8 位是 alpha 通道。复制 32 位图时如果只按 RGB 处理alpha 信息会永久丢失通常所说的“BMP 通道图”指的就是这种带 alpha 或单通道的 32 位图。2.3 stride每行像素按 4 字节对齐填充字节也要原样带走BMP 规定每行像素字节数必须是 4 的倍数不足补 0。这个值叫 stride公式是stride ((width * bpp 31) // 32) * 4位深度bpp1 像素宽原始字节实际 stride每行额外填充110.1254388141161624224243433232440比如 3 像素宽的 24 位 bmp图像原始行宽 9 字节实际 stride 是 12每行末尾多 3 个填充字节。执行复制时不能只拷贝width * height * bpp / 8个字节否则从第二行开始全部错位必须按每行stride字节搬运行尾填充一并保留。很多转换工具报“图像撕裂”根因就是脚本把 stride 当成了原始行宽。3. 用 Python 实现多深度 BMP 复制从读头到重写一串代码3.1 先写一个通用 BMP 解析函数复制脚本的核心是“解析出来的每一段都能原样写回去”所以第一步先写一个只读不动的解析函数把文件头、信息头、extra 区和像素区分开。这里用 Python 的struct模块处理二进制测试环境是 Python 3.8不依赖第三方库。import struct def parse_bmp(path): with open(path, rb) as f: fb f.read(14) if fb[:2] ! bBM: raise ValueError(f{path} 不是标准 BMP 文件) bf_type, bf_size, r1, r2, bf_off struct.unpack(2sIHHI, fb) f.seek(14) bi_size struct.unpack(I, f.read(4))[0] f.seek(14) info f.read(bi_size) width, height struct.unpack(ii, info[4:12]) bpp struct.unpack(H, info[14:16])[0] compression struct.unpack(I, info[16:20])[0] size_image struct.unpack(I, info[20:24])[0] clr_used struct.unpack(I, info[32:36])[0] extra_len max(0, bf_off - 14 - bi_size) extra f.read(extra_len) if extra_len else b pixels f.read() return { bi_size: bi_size, info: info, extra: extra, pixels: pixels, width: abs(width), height: abs(height), bpp: bpp, compression: compression, size_image: size_image, clr_used: clr_used, }逻辑说明struct.unpack(2sIHHI, fb)一次解出 14 字节文件头依次是 2 字节 BM 标识、4 字节文件总大小、两个 2 字节保留字段、4 字节像素偏移。info按biSize长度读取而不是固定 40是为了兼容 BITMAPV4HEADER108 字节和 BITMAPV5HEADER124 字节。extra_len bf_off - 14 - bi_size是关键参数它把调色板、BI_BITFIELDS 掩码、ICC 配置全部收进extra避免之后逐位深度单独判断。3.2 复制 1 位和 8 位256 色BMP调色板必须原样带走解析函数拿到extra之后复制本身只有几行def copy_bmp(src, dst): m parse_bmp(src) new_off 14 m[bi_size] len(m[extra]) new_size new_off len(m[pixels]) with open(dst, wb) as f: f.write(struct.pack(2sIHHI, bBM, new_size, 0, 0, new_off)) f.write(m[info]) f.write(m[extra]) f.write(m[pixels])调用方式python3 -c from bmp_copy import copy_bmp; copy_bmp(old.bmp, new.bmp)参数说明new_off是重新计算后的像素区偏移等于“14 字节文件头 信息头长度 extra 区长度”。对 1 位和 8 位图extra保存的就是调色板新文件的调色板内容和原文件完全一致索引值不用做任何改映射。这里不要用1 bpp去猜调色板大小有些软件写出的 8 位图只用了 16 或 64 色extra区只有 64 或 256 字节猜固定 1024 会把像素区开头的字节吞进调色板导致图像整体偏移。3.3 16 位、24 位、32 位的复制差异按 bpp 改 stride掩码和 alpha 一并保留上面的copy_bmp对 16、24、32 位同样适用差别主要在验证环节。复制时不会丢掩码因为 BI_BITFIELDS 的 12 字节被归入extraalpha 也不会丢因为像素区是整体搬运没有按通道剥离。为了确认搬了多少数据加一个 stride 检查def bmp_stride(width, bpp): return ((width * bpp 31) // 32) * 4 m parse_bmp(16bit.bmp) expect bmp_stride(m[width], m[bpp]) * m[height] print(实际像素字节:, len(m[pixels]), 预期:, expect)参数说明bpp是biBitCount里的数值16、24、32 分别对应每像素 2、3、4 字节height在前面已经用abs()转成正数计算只关心行数。未压缩 BMP 的len(pixels)应该等于expect若小于expect大概率是文件被裁剪或缺字节若大于expect说明文件尾部带有附加数据复制时一并带走即可不需要截断。特别注意RLE 压缩的 8 位 BMP 不满足这个等式压缩后像素字节数会明显小于expect遇到compression1时不要拿这个检查去报错。4. 复制 BMP 时最容易翻车的四个坑biSizeImage、掩码、256 位和 RLE4.1 biSizeImage 为 0 时用 stride 反推像素区长度biSizeImage在未压缩 BMP 里经常是 0尤其命令行工具批量导出时。这时候不能直接拿 0 当像素长度要用 stride 重算pixel_bytes bmp_stride(width, bpp) * abs(height) if m[size_image] 0: m[size_image] pixel_bytes原理是未压缩 BMP 的像素区就是“每行对齐后的字节数乘行数”。需要注意bfSize也可能不准确有的工具写 0 或不更新所以最可靠的像素长度是“文件总大小减bfOffBits”即parse_bmp里的len(pixels)。复制时优先信任len(pixels)再把struct.pack_into(I, info_bytes, 20, pixel_bytes)把biSizeImage修正回去这样换到要求严格的上游系统就不会因为字段为 0 被拒。4.2 16 位 BMP 的 BI_BITFIELDS 掩码不能丢16 位 BMP 最容易翻车。compression3时信息头之后紧跟着 3 个 DWORD分别声明 R、G、B 的掩码共 12 字节有时还会追加一个 alpha 掩码变成 16 字节。复制脚本如果只按固定 54 字节计算偏移掩码会被当成像素区开头图像显示成一片噪点。用第 3 章的extra_len逻辑就没有这个问题bf_off会指向掩码之后。遇到这种文件我还习惯把掩码打出来检查if m[compression] 3: masks m[extra][:12] r_mask, g_mask, b_mask struct.unpack(III, masks) print(R_mask:, hex(r_mask), G_mask:, hex(g_mask), B_mask:, hex(b_mask))如果你看到三个掩码是0x7C00、0x03E0、0x001F是 RGB5550xF800、0x07E0、0x001F是 RGB565。复制时这两个掩码区都必须原样写回不能因为目标程序只支持 555 就把 565 硬转那是转换任务不是复制任务。4.3 “256 位”其实是 256 色调色板按实际字节数处理BMP 的位深度最大值就是 32不存在 256 位。素材包命名里的“256 位图片”一般指 8 位 256 色调色板图也就是每个像素 1 字节、查 256 色调色板的文件。复制这类图时最容易踩的坑是按“1 像素 1 字节”直接算文件大小忘记行尾对齐一个宽 255 像素的 8 位图每行有效数据 255 字节但 stride 是 256 字节每行多 1 个填充字节高度 100 就是 100 字节的误差。处理 256 色图时调色板条目数以biClrUsed为准biClrUsed0才默认用1 bpp否则按字段值读取避免把没使用的调色板条目也复制出去。4.4 负高度和 RLE8 文件先确认行序和压缩模式biHeight为负表示像素行按自顶向下顺序存储绝不能在复制时顺手转成正的再重排。复制是保真操作行序必须原样保留转换位深度时才需要考虑翻转。另外compression1BI_RLE8和compression2BI_RLE4的 BMP 在素材包里也会出现这类文件的像素区是 RLE 编码流不是原始像素。如果只是复制文件保留compression字段和像素字节即可但想同时修正残缺头部就得先对 RLE 解码再决定是否换成未压缩格式。实操中我不会在复制层做 RLE 解码而是先复制、再用验证脚本检查目标文件能否被 Pillow 打开保持职责单一。5. 批量复制一整个 BMP 目录的最小命令与像素级校验5.1 用 Bash 循环批量覆盖不同位深度把第 3 章的copy_bmp保存成bmp_copy.py后可以用一个管道循环处理整个目录SRC_DIR./batch_bmp DST_DIR./batch_out mkdir -p $DST_DIR find $SRC_DIR -type f -iname *.bmp | while read -r f; do name$(basename $f) python3 -c from bmp_copy import copy_bmp; copy_bmp($f, $DST_DIR/$name) done参数说明find -iname *.bmp兼容大写.BMP后缀basename去掉源目录前缀避免重名冲突。循环里每次起一个 Python 进程10 万张图会偏慢但胜在稳定量更大时可以改成在一个进程里批量调用copy_bmp逻辑不变。5.2 用像素区 md5 验证位深度和内容都没变文件级 md5 对比会因为你重写了文件头而失配所以校验要跳过头部只对比像素区。把下面这段追加到bmp_copy.pyimport hashlib def pixel_md5(path): m parse_bmp(path) return hashlib.md5(m[pixels]).hexdigest()批量验证for f in ./batch_bmp/*.bmp; do b$(basename $f) src_md5$(python3 -c from bmp_copy import pixel_md5 as p; print(p($f))) dst_md5$(python3 -c from bmp_copy import pixel_md5 as p; print(p(./batch_out/$b))) if [ $src_md5 ! $dst_md5 ]; then echo $b mismatch; fi done这个pixel_md5的校验粒度恰好覆盖复制核心只要bfOffBits之后的数据没有变动就说明调色板、掩码、像素和行尾填充全部原样落地。结合前面的parse_bmp再检查一遍bpp、width、height、compression是否一致四个字段全对上复制任务才算真正完成。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进