美文网首页iOS 视频图像高级OpenCvopencv
OpenCV学习之路(二)——Mat对象

OpenCV学习之路(二)——Mat对象

作者: _imp_ | 来源:发表于2016-12-18 21:01 被阅读575次

    早期的 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);
    

    相关文章

      网友评论

        本文标题:OpenCV学习之路(二)——Mat对象

        本文链接:https://www.haomeiwen.com/subject/ktdzmttx.html