我很早就学习了BMP位图。印象中,那时应该是在研究AVI视频文件格式时顺便研究的,或者是研究YUV转RGB时顺便研究的。但未写文章出来,我一直以为我的学习只有在发表了文章才算是完结,否则不能算是我做过了这个事。在这里补上当初读、写BMP的函数代码。
头文件如下:
/**
* @file bmp_utils.h
* @author Late Lee
* @date 2012-7-2 13:21:53
* @brief
* BMP相关工具函数,目前只针对24位图片测试
*
* 1、在VS003及GCC下编译测试通过;
* 2、解决了BMP图片倒立、偏色、倾斜等问题。
* 3、BMP图像每行数据需要4字节对齐,即一行数据不足4的倍数,以0补。
* 解决此问题方法:设置2个变量:
* width_byte:实际的RGB每一行的字节数
* stride_byte:4字节对齐的每一行的字节数(已对齐时两者相等)
* 保存时,另外开辟一个考虑了4字节对齐的缓冲区,每拷贝一行数据(width_byte),
* 跳过stride_byte个字节,即跳到4字节对齐的下一行。
* 读取时,只读width_byte,并且跳过每行最后补的0。
* 4、图像倒立:读取与保存BMP时,将数据倒过来:
* 读取时,将读到的数据由下往上存放到缓冲区
* 保存时,将数据由下往上拷贝到缓冲区
* 5、偏色:BMP排序为BGR,将RGB数据的G、B调换位置即可。
* 6、倾斜:读取BMP时,未跳过补充的0。
*
* 笔记:
BMP图片结构,基中第1、第2部分占54字节,真彩色图没有第三部分
_______________________________
| BITMAPFILEHEADER |
|_______________________________|
| BITMAPINFOHEADER |
|_______________________________|
| n * RGBQUAD |
|_______________________________|
| image data |
|_______________________________|
*
对于2色位图,用1位表示该象素的颜色(一般0表示黑,1表示白),一个字节可以表示8个象素。调色板:2*4=8
对于16色位图,用4位表示一个象素的颜色,以一个字节可以表示2个象素。调色板:16*4=64
对于256色位图,一个字节表示1个象素。调色板:256*4=1024
对于真彩色图,三个字节表示1个象素。无调色板
* 单色BMP图:调色板占8字节,故头部占用54+8=62字节,后面为像素字节,
注意每行字节需要4字节对齐,
举例:16*16像素单色位图,一行占16/8 = 2字节,需要补2字节。
实际像素字节:16*16/2 = 32字节,补齐字节:2*16 = 32,共64字节
头部共62字节,故该图片总大小为64+62=126字节
*/
#ifndef _BMP_UTILS_H
#define _BMP_UTILS_H
#ifdef __cplusplus
extern "C" {
#endif
#ifdef WIN32
#include <Windows.h>
#else
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned long DWORD;
typedef long LONG;
#pragma pack(push)
// 2字节对齐,共14
#pragma pack(2)
typedef struct tagBITMAPFILEHEADER {
WORD bfType; // 文件类型, 0x4d42
DWORD bfSize; // 文件总大小
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits; // 实际位图数据偏移
} BITMAPFILEHEADER; //__attribute__ ((packed));
// 40
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; // 本结构体长度
LONG biWidth; // 宽(单位像素)
LONG biHeight; // 高(单位像素)
WORD biPlanes; // 为1
WORD biBitCount; // 像素占用位数 1(2^1=2黑白二色), 4(2^4=16色),8(2^8=256色),24(真彩色),32
DWORD biCompression; // 压缩类型,不压缩:BI_RGB(0)
DWORD biSizeImage; // 位图数据大小,如果是不压缩类型,可以为0
LONG biXPelsPerMeter; // 水平分辨率,单位是每米的象素个数
LONG biYPelsPerMeter; // 垂直分辨率
DWORD biClrUsed; // 位图实际使用的颜色表中的颜色数
DWORD biClrImportant; // 位图显示过程中重要的颜色数
} BITMAPINFOHEADER; //__attribute__ ((aligned(2)));
typedef struct tagRGBQUAD {
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;
typedef struct tagBITMAPINFO{
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
} BITMAPINFO; // __attribute__ ((aligned(2)));
#pragma pack(pop)
#endif
#undef ALIGN
#define ALIGN(x, n) (((x)+(n)-1)&~((n)-1))
/**
* RGB互换R、B顺序
*
* @param[IN] rgb_buffer RGB缓冲区
* @param[IN] len 缓冲区大小
*
* @return none
*
* @note
* 缓冲区数据可以是RGB,也可以是BGR,该函数只是将B、G进行互换
*/
void swap_rgb(unsigned char* rgb_buffer, int len);
/**
* 分析BMP文件头部
*
* @param[IN] bmp_file BMP图片文件名称
*
* @return
* 0: 成功
* -1: 文件不存在或不是BMP文件
*/
int analyse_bmp_file(const char* bmp_file);
/**
* 读取BMP图片文件
*
* @param[IN] bmp_file BMP图片文件名称
*
* @param[OUT] rgb_buffer RGB数据(实际为BGR)
* @param[OUT] size RGB数据大小
* @param[OUT] width 图片宽
* @param[OUT] height 图片高
*
* @return
* 0:成功
* -1:读取文件失败,或不是BMP文件,或申请内存失败
* @note
* rgb_buffer为二级指针,内存由该函数分配,需要自行释放
* rgb_buffer数据排列顺序为BGR,因此,处理时可能需要转换成RGB顺序
*/
int read_bmp_file(const char* bmp_file, unsigned char** rgb_buffer,
int* size, int* width, int* height);
int read_bmp_file_1(const char* bmp_file, unsigned char** rgb_buffer, int* rgb_size,
unsigned char** palette_buf, int* palette_len,
int* width, int* height);
/**
* 保存BMP文件
*
* @param[IN] bmp_file BMP图片文件名称
*
* @param[IN] rgb_buffer RGB数据(实际为BGR)
* @param[IN] width 图片宽
* @param[IN] height 图片高
*
* @return
* 0:成功
* -1:打开文件失败
* @note
* BMP图片颜色分量实际为BGR,因此,需要事先将rgb_buffer数据排列顺序转换成BGR。
*/
int write_bmp_file(const char* bmp_file, unsigned char* rgb_buffer, int width, int height);
int write_bmp_file_1(const char* bmp_file, unsigned char* rgb_buffer,
unsigned char* palette_buf, int* palette_len,
int width, int height);
#ifdef __cplusplus
};
#endif
#endif /* _BMP_UTILS_H */
实现代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <limits.h>
#include "bmp_utils.h"
//#include "debug.h"
// 注:只针对24位图片
int analyse_bmp_file(const char* bmp_file)
{
#if 0
FILE* fp;
BITMAPFILEHEADER bmpHeader;
BITMAPINFOHEADER bmpInfo;
int rgb_size1 = 0;
int rgb_size2 = 0;
int width = 0;
int height = 0;
int padding = 0;
int stride_byte = 0;
int color_num = 0;
int paltette_len = 0;
char* palette = NULL;
fp = fopen(bmp_file, "rb");
if (fp == NULL)
{
printf("open file %s failed.\n", bmp_file);
return -1;
}
fread(&bmpHeader, 1, sizeof(BITMAPFILEHEADER), fp);
fread(&bmpInfo, 1, sizeof(BITMAPINFOHEADER), fp);
if (bmpHeader.bfType != (('M' << 8) | 'B'))
{
printf("Sorry, not bmp picture.\n");
return -1;
}
width = bmpInfo.biWidth;
height = (int)fabs((double)bmpInfo.biHeight);
switch(bmpInfo.biBitCount)
{
case 1:
color_num = 2;
break;
case 4:
color_num = 16;
break;
case 8:
color_num = 256;
break;
case 24:
default:
color_num = 0;
break;
}
stride_byte = ALIGN(width*bmpInfo.biBitCount/8, 4);
padding = stride_byte - width*bmpInfo.biBitCount/8;
paltette_len = color_num * sizeof(RGBQUAD);
rgb_size1 = bmpHeader.bfSize - sizeof(BITMAPFILEHEADER) - sizeof(BITMAPINFOHEADER) - paltette_len;
rgb_size2 = stride_byte*height;
// 打印结构体中每个成员
printf("file name: %s\n", bmp_file);
printf("file type: %c%c %x\n", (bmpHeader.bfType)>>8, (bmpHeader.bfType)&0xff, bmpHeader.bfType);
printf("file size: %d(B) = %0.2f(KB) = %0.2f(MB)\n", bmpHeader.bfSize, (float)bmpHeader.bfSize/1024.00, (float)bmpHeader.bfSize/1024.00/1024.00);
printf("offset of image data: %d\n", bmpHeader.bfOffBits);
//
printf("biSize: %d\n", bmpInfo.biSize);
printf("width: %d\n", bmpInfo.biWidth);
printf("height: %d\n", bmpInfo.biHeight);
printf("Plane: %d\n", bmpInfo.biPlanes);
printf("BitCount: %d\n", bmpInfo.biBitCount);
printf("biCompression: %d\n", bmpInfo.biCompression);
printf("biSizeImage: %d\n", bmpInfo.biSizeImage);
printf("XPelsPerMeter: %d\n", bmpInfo.biXPelsPerMeter);
printf("YPelsPerMeter: %d\n", bmpInfo.biYPelsPerMeter);
printf("biClrUsed: %d\n", bmpInfo.biClrUsed);
printf("biClrImportant: %d\n", bmpInfo.biClrImportant);
printf("width*3: %d stride byte: %d padding: %d\n", width*3, stride_byte, padding);
printf("rgb buffer size: %d %d\n", rgb_size1,rgb_size2);
if (color_num != 0)
{
palette = (char *)malloc(paltette_len * sizeof(char));
fread(palette, paltette_len, 1, fp);
printf("palette:\n");
//dump(palette, paltette_len);
}
#endif
return 0;
}
int read_bmp_file(const char* bmp_file, unsigned char** rgb_buffer,
int* size, int* width, int* height)
{
int ret = 0;
FILE* fp;
BITMAPFILEHEADER bmpHeader;
BITMAPINFOHEADER bmpInfo;
int tmp_width = 0;
int tmp_height = 0;
int rgb_size = 0;
int stride_byte = 0; // 每行占用字节数(4字节对齐)
int width_byte = 0; // 每行真正有效字节数
int padding = 0; // 需要对齐的字节数
unsigned char* tmp_buf = 0;
int color_num = 0;
int palette_len = 0;
int i = 0;
fp = fopen(bmp_file, "rb");
if (fp == NULL)
{
printf("open file %s failed.\n", bmp_file);
return -1;
}
ret = fread(&bmpHeader, 1, sizeof(BITMAPFILEHEADER), fp);
if (ret != sizeof(BITMAPFILEHEADER))
{
printf("read BITMAPFILEHEADER failed.\n");
return -1;
}
ret = fread(&bmpInfo, 1, sizeof(BITMAPINFOHEADER), fp);
if (ret != sizeof(BITMAPINFOHEADER))
{
printf("read BITMAPINFOHEADER failed read: %d %d.\n", ret, sizeof(BITMAPINFOHEADER));
return -1;
}
if (bmpHeader.bfType != (('M' << 8) | 'B'))
{
printf("Sorry, not bmp picture.\n");
return -1;
}
tmp_width = bmpInfo.biWidth;
tmp_height = (int)fabs((double)bmpInfo.biHeight); // 预防高为负数的情况
// 真正RGB数据大小
rgb_size = tmp_width * tmp_height * bmpInfo.biBitCount/8;
*width = tmp_width;
*height = tmp_height;
*size = rgb_size;
/**
* 每行占用字节数,与下式结果相同
* stride_byte = (width * bmpInfo.biBitCount/8+3)/4*4;
*/
stride_byte = ALIGN(tmp_width*bmpInfo.biBitCount/8, 4);
width_byte = tmp_width * bmpInfo.biBitCount/8;
/**
* 补齐字节,与下式结果相同
* padding = (4 - width * 3 % 4) % 4;
* 实现未使用
*/
padding = stride_byte - width_byte;
// 判断调色板
switch(bmpInfo.biBitCount)
{
case 1:
color_num = 2;
break;
case 4:
color_num = 16;
break;
case 8:
color_num = 256;
break;
case 24:
default:
color_num = 0;
break;
}
// todo:读取调色板
palette_len = color_num * sizeof (RGBQUAD);
// 计算偏移量与实际偏移量比较,如不等,颜色数出错
if (bmpHeader.bfOffBits != sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + palette_len)
{
return -1;
}
printf("debug--:\nfile size: %d rgb size: %d %d stride byte: %d padding: %d BitCount: %d\n",
(int)bmpHeader.bfSize, rgb_size, stride_byte*tmp_height, stride_byte, padding, bmpInfo.biBitCount);
if (color_num != 0)
{
// 跳到图像数据处
fseek(fp, palette_len, SEEK_CUR);
}
// 申请合适的内存
*rgb_buffer = (unsigned char *)malloc(sizeof(char) * rgb_size);
if (*rgb_buffer == NULL)
{
return -1;
}
// 将读取的数据倒着存放到缓冲区(即BMP图像第一行数据放到缓冲区最后一行,等等),
// 这样图像才是正常的,否则图像是倒立的
tmp_buf = *rgb_buffer + rgb_size;
for (i = 0; i < tmp_height; i++)
{
tmp_buf -= width_byte;
ret = fread(tmp_buf, 1, width_byte, fp);
if (ret != width_byte)
{
free(*rgb_buffer);
return -1;
}
fseek(fp, padding, SEEK_CUR);
}
#if 0
// 顺序读文件,读到的图像是倒立的
unsigned char* tmp_buf = *rgb_buffer;
size_t readByte = 0;
for (int i = 0; i < tmp_height; i++)
{
readByte += fread(tmp_buf, 1, width_byte, fp);
fseek(fp, padding, SEEK_CUR);
tmp_buf += width_byte;
}
#endif
return 0;
}
int write_bmp_file(const char* bmp_file, unsigned char* rgb_buffer, int width, int height)
{
#define BPP 24 // 目前只考虑24色位图
BITMAPFILEHEADER bmpHeader;
BITMAPINFOHEADER bmpInfo;
FILE* fp = NULL;
int offset = 0;
int stride_byte = 0; // 每行占用字节数(4字节对齐)
int width_byte = 0; // 每行真正有效字节数
int rgb_size = 0;
int padding = 0;
unsigned char* tmp_buf = NULL;
int i = 0;
fp = fopen(bmp_file, "wb");
if (fp == NULL)
{
printf("open %s failed\n", bmp_file);
return -1;
}
offset = sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER); //54字节
// 4字节对齐 ((width * 24 + 31) / 32) * 4
// 如已经对齐,则rowStride与实际宽一致,如不对齐rowStride会比宽大一些
// stride_byte = ((width * 24 + 31) >> 5) << 2;
stride_byte = ALIGN(width*BPP/8, 4);
width_byte = width*BPP/8;
rgb_size = stride_byte * height; // 已考虑对齐
bmpHeader.bfType = ('M' << 8) | 'B';
bmpHeader.bfSize = offset + rgb_size; // BMP文件总大小
bmpHeader.bfReserved1 = 0;
bmpHeader.bfReserved2 = 0;
bmpHeader.bfOffBits = offset;
bmpInfo.biSize = sizeof(BITMAPINFOHEADER);
bmpInfo.biWidth = width;
bmpInfo.biHeight = height;
bmpInfo.biPlanes = 1;
bmpInfo.biBitCount = BPP;
bmpInfo.biCompression = 0;
bmpInfo.biSizeImage = rgb_size;
bmpInfo.biXPelsPerMeter = 0;
bmpInfo.biYPelsPerMeter = 0;
bmpInfo.biClrUsed = 0;
bmpInfo.biClrImportant = 0;
// 需要填充字节,BMP要求每一行数据必须4字节对齐,不足以0补。
//padding = (4 - width * 3 % 4) % 4;
// 实际未使用到
padding = stride_byte - width_byte;
printf("debug--:\nwidth: %d height: %d padding: %d rgb_size: %d, stride_byte: %d\n",
width, height, padding, rgb_size, stride_byte);
tmp_buf = (unsigned char *)malloc(sizeof(char) * rgb_size);
if (tmp_buf == NULL)
{
return -1;
}
memset(tmp_buf, '\0', sizeof(char) * rgb_size);
// 倒着拷贝到缓冲区
for (i = 0; i < height; i++)
{
// 每一行的实际数据为width * 3(R、G、B)
memcpy(tmp_buf + i * stride_byte, rgb_buffer + (height - i - 1) * width_byte, width_byte);
}
fwrite(&bmpHeader, 1, sizeof(BITMAPFILEHEADER), fp);
fwrite(&bmpInfo, 1, sizeof(BITMAPINFOHEADER), fp);
fwrite(tmp_buf, 1, rgb_size, fp);
free(tmp_buf);
return 0;
}
// rgb --> bgr or
// bgr --> rgb
void swap_rgb(unsigned char* rgb_buffer, int len)
{
int i = 0;
for (i = 0; i < len; i += 3)
{
unsigned char tmp;
tmp = rgb_buffer[i];
rgb_buffer[i] = rgb_buffer[i + 2];
rgb_buffer[i + 1] = rgb_buffer[i + 1];
rgb_buffer[i + 2] = tmp;
}
}
说明:
最后写的那个RGB交换函数,是因为当初未了解libjpeg解压jpeg可以选择RGB的格式,以为只有是RGB,但BMP却只认BGR,因此就自己写了个R、B交换函数。但libjpeg本身是支持BGR的格式的。
看了几年前自己写的代码,觉得能写Doxygen风格的注释很不容易,希望自己能坚持下去,而不用理会其它的影响。
李迟 2015.7.9