美文网首页
CUDA01-00BMP图像处理

CUDA01-00BMP图像处理

作者: 杨强AT南京 | 来源:发表于2020-01-08 14:57 被阅读0次

      开始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
    
    

    相关文章

      网友评论

          本文标题:CUDA01-00BMP图像处理

          本文链接:https://www.haomeiwen.com/subject/cxcsactx.html