剖析BMP文件结构及读写方式

发布时间 2023-05-06 11:15:17作者: 小金乌会发光-Z&M

一、前言

位图(Bitmap)格式其实并不能说是一种很常见的格式(从我们日常的使用频率上来讲,远不如 .jpg .png .gif 等),因为其数据没有经过压缩,或最多只采用行程长度编码(RLE,run-length encoding)来进行轻度的无损数据压缩。以至于,LaTeX 并不能像插入 .jpg 甚至于矢量图那样便捷地插入 BMP 图片,知乎的专栏封面上传也不支持 BMP。

但是,.bmp 仍然发挥着很重要的角色。正是因为它没有进行数据压缩,其内部存储的色彩信息(灰度图,RGB 或 ARGB)直接以二进制的形式暴露在外,也十分方便借助计算机软件进行简单或深入的分析。

 

二、BMP文件格式

下图来自:https://en.wikipedia.org/wiki/BMP_file_format

bmp图像结构的构成:位图文件头、位图信息头、颜色表、位图数据四部分构成。

在c++中,四部分分别由结构体进行实现。

注意

  1. bmp的扫描每行所占的字节数必须是4的倍数,不足4的倍数要进行扩充;
  2. bmp图像的坐标原点在左下角,读取方式是从下到上、从左到右。

 

三、实战分析

 C++读取bmp文件代码示例:

//注意:BITMAPFILEHEADER、BITMAPINFOHEADER等定义在Windows系统下的WinGDI.h里面,在Linux下则没有该头文件
unsigned char* AnalysisHelper::readBmp(const std::string& bmpName, const int& img_width, const int& img_height)
{
    //二进制读方式打开指定的图像文件
    FILE* fp = fopen(bmpName.c_str(), "rb");
    if (fp == NULL)
    {
        LOG_DEBUG << "readBmp: can not open invalid file:" << bmpName;
        return nullptr;
    }

    /****正常情况下应该读取头信息中的图像宽、高等信息****
    //跳过位图文件头结构BITMAPFILEHEADER(14Byte)
	fseek(fp, sizeof(BITMAPFILEHEADER), 0);
	
	//定义位图信息头结构变量,读取位图信息头BITMAPINFOHEADER(40Byte)进内存,存放在变量head中
	BITMAPINFOHEADER head;  
	fread(&head, sizeof(BITMAPINFOHEADER), 1, fp); 
	
	//获取图像宽、高、每像素所占位数等信息
	int bmpWidth = head.biWidth;
	int bmpHeight = head.biHeight;
	int biBitCount = head.biBitCount;
    ****/

    //跳过位图文件头结构BITMAPFILEHEADER(14Byte)+BITMAPINFOHEADER(40Byte)
    fseek(fp, 54, 0);

    //灰度图像有颜色表,且颜色表表项为256
    if (BIBITCOUNT == 8)
    {
        //申请颜色表所需要的空间,读颜色表进内存
        RGBQUAD* pColorTable = new RGBQUAD[256];
        fread(pColorTable, sizeof(RGBQUAD), 256, fp);
        delete[] pColorTable;
    }
    //定义变量,计算图像每行像素所占的字节数(必须是4的倍数)
    int lineByte = (img_width * BIBITCOUNT / 8 + 3) / 4 * 4;
    //申请位图数据所需要的空间,读位图数据进内存
    unsigned char* pBmpBuf = new unsigned char[lineByte * img_height];
    fread(pBmpBuf, 1, lineByte * img_height, fp);
    fclose(fp);
    return pBmpBuf;
}

 

下面我们将问题简化,请问要读取没有头信息的bmp数据(即只有像素矩阵),如何操作?

也许你很快能写出下面的代码:

unsigned char* readBmp_noheader(const std::string& bmpName, const int& img_width, const int& img_height)
{
    //二进制读方式打开指定的图像文件
    FILE* fp = fopen(bmpName.c_str(), "rb");
    if (fp == NULL)
    {
        LOG_DEBUG << "readBmp_noheader: can not open invalid file:" << bmpName;
        return nullptr;
    }
    const int bytes_per_pixel = BIBITCOUNT / 8;
    const int lineByte = (img_width * bytes_per_pixel + 3) / 4 * 4;
    //申请位图数据所需要的空间,读位图数据进内存
    unsigned char* pBmpBuf = new unsigned char[lineByte * img_height];
    fread(pBmpBuf, 1, lineByte * img_height, fp);
    fclose(fp);  //关闭文件
    return pBmpBuf;
}

此外,将位图数据转换为Mat数据:

void bmp2Mat(unsigned char* pBmp, cv::Mat& mat, const int& img_width, const int& img_height)
{
    // bmp的扫描每行所占的字节数必须是4的倍数,不足4的倍数要进行扩充
    // bmp数据填充数据为至下而上、至左而右,mat为至上而下、至左而右
    unsigned int lineSize = (img_width * BIBITCOUNT / 8 + 3) / 4 * 4;
    unsigned long dataSize = lineSize * img_height;
    mat.create(img_height, img_width, CV_MAKETYPE(CV_8UC1, BIBITCOUNT / 8));
    memcpy(mat.data, pBmp, dataSize);
}

如果bmp文件的宽和高不是4的整数倍呢?运行上述的代码会报错,具体为memcpy时会出现访问非法内存的问题!

解决方案:

在读取没有头信息的bmp数据时,如果img_width和img_height不是4的倍数,可能会出现字节对齐的问题,导致读取的数据不完整或出现错误。因此,在使用fread读取时,需要进行补0操作以保证每一行的数据字节数都是4的倍数,从而保证数据的正确性。

根据代码中的实现,当img_width是4的倍数时,每行数据已经满足4字节对齐,因此可以直接使用fread读取。当img_width不是4的倍数时,需要在读取每一行的数据时进行补0操作,直到满足4字节对齐。

补0操作的方法是在读取每一行的数据时,根据每一行实际的字节数lineByte,读取数据的同时判断是否达到img_width字节的位置,如果没有,则继续读取,同时将多出来的字节填充为0。具体实现可以在for循环中增加一个if判断来实现。

  

 

 

参考:

浅谈图像格式 .bmp

c++数字图像处理基础编程