手搓 C 语言 bmp 图像编解码
git 传送门:https://gitee.com/louistark/lib_bmp
1. 手搓流程简述
前两篇文章我们已经对 BMP 图像格式的内容和细节有了较为深入的了解,并且通过手动解析两张 BMP 图片,对 BMP 格式的编解码方式有了直观认识。所以,在这篇文章中,我们可以尝试手搓一下 BMP 图片的编解码源码。
1.1 格式约束
BMP 的格式和用法比较多样,此处我们只讲 最常用的 BMP 类型
,因此做以下几点约束:
- 图像格式:
Windows V1 BMP 格式
- 像素位深:
24 bit
- 图像压缩:
不进行压缩
- 调色板:
不使用调色板
1.2 数据结构
手搓源码第一步,我们需要定义一些 BMP 图像相关的 数据结构
:
- BMP 文件头数据结构
- RGB 图像数据结构
- BMP 图像在内存中的数据结构
1.3 处理函数
手搓源码第二步,我们需要写一些 BMP 编解码需要用到的几类 处理函数
:
- BMP 图像内存管理函数(分配、释放内存)
- BMP 图像文件头读写函数(读取、解析、写入文件头)
- BMP 图像数据读写函数(读取、写入 rgb 图像数据)
- BMP 文件读写函数(读取、写入 bmp 文件)
2. 数据结构
2.1 BMP 文件头数据结构
根据我们之前列出的 BMP 文件头数据结构表,我们可以非常轻松的写出 位图文件头
和 文件信息头
数据结构。
序号 | 字节数 | 变量名 | 变量类型 | 字段名 |
---|---|---|---|---|
1 | 2 | bfType | unsigned short | 文件类型 |
2 | 4 | bfSize | unsigned int | 文件大小 |
3 | 2 | bfReserved1 | unsigned short | 保留字段 1 |
4 | 2 | bfReserved2 | unsigned short | 保留字段 2 |
5 | 4 | bfOffBits | unsigned int | 数据偏移量 |
6 | 4 | biSize | unsigned int | DIB 头大小 |
7 | 4 | biWidth | int | 图像宽度 |
8 | 4 | biHeight | int | 图像高度 |
9 | 2 | biPlanes | unsigned short | 颜色平面数 |
10 | 2 | biBitCount | unsigned short | 每个像素的位数 |
11 | 4 | biCompression | unsigned int | 压缩类型 |
12 | 4 | biSizeImages | unsigned int | 图像数据大小 |
13 | 4 | biXPelsPerMeter | int | 水平分辨率 |
14 | 4 | biYPelsPerMeter | int | 垂直分辨率 |
15 | 4 | biClrUsed | unsigned int | 调色板大小 |
16 | 4 | biClrImportant | unsigned int | 重要颜色数 |
typedef struct __bmp_file_header
{
unsigned short bfType;
unsigned int bfSize;
unsigned short bfReserved1;
unsigned short bfReserved2;
unsigned int bfOffBits;
} bmp_file_header_t;
typedef struct __bmp_info_header
{
unsigned int biSize;
int biWidth;
int biHeight;
unsigned short biPlanes;
unsigned short biBitCount;
unsigned int biCompression;
unsigned int biSizeImage;
int biXPelsPerMeter;
int biYPelsPerMeter;
unsigned int biClrUsed;
unsigned int biClrImportant;
} bmp_info_header_t;
2.2 RGB 图像数据结构
此处定义了两种图像像素数据结构:
1. bmp_pixel_t
: BMP 文件图像像素数据在内存中存在时的数据格式,注意 B G R
通道顺序,他一定是与我们架构中所使用的 BMP 图像格式类型保持一致的,但未必会与整体架构中的数据流格式保持一致;定义这么一层数据结构有利于和图像处理架构中的数据流对接。
2. rgb8_pixel_t
: 定义了一种在整个图像处理架构中通用的数据结构,采用较为普遍的 RGB888 格式;这样做是模拟整体架构中的数据流,我们需要将输入的 BMP 图像内容读取到该数据流中去,或是将数据流中的数据写到 BMP 文件中去。
注:大部分情况下这两个数据格式可能是不同的,此处写成类似的形式,只是便于演示。
typedef struct __bmp_pixel
{
unsigned char blue;
unsigned char green;
unsigned char red;
} bmp_pixel_t;
typedef struct __rgb8_pixel
{
unsigned char red;
unsigned char green;
unsigned char blue;
} rgb8_pixel_t;
2.3 BMP 图像在内存中的数据结构
一个 BMP 图像文件数据包含了 位图文件头
、文件信息头
和 图像像素数据
三部分;此处图像像素数据用一个二维指针去表示,其本质上就是一个二维数组,相比内存中常见的一维数组可读性高,处理方便。
typedef struct __bmp_img
{
bmp_file_header_t img_file_header;
bmp_info_header_t img_info_header;
bmp_pixel_t **img_pixels;
} bmp_img_t;
3. 处理函数
3.1 BMP 图像内存管理函数
bmp_img_size
:获取 bmp 文件尺寸,便于在分配内存或创建文件时知道需要的空间大小;bmp_img_pixels_alloc
:根据图像的尺寸,来分配 RGB 数据所需占用的内存空间;bmp_img_pixels_free
:根据图像尺寸,释放 RGB 数据所占用的内存空间;bmp_init
:根据图像的尺寸,对传入的 bmp_img_t 空指针进行初始化,分配内存;bmp_free
:释放 bmp_img_t 指针指向的 bmp 图片数据内存;
/// Acquire bmp image file size
unsigned int
bmp_img_size(bmp_img_t *img)
{
return img->img_file_header.bfSize;
}
/// Allocate memory for bmp image pixels
void
bmp_img_pixels_alloc(bmp_img_t *img,
const int width,
const int height)
{
img->img_pixels = malloc(sizeof(bmp_pixel_t *) * height);
memset(img->img_pixels, 0, sizeof(bmp_pixel_t *) * height);
for (size_t j = 0; j < height; j++)
{
img->img_pixels[j] = malloc(sizeof(bmp_pixel_t) * width);
memset(img->img_pixels[j], 0, sizeof(bmp_pixel_t) * width);
}
}
/// Free memory of bmp image pixels
void
bmp_img_pixels_free(bmp_img_t *img)
{
const size_t height = abs(img->img_info_header.biHeight);
for (size_t y = 0; y < height; y++)
{
free(img->img_pixels[y]);
}
free(img->img_pixels);
}
/// Initialize bmp image struct
void
bmp_init(bmp_img_t **img,
const int width,
const int height)
{
*img = malloc(sizeof(bmp_img_t));
memset(*img, 0, sizeof(bmp_img_t));
(*img)->img_info_header.biWidth = width;
(*img)->img_info_header.biHeight = height;
bmp_img_pixels_alloc(*img, width, height);
}
/// Free bmp image struct
void
bmp_free(bmp_img_t **img)
{
bmp_img_pixels_free(*img);
free(*img);
*img = NULL;
}
3.2 BMP 图像文件头读写函数
bmp_header_init_df
:对 bmp 图像的文件头进行默认初始化(位深 24 bit,无调色板,不压缩);bmp_header_read_buf
:从内存中的 bmp 图像数据中解码出 bmp header 信息;bmp_header_write_buf
:将 bmp header 信息编码后写入指定的 bmp 图像数据内存地址;
注:为避免图像库直接操作文件 IO,库中所有的编解码对象都是一块指定内存,而非文件。
/// Set bmp image header to default value
void
bmp_header_init_df(bmp_img_t *img)
{
bmp_file_header_t *file_header = &img->img_file_header;
bmp_info_header_t *info_header = &img->img_info_header;
const int width = info_header->biWidth;
const int height = info_header->biHeight;
/// Set default bmp file header
file_header->bfType = BMP_MAGIC;
file_header->bfSize = (sizeof(bmp_pixel_t) * abs(width) + GET_PADDING_BYTES(abs(width)))
* abs(height) + BMP_FILE_HEADER_SIZE + BMP_DIB_HEADER_SIZE;
file_header->bfReserved1 = 0;
file_header->bfReserved2 = 0;
file_header->bfOffBits = BMP_FILE_HEADER_SIZE + BMP_DIB_HEADER_SIZE;
/// Set default bmp info header
info_header->biSize = BMP_DIB_HEADER_SIZE;
info_header->biPlanes = 1;
info_header->biBitCount = 24;
info_header->biCompression = 0;
info_header->biSizeImage = 0;
info_header->biXPelsPerMeter = 0;
info_header->biYPelsPerMeter = 0;
info_header->biClrUsed = 0;
info_header->biClrImportant = 0;
}
/// Read bmp header from buffer
bmp_ret_t
bmp_header_read_buf(bmp_file_header_t *file_header,
bmp_info_header_t *info_header,
const unsigned char *buf)
{
bmp_ret_t res = BMP_OK;
if (NULL == file_header || NULL == info_header || NULL == buf)
{
res = BMP_BUFFER_EMPTY;
}
else
{
memcpy(&file_header->bfType, (void *)((char *)buf + 0), 2);
memcpy(&file_header->bfSize, (void *)((char *)buf + 2), 4);
memcpy(&file_header->bfReserved1, (void *)((char *)buf + 6), 2);
memcpy(&file_header->bfReserved2, (void *)((char *)buf + 8), 2);
memcpy(&file_header->bfOffBits, (void *)((char *)buf + 10), 4);
memcpy(info_header, (void *)((char *)buf + 14), 40);
}
return res;
}
/// Write bmp header to buffer
bmp_ret_t
bmp_header_write_buf(bmp_file_header_t *file_header,
bmp_info_header_t *info_header,
unsigned char *buf)
{
bmp_ret_t res = BMP_OK;
if (NULL == file_header || NULL == info_header || NULL == buf)
{
res = BMP_BUFFER_EMPTY;
}
else
{
memcpy((void *)((char *)buf + 0), &file_header->bfType, 2);
memcpy((void *)((char *)buf + 2), &file_header->bfSize, 4);
memcpy((void *)((char *)buf + 6), &file_header->bfReserved1, 2);
memcpy((void *)((char *)buf + 8), &file_header->bfReserved2, 2);
memcpy((void *)((char *)buf + 10), &file_header->bfOffBits, 4);
memcpy(buf + BMP_FILE_HEADER_SIZE, info_header, BMP_DIB_HEADER_SIZE);
}
return res;
}
3.3 BMP 图像数据读写函数
bmp_image_export
:将 bmp 数据结构中的像素 RGB 数据转化为 c-model 数据流所使用的一维数组;bmp_image_import
:将 c-model 数据流所使用的一维数组转化为 bmp 数据结构中的像素 RGB 数据;
注:这两个函数是提供 bmp 数据结构中的像素数据与 c-model 数据流转换的接口。
/// Export bmp image pixels data to RGB888 struct
bmp_ret_t
bmp_image_export(bmp_img_t *img,
rgb8_pixel_t *pRgb8)
{
bmp_ret_t res = BMP_OK;
if (NULL == img || NULL == pRgb8)
{
res = BMP_BUFFER_EMPTY;
}
else
{
const size_t width = abs(img->img_info_header.biWidth);
const size_t height = abs(img->img_info_header.biHeight);
for (size_t row = 0; row < height; row++)
{
for (size_t col = 0; col < width; col++)
{
pRgb8[row * width + col].blue = img->img_pixels[row][col].blue;
pRgb8[row * width + col].green = img->img_pixels[row][col].green;
pRgb8[row * width + col].red = img->img_pixels[row][col].red;
}
}
}
return res;
}
/// Import bmp image pixels data from RGB888 struct
bmp_ret_t
bmp_image_import(bmp_img_t *img,
rgb8_pixel_t *pRgb8)
{
bmp_ret_t res = BMP_OK;
if (NULL == img || NULL == pRgb8)
{
res = BMP_BUFFER_EMPTY;
}
else
{
const size_t width = abs(img->img_info_header.biWidth);
const size_t height = abs(img->img_info_header.biHeight);
for (size_t row = 0; row < height; row++)
{
for (size_t col = 0; col < width; col++)
{
img->img_pixels[row][col].blue = pRgb8[row * width + col].blue;
img->img_pixels[row][col].green = pRgb8[row * width + col].green;
img->img_pixels[row][col].red = pRgb8[row * width + col].red;
}
}
}
return res;
}
3.4 BMP 文件读写函数
bmp_read_buffer
:从内存中读取 bmp 图像数据,并转化为我们设计的 bmp_img_t 数据结构;bmp_write_buffer
:将 bmp_img_t 数据结构中的内容编码成连续的数据写进内存中去;
注:为避免图像库直接操作文件 IO,库中所有的编解码对象都是一块指定内存,而非文件。
/// Read bmp image from buffer
bmp_ret_t
bmp_read_buffer(bmp_img_t *img,
const unsigned char *buf)
{
bmp_ret_t res = BMP_OK;
if (NULL == img || NULL == buf)
{
res = BMP_BUFFER_EMPTY;
}
/// 1. Read bmp header
res = bmp_header_read_buf(&img->img_file_header, &img->img_info_header, (void *)buf);
/// 2. Read image pixels
if (BMP_OK == res)
{
const size_t offset = img->img_file_header.bfOffBits;
const size_t width = abs(img->img_info_header.biWidth);
const size_t height = abs(img->img_info_header.biHeight);
const size_t padding = GET_PADDING_BYTES(width);
for (size_t y = 0; y < height; y++)
{
size_t row = img->img_info_header.biHeight > 0 ? height - y - 1 : y;
for (size_t x = 0; x < width; x++)
{
size_t col = img->img_info_header.biWidth > 0 ? x : width - x - 1;
img->img_pixels[y][x].blue
= buf[offset + row * (width * 3 + padding) + col * 3 + 0];
img->img_pixels[y][x].green
= buf[offset + row * (width * 3 + padding) + col * 3 + 1];
img->img_pixels[y][x].red
= buf[offset + row * (width * 3 + padding) + col * 3 + 2];
}
}
}
return res;
}
/// Write bmp image to buffer
bmp_ret_t
bmp_write_buffer(bmp_img_t *img,
unsigned char *buf)
{
bmp_ret_t res = BMP_OK;
if (NULL == img || NULL == buf)
{
res = BMP_BUFFER_EMPTY;
}
/// 1. Initialize target buffer
size_t file_size = img->img_file_header.bfSize;
memset(buf, 0, file_size);
/// 2. Write bmp header
res = bmp_header_write_buf(&img->img_file_header, &img->img_info_header, buf);
/// 3. Write image pixels
if (BMP_OK == res)
{
const size_t offset = img->img_file_header.bfOffBits;
const size_t width = abs(img->img_info_header.biWidth);
const size_t height = abs(img->img_info_header.biHeight);
const size_t padding = GET_PADDING_BYTES(width);
for (size_t y = 0; y < height; y++)
{
size_t row = img->img_info_header.biHeight > 0 ? height - y - 1 : y;
for (size_t x = 0; x < width; x++)
{
size_t col = img->img_info_header.biWidth > 0 ? x : width - x - 1;
buf[offset + row * (width * 3 + padding) + col * 3 + 0]
= img->img_pixels[y][x].blue;
buf[offset + row * (width * 3 + padding) + col * 3 + 1]
= img->img_pixels[y][x].green;
buf[offset + row * (width * 3 + padding) + col * 3 + 2]
= img->img_pixels[y][x].red;
}
}
}
return res;
}
4. 测试代码
4.1 BMP 文件读取测试
这段测试代码允许用户传入一张 bmp 图片,程序将图片数据解码完成后,会打印出这张 bmp 图片的所有信息。
#include <stdio.h>
#include <stdlib.h>
#include "libbmp.h"
#ifndef DATA_SET_DIR
#define DATA_SET_DIR "./"
#endif
#ifndef OUTPUT_DIR
#define OUTPUT_DIR "./"
#endif
void print_bmp_header(bmp_img_t *img)
{
printf("BMP file header:\n");
printf("Type: %#x\n", img->img_file_header.bfType);
printf("Size: %u\n", img->img_file_header.bfSize);
printf("Reserved1: %#x\n", img->img_file_header.bfReserved1);
printf("Reserved2: %#x\n", img->img_file_header.bfReserved2);
printf("Offset: %u\n", img->img_file_header.bfOffBits);
printf("BMP info header:\n");
printf("Size: %u\n", img->img_info_header.biSize);
printf("Width: %d\n", img->img_info_header.biWidth);
printf("Height: %d\n", img->img_info_header.biHeight);
printf("Planes: %#x\n", img->img_info_header.biPlanes);
printf("Bits: %#x\n", img->img_info_header.biBitCount);
printf("Compression: %u\n", img->img_info_header.biCompression);
printf("Image size: %u\n", img->img_info_header.biSizeImage);
printf("X pixels per meter: %d\n", img->img_info_header.biXPelsPerMeter);
printf("Y pixels per meter: %d\n", img->img_info_header.biYPelsPerMeter);
printf("Colors used: %u\n", img->img_info_header.biClrUsed);
printf("Colors important: %u\n", img->img_info_header.biClrImportant);
}
int main(int argc, char *argv[])
{
char input_filename[511];
FILE *input_file;
unsigned char *input_buf;
if (argc != 2)
{
printf("\nUsage: %s <input_file>\n\n", argv[0]);
return 1;
}
sprintf(input_filename, "%s/%s", DATA_SET_DIR, argv[1]);
input_file = fopen(input_filename, "rb");
if (input_file == NULL)
{
printf("Error: could not open %s\n", input_filename);
return 1;
}
/// Acquire file size
fseek(input_file, 0, SEEK_END);
int fileSize = ftell(input_file);
fseek(input_file, 0, SEEK_SET);
input_buf = (unsigned char *)malloc(fileSize);
fread(input_buf, fileSize, 1, input_file);
fclose(input_file);
/// Read bmp image from buffer
bmp_ret_t res = BMP_OK;
int width = 512;
int height = 512;
bmp_img_t *pImage;
bmp_init(&pImage, width, height);
res = bmp_read_buffer(pImage, input_buf);
if (res != BMP_OK)
{
printf("Error: read bmp fail! res = %d.\n", res);
bmp_free(&pImage);
free(input_buf);
return 1;
}
print_bmp_header(pImage);
bmp_free(&pImage);
free(input_buf);
return 0;
}
测试输出结果:
root@DESKTOP-GQBECKU:/project/gitee/lib_bmp/build# ./out/image_access Lena.bmp
BMP file header:
Type: 0x4d42
Size: 786486
Reserved1: 0
Reserved2: 0
Offset: 54
BMP info header:
Size: 40
Width: 512
Height: 512
Planes: 0x1
Bits: 0x18
Compression: 0
Image size: 786432
X pixels per meter: 0
Y pixels per meter: 0
Colors used: 0
Colors important: 0
4.2 BMP 文件写入测试
这段代码会输出一张 512 x 512 的 bmp 图片,图片内容为 8 x 8 的黑白棋盘格。
#include <stdio.h>
#include <stdlib.h>
#include "libbmp.h"
#ifndef DATA_SET_DIR
#define DATA_SET_DIR "./"
#endif
#ifndef OUTPUT_DIR
#define OUTPUT_DIR "./"
#endif
int main(int argc, char *argv[])
{
char output_filename[511];
FILE *output_file;
unsigned char *output_buf;
bmp_img_t *img;
int width = 512;
int height = 512;
bmp_init(&img, width, height);
bmp_header_init_df(img);
/// Draw a checkerboard pattern
rgb8_pixel_t *pRgb8 = malloc(sizeof(rgb8_pixel_t) * width * height);
for (size_t y = 0; y < height; y++)
{
for (size_t x = 0; x < width; x++)
{
if ((y % 128 < 64 && x % 128 < 64) ||
(y % 128 >= 64 && x % 128 >= 64))
{
pRgb8[y * width + x].red = 250;
pRgb8[y * width + x].green = 250;
pRgb8[y * width + x].blue = 250;
}
else
{
pRgb8[y * width + x].red = 0;
pRgb8[y * width + x].green = 0;
pRgb8[y * width + x].blue = 0;
}
}
}
bmp_image_import(img, pRgb8);
output_buf = (unsigned char *)malloc(bmp_img_size(img));
bmp_write_buffer(img, output_buf);
sprintf(output_filename, "%s/%s", OUTPUT_DIR, "checkerboard.bmp");
output_file = fopen(output_filename, "wb");
fwrite(output_buf, bmp_img_size(img), 1, output_file);
fclose(output_file);
free(pRgb8);
bmp_free(&img);
return 0;
}
测试输出结果: