在C语言中进行图像处理相关的开发时,读取图像像素是基础且核心的操作,不同格式的图像存储结构不同,读取方式也存在差异,其中BMP格式因为存储结构简单,没有复杂的压缩规则,是学习C语言读取像素的最佳入门案例。

BMP图像基本结构
BMP图像文件主要由三部分组成,分别是文件头、信息头和像素数据区,我们读取像素的核心就是跳过前两部分,直接读取像素数据区的内容。
- 文件头:固定14字节,包含文件类型、文件大小等基础信息
- 信息头:固定40字节,包含图像宽度、高度、位深等关键参数
- 像素数据区:存储每个像素的RGB值,位深为24位时每个像素占3字节,顺序是BGR
读取像素的完整实现步骤
1. 定义BMP结构对应的结构体
首先需要根据BMP的存储规则定义对应的结构体,方便我们解析文件头和信息头的数据。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
// BMP文件头结构体,14字节
#pragma pack(push, 1)
typedef struct {
uint16_t bfType; // 文件类型,必须是0x4D42即字符BM
uint32_t bfSize; // 文件总大小
uint16_t bfReserved1; // 保留字段,为0
uint16_t bfReserved2; // 保留字段,为0
uint32_t bfOffBits; // 像素数据起始位置偏移量
} BITMAPFILEHEADER;
// BMP信息头结构体,40字节
typedef struct {
uint32_t biSize; // 信息头大小,固定为40
int32_t biWidth; // 图像宽度,单位像素
int32_t biHeight; // 图像高度,单位像素,正数表示倒序存储
uint16_t biPlanes; // 颜色平面数,固定为1
uint16_t biBitCount; // 位深,24表示24位真彩色
uint32_t biCompression; // 压缩方式,0表示无压缩
uint32_t biSizeImage; // 像素数据大小
int32_t biXPelsPerMeter; // 水平分辨率
int32_t biYPelsPerMeter; // 垂直分辨率
uint32_t biClrUsed; // 使用的颜色数
uint32_t biClrImportant; // 重要颜色数
} BITMAPINFOHEADER;
#pragma pack(pop)
// 像素结构体,存储单个像素的RGB值
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
} Pixel;
2. 读取BMP文件并解析头部信息
打开BMP文件后,先读取文件头和信息头,验证文件格式是否正确,同时获取图像的宽度、高度和像素数据起始位置。
int read_bmp(const char* filename, Pixel** pixels, int* width, int* height) {
FILE* fp = fopen(filename, "rb");
if (fp == NULL) {
printf("文件打开失败n");
return -1;
}
BITMAPFILEHEADER file_header;
// 读取文件头
fread(&file_header, sizeof(BITMAPFILEHEADER), 1, fp);
// 验证是否为BMP文件
if (file_header.bfType != 0x4D42) {
printf("不是合法的BMP文件n");
fclose(fp);
return -1;
}
BITMAPINFOHEADER info_header;
// 读取信息头
fread(&info_header, sizeof(BITMAPINFOHEADER), 1, fp);
// 获取图像宽高
*width = info_header.biWidth;
*height = info_header.biHeight;
// 如果高度为正,说明像素是从下往上存储的,需要后续处理
int is_reverse = 0;
if (*height > 0) {
is_reverse = 1;
*height = abs(*height);
}
// 计算每行像素的字节数,BMP每行字节数需要是4的倍数
int line_bytes = (*width * 3 + 3) / 4 * 4;
// 跳转到像素数据起始位置
fseek(fp, file_header.bfOffBits, SEEK_SET);
// 分配像素内存
*pixels = (Pixel*)malloc(*width * *height * sizeof(Pixel));
if (*pixels == NULL) {
printf("内存分配失败n");
fclose(fp);
return -1;
}
// 临时存储每行的数据
uint8_t* line_data = (uint8_t*)malloc(line_bytes);
if (line_data == NULL) {
printf("内存分配失败n");
free(*pixels);
fclose(fp);
return -1;
}
// 读取像素数据
for (int i = 0; i < *height; i++) {
fread(line_data, 1, line_bytes, fp);
// 处理存储顺序,如果是倒序存储,需要调整行索引
int row_idx = is_reverse ? (*height - 1 - i) : i;
for (int j = 0; j < *width; j++) {
// BMP像素存储顺序是BGR,这里转换为RGB
(*pixels)[row_idx * *width + j].b = line_data[j * 3];
(*pixels)[row_idx * *width + j].g = line_data[j * 3 + 1];
(*pixels)[row_idx * *width + j].r = line_data[j * 3 + 2];
}
}
free(line_data);
fclose(fp);
return 0;
}
3. 测试读取像素功能
编写测试代码,读取指定BMP文件的像素,并打印左上角第一个像素的RGB值。
int main() {
Pixel* pixels = NULL;
int width = 0;
int height = 0;
// 替换为你的BMP文件路径,比如test.bmp
if (read_bmp("test.bmp", &pixels, &width, &height) == 0) {
printf("图像宽度:%d,高度:%dn", width, height);
if (width > 0 && height > 0) {
printf("左上角第一个像素的RGB值:R=%d, G=%d, B=%dn",
pixels[0].r, pixels[0].g, pixels[0].b);
}
free(pixels);
}
return 0;
}
注意事项
- 编译时需要保证结构体没有内存对齐,所以使用了
#pragma pack(push, 1),不同编译器语法可能略有差异 - BMP文件的位深如果不是24位,上述代码需要调整,比如32位位深每个像素占4字节,包含Alpha通道
- 读取其他格式的图像如PNG、JPG时,因为存在压缩算法,无法直接按字节解析,需要借助libpng、libjpeg等第三方库实现
- 处理大尺寸图像时需要注意内存占用,避免一次性分配过大内存导致程序崩溃