BMP详解及C语言实现BMP生成

由于学习需要使用到图形绘制方面的知识,而BMP(位图),可以说是最简单直观的图像文件了,以下我将介绍以下我在BMP学习上的一些心得以及如何利用C语言实现BMP的生成。(注意:文中涉及的BMP皆为24bitBMP,也即无需配置调色板)

本人能力有限,如果文中所讲有误,希望指正,谢谢。


BMP格式详解

这里以我利用程序生成的BMP为例,结合例子理解起来更加清晰。

如图所示,这是一个20*20像素的,左边为红色,右边为蓝色(不是纯蓝色)的BMP图片。

接下来我们利用ultraedit将这个图片文件以16进制的形式打开,得到以下结果。

24bit BMP主要由三部分组成,文件头、信息头、位图数据。

文件头

文件头总共14字节,具体如下

变量名       大小解释
bfType2 bytes表明文件类型,一般情况下为'BM'
bfSize4 bytes表明整个文件的大小,以字节为单位
bfReserved12 bytes保留,为0
bfReserved22 bytes

保留,为0

bfOffbits4 bytes

位图数据的偏移量,以字节为单位

信息头

信息头共40字节,具体如下

变量名大小解释
biSize4 bytes信息头的大小,以字节为单位
biWidth4 bytes图像宽度,以像素为单位
biHeight4 bytes

图像高度,以像素为单位

注意其可以为负,此时表示图像方向

后面会讲到BMP的正向与倒向

biPlanes2 bytes表示颜色平面数,一般取1
biBitCount2 bytes

表示一个像素对应的bit数,

取值为1、4、8、16、24或32,

本文以24为例

biCompression4 bytes

表示图像数据压缩的类型

0 表示BI_RGB 不压缩(本文涉及)

1 表示BI_RLE8 8bit游程编码,用于8bit位图

2 表示BI_RLE4 4bit游程编码,用于8bit位图

3 表示BI_BITFIELDS 比特域,用于16/32bit位图

4 表示BI_JPEG JPEG位图

5 表示BI_PNG PNG位图

biSizeImage4 bytes表示图像大小,当格式为 BI_RGB时可设置为0
biXPelsPerMeter4 bytes水平分辨率,本文不涉及
biYPelsPerMeter4 bytes垂直分辨率,本文不涉及
biClrUesd4 bytes

表示位图实际采用的彩色表中颜色索引数,0表示全使用

本文不涉及

biClrImportant4 bytes表示重要颜色索引的数目,0表示全重要

位图数据

顾名思义,就是实际表示图像的数据,在24bit位图中,1像素就对应位图数据中的3字节(即24bits),然而位图数据的字节数不一定等于 像素数*3 ,这其中涉及为了对齐而补位。后文在代码中详解。而关于视图数据的倒向正向,会结合例子讲更加直观。

结合例子

例子中,这14字节对应的就是文件头

42 4D 对应bfType,而42 4D也就是B M的ASCII码值,即bfType为BM

F0 04 00 00 对应 bfSize,即该文件的大小

当然不可能这么大,这只是20*20像素的位图,这里就涉及到大小端的知识。

大端与小端

以数字 0x12345678为例,

大端就是高位存低地址,低位存高地址,所以其存储形式为,(低地址)12 34 56 78(高地址)

低位就是高位存高地址,低位存低地址,所以其存储形式为,(低地址)78 56 34 12(高地址)

为什么不是 87 65 43 21,因为存储是以字节为单位。

78 56 34 12 即 01111000 01010110 00110100 00010010,红色就表示一字节。

粗略讲完大小端后,也能够看出BMP的存储是采取的小端形式。

所以实际上这个位图文件大小为1264字节。

续讲

后续的 00 00 00 00 对应的为bfReserved1, bfReserved2,都为0

40 00 00 00,即表示位图数据偏移量为64字节。

这对应的就是信息头

28 00 00 00 表示信息头长度为40字节

14 00 00 00 表示图片宽度为20像素

14 00 00 00 表示图片高度为20像素(倒向)

01 00 表示 biPlanes为1

18 00 表示biBitCount为24,即每一像素为24bit

00 00 00 00 表示biCompression为0, 对应BI_RGB

B0 04 00 00 表示biSizeImage为1200,即位图数据大小为1200字节,也就是bfSize(文件大小) - bfOffBits(位图数据偏移量)

后续的几个参数全部设定为0,本文也不考虑那几个参数。

补充

也许会注意到,信息头加文件头共54字节,但位图数据偏移量为64字节,多出来的部分(也即信息头图片中最后红线覆盖的区域)是什么呢,答案就是什么都不是,这部分的值无论如何设置都不会影响文件本身。所以bfOffBIts的大小只要大于等于54字节(信息头+文件头)即可,当然,如果不是等于的话,要注意位图数据开始的位置。

综合例子-位图数据

这里换一个例子,为了讲正向倒向时有所区分

倒立(高度为正)

这种情况下,位图数据首先看到的为最后一行的数据。

最后一行最后一个像素后,再是倒数第二行第一个像素。。

正向(高度为负,更加常见,例子也是如此)

可以看到例子中的高度为负

然后位图数据的最开始为00 00 FF,注意这里也是小端,所以其实际对应的为BGR,即为红色,与实际相符。

对齐问题

BMP图片是每一行像素的字节数必须为4的倍数,如若不是,则需要利用0补位。

即9像素宽的位图,其每行有9*3=27字节,则需要补充1字节。

基于C语言的BMP生成

头文件定义部分

#ifndef BMP_H

#define BMP_H

#include<stdio.h>
#include<math.h>
#include<stdlib.h>
#include<string.h>

#pragma pack(1)

#define PER_BYTE 3
#define COLOR_DATA(x, y, z) color_data[x*width*3 + y*3 + z]


typedef struct{
	unsigned char		bfType[2];
	unsigned int		bfSize;
	unsigned short int	bfReserved1;
	unsigned short int	bfReserved2;
	unsigned int		bfOffBits;
}BMP_FILE_HEADER;

typedef struct{
	unsigned int		biSize;
	int					biWidth;
	int					biHeight;
	unsigned short int	biPlane;
	unsigned short int	biBitCount;
	unsigned int		biCompression;
	unsigned int		biSizeImage;
	int					biXPelsPerMeter;
	int					biYPelsPerMeter;
	unsigned int		biClrUsed;
	unsigned int		biClrImportant;
}BMP_INFO_HEADER;


 int bmp_generate(const char* file, unsigned char* color_data, int width, int height);


#endif

其中一定要注意 #pragme pack(1),我最开始就在这踩了很久的坑,这涉及到结构体对齐的问题。具体可以去看文章最后给出的链接,本文只做简述。

结构体对齐

结构体第一个成员存储地址偏移量为0

结构体后续成员存储地址为min{自身大小, 指定对齐值}的最小整数倍处

在64位系统中,默认指定对齐值为8字节,以此为例:

有结构体

typedef struct{
    char    a;
    short   b;
    int     c;
}s1;
c7
c6
c5
c4
b3
b2
1
a

结构体s1的存储格式如上,首先是a,其存储地址为0开始,大小为1字节;然后是b,其大小为2,存储地址为min{2,8}的整数倍地址处,即2*n,2*0=0,但是0已经有a了,2*1=2,可存储,所以b的地址为2、3;最后是c,其存储地址为min{4,8}的整数倍,4*1=4,可存储,所以c的地址为4、5、6、7,综上,s1的大小为8字节。

而#pragma pack(n)的作用就是令指定对齐值为n(n为2的整幂次方),所以#pragma pack(1),就可以让结构体完全紧凑地存储,如果不这样,会导致我们定义出的文件头、信息头结构体实际大小与文件头、信息头本身大小14、40不同。

#define COLOR_DATA(x, y, z) color_data[x*width*3 + y*3 + z]可以方便后续对像素颜色的操作,本代码采取的是三位数组存color_data,所以这个宏定义没有使用到。

BMP文件生成函数

// 生成BMP
// file为输出文件名
// color_data为位图数据(但是以倒向、单像素以RGB形式而非BGR,便于理解,不完全等同于位图数据)
int bmp_generate(const char* file, unsigned char* color_data, int width, int height)
{
	FILE* fp;
	int				image_size;		// 图像大小,对应biSizeImage
	unsigned char*	bmp_data;		// 对应位图数据
	int				i;
	int				j;
	int				k;
	int				pixel_pointer;	// 用于去指向单个像素首字节
	int				byte_count;		// 单像素中byte计数
	int				supplement;		// 每一行补充字节数
	int				offset;			// 位图数据偏移量

	fp = fopen(file, "wb+");
	if (fp == NULL) {
		printf("open file error\n");
		return -1;
	}

	if (width <= 0) {
		printf("width error\n");
		return -2;
	}

	offset = 64;

	// 计算每一行需要补充多少字节
	supplement = 0;
	while ((width * PER_BYTE + supplement) % 4 != 0) {
		supplement++;
	}

	// 计算位图数据大小
	image_size = width * fabs(height) * PER_BYTE + supplement * fabs(height);
	bmp_data = (unsigned char*)malloc(sizeof(char)*image_size);

	// 定义文件头、信息头
	BMP_FILE_HEADER file_header;
	BMP_INFO_HEADER info_header;

	file_header.bfType[0] = 'B';
	file_header.bfType[1] = 'M';
	file_header.bfSize = image_size + offset;
	file_header.bfReserved1 = 0;
	file_header.bfReserved2 = 0;
	file_header.bfOffBits = 64;

	info_header.biSize = 40;
	info_header.biWidth = width;
	info_header.biHeight = height;
	info_header.biPlane = 1;
	info_header.biBitCount = 24;
	info_header.biCompression = 0;
	info_header.biSizeImage = image_size;
	info_header.biXPelsPerMeter = 0;
	info_header.biYPelsPerMeter = 0;
	info_header.biClrUsed = 0;
	info_header.biClrImportant = 0;

	// 将头部信息写入
	fwrite(&file_header, sizeof(BMP_FILE_HEADER), 1, fp);
	fwrite(&info_header, sizeof(BMP_INFO_HEADER), 1, fp);

	// 将color_data处理为真实的位图数据
	// 正向
	if (height < 0) {
		for (i = 0, j = 0 , pixel_pointer = 0, byte_count = PER_BYTE - 1; i < image_size;) {
			bmp_data[i] = color_data[pixel_pointer + byte_count];
			i++;
			byte_count--;
			// 处理完一个像素,重置bit_count,增加pixel_count
			if (byte_count < 0) {
				byte_count = PER_BYTE - 1;
				pixel_pointer += PER_BYTE;
			}
			// 当一行数据写完后,后续是填充数据,直接跳过不写
			if (++j == width * PER_BYTE) {
				j = 0;
				i += supplement;
			}
		}
	}
	// 倒向
	else {
		for (i = 0, j = 0, k = height - 1, pixel_pointer = k * width * 3, byte_count = PER_BYTE - 1; i < image_size;) {
			bmp_data[i++] = color_data[pixel_pointer + byte_count];
			if (--byte_count < 0) {
				byte_count = PER_BYTE - 1;
				pixel_pointer += PER_BYTE;
			}
			if (++j == width * PER_BYTE) {
				j = 0;
				i += supplement;
				k--;
				pixel_pointer = k * width * 3;
			}
		}
	}
	// 定位文件读写处
	fseek(fp, offset, SEEK_SET);
	// 写入位图数据
	fwrite(bmp_data, 1, image_size, fp);

	fclose(fp);

	return 0;
}

主函数测试

const int width = 17;
const int height = 16;
unsigned char color[height][width][3];

int main(int argc, char* argv[])
{
	const char* file;
	int		x, y;

	file = "11.bmp";


	for (x = 0; x < width / 2; x++) {
		for (y = 0; y < height; y++) {
			color[y][x][0] = 0xff;
			color[y][x][1] = 0;
			color[y][x][2] = 0;
		}
	}
	for (x = width / 2; x < width; x++) {
		for (y = 0; y < height; y++) {
			color[y][x][0] = 0;
			color[y][x][1] = 0xff;
			color[y][x][2] = 0xff;
		}
	}
	bmp_generate(file, (unsigned char*)color, width, -height);
	return 0;
}

最后结果如下

结构体对齐参考:C语言--结构体内存对齐规则_结构体对齐原则-CSDN博客

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值