Mat 和 bmp图像格式的相互转换

前言

格式转换很常见,其实在我实现了 Mat 转 bmp 之后才发现原来 imwrite 接口可以直接将 Mat 数据保存为 .bmp图像,不过下文所谈及的转换是在内存中的转换,因为将图像发送给识别服务器时显然不能先将 Mat 保存为 .bmp 文件,然后再读该文件以二进制形式发送给识别服务,而是应该直接在内存中完成其转换。

一、Mat 和 bmp 数据结构

1、Mat数据结构

Mat 数据结构由矩阵头和指向矩阵数据的指针构成:Mat = 矩阵头 + 矩阵数据指针,下面代码是 Mat 类的代码片段,其中 uchar* data 比较常见,UMatData* u 为GPU版的 Mat,将图像交由 GPU 处理之前需要将 Mat 转为 UMat。

int flags;
//! the matrix dimensionality, >= 2
int dims;
//! the number of rows and columns or (-1, -1) when the matrix has more than 2 dimensions
int rows, cols;
//! pointer to the data
uchar* data;
//! helper fields used in locateROI and adjustROI
const uchar* datastart;
const uchar* dataend;
const uchar* datalimit;

//! custom allocator
MatAllocator* allocator;
//! and the standard allocator
static MatAllocator* getStdAllocator();
static MatAllocator* getDefaultAllocator();
static void setDefaultAllocator(MatAllocator* allocator);

//! internal use method: updates the continuity flag
void updateContinuityFlag();

//! interaction with UMat
UMatData* u;

MatSize size;
MatStep step;

2、Mat数据结构

Bitmap 是Windows显示图片的基本格式,Windows下图片的显示都必须先转化为位图然后进行显示。BitMap俗称位图,没有任何压缩,图片比较大,其它的图片格式都是在其基础上采用相应的压缩算法生成的。

Bitmap = 文件头 + 信息头 + 调色板 + 像素数据

文件头:BITMAPFILEHEADER,大小为14个字节

信息头:BITMAPINFOHEADER,大小为40个字节

调色板:RGBQUAD[n], 大小为 4*n 个字节,调色板的作用是为了减少图片尺寸。

比如一个16色灰度图像,每一种颜色取值为0-255,可以使用8bit,即一个字节来表示,一个30万像素的图片需要640x480x8bit数据量,如果采用调色板,16种颜色每种颜色都可以使用8bit数据来表示,罗列成为一个表格,每个像素只需要表示为其在调色板中的位置即可(共16个位置,使用4bit数据来表示),数据量为640x480x4+16x8bit,数据量远远小于不使用调色板的情况。

但是对于颜色种类很多的情况就不适用,比如真彩色共256x256x256=1677万多色,单单调色板就非常大,这种情况调色板就不适用了。目前来看对于单色,16色,256色的位图是用调色板方式进行处理,对于16位以及24位真彩色则直接按照rgb分量进行存储。16位图可以是RGB(556)或者RGB(565),24位图是RGB(888),32位图是RGBA(8888)。

像素数据:uchar * p。

二、实现思路

搞清楚了 Mat 和 BMP 的数据结构,转换也就很简单了,Mat转BMP时将 Mat 头里(也就是Mat类的相关成员变量,长宽等)赋值给 BMP 的文件头和信息头,数据指针指向的数据拷贝到 BMP 的数据部分;BMP转Mat也是同样的道理。

三、代码实现

1、Mat转bmp

int Mat2Bmp(cv::Mat * pMat, uchar * & pBmp, ulong & size)
{
	if (!pMat)
	{
		return -1;
	}

	/////////////////////////////////创建bmp空白图片///////////////////////////
	// #define CV_8U   0
	// #define CV_8S   1
	// #define CV_16U  2
	// #define CV_16S  3
	// #define CV_32S  4
	// #define CV_32F  5
	// #define CV_64F  6
        // depth 代表每个像素的每个通道的精度,就是每个通道用几个字节表示
	int depth = pMat->depth(); 
	int channels = pMat->channels();
	int width = pMat->cols;
	int height = pMat->rows;

	// 获取图像每个像素的宽度
	uint pixelSize = (8 << (depth / 2)) * channels; // pixelSize >= 8

	colorTableSize = 256 * sizeof(RGBQUAD);

	RGBQUAD* pColorTable = (RGBQUAD*)(&pBmp[sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)]);

	for (int i = 0; i < 256; ++i)
	{
		// 灰阶调色板
		pColorTable[i].rgbRed = i;
		pColorTable[i].rgbGreen = i;
		pColorTable[i].rgbBlue = i;

		// 也可以创建彩色调色版
	}

	// bmp图片的大小, sizeof(BITMAPFILEHEADER) = 14, sizeof(BITMAPINFOHEADER) = 40
	size = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + colorTableSize + height * lineSize;

	pBmp = (uchar*)malloc(size);
	if (!pBmp)
	{
	    return -2;
	}
	memset(pBmp, 0, size);

	/////////////////////////////////为bmp图片的文件头赋值/////////////////////////////////
	BITMAPFILEHEADER * pFileHead = (BITMAPFILEHEADER *)pBmp;
	pFileHead->bfType = 0x4D42; // 0x4D42 代表 “BM”,位图标志
	pFileHead->bfSize = size;
	pFileHead->bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + colorTableSize; // 图像数据偏移量
																						 /////////////////////////////////为bmp图片的信息头赋值/////////////////////////////////
	BITMAPINFOHEADER * pInfoHead = (BITMAPINFOHEADER *)(&pBmp[sizeof(BITMAPFILEHEADER)]);
	pInfoHead->biSize = 40;    // 信息头的大小
	pInfoHead->biWidth = width;
	pInfoHead->biHeight = height;
	pInfoHead->biPlanes = 1;   // 图像平面数,rgb为1?什么时候大于1?
	pInfoHead->biBitCount = pixelSize; // 图像每个像素所占的位数
	pInfoHead->biCompression = 0;  // 0:不压缩,1:REL8, 2:REL4
	pInfoHead->biSizeImage = height * lineSize;  // 图像数据大小
	pInfoHead->biXPelsPerMeter = 0;  // 水平方向像素/米,分辨率
	pInfoHead->biYPelsPerMeter = 0;  // 垂直方向像素/米,分辨率
	pInfoHead->biClrUsed = 0;   // BMP图像使用的颜色,0:表示使用全部颜色
	pInfoHead->biClrImportant = 0;  // 重要的颜色数,0:所有的颜色都重要,当显卡不能够显示所有颜色时,辅助驱动程序显示颜色

	///////////////////////////////////为bmp图片的调色板赋值/////////////////////////////////


	/////////////////////////////////为bmp图片的图像数据赋值/////////////////////////////////
	// BMP 和 Mat 数据都是自左向右,但是BMP是自下而上,Mat是自上而下,故而在数据转换时需要颠倒数据上下位置
	uchar * pBmpData = pBmp + pFileHead->bfOffBits + height * lineSize; // 最后一行尾地址
	uchar * pMatData = pMat->data; // 第一行首地址

								   // 将Mat从上往下一行一行拷给BMP
	for (int i = 0; i < height; ++i)
	{
		// 这里的 width 代表水平方向的像素个数,但是每个像素占1个字节,通过查表索引
		// 每次拷贝一行
		pBmpData -= lineSize;
		memcpy(pBmpData, pMatData, lineSize);
		pMatData += lineSize;
	}

	return 0;
}

2、bmp转Mat

int Bmp2Mat(uchar * pBmp, cv::Mat & mat)
{
	// 获取文件头信息
	if (!pBmp)
	{
		return -1;
	}

	BITMAPFILEHEADER * pFileHead = (BITMAPFILEHEADER *)pBmp;
	if (pFileHead->bfType != 0x4D42)
	{
		return -2;
	}

	BITMAPINFOHEADER* pInfoHead = (BITMAPINFOHEADER *)(pBmp + sizeof(BITMAPFILEHEADER));

	long height = pInfoHead->biHeight;
	long width = pInfoHead->biWidth;
	ulong dataSize = pInfoHead->biSizeImage;

	uchar * pMatData = (uchar *)malloc(dataSize);
	memset(pMatData, 0, dataSize);

	// bmp数据填充数据为至下而上、至左而右,mat为至上而下、至左而右
	uint lineSize = width * (pInfoHead->biBitCount) / 8;

	// 最后一行尾地址
	uchar * pBmpData = (uchar *)(pBmp + pFileHead->bfSize); // 每个像素占一个字节
	for (int h = 0; h < height; ++h)
	{
		pBmpData -= lineSize;
		memcpy(pMatData, pBmpData, lineSize);	// 复制整行
		pMatData += lineSize;
	}

	// Mat数据指针移到最前面
	pMatData -= dataSize;

	mat.create(height, width, CV_MAKETYPE(CV_8U, (pInfoHead->biBitCount) / 8));
	memcpy(mat.data, pMatData, dataSize);

	free(pMatData);

	return 0;
}

四、总结

难点在于理解bmp的数据结构,特别是调色板;另外bmp数据填充数据为至下而上、至左而右,mat为至上而下、至左而右也要注意。

weinxin
我的微信
一个码农、工程狮、集能量和智慧于一身的、DIY高手、小伙伴er很多的、80后奶爸。
Igor
  • 版权声明: 发表于 2023-01-1511:33:51
  • 转载注明:http://blog.tsingmac.com/prolions/imagetech/opencv/6022/
图像均匀性比拼:面阵扫描PK线阵扫描(一) 图形图像

图像均匀性比拼:面阵扫描PK线阵扫描(一)

         艺术品数字化中,为避免对古字画的损坏,需要进行非接触扫描,目前适用于艺术品数字化的技术主要有两种,一种是采用面阵扫描,多采用高像素水平的数码后背进行拍照,然后进行拼接处理;另一种是采...
opencv和pillow对图片的读写耗时对比 OpenCV

opencv和pillow对图片的读写耗时对比

🧁生成数组 不同的包对于读写图片有不同的优化方式,导致他们的读写时间有差异,这个差异一般情况下可能无所谓,但是在大量图片数据的读写时,却可以节约大量的时间。 生成二维数组: import cv2 im...
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: