开始GPU性能运算编程,使用图像处理是比较理想的方式,因为图像运算的性能是个问题;为了避免其他图像库带来的性能优化干扰,我们采用原始的图像内存来验证运算性能,本主题说明BMP这种位图的操作。
1. 读图像;
2. 写图像;
3. 像素处理;
-
BMP的24与32位图像是无压缩的,读写非常方便,如果不使用专门的图像读取模块,则我们都使用BMP格式的图像;
-
BMP是微软提出的用于Window系统的一种图像格式。
- 目前很多平台也支持;不过主流的图像还是:
- JPG:压缩
- PNG:压缩,带透明
- GIF:压缩,带透明,图像帧
- 目前很多平台也支持;不过主流的图像还是:
-
在分析算法的性能的时候,为了避免第三方图像处理库带来的性能影响,我们都使用纯粹的文件读写与自己分配的图像内存来处理。
- BMP因为其简单性,是我们的首选图像格式。
BMP图像的说明
BMP的主要结构
数据段名称 | 大小(byte) | 开始地址 | 结束地址 |
---|---|---|---|
位图文件头(bitmap-file header) | 14 | 0000h | 000Dh |
位图信息头(bitmap-information header) | 40 | 000Eh | 0035h |
调色板(color table) | 由biBitCount决定 | 0036h | 未知 |
图片点阵数据(bitmap data) | 由图片大小和颜色定 | 未知 | 未知 |
- 文件头区域,信息头区域,调色板区域,图片数据区域是按顺序连续存储的。
- 每个区域还有细分的字段,每个字段描述不同的信息。
- 在24与32位的BMP图,已经不采用调色板,所以调色板相关的区域与字段可以忽略。
文件头区域结构
- 有用的描述信息(字段):
- 文件大小
- 数据区偏移位置(开始位置)
变量名 | 地址偏移 | 大小 | 作用说明 |
---|---|---|---|
bfType | 0000h | 2Bytes | 文件标识符,必须为"BM",即0x424D 才是Windows位图文件 ‘BM’:Windows 3.1x, 95, NT ‘BA’:OS/2 Bitmap Array ‘CI’:OS/2 Color Icon ‘CP’:OS/2 Color Pointer ‘IC’:OS/2 Icon ‘PT’:OS/2 Pointer<p>因为OS/2系统并没有被普及开,所以在编程时,你只需判断第一个标识“BM”就行</p> |
bfSize | 0002h | 4Bytes | 整个BMP文件的大小(以位B为单位) |
bfReserved1 | 0006h | 2Bytes | 保留,必须设置为0 |
bfReserved2 | 0008h | 2Bytes | 保留,必须设置为0 |
bfOffBits | 000Ah | 4Bytes | 说明从文件头0000h开始到图像像素数据的字节偏移量(以字节Bytes为单位),以为位图的调色板长度根据位图格式不同而变化,可以用这个偏移量快速从文件中读取图像数据 |
信息头区域结构
- 有用的描述信息(字段):
- 图像的大小(高与宽):高度有可能为负数,表示图像是正向还是倒向存储。
- 像素的位数
- 图像的压缩方式:目前基本上都是无压缩。
- 图像的像素格式:BI_RGB
变量名 | 地址偏移 | 大小 | 作用说明 |
---|---|---|---|
biSize | 000Eh | 4Bytes | BMP信息头即BMP_INFOHEADER结构体所需要的字节数(以字节为单位) |
biWidth | 0012h | 4Bytes | 说明图像的宽度(以像素为单位) |
biHeight | 0016h | 4Bytes | 说明图像的高度(以像素为单位)。这个值还有一个用处,指明图像是正向的位图还是倒向的位图,该值是正数说明图像是倒向的即图像存储是由下到上;该值是负数说明图像是倒向的即图像存储是由上到下。大多数BMP位图是倒向的位图,所以此值是正值。 |
biPlanes | 001Ah | 2Bytes | 为目标设备说明位面数,其值总设置为1 |
biBitCount | 001Ch | 2Bytes | 说明一个像素点占几位(以比特位/像素位单位),其值可为1,4,8,16,24或32 |
biCompression | 001Eh | 4Bytes | 说明图像数据的压缩类型,取值范围为: 0) BI_RGB 不压缩(最常用) 1) BI_RLE8 8比特游程编码(BLE),只用于8位位图 2) BI_RLE4 4比特游程编码(BLE),只用于4位位图 3) BI_BITFIELDS比特域(BLE),只用于16/32位位图 |
biSizeImage | 0022h | 4Bytes | 说明图像的大小,以字节为单位。当用BI_RGB格式时,总设置为0 |
biXPelsPerMeter | 0026h | 4Bytes | 说明水平分辨率,用像素/米表示,有符号整数 |
biYPelsPerMeter | 002Ah | 4Bytes | 说明垂直分辨率,用像素/米表示,有符号整数 |
biClrUsed | 002Eh | 4Bytes | 说明位图实际使用的调色板索引数,0:使用所有的调色板索引 |
biClrImportant | 0032h | 4Bytes | 说明对图像显示有重要影响的颜色索引的数目,如果是0,表示都重要。 |
BMP图像格式的特殊说明
-
数据存储区的对齐
- 图像的数据存储因为系统的位数关系,所以位图的每一行的数据大小必须是4的倍数(与C的结构体对齐一个道理)
- 比如:图像的宽度是5,每个像素是RGB三个字节,这样一行的字节数15,但是BMP图像会补0,对齐位16字节。
- 图像的数据存储因为系统的位数关系,所以位图的每一行的数据大小必须是4的倍数(与C的结构体对齐一个道理)
-
获取图像高度的时候,返回值可能是负数;
- 负数主要说明图像的正向存,还是倒向存的。(显示出来就是图像可能是倒过来的)
-
调色板说明
- 调色板存放的是颜色,由结构体表示RGB+保留位(4字节)
- 调色板的个数由biBitCount像素位数决定:
- 1:2色,一共2*4字节
- 4:16色,一共16*4字节
- 8:256色,一共256*4字节
- 16,24,32:没有调色板
- 使用调色板后,图像数据区存放的就是调色板索引。
- 现在基本上没有调色板
BMP的格式代码实例
- 头文件
#include <stdlib.h>
#include <stdio.h>
文件头
- 文件头的信息对图像处理来说基本没有用。
- 打开文件与关闭文件
FILE* f = fopen("gpu.bmp", "rb");
-
关闭文件在后面关闭:
fclose(f);
-
打开失败判别
- 可以判定返回的文件句柄是否为0或者NULL
if(f == NULL){
printf("打开文件失败!\n");
// exit(1); // 在交互式编程,这个命令就不要玩了,容易die
}
else{
printf("打开文件成功!\n");
}
打开文件成功!
- 读取魔法字
- 两字节:一定是BM,其他的都基本上不用了。
char f_magic[3]={0};
fread(f_magic, 1, 2, f);
printf("读取魔法字:%s\n", f_magic);
读取魔法字:BM
(int) 21
- 读取文件大小
// 读取文件大小
fseek(f, 2, SEEK_SET); // 如果顺序读取,则读取位置的定位完全没有必要。
size_t size;
fread(&size, 1, 4, f);
printf("文件大小:%zu\n", size);
文件大小:8294454
(int) 23
信息头
- 图像大小
- 注意:高度是负数
// 读取图像宽度
fseek(f, 18, SEEK_SET);
size_t width;
fread(&width, 1, 4, f);
printf("图像宽度:%zu\n", width);
// 读取图像高度
fseek(f, 22, SEEK_SET);
int height;
fread(&height, 1, 4, f);
printf("图像高度:%d\n", height);
图像宽度:1920
图像高度:-1080
(int) 21
- 像素位数
// 像素的位数
fseek(f, 28, SEEK_SET);
short int bits;
fread(&bits, 1, 2, f);
printf("像素位数%d\n", bits); // 16,24,32位图没有调色板
像素位数32
(int) 15
- 压缩格式
// 判定图像数据是否压缩
fseek(f, 30, SEEK_SET);
int zip;
fread(&zip, 1, 4, f);
printf("数据压缩方式%d\n", zip); // 0表示不压缩
数据压缩方式0
(int) 20
- 图像大小
// 图像大小
fseek(f, 34, SEEK_SET);
int imgsize;
fread(&imgsize, 1, 4, f);
printf("图像字节数%d\n", imgsize); // 当用BI_RGB格式时,总设置为0
图像字节数0
(int) 17
调色板
- 获取数据区的位置来判定调色板是否存在
// 数据区开始位置
fseek(f, 10, SEEK_SET);
int data_off;
fread(&data_off, 1, 4, f);
printf("数据区开始位置%d\n", data_off); // 当用BI_RGB格式时,总设置为0
数据区开始位置54
(int) 24
- 说明:
- 数据区开始位置刚好是(文件头 + 信息头)的大小,说明没有调色板表。
图像数据
- 读取数据区第一个像素
// 定位到数据区开始位置
fseek(f, data_off, SEEK_SET);
unsigned char a_pixel[4]; // 位数/8 = 字节数
fread(a_pixel, 1, bits/8, f); // 读取bits/8个字节
printf("第一个像素:(");
printf("%hhu", a_pixel[0]); // 第一个颜色通道
for (int i = 1; i < bits/8; i++){
printf(",%hhu", a_pixel[i]); // 循环打印其他颜色通道
}
printf(")\n");
第一个像素:(106,31,0,255)
(int) 2
- 注意:格式应该是RGBA四个字节。
- 读取第一行数据
// 计算一行的字节数(记得一定是4的倍数)
int h_bytes = bits / 8 * width;
// 按照4的倍数对齐
// h_bytes = ((h_bytes + 3)/ 4) * 4;
// 还有一种对齐的技巧:用位运算理解
h_bytes = (h_bytes + 3) & (~3); // 速度优于上面方式的速度
printf("图像一行的字节数:%d\n", h_bytes);
// 动态分配一行像素的内存
unsigned char *line_1st = (unsigned char *)malloc(h_bytes); // 图像一行的数据存储空间
// 字节读取一行长的数据到内存
fseek(f, data_off, SEEK_SET);
size_t re = fread(line_1st, h_bytes, 1, f); // 返回的数读取对象的个数,每个对象大小是h_bytes
printf("成功读取数据字节数:%zd\n", re * h_bytes);
图像一行的字节数:7680
成功读取数据字节数:7680
(int) 35
- 读取所有行的数据
// 计算图像行数
// 行数就是高度,可能存在负数的情况
height = height >=0 ? height:-height;
// 定义存放图像数据的内存(存放没有行的指针:可以考虑连续分配一张图像的内存)
unsigned char **img_data = (unsigned char **)malloc(height * sizeof(unsigned char *));
long data_len = 0; // 存放实际读取数据大小
fseek(f, data_off, SEEK_SET);
for(int i = 0; i < height; i++){
img_data[i] = (unsigned char *)malloc(h_bytes); // 分配一行的内存
size_t re = fread(img_data[i], 1, h_bytes, f);
// printf("%d成功读取数据字节数:%zd\n", (i+1), re);
if(re <= 0) {
printf("读取结束!\n");
break;
}
data_len += re;
}
fclose(f);
printf("实际读取数据长度:%ld\n", data_len);
printf("实际得到的文件长度:%ld\n", data_len + 54); //54=文件头
printf("从文件头读取的文件大小:%zu\n", size);
实际读取数据长度:8294400
实际得到的文件长度:8294454
从文件头读取的文件大小:8294454
(int) 44
- 释放图像的内存数据
for(int i = 0; i < height; i++){
free(img_data[i]); // 释放每一行
}
free(img_data); // 释放存放行指针的内存
(void) @0x70000a77bae8
完整的图像读写与处理实现
-
下面列子是基于32位图像,没有动态处理16、24位的图像;根据需要可以扩展。
-
上面代码我们就可以读取到完整的图像数据。下面我们处理图像数据,并写入到文件;完成BMP图像的读与写。
-
实现步骤:
- 读取头;
- 读取图像数据;
- 处理图像数据;
- 写头;
- 写图像数据;
-
我们在读取文件头的时候,采用结构化数据处理;
- 注意:编译器存在对齐机制;
#pragma pack(1)
struct img_header{
// 文件头
char magic[2]; // 魔法字
unsigned int file_size; // 文件大小
unsigned char reserve1[4]; // 跳4字节
unsigned int data_off; // 数据区开始位置
// 信息头
unsigned char reserve2[4]; // 跳4字节
int width; // 图像宽度
int height; // 图像高度
unsigned char reserve3[2]; // 跳2字节
unsigned short int bit_count; // 图像位数1,4,8,16,24,32
unsigned char reserve4[24]; // 跳24字节
};
struct img_pixel{
unsigned char red;
unsigned char green;
unsigned char blue;
unsigned char alpha;
};
printf("定义的头结构体大小:%lu\n", sizeof(img_header));
printf("定义的像素结构体大小:%lu\n", sizeof(img_pixel));
定义的头结构体大小:54
定义的像素结构体大小:4
(int) 35
- 读取头
- 包含文件头14
- 包含信息头40
- 包含调色板头0(16,24,32位图像无调色板)
struct img_header header = {0};
FILE* file = fopen("gpu.bmp", "rb");
size_t n_bytes = fread(&header, 1, 54, file);
printf("读取的数据字节数:%zd\n", n_bytes);
printf("图像大小:(%d,%d)\n",header.width, header.height);
printf("像素位数:%hd\n",header.bit_count);
读取的数据字节数:54
图像大小:(1920,-1080)
像素位数:32
(int) 18
- 读取图像数据
- 因为我们使用的图像是32位的,这样计算的行的字节数本身就是4的倍数,所以行的内存字节不用对齐,直接使用即可。
- 存放数据的内存还是采用动态内存。采用数组对程序的启动与编译的执行文件是一个挑战。
header.height = header.height >= 0? header.height : -header.height;
// 存放每行的数据指针
struct img_pixel **imgs = (struct img_pixel **)malloc(header.height * sizeof(struct img_pixel *));
// 分配每行的空间,并读取数据
for (int h = 0; h < header.height; h++){
// 分配空间
imgs[h] = (struct img_pixel *)malloc(4 * header.width); // 宽度有多个像素构成,每个像素4字节;
size_t n_obj = fread(imgs[h], 1, 4 * header.width, file);
if(n_obj <= 0){
printf("读取错误,或者读取结束");
break;
}
}
fclose(file); // 关闭文件
(int) 0
- 处理图像数据
- 前面多图像的数据做了结构化处理,按照一个像素为单位存放。处理起来比较方便;
for(int h = 0; h < header.height; h++){
for(int w = 0; w < header.width; w++){
// 交换RGB三通道的位置,得到新图像;
unsigned char red = imgs[h][w].red;
unsigned char green = imgs[h][w].green;
unsigned char blue = imgs[h][w].blue;
imgs[h][w].red = green;
imgs[h][w].green = blue;
imgs[h][w].blue = red;
}
}
- 写头
- 存储文件的时候,头保持不变;
size_t fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
FILE* o_file = fopen("gpu_out.bmp", "wb");
size_t o_size = fwrite(&header, 1, 54, o_file);
printf("数据写入大小:%zd\n", o_size)
数据写入大小:54
(int) 24
- 写图像数据
- 因为数据不是连续内存,所以需要一行一行写入;
for(int h = 0; h < header.height; h++){
o_size = fwrite(imgs[h], sizeof(struct img_pixel), header.width, o_file);
// printf("数据写入大小:%zd\n", o_size);
}
// 关闭文件
fclose(o_file);
// 此处的内存记得释放下
for(int i = 0; i < header.height; i++){
free(imgs[i]); // 释放每一行
}
free(imgs); // 释放存放行指针的内存
(int) 0
- 处理前后的图像的对比:
- 把其中高度改成负数,图像就是正向的了。
附录
完整代码
/*
* BMP图像的读写,并实现图像的像素处理(交换颜色通道)
*/
#include <stdlib.h>
#include <stdio.h>
#pragma pack(1)
struct img_header{
// 文件头
char magic[2]; // 魔法字
unsigned int file_size; // 文件大小
unsigned char reserve1[4]; // 跳4字节
unsigned int data_off; // 数据区开始位置
// 信息头
unsigned char reserve2[4]; // 跳4字节
int width; // 图像宽度
int height; // 图像高度
unsigned char reserve3[2]; // 跳2字节
unsigned short int bit_count; // 图像位数1,4,8,16,24,32
unsigned char reserve4[24]; // 跳24字节
};
struct img_pixel{
unsigned char red;
unsigned char green;
unsigned char blue;
unsigned char alpha;
};
int main(int argc, const char *argv[]){
// 1. 读取文件头
struct img_header header = {0};
FILE* file = fopen("gpu.bmp", "rb");
size_t n_bytes = fread(&header, 1, 54, file);
printf("读取的数据字节数:%zd\n", n_bytes);
printf("图像大小:(%d,%d)\n",header.width, header.height);
printf("像素位数:%hd\n",header.bit_count);
// 2. 读取图像数据
header.height = header.height >= 0? header.height : -header.height;
// 存放每行的数据指针
struct img_pixel **imgs = (struct img_pixel **)malloc(header.height * sizeof(struct img_pixel *));
// 分配每行的空间,并读取数据
for (int h = 0; h < header.height; h++){
// 分配空间
imgs[h] = (struct img_pixel *)malloc(4 * header.width); // 宽度有多个像素构成,每个像素4字节;
size_t n_obj = fread(imgs[h], 1, 4 * header.width, file);
if(n_obj <= 0){
printf("读取错误,或者读取结束");
break;
}
}
fclose(file); // 关闭文件
// 3. 处理图像
for(int h = 0; h < header.height; h++){
for(int w = 0; w < header.width; w++){
// 交换RGB三通道的位置,得到新图像;
unsigned char red = imgs[h][w].red;
unsigned char green = imgs[h][w].green;
unsigned char blue = imgs[h][w].blue;
imgs[h][w].red = green;
imgs[h][w].green = blue;
imgs[h][w].blue = red;
}
}
// 4. 存储处理后的图像
FILE* o_file = fopen("gpu_out.bmp", "wb");
// 写头
size_t o_size = fwrite(&header, 1, 54, o_file);
printf("数据写入大小:%zd\n", o_size);
// 写图像数据
for(int h = 0; h < header.height; h++){
o_size = fwrite(imgs[h], sizeof(struct img_pixel), header.width, o_file);
// printf("数据写入大小:%zd\n", o_size);
}
// 关闭文件
fclose(o_file);
for(int i = 0; i < header.height; i++){
free(imgs[i]); // 释放每一行
}
free(imgs); // 释放存放行指针的内存
}
Makefile
all:bmp_rw.c
gcc bmp_rw.c -omain
网友评论