No.
2022-10-07
  • Jan
  • Feb
  • Mar
  • Apr
  • May
  • Jun
  • Jul
  • Aug
  • Sep
  • Oct
  • Nov
  • Dec
  • Sun
  • Mon
  • Tue
  • Wed
  • Thu
  • Fri
  • Sat
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

BMP 图片格式及代码实现

介绍

BMP(Bitmap-File)图形文件是 Windows 采用的图形文件格式,以 RGB 为基础存储图像数据。这种格式特点是包含图像信息较丰富,几乎不进行压缩,但因此占用磁盘空间也会过大。

文件结构

位图文件可看成由4个部分组成:图像文件头、图像信息头、调色板和图像数据。

图像文件头与图像信息头是每个文件都会有的,文件结构表可参考下表:

偏移量字段说明占用空间值说明
图像文件头0000h位图类型2 bytes‘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 Pointe
除 ‘BM’ 外其它可不考虑
0002h整个文件大小4 bytes
0006h保留4 bytes保留位,必须设置为0
000Ah位图数据距离文件开始的偏移量4 bytes
图像信息头000Eh位图信息头长度4 bytes28h - Windows 3.1x,95,NT,…
0Ch - OS/2 1.x
F0h - OS/2 2.x
0012h位图宽度4 bytes单位:像素
0016h位图高度4 bytes单位:像素
001Ah位图位面数2 bytes该值总是 1
001Ch每个像素位数2 bytes1 - 单色位图(实际上可有两种颜色,缺省情况下是黑色和白色。你可以自己定义这两种颜色)
4 - 16 色位图
8 - 256 色位图
16 - 16bit 高彩色位图
24 - 24bit真彩色位图
32 - 32bit 增强型真彩色位图
其中,1, 4, 8为索引模式,需要有调色板
001Eh压缩方式4 bytes0 - 不压缩 (使用BI_RGB表示)
1 - RLE 8-使用8位RLE压缩方式(用BI_RLE8表示)
2 - RLE 4-使用4位RLE压缩方式(用BI_RLE4表示)
3 – Bitfields-位域存放方式(用BI_BITFIELDS表示)
0022h位图图像数据的大小4 bytes该数必须是 4 的倍数
0026h水平分辨率4 bytes单位:像素/米
002Ah垂直分辨率4 bytes单位:像素/米
002Eh位图使用的颜色数4 bytes
0032h指定重要的颜色数4 bytes当该域的值等于颜色数时(或者等于0时),表示所有颜色都一样重要

图像文件头

图像文件头只有四个字段,文件类型,文件大小,保留字段和文件图像数据的偏移量。

代码定义:

#define uint8 unsigned char
#define uint16 unsigned short
#define uint32 unsigned int

//文件头
typedef struct {
    uint16 bfHeader;    //文件类型,'BM',其它可忽略
    uint32 bfSize;    //文件大小
    uint32 bfReserved;    //保留字段 ,必须设置为0
    uint32 bfOffset;    //从文件开始到位图数据的偏移量
}__attribute__((packed)) bmpFileHeader;

文件类型用于判定图像格式。
文件大小记录整个文件字节数。
文件图像数据的偏移量可以用来定位图像数据的位置。

图像信息头

图像信息头包含了图像的一些信息,包括图像宽度,图像长度,每像素使用的位数,压缩方法等信息。

代码实现:

//图像信息头
typedef struct {
    uint32 biSize;    //位图信息头长度
    uint32 biWidth;    //位图宽度(像素)
    uint32 biHeight;    //位图长度(像素)
    uint16 biPlanes;    //位图位面数,该值总是为1
    uint16 biBitsPerPixel;    //每个像素的位数
    uint32 biCompression;
    uint32 biImgSize;    //位图数据大小
    uint32 biHResolution;    //水平分辨率(像素/米)
    uint32 biVResolution;    //垂直分辨率(像素/米)
    uint32 biUsedColors;    //位图使用的颜色数,如果为0,则颜色数为2的biBitsPerPixel次方
    uint32 biImportantColors;    //指定重要的颜色数,如果为0,则代表所有颜色一样重要
}__attribute__((packed)) bmpImgHeader;

其中:

1). 图像信息头长度记录了信息头的长度,一般为 0x28h,但在一些早期系统或新的 BMP 文件格式中,其信息头结构变化较大,因此最好从文件中读取该信息头大小信息。

2). 位图宽度,指位图中每行包含多少像素。位图是以行为单位存储的,每行占用的字节数为(位图宽度 * 每个像素的位数 + 7) / 8 。需要注意的是,bmp 图片在存储时会进行行对齐,即每行按四字节对齐,当每行占用的字节数不够四的倍数时,会在每行后面补齐缺少的字节数,以 0 填充。例:一个宽度为 1001 像素的单色位图,每个像素位数为 1,则每行占用的字节数为 (1001 * 1 + 7) / 8 = 126 字节,此时每行不够四字节的倍数,则添加两个字节的 0,补到 128 字节。

获取每行字节数:

//width为每行的宽度,单位为像素,type为每像素占用的位数类型,参见 4)
static uint32 bmpGetLineAlignBytes(uint32 width, eBitsPerPixel type)
{
    uint32 lineBytes = ((width * type + 31) / 32) * 4;
    return lineBytes;
}

3). 位图高度,一般指位图有多少行。注意:当值为正时表示位图从下往上存储,左下角是起点。为负时,表示从上往下存储,左上角是起点。

4). 每像素占用的位数,当像素位数为 1, 4, 8 时,类型分别为 RGB1, RGB4, RGB8,值为调色板的索引(见认识RGB)。类型定义:

//每像素占用的位数类型
typedef enum {
    E_RGB1 = 1,
    E_RGB4 = 4,
    E_RGB8 = 8,
    E_RGB16 = 16,
    E_RGB24 = 24,
    E_RGB32 = 32
}eBitsPerPixel;

5). 压缩方法。压缩方法有无压缩,BI_RLE8, BI_RLE4 和 BI_BITFIELDS 压缩方法,这里只关注无压缩的方式。其它压缩方式可参见位图压缩

//压缩方式
#define BI_RGB     0    //BI_RGB:没有压缩;
#define BI_RLE8    1    //BI_RLE8:每个像素 8 比特的RLE压缩编码,压缩格式由 2 字节组成(重复像素计数和颜色索引);  
#define BI_RLE4    2    //BI_RLE4:每个像素 4 比特的RLE压缩编码,压缩格式由 2 字节组成
#define BI_BITFIELDS    3    //BI_BITFIELDS:每个像素的比特由指定的掩码决定。

调色板

图像信息头中每个像素位数为 1, 4, 8 时,图像中需要有调色板数据。调色板数据是一个 RGB 颜色数组,每个颜色包含红绿蓝分量及一个保留字节,数组包含的颜色个数为 21,24,28

调色板数据定义:

typedef struct {
    uint8 blue;    //蓝
    uint8 green;    //绿
    uint8 red;    //红
    uint8 reserved;    //保留
}colorPanelMeta;

需要调色版与不带调色板的图像文件头对比:

24 位位图不需要调色板,文件头中图像数据偏移量一般为 0x36h,而需要调色板的图像数据偏移量会比 0x36h 大一个存储调色板的数据:

bmp89-24b.png

256 色位图需要调色板,调色板占用空间为 28 * 4 bytes = 0x400h,所以图像数据偏移量为0x436h:

bmp89-256c.png

图像数据

图像数据以行来存储,每行按四字节对齐,位图高度就是图像有多少行。

读取 bmp 文件

结合之前定义的数据结构来读取 bmp 文件:

#define BMP_OK (0)
#define BMP_ERR    (1)
#define BMP_FILE_ERR    (2)

//图像头信息组合
typedef struct {
    bmpFileHeader fileHeader;
    bmpImgHeader imgHeader;
    colorPanelMeta colorPanel[256];
    uint32 metaCount;
}__attribute__((packed)) bmpHeader;

读取文件头:

//初始化调色板
static uint32 bmpInitColorPanel(colorPanelMeta *panelTable, uint32 *panelCount,  uint16 bitsPerPixel, uint32 imgHeaderSize, FILE *fp)
{
    uint32 offset = 0;

    if (bitsPerPixel >= E_RGB16){
        *panelCount = 0;
        return 0;
    }

    fseek(fp, sizeof(bmpFileHeader) + imgHeaderSize, SEEK_SET);

    *panelCount = (bitsPerPixel == E_RGB1 ? 2 : (bitsPerPixel == E_RGB4 ? 16 : 256));
    colorPanelMeta *p = panelTable;
    for (uint32 i = 0; i < *panelCount; i++) {
        offset += fread(p, 1, sizeof(colorPanelMeta), fp);
        p ++;    //= sizeof(colorPanelMeta);
    }

    return offset;
}

uint32 bmpReadHeader(bmpHeader *header, FILE *fp)
{
    uint32 offset = 0;

    //置文件指针到文件开头,读取图像文件头和图像信息头
    rewind(fp);
    offset = fread(header, 1, sizeof(bmpFileHeader) + sizeof(bmpImgHeader), fp);

    //读取调色板信息
    header->metaCount = 0;
    offset += bmpInitColorPanel(&header->colorPanel[0], &header->metaCount, header->imgHeader.biBitsPerPixel, header->imgHeader.biSize, fp);

    return offset;
}

单/16/256 色 Bitmap 文件转 24 位 Bitmap 文件

根据 BMP 文件结构可以实现单色,16 色,256 色 bmp 文件转为 24 位 bmp 文件方法:

//buffer为每行的数据,pixelIndex为当前行的第 pixelIndex 个像素
//函数用于获取第 pixelIndex 像素在调色板中的索引。
static uint32 bmpGetColorPanelIndex(uint8 *buffer, uint32 pixelIndex, eBitsPerPixel type)
{
    uint32 index = (pixelIndex + (8 / type - 1)) / (8 / type);
    uint8 input = buffer[index];

    uint8 unit = 0x01;
    switch (type) {
        case E_RGB1:
            unit = 0x01;
            break;
        case E_RGB4:
            unit = 0x0f;
            break;
        case E_RGB8:
            unit = 0xff;
            break;
        default:
            break;
    }
    uint32 shift = (8 - ((pixelIndex % (8 / type)) * type)) % 8;
    return ((input >> shift) & unit);
}

//将调色板像素转为 RGB24 像素
static inline uint32 colorChangeRGB24(colorPanelMeta *meta, uint8 *output)
{
    output[0] = meta->blue;
    output[1] = meta->green;
    output[2] = meta->red;

    return 3;
}

uint32 bmpFileTransformRGB24(char *srcFile, char *destFile)
{
    if (srcFile == NULL || destFile == NULL)
        return BMP_FILE_ERR;

    FILE *srcFp = fopen(srcFile, "rb");
    FILE *dstFp = fopen(destFile, "wb");
    if (srcFp == NULL || dstFp == NULL)
        return BMP_FILE_ERR;

    bmpHeader srcHeader;
    bmpReadHeader(&srcHeader, srcFp);
    if (srcHeader.imgHeader.biBitsPerPixel >= E_RGB16) {
        fclose(dstFp);
        fclose(srcFp);
        return BMP_OK;
    }

    uint32 fileSize = 0;
    uint32 imgSize = 0;
    bmpHeader dstHeader;
    dstHeader.fileHeader = srcHeader.fileHeader;
    dstHeader.imgHeader = srcHeader.imgHeader;

    dstHeader.fileHeader.bfOffset = sizeof(bmpFileHeader) + sizeof(bmpImgHeader);

    dstHeader.imgHeader.biSize = sizeof(bmpImgHeader);
    dstHeader.imgHeader.biBitsPerPixel = E_RGB24;
    dstHeader.imgHeader.biCompression = BI_RGB;

    fileSize = sizeof(bmpFileHeader) + sizeof(bmpImgHeader);
    //先写入文件头用于占位,后续再修改文件头相关信息
    fwrite(&dstHeader, 1, fileSize, dstFp);

    fseek(srcFp, srcHeader.fileHeader.bfOffset, SEEK_SET);
    uint32 srcLineBytes = bmpGetLineAlignBytes(srcHeader.imgHeader.biWidth, srcHeader.imgHeader.biBitsPerPixel);
    uint8 buffer[srcLineBytes];
    uint32 dstLineBytes = bmpGetLineAlignBytes(srcHeader.imgHeader.biWidth, E_RGB24);
    uint8 *writeBuf = malloc(dstLineBytes);
    if (writeBuf == NULL) {
        fclose(dstFp);
        fclose(srcFp);
        return BMP_OK;
    }

    //每次读取一行,写入新文件,注意bmp文件每行会按四字节对齐,不够四字节以0补齐
    while (!feof(srcFp)) {
        uint32 readBytes = fread(buffer, 1, srcLineBytes, srcFp);
        if (readBytes < srcLineBytes) {
            break;
        }

        uint32 bufOffSize = 0;
        for (uint32 i = 0; i < srcHeader.imgHeader.biWidth; i++) {
            uint32 panelIndex = bmpGetColorPanelIndex(buffer, i, srcHeader.imgHeader.biBitsPerPixel);
            if (panelIndex >= srcHeader.metaCount)
                printf("panelIndex error:%d:%d\n", i, panelIndex);
            bufOffSize += colorChangeRGB24(&srcHeader.colorPanel[panelIndex], writeBuf + bufOffSize);
        }

        while (bufOffSize < dstLineBytes){
            *(writeBuf + bufOffSize) = 0;
            bufOffSize++;
        }

        fwrite(writeBuf, 1, bufOffSize, dstFp);
        imgSize += bufOffSize;
    }

    free(writeBuf);

    fileSize += imgSize;
    dstHeader.fileHeader.bfSize = fileSize;
    //dstHeader.imgHeader.biHeight = -dstHeader.imgHeader.biHeight;
    dstHeader.imgHeader.biImgSize = imgSize;

    //将文件定位到文件头,修改文件头
    rewind(dstFp);
    fwrite(&dstHeader, 1, sizeof(bmpFileHeader) + sizeof(bmpImgHeader), dstFp);

    fclose(dstFp);
    fclose(srcFp);

    return BMP_OK;
}

参考
百度百科-BMP格式
音视频入门-03-RGB转成BMP图片
BMP文件格式及RlE压缩算法
Windows 应用开发-位图