早期的 OpenCV 中,使用 IplImage 和 CvMat 数据结构来表示图像。IplImage和 CvMat 都是 C 语言的结构。使用这两个结构的问题是内存需要手动管理,开发者必须清楚的知道何时需要申请内存,何时需要释放内存。这个开发者带来了一定的负担,开发者应该将更多精力用于算法设计,因此在新版本的 OpenCV 中引入了 Mat 类。
新加入的 Mat 类能够自动管理内存。使用 Mat 类,你不再需要花费大量精力在内存管理上。而且你的代码会变得很简洁,代码行数会变少。但 C++接口唯一的不足是当前一些嵌入式开发系统可能只支持 C 语言,如果你的开发平台支持C++,完全没有必要再用 IplImage 和 CvMat。在新版本的 OpenCV 中,开发者依然可以使用 IplImage 和 CvMat,但是一些新增加的函数只 供了 Mat 接口。
Mat 类的定义如下所示,关键的属性如下方代码所示:
class CV_EXPORTS Mat
{
public:
//一系列函数
...
/* flag 参数中包含许多关于矩阵的信息,如:
-Mat 的标识
-数据是否连续
-深度
-通道数目
*/
int flags;
//矩阵的维数,取值应该大于或等于 2
int dims;
//矩阵的行数和列数,如果矩阵超过 2 维,这两个变量的值都为-1
int rows, cols;
//指向数据的指针
uchar* data;
//指向引用计数的指针
//如果数据是由用户分配的,则为 NULL
int* refcount;
//其他成员变量和成员函数
...
};
Mat属性的理解
- data:uchar类型的指针,指向Mat数据矩阵的首地址。可以理解为标示一个房屋的门牌号;
- dims:Mat矩阵的维度,若Mat是一个二维矩阵,则dims=2,三维则dims=3,大多数情况下处理的都是二维矩阵,是一个平面上的矩阵;
- rows:Mat矩阵的行数。可理解为房屋内房间行数;
- cols:Mat矩阵的列数。可理解为房屋内房间列数;
- size():首先size是一个结构体,定义了Mat矩阵内数据的分布形式,数值上有关系式:
image.size().width==image.cols;
image.size().height==image.rows;
可以理解为房屋内房间的整体布局,这其中包括了房间分别在行列上分布的数量信息; - channels():Mat矩阵元素拥有的通道数。例如常见的RGB彩色图像,channels==3;而灰度图像只有一个灰度分量信息,channels==1。可以理解为每个房间内放有多少床位,3通道的放了3张床,单通道的放了1张床;
- depth:用来度量每一个像素中每一个通道的精度,但它本身与图像的通道数无关!depth数值越大,精度越高。在Opencv中,Mat.depth()得到的是一个0~6的数字,分别代表不同的位数,对应关系如下:
enum{CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6}
可见 0和1都代表8位,2和3都代表16位,4和5代表32位,6代表64位;其中U是unsigned的意思,S表示signed,也就是有符号和无符号数。可以理解为房间内每张床可以睡多少人,这个跟房间内有多少床并无关系; - elemSize:elem是element(元素)的缩写,表示矩阵中每一个元素的数据大小,如果Mat中的数据类型是CV_8UC1,那么elemSize==1;如果是CV_8UC3或CV_8SC3,那么elemSize==3;如果是CV_16UC3或者CV_16SC3,那么elemSize==6;即elemSize是以8位(一个字节)为一个单位,乘以通道数和8位的整数倍;可以理解为整个房间可以睡多少人,这个时候就得累计上房间内所有床位数(通道)和每张床的容纳量了;
- elemSize1:elemSize加上一个“1”构成了elemSize1这个属性,1可以认为是元素内1个通道的意思,这样从命名上拆分后就很容易解释这个属性了:表示Mat矩阵中每一个元素单个通道的数据大小,以字节为一个单位,所以有:
eleSize1==elemSize/channels; - step:可以理解为Mat矩阵中每一行的“步长”,以字节为基本单位,每一行中所有元素的字节总量,是累计了一行中所有元素、所有通道、所有通道的elemSize1之后的值;
- step1():以字节为基本单位,Mat矩阵中每一个像素的大小,累计了所有通道的elemSize1之后的值,所以有:
step1==step/elemSize1;
M.step[m-1] 总是等于 elemSize;M.step1(m-1)总是等于 channels。
补充:
step1(i):每一维元素的通道数
step[i]:每一维元素的大小,单位字节
size[i]:每一维元素的个数
elemSize():每个元素大小,单位字节
elemSize1():每个通道大小,单位字节
每一维的元素表示什么意思呢?
这里我们以空间几何的角度来解释,能够更加容易理解一点。
三维矩阵,一共有三维,我们分别类比为
面:每个二维矩阵,表示第1维的元素
线:矩阵的每一行,表示第2维的元素
点:矩阵中每行的每个元素,表示第3维的元素
那么这样子就可以解释清楚每一维元素的含义了。
以step[i]为例
step[0]:面的大小,第1维的元素的大小,也就是二维矩阵的大小,一个二维矩阵有8行,所以
step[0] = step[1] * 8 = 480
step[1]:线的大小,第2维的元素的大小,也就是二维矩阵每一行的大小,由于每个元素大小为6,每行有10个元素,所以
step[1] = 10 * 6 = 60
step[2]:点的大小,第3维的元素的大小,这里矩阵的每个元素类型为CV_16UC3,所以
step[2] = 2 * 3 = 6
这里注意:
1.step的大小是字节
2.注意下标与维数的对应关系:下标2对应点,1对应线,0对应面
3.矩阵有几维,step[]数组就有几个元素,如3维,则有3个元素,step[0],step[1],step[2].分别对应面,线,点
只要记住,最后一个总是表示点,然后依次向前为线,面...
4.第2,3 点 ,对于size和step1()也一样。
step1(i)和size[]与step[i]原理相同。
Mat对象构造函数与常用方法
常用的构造函数有:
- Mat::Mat()
无参数构造方法; - Mat::Mat(int rows, int cols, int type)
创建行数为 rows,列数为 col,类型为 type 的图像; - Mat::Mat(Size size, int type)
创建大小为 size,类型为 type 的图像; - Mat::Mat(int rows, int cols, int type, const Scalar& s)
创建行数为 rows,列数为 col,类型为 type 的图像,并将所有元素初始化为值 s; - Mat::Mat(Size size, int type, const Scalar& s)
创建大小为 size,类型为 type 的图像,并将所有元素初始化为值 s; - Mat::Mat(const Mat& m)
将 m 赋值给新创建的对象,此处不会对图像数据进行复制,m 和新对象共用图像数据; - Mat::Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP)
创建行数为 rows,列数为 col,类型为 type 的图像,此构造函数不创建图像数据所需内存,而是直接使用 data 所指内存,图像的行步长由 step指定。 - Mat::Mat(Size size, int type, void* data, size_t step=AUTO_STEP)
创建大小为 size,类型为 type 的图像,此构造函数不创建图像数据所需内存,而是直接使用 data 所指内存,图像的行步长由 step 指定。 - Mat::Mat(const Mat& m, const Range& rowRange, const Range& colRange)
创建的新图像为 m 的一部分,具体的范围由 rowRange 和 colRange 指定,此构造函数也不进行图像数据的复制操作,新图像与 m 共用图像数据; - Mat::Mat(const Mat& m, const Rect& roi)
创建的新图像为 m 的一部分,具体的范围 roi 指定,此构造函数也不进行图像数据的复制操作,新图像与 m 共用图像数据。
这些构造函数中,很多都涉及到类型 type。type 可以是 CV_8UC1,CV_16SC1,...,CV_64FC4 等。里面的 8U 表示 8 位无符号整数,16S 表示 16 位有符号整数,64F表示 64 位浮点数(即 double 类型);C 后面的数表示通道数,例如 C1 表示一个通道的图像,C4 表示 4 个通道的图像,以此类推。
如果你需要更多的通道数,需要用宏 CV_8UC(n),例如:
Mat M(3,2, CV_8UC(5));//创建行数为3,列数为2,通道数为5的图像
有些 type 参数如 CV_32F未注明通道数目,这种情况下它表示单通道。
常用方法
void copyTo(); //拷贝
Mat clone(); //拷贝
int channels(); //通道,矩阵中的每一个矩阵元素拥有的值的个数
int depth(); //深度,即每一个像素的位数(bits)
bool empty() const; //判断是否为空
uchar* ptr(int i0=0); //指针取第0行数据
void convertTo(oclMat& m, int rtype, double alpha=1, double beta=0);
//m:转为目标数据类型的矩阵;
//rtype: 指定目标数据类型,或者是depth(通道数),如果rtype:是负值,那么目标矩阵的数据类型和源矩形的数据类型是一致的;
//alpha:基于尺度的变化值;
//beta:在尺度上的加和;
Mat类的内存管理
Mat 是一个类,由两个数据部分组成:矩阵头(包含矩阵尺寸,存储方法,存储地址等信息)和一个指向存储所有像素值的矩阵的指针。矩阵头的尺寸是常数值,但矩阵本身的尺寸会依图像的不同而不同,通常比矩阵头的尺寸大数个数量级。复制矩阵数据往往花费较多时间,因此除非有必要,不要复制大的矩阵。
为了解决矩阵数据的传递,OpenCV 使用了引用计数机制。其思路是让每个Mat 对象有自己的矩阵头信息,但多个 Mat 对象可以共享同一个矩阵数据。让矩阵指针指向同一地址而实现这一目的。很多函数以及很多操作(如函数参数传值)只复制矩阵头信息,而不复制矩阵数据。
如果 Mat 类自己申请数据空间,那么该类会多申请 4 个字节,多出的 4 个字节存储数据被引用的次数。引用次数存储于数据空间的后面,refcount 指向这个位置,如图所示。当计数等于 0 时,则释放该空间。
Mat类.jpeg
关于多个矩阵对象共享同一矩阵数据,我们可以看这个例子:
Mat A(100,100, CV_8UC1);
Mat B = A;
Mat C = A(Rect(50,50,30,30));
上面代码中有三个Mat对象,分别是A,B和C。这三者共有同一矩阵数据,其示意图如图:
三个矩阵头共用共用同一矩阵数据.jpeg
部分复制:一般情况下只会复制Mat对象的头和指针部分,不会复制数据部分。如:
Mat A = imread(filePath);
Mat B = A;
完全复制:如果想把Mat对象的头部和数据部分一起复制,如下:
Mat F = A.clone();
Mat G;
A.copyTo(G);
四个要点:
- 输出图像的内存是自动分配的
- 使用OpenCV的C++接口,不需要考虑内存分配的问题
- 赋值操作和拷贝构造函数只会复制头部分
- 使用clone()与copyTo()两个函数实现数据完全复制
create()函数创建对象
除了在构造函数中可以创建图像,也可以使用 Mat 类的 create()函数创建图像。如果 create()函数指定的参数与图像之前的参数相同,则不进行实质的内存申请操作;如果参数不同,则减少原始数据内存的索引,并重新申请内存。使用方法如下面例程所示:
Mat M(2,2, CV_8UC3);//构造函数创建图像
M.create(3,2, CV_8UC2);//释放内存重新创建图像
需要注意的时,使用 create()函数无法设置图像像素的初始值。
Matlab 风格的创建对象方法
OpenCV 2 中 供了 Matlab 风格的函数,如 zeros(),ones()和 eyes()。这种方法使得代码非常简洁,使用起来也非常方便。使用这些函数需要指定图像的大小和类型,使用方法如下:
Mat Z = Mat::zeros(3, 3, CV_8UC1);
cout << "Z = " << endl << " " << Z << endl;
Mat O = Mat::ones(3, 3, CV_32F);
cout << "O = " << endl << " " << O << endl;
Mat E = Mat::eye(3, 3, CV_64F);
cout << "E = " << endl << " " << E << endl;
show.jpeg
Mat 与 IplImage 和 CvMat 的转换
1.Mat 转为 IplImage 和 CvMat 格式
假如你有一个以前写的函数,函数的定义为:
void mycvOldFunc(IplImage * p, ...);
函数的参数需要 IplImage 类型的指针。Mat 转为 IplImage,可以用简单的等号赋值操作来进行类型转换,这样实现:
Mat img(Size(320, 240), CV_8UC3);
...
IplImage iplimg = img; //转为IplImage结构
mycvOldFunc( & iplimg, ...);//对 iplimg 取地址
如果要转为 CvMat 类型,操作类似:
CvMat cvimg = img; //转为CvMat结构
需要特别注意的是,类型转换后,IplImage 和 CvMat 与 Mat 共用同一矩阵数据,而 IplImage 和 CvMat 没有引用计数功能,如果上例中的 img 中数据被释放,iplimg 和 cvimg 也就失去了数据。因此要牢记不可将 Mat 对象 前释放。
2.IplImage 和 CvMat 格式转为 Mat
Mat 类有两个构造函数,可以实现 IplImage 和 CvMat 到 Mat 的转换。这两个函数都有一个参数 copyData。如果 copyData 的值是 false,那么 Mat 将与 IplImage或 CvMat 共用同一矩阵数据;如果值是 true,Mat 会新申请内存,然后将 IplImage或 CvMat 的数据复制到 Mat 的数据区。
如果共用数据,Mat 也将不会使用引用计数来管理内存,需要开发者自己来管理。
Mat::Mat(const CvMat* m, bool copyData=false)
Mat::Mat(const IplImage* img, bool copyData=false)
例子代码如下:
IplImage * iplimg = cvLoadImage("lena.jpg");
Mat im(iplimg, true);