22、序列化与图像处理:BMP、PNG等图像格式的序列化原理
图像格式的序列化,说白了就是把像素数据按照特定规则打包成文件。我做了这么多年嵌入式开发,跟图像格式打过不少交道。从最简单的BMP到复杂的PNG,每种格式都有自己的脾气。今天咱们就聊聊这些格式背后的序列化原理。
BMP格式:最朴素的序列化方式
BMP是Windows下的标准图像格式,也是我最早接触的图像格式。它的序列化方式特别直白——就是文件头+信息头+像素数据。嗯,这里要注意,BMP的像素数据是从下往上存的,也就是左下角第一个像素。
核心要点:BMP的序列化就是按固定结构把数据写进文件。没有压缩,没有编码,纯粹的内存转储。
我曾经在一个工业相机项目里用过BMP格式。当时需要快速保存图像做调试,BMP的简单结构帮了大忙。来看看它的文件结构:
// BMP文件头结构
typedef struct {
uint16_t bfType; // 固定为0x4D42 ("BM")
uint32_t bfSize; // 文件大小
uint16_t bfReserved1; // 保留
uint16_t bfReserved2; // 保留
uint32_t bfOffBits; // 像素数据偏移
} __attribute__((packed)) BMP_FILEHEADER;
// BMP信息头结构
typedef struct {
uint32_t biSize; // 结构体大小
int32_t biWidth; // 图像宽度
int32_t biHeight; // 图像高度(正数表示从下往上)
uint16_t biPlanes; // 颜色平面数,固定为1
uint16_t biBitCount; // 每像素位数(24表示RGB)
uint32_t biCompression; // 压缩方式(0表示不压缩)
uint32_t biSizeImage; // 像素数据大小
int32_t biXPelsPerMeter; // 水平分辨率
int32_t biYPelsPerMeter; // 垂直分辨率
uint32_t biClrUsed; // 使用的颜色数
uint32_t biClrImportant; // 重要颜色数
} __attribute__((packed)) BMP_INFOHEADER;
序列化BMP时,我习惯这样处理:
// BMP序列化函数
int bmp_serialize(const char* filename, uint8_t* pixels,
int width, int height, int bpp) {
BMP_FILEHEADER fh;
BMP_INFOHEADER ih;
// 计算行对齐(每行必须是4字节对齐)
int row_size = ((width * bpp / 8 + 3) / 4) * 4;
int data_size = row_size * height;
// 填充文件头
fh.bfType = 0x4D42;
fh.bfSize = sizeof(fh) + sizeof(ih) + data_size;
fh.bfOffBits = sizeof(fh) + sizeof(ih);
// 填充信息头
ih.biSize = sizeof(ih);
ih.biWidth = width;
ih.biHeight = height;
ih.biBitCount = bpp;
ih.biCompression = 0;
ih.biSizeImage = data_size;
// 写入文件
FILE* fp = fopen(filename, "wb");
fwrite(&fh, 1, sizeof(fh), fp);
fwrite(&ih, 1, sizeof(ih), fp);
// 写入像素数据(注意BGR顺序)
for (int y = height - 1; y >= 0; y--) {
for (int x = 0; x < width; x++) {
int idx = (y * width + x) * 3;
uint8_t bgr[3] = {pixels[idx+2], pixels[idx+1], pixels[idx]};
fwrite(bgr, 1, 3, fp);
}
// 补齐对齐字节
uint8_t pad = 0;
fwrite(&pad, 1, row_size - width * 3, fp);
}
fclose(fp);
return 0;
}
避坑指南:我曾经在BMP序列化时忘记处理行对齐,结果图像在Windows上显示错位。BMP要求每行数据必须是4字节对齐,不足的用0填充。这个细节很容易被忽略。
PNG格式:带压缩的序列化
PNG比BMP复杂多了。它用了Deflate压缩算法,还支持透明通道。我个人觉得PNG的序列化设计得很巧妙——用chunk(数据块)的方式组织数据,每个chunk都有类型标识和CRC校验。
为什么会这样设计?因为PNG要考虑网络传输的可靠性。每个chunk独立校验,就算某个chunk损坏,其他chunk还能正常解析。
// PNG chunk结构
typedef struct {
uint32_t length; // 数据长度
char type[4]; // chunk类型(如IHDR、IDAT、IEND)
uint8_t* data; // chunk数据
uint32_t crc; // CRC32校验
} PNG_CHUNK;
// PNG关键chunk类型
// IHDR: 图像头信息
// PLTE: 调色板
// IDAT: 图像数据(压缩后)
// IEND: 图像结束标记
PNG的序列化流程是这样的:
- 写入PNG文件签名(8字节固定值)
- 写入IHDR chunk(图像尺寸、位深、颜色类型等)
- 如果有调色板,写入PLTE chunk
- 将像素数据经过过滤、压缩后,写入一个或多个IDAT chunk
- 写入IEND chunk结束
我记得第一次实现PNG序列化时,被过滤步骤搞晕了。PNG有5种过滤方式,每种都针对不同的图像特征优化。我建议初学者先实现最简单的None过滤(不过滤),等跑通了再优化。
// PNG过滤函数(None模式)
void png_filter_none(uint8_t* row, uint8_t* filtered, int width, int bpp) {
filtered[0] = 0; // 过滤类型:None
memcpy(filtered + 1, row, width * bpp / 8);
}
// PNG过滤函数(Sub模式)
void png_filter_sub(uint8_t* row, uint8_t* filtered, int width, int bpp) {
int bytes_per_pixel = bpp / 8;
filtered[0] = 1; // 过滤类型:Sub
for (int i = 0; i < width * bytes_per_pixel; i++) {
if (i < bytes_per_pixel) {
filtered[i + 1] = row[i];
} else {
filtered[i + 1] = row[i] - row[i - bytes_per_pixel];
}
}
}
注意:PNG的CRC校验范围是chunk type + chunk data,不包括length字段。这个细节我踩过坑,写出来的PNG文件用标准库打开就报CRC错误。
图像格式序列化的核心差异
| 特性 | BMP | PNG |
|---|---|---|
| 压缩方式 | 无压缩 | Deflate压缩 |
| 颜色顺序 | BGR | RGB |
| 透明通道 | 不支持 | 支持(RGBA) |
| 数据组织 | 连续像素数据 | chunk结构 |
| 校验方式 | 无 | CRC32 |
| 序列化复杂度 | 低 | 高 |
实战:从BMP到PNG的转换
在实际项目中,我经常需要把BMP转成PNG。BMP的序列化简单,适合快速保存;PNG的压缩率高,适合存储和传输。来看看转换的核心逻辑:
// BMP转PNG的核心流程
int bmp_to_png(const char* bmp_file, const char* png_file) {
// 1. 读取BMP文件
BMP_FILEHEADER fh;
BMP_INFOHEADER ih;
uint8_t* pixels = read_bmp(bmp_file, &fh, &ih);
// 2. 转换为RGB顺序(BMP是BGR)
int pixel_count = ih.biWidth * ih.biHeight;
for (int i = 0; i < pixel_count; i++) {
uint8_t tmp = pixels[i * 3];
pixels[i * 3] = pixels[i * 3 + 2];
pixels[i * 3 + 2] = tmp;
}
// 3. 创建PNG写入器
png_structp png_ptr = png_create_write_struct(
PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
png_infop info_ptr = png_create_info_struct(png_ptr);
// 4. 设置PNG头信息
png_set_IHDR(png_ptr, info_ptr,
ih.biWidth, ih.biHeight,
8, PNG_COLOR_TYPE_RGB,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
// 5. 写入像素数据
png_write_info(png_ptr, info_ptr);
for (int y = 0; y < ih.biHeight; y++) {
png_write_row(png_ptr,
pixels + y * ih.biWidth * 3);
}
png_write_end(png_ptr, NULL);
// 6. 清理资源
png_destroy_write_struct(&png_ptr, &info_ptr);
free(pixels);
return 0;
}
个人经验:在嵌入式系统里做图像格式转换,内存管理是个大问题。BMP转PNG时,我习惯用流式处理——读一行BMP数据,过滤压缩一行,再写入PNG。这样内存占用只有几KB,而不是整个图像的大小。
图像序列化的性能考量
你想想看,在嵌入式设备上处理图像,性能瓶颈往往在内存拷贝和压缩计算上。我做过一个项目,用STM32F4采集摄像头数据,需要实时保存为PNG。当时遇到两个问题:
- Deflate压缩太慢,一帧640x480的图像要压缩好几秒
- 内存不够,没法一次性缓存整帧图像
解决方案是:
- 用BMP格式做临时存储,速度快
- 后台线程慢慢转成PNG
- 用行缓冲方式处理,内存占用控制在4KB以内
说白了,没有最好的格式,只有最适合场景的格式。BMP适合快速读写,PNG适合存储传输。理解它们的序列化原理,你就能根据实际需求做选择。
图像序列化说到底就是数据格式转换。BMP像是个老实人,有什么说什么;PNG则像个精明的商人,总想着怎么压缩打包。理解它们的原理,你就能在项目中游刃有余。
总结:图像格式的序列化核心在于数据组织方式和压缩策略。BMP用固定结构,简单可靠;PNG用chunk结构,灵活可扩展。实际开发中,我建议根据应用场景选择:调试阶段用BMP,产品阶段用PNG。
公众号:蓝海资料掘金营,微信deep3321