CUDA01-00BMP图像处理

  开始GPU性能运算编程,使用图像处理是比较理想的方式,因为图像运算的性能是个问题;为了避免其他图像库带来的性能优化干扰,我们采用原始的图像内存来验证运算性能,本主题说明BMP这种位图的操作。
    1. 读图像;
    2. 写图像;
    3. 像素处理;


  • BMP的24与32位图像是无压缩的,读写非常方便,如果不使用专门的图像读取模块,则我们都使用BMP格式的图像;

  • BMP是微软提出的用于Window系统的一种图像格式。

    • 目前很多平台也支持;不过主流的图像还是:
      1. JPG:压缩
      2. PNG:压缩,带透明
      3. 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图像格式的特殊说明

  1. 数据存储区的对齐

    • 图像的数据存储因为系统的位数关系,所以位图的每一行的数据大小必须是4的倍数(与C的结构体对齐一个道理)
      • 比如:图像的宽度是5,每个像素是RGB三个字节,这样一行的字节数15,但是BMP图像会补0,对齐位16字节。
  2. 获取图像高度的时候,返回值可能是负数;

    • 负数主要说明图像的正向存,还是倒向存的。(显示出来就是图像可能是倒过来的)
  3. 调色板说明

    • 调色板存放的是颜色,由结构体表示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>

文件头

  • 文件头的信息对图像处理来说基本没有用。
  1. 打开文件与关闭文件
FILE* f = fopen("gpu.bmp", "rb");
  • 关闭文件在后面关闭:

    • fclose(f);
  • 打开失败判别

    • 可以判定返回的文件句柄是否为0或者NULL
if(f == NULL){
    printf("打开文件失败!\n");
    // exit(1);    // 在交互式编程,这个命令就不要玩了,容易die
}
else{
    printf("打开文件成功!\n");
}
打开文件成功!
  1. 读取魔法字
    • 两字节:一定是BM,其他的都基本上不用了。
char f_magic[3]={0};
fread(f_magic, 1, 2, f); 
printf("读取魔法字:%s\n", f_magic);
读取魔法字:BM

(int) 21
  1. 读取文件大小
// 读取文件大小
fseek(f, 2, SEEK_SET);   // 如果顺序读取,则读取位置的定位完全没有必要。
size_t size;
fread(&size, 1, 4, f); 
printf("文件大小:%zu\n", size);
文件大小:8294454

(int) 23

信息头

  1. 图像大小
    • 注意:高度是负数
 // 读取图像宽度
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
  1. 像素位数
// 像素的位数
fseek(f, 28, SEEK_SET);
short int bits;
fread(&bits, 1, 2, f); 
printf("像素位数%d\n", bits);    // 16,24,32位图没有调色板
像素位数32

(int) 15
  1. 压缩格式
// 判定图像数据是否压缩
fseek(f, 30, SEEK_SET);
int zip;
fread(&zip, 1, 4, f); 
printf("数据压缩方式%d\n", zip);    // 0表示不压缩
数据压缩方式0

(int) 20
  1. 图像大小
// 图像大小
fseek(f, 34, SEEK_SET);
int imgsize;
fread(&imgsize, 1, 4, f); 
printf("图像字节数%d\n", imgsize);    // 当用BI_RGB格式时,总设置为0
图像字节数0

(int) 17

调色板

  1. 获取数据区的位置来判定调色板是否存在
// 数据区开始位置
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
  • 说明:
    • 数据区开始位置刚好是(文件头 + 信息头)的大小,说明没有调色板表。

图像数据

  1. 读取数据区第一个像素
// 定位到数据区开始位置
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四个字节。
  1. 读取第一行数据
// 计算一行的字节数(记得一定是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
  1. 读取所有行的数据
// 计算图像行数
// 行数就是高度,可能存在负数的情况
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
  1. 释放图像的内存数据
for(int i = 0; i < height; i++){
    free(img_data[i]);   // 释放每一行
}
free(img_data);    // 释放存放行指针的内存
(void) @0x70000a77bae8

完整的图像读写与处理实现

  • 下面列子是基于32位图像,没有动态处理16、24位的图像;根据需要可以扩展。

  • 上面代码我们就可以读取到完整的图像数据。下面我们处理图像数据,并写入到文件;完成BMP图像的读与写。

  • 实现步骤:

    1. 读取头;
    2. 读取图像数据;
    3. 处理图像数据;
    4. 写头;
    5. 写图像数据;
  • 我们在读取文件头的时候,采用结构化数据处理;

    • 注意:编译器存在对齐机制;
#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
  1. 读取头
    • 包含文件头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
  1. 读取图像数据
    • 因为我们使用的图像是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
  1. 处理图像数据
    • 前面多图像的数据做了结构化处理,按照一个像素为单位存放。处理起来比较方便;
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;
    }
}
  1. 写头
    • 存储文件的时候,头保持不变;
    • 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
  1. 写图像数据
    • 因为数据不是连续内存,所以需要一行一行写入;
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
  • 处理前后的图像的对比:
    • 把其中高度改成负数,图像就是正向的了。
图像处理前后的效果(RGB颜色通道交换)

附录

完整代码

/*
 * 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容