一、前言
位图(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++中,四部分分别由结构体进行实现。
注意:
- bmp的扫描每行所占的字节数必须是4的倍数,不足4的倍数要进行扩充;
- 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判断来实现。
参考: