视觉SLAM——OpenCV之Mat结构详解 数据成员和构造函数 创建Mat方法 遍历Mat方法

前言

OpenCV1时代采用基于C语言接口构建函数库,使用名为IplImage的结构体在内存中存储图像,其问题在于需要用户手动管理内存,如果不手动释放内存会造成内存泄漏。
OpenCV2引入面向对象编程思想,加入了一个c++接口,使用Mat类数据结构作为主打,可以实现自动内存管理,且扩展性大大提高。

Mat概述

对于Mat类,首先要知道的是
1)不必手动为其开辟空间;
2)不必再在不需要时将空间释放。
但手动做还也是可以的:大多数OpenCV函数仍会手动地为输出数据开辟空间。当传递一个已经存在的 Mat 对象时,开辟好的矩阵空间会被重用。

Mat是一个类,由两个数据部分组成:矩阵头(包含矩阵尺寸、存储方法、存储地址等)和一个指向存储所有像素值矩阵的指针。

Mat类最重要的一点是浅拷贝和深拷贝问题。由于OpenCV处理图像时很多时候没有必要重新复制一份图像矩阵,因而采用了引用计数机制。其思路是让每个Mat对象有自己的信息头,但共享一个图像矩阵(矩阵指针指向同一地址)。赋值运算符和拷贝构造函数只复制矩阵头和矩阵指针,而不复制矩阵。

Mat A , C;
A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 为矩阵开辟内存
Mat B(A);                                 // 拷贝构造函数
C = A;                                    // 赋值

这里A、B、C矩阵头不同,但指向了相同的图像矩阵,其中一个对象对矩阵数据进行改变也会影响其他对象。如果矩阵属于多个Mat对象,由最后一个使用它的对象进行内存清理。这通过引用计数来判断,每复制一个Mat对象,计数器加一,每释放一个计数器减一,当计数器值为0时矩阵就会被清理。

如果需要进行对象的深拷贝可以采用clone()函数或者copyTo()。

Mat A;
A = imread(argv[1], CV_LOAD_IMAGE_COLOR); 
Mat B = A.clone();
Mat C;
A.copyTo(C);

Mat类的数据成员

	/*
	flag的详细解释可以看 https://blog.csdn.net/yiyuehuan/article/details/43701797
	0-2位 depth:每一个像素的位数,也就是每个通道的位数,即数据类型(如CV_8U)
		enum { CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6 }
		8U 表示 8 位无符号整数,16S 表示 16 位有符号整数,64F表示 64 位浮点数
	3-11位 number of channels:代表通道数channels,最高512位
	0-11位共同代表type:矩阵元素的类型,即通道数和数据类型(如CV_8UC3、CV_16UC2)
	14位 continuity flag:代表Mat的内存是否连续
	15位 submat flag:代表该Mat是否为某一个Mat的submatrix
	16-31位 the magic signature:用来区分Mat的类型,如果Mat和SparseMat
	*/
	int flags;
	
    //矩阵的维数,一般大于2
    int dims;
    
    //矩阵的行数与列数,超过2维矩阵时(-1,-1)
    int rows, cols;
    
    //指向存放矩阵数据的内存
    uchar* data;
    
    //用来控制ROI区域,来获取一些图像的局部切片,减少计算量或者特殊需求的。
    const uchar* datastart;
    const uchar* dataend;
    const uchar* datalimit;
    
	//如果需要创建一个新矩阵的内存空间,会调用MatAllocator类作为分配符进行内存的分配。
    MatAllocator* allocator;
    
    //interaction with UMat
    //UMatData结构体总有一个成员refcount:记录了矩阵的数据被其他变量引用了多少次
    UMatData* u;
    
    //返回矩阵大小
    MatSize size;
    
    //矩阵元素寻址,step[i]表示第i维的总大小,单位字节
    //对于2维矩阵:step[0]是矩阵中一行元素的字节数,step[1]是矩阵中一个元素的字节数
    //以下公式可以得到Mat中任意元素地址
    //addr(M{i,j})=M.data+M.step[0]∗i+M.step[1]∗j;
    MatStep step;

此外其他版本还存在以下成员:
elemSize :矩阵一个元素占用的字节数,例如:type是CV_16SC3,那么elemSize = 3 * 16 / 8 = 6 bytes。
elemSize1 :矩阵元素一个通道占用的字节数,例如:type是CV_16CS3,那么elemSize1 = 16 / 8 = 2 bytes = elemSize / channels。

Mat类的构造函数

//1、默认构造函数,无参数
Mat::Mat();
	
//2、行数为rows,列数为cols,类型为type(如CV_8UC1、CV_16UC2)
Mat(int rows, int cols, int type);

//3、矩阵大小为size,类型为type
//注意size的构造函数是Size_(_Tp _width,_Tp _height) 先列后行
Mat(Size size, int type);

//4、行数为rows,列数为cols(或矩阵大小为size),类型为type,所有元素初始化为s
//Scalar表示具有4个元素的数组,如Scalar(a,b,b),其原型为Scalar_<double>
Mat(int rows, int cols, int type, const Scalar& s);
Mat(Size size, int type, const Scalar& s);

//5、矩阵维数为ndims,sizes为指定ndims维数组形状的整数数组,所有元素初始化为s
Mat(int ndims, const int* sizes, int type);
Mat(const std::vector<int>& sizes, int type);
Mat(int ndims, const int* sizes, int type, const Scalar& s);
Mat(const std::vector<int>& sizes, int type, const Scalar& s);

//6、拷贝构造函数,将m赋值给新创建的对象,浅拷贝
Mat(const Mat& m);

//7、行数为rows,列数为cols,类型为type,矩阵数据为data,直接使用data所指内存,浅拷贝
Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP);
Mat(Size size, int type, void* data, size_t step=AUTO_STEP);
Mat(int ndims, const int* sizes, int type, void* data, const size_t* steps=0);
Mat(const std::vector<int>& sizes, int type, void* data, const size_t* steps=0);

//8、创建的新图像为m的一部分,范围由rowRange和colRange指定,新图像与m共用图像数据,浅拷贝
Mat(const Mat& m, const Range& rowRange, const Range& colRange=Range::all());
Mat(const Mat& m, const Range* ranges);
Mat(const Mat& m, const std::vector<Range>& ranges);

//创建的新图像为m的一部分,具体的范围由矩阵对象roi指定
//Rect的成员函数有x,y,width,height,分别为左上角点的坐标好矩阵宽和高
Mat(const Mat& m, const Rect& roi);

创建Mat对象

1、使用Mat()构造函数

Mat M(2,2, CV_8UC3, Scalar(0,0,255)); 

需要指定行数、列数、存储元素的数据类型以及每个矩阵点的通道数。

2、使用Mat()构造函数2

int sz[3] = {2,2,2}; 
Mat L(3,sz, CV_8UC(1), Scalar::all(0));

常用于创建一个超过两维的矩阵:指定维数,然后传递一个指向一个数组的指针,这个数组包含每个维度的尺寸,其余的相同

3、为已存在IplImage指针创建信息头(一般不用)

IplImage* img = cvLoadImage("greatwave.png", 1);
Mat mtx(img); // convert IplImage* -> Mat

4、利用create()函数
这个创建方法不能为矩阵设初值,它只是在改变尺寸时重新为矩阵数据开辟内存

 M.create(4,4, CV_8UC(2));//4X4的图像矩阵,通道数为2,没有初值

5、MATLAB形式的初始化方式: zeros(), ones(), eyes()

 Mat E = Mat::eye(4, 4, CV_64F);//4X4的单位矩阵   
  
 Mat O = Mat::ones(2, 2, CV_32F);//2X2的全为1矩阵

 Mat Z = Mat::zeros(3,3, CV_8UC1);//3X3的零矩阵

6、小矩阵使用逗号分隔式初始化

 Mat C = (Mat_<double>(3,3) << 1, 0, 0, 0, 1, 0, 0, 0, 1); //3X3的单位矩阵

7、使用clone()或copyto()为已存在对象创建新信息头

Mat RowClone = C.row(1).clone();//复制 C中的第2行[0,1,0]作为新矩阵(深拷贝)

遍历Mat对象

首先假设需要对图像将那些颜色空间缩减,如 i m g n e w = i m g o l d / 10 10 img_{new} = img_{old} / 10 * 10 。对于较大的图像,一般不是对每个像素点每个通道的值进行如上计算,而是预先计算所有可能的值,构建查找表,然后利用查找表对其直接复制。

	//构建查找表table
	int divideWith; 
    cin >> divideWith;
    uchar table[256]; 
    for (int i = 0; i < 256; ++i)
       table[i] = divideWith* (i/divideWith);

1、ptr指针单像素单通道值访问
即通过uchar* ptr =img.ptr<char>(i); 得到第i行首地址,然后采用指针运算(++)或者操作符[]遍历。

Mat& ScanImage(Mat& img,const uchar* const table)
{
	CV_Assert(img.depth() != sizeof(uchar))//只接收uchar类型矩阵
	int channels = img.channel();
	int nRows = img.rows * channels;//注意行数乘以通道数
	int nCols = img.cols;
	
	if(img.isContinuous())//如果存储连续则可以使用一次循环
	{
		nCols *= nRows;
		nRows = 1; 
	}
	uchar* ptr;
	for(int i=0; i<nRows; ++i)
	{
		ptr = img.ptr<char>(i);
		for( j=0; j<nCols; ++j)
		{
			ptr[j] = table[ptr[j]]
			//*ptr++ =  table[*ptr]
		}
	}
	return I;
}

2、ptr指针单像素访问
利用cv::Vec3b *ptr =img.ptr<cv::Vec3b>(i);得到第i行第一个像素的3个通道地址。

Mat& ScanImage(Mat& img,const uchar* const table)
{
	CV_Assert(img.depth() != sizeof(uchar))//只接收uchar类型矩阵
	int channels = img.channel();
	int nRows = img.rows;//每一行
	int nCols = img.cols;
	Vec3b *ptr;
	for(int i=0; i<nRows; ++i)
	{
		ptr = img.ptr<Vec3b>(i);
		for( j=0; j<nCols; ++j)
		{
			ptr[j][0] = table[ptr[j][0]];
			ptr[j][1] = table[ptr[j][1]];
			ptr[j][2] = table[ptr[j][2]];
		}
	}
	return I;
}

3、对data操作
data会从Mat中返回指向矩阵第一行第一列的指针。常用来检查图像是否被成功读入(如果指针为NULL则表示无输入)。当矩阵式连续存储时,可以通过data遍历矩阵。
也可以采用addr(M{i,j})=M.data+M.step[0]∗i+M.step[1]∗j;得到每个元素的地址,但是不常用。

if(I.isContinuous())
{
	uchar* p = img.data;
	for( unsigned int i =0; i < ncol * nrows; ++i)
    	*p++ = table[*p];
}

4、迭代器iterator访问
使用迭代器操作像素,与STL库用法相似,只需要获得图像矩阵的begin和end,然后增加迭代直至从begin到end。将*操作符添加在迭代指针前,即可访问当前指向的内容。
迭代器可以采用MatIterator_以及Mat的模板子类Mat_,它重载了operator()。

MatIterator_<uchar> it = img.begin<uchar>();
Mat_<uchar>::iterator it = img.begin<uchar>();
Mat& ScanImage(Mat& I,const uchar* const table)
{
	CV_Assert(img.depth() != sizeof(uchar));
	const int channels = img.channels();
	switch(channels)
	{
		case 1:
			{
				Mat_<uchar>::iterator it = img.begin<uchar>();
				Mat_<uchar>::iterator itend = img.end<uchar>();
				for(;it != itend; ++it)
					*it = table[*it];
				break;
			}
		case 3:
			{
				Mat_<Vec3b>::iterator it = img.begin<uchar>();
				Mat_<Vec3b>::iterator itend = img.end<uchar>();
				for(; it != itend ; ++it)
				{
					(*it)[0] = table[(*it)[0]];
					(*it)[1] = table[(*it)[1]];
					(*it)[2] = table[(*it)[2]];
				}
			}
	}
	return img;
}

5、动态地址计算(at)
该方法一般用于获取或更改图像中的某个元素(或随机元素),而不用于进行全图扫描。它的基本用途是要确定你试图访问的元素的所在行数与列数。
在debug模式下该方法会检查你的输入坐标是否有效或者超出范围,如果坐标有误则会输出一个标准的错误信息;在release模式下,它和其他的区别仅仅是对于矩阵的每个元素,都会获取一个新的行指针,然后通过该指针和[]操作获取元素。

Mat& ScanImage(Mat& img,const uchar* const table)
{
	CV_Assert(img.depth() != sizeof(uchar));
	const int channels = img.channels();
	switch(channels)
	{
		case 1:
			{
				for(int i=0; i<img.rows; ++i)
					for(int j=0; j<img.cols; ++i)
						img.at<uchar>(i,j) = table[img.at<uchar>(i,j)];
				break;
			}
		case 3:
			{
				for(int i=0; i<img.rows; ++i)
				{
					for(int j=0; j<img.cols; ++i)
					{
						img.at<vec3b>(i,j)[0] = table[img.at<vec3b>(i,j)[0]];
						img.at<vec3b>(i,j)[1] = table[img.at<vec3b>(i,j)[1]];
						img.at<vec3b>(i,j)[2] = table[img.at<vec3b>(i,j)[2]];
					}
				}
				break;
			}
	}
	return img;
}

6、LUT函数
LUT(Look up table)被官方推荐用于实现批量图像元素查找和更改。对于修改像素值,OpenCV提供函数operationsOnArray:LUT()<lut>,直接实现该操作,而不需要扫描图像。

//建立mat类对象用于查表
Mat LookUpTable(1,256,CV_8U);
uchar* p = LookUpTable.data;
for(int i=0; i<256; ++i)
	p[i] = table[i];
//调用函数(I时输入,J是输出)
LUT(I, LookUpTable, J);

一般认为,采用ptr指针访问图像像素的效率更高;而迭代器则被认为是更安全的方式,仅需要获得图像矩阵的begin和end;at方法一般不用于对图像进行遍历;而调用LUT函数可以获得最快的速度,因为OpenCV库可以通过英特尔线程架构启用多线程。其他也存在一些比较偏的遍历方法,不过这几种最为常见,效率和安全性也相对较好。

猜你喜欢

转载自blog.csdn.net/qq_41839222/article/details/88786561
Mat
今日推荐