【学习Opencv3笔记】4、图像和大型数组类型

opencv引入了Mat类用于管理图像(从数字图像处理的角度看,图像其实就是矩阵),Mat类采用了很巧妙的内存管理机制,可以让开发人员不必把注意力放在内存的管理上,这大大降低了opencv的学习门槛。

cv::Mat可以作为任意维度的数组使用,在一维数组中元素是按照顺序存储的,二维数组是按照行排列的方式排列,每一行元素同样按照顺序存储,对于三维数组,每个通道被一个二维数组填充,每个通道的存储方式与二维数组是一样的。

0、概述

cv::Mat最重要的几个信息:元素数据类型、通道数、每个通道的尺寸(rows和cols),指明数据存储位置的data指针。

  • 信息头:包含尺寸、存储方法、存储地址等信息。
  • 数据指针:指向数据实体。
  • data:数据存储的起始地址 (uchar*类型);
  • dims:矩阵维度。如 3 * 4 的矩阵为 2 维, 3 * 4 * 5 的为3维;
  • channels():通道数量,矩阵中表示一个元素所需要的值的个数。例:3 * 4矩阵中共有12个元素,如果每个元素需要3个值表示,那么此矩阵的通道数为3。常见的是一张彩色图片有B蓝、G绿、R红3个通道;
  • depth():深度,即表示单通道中元素值的位数(bits)。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位;
  • elemSize() : 单个元素在所有通道中大小之和,elemSize() = sizeof(数据类型) * channels()。如果Mat中的数据的数据类型是 CV_8U 那么 elemSize() = 1;CV_8UC3 那么 elemSize() = 3,CV_16UC2 那么 elemSize() = 4;
  • elemSize1() : 单个元素在1个通道中的大小。elemSize1() = sizeof(数据类型) = elemSize()  / channels()。 
  • step[]:可以看作是一个有dims个元素的动态数组,定义了矩阵的在各维度之间跳变时的最大步长(单位:字节。如2维Mat时,step[0]表示行长度,step[1]表示元素长度)。
    step1(n):与step[n]对应范围的总通道数, step1(n) = step[n] / elemSize1();

尺寸是常数,但是矩阵本身的尺寸是可变的。创建和复制图像的开支,主要来源于图像矩阵的操作,可想而知复制一个图像是一笔很大的开销。cv::Mat类处理这个问题的机制是:在保留对象自己的信息头的情况下共用矩阵。

m和n是两个cv::Mat对象,当用其中一个对象赋值给另一个时m=n,m内部的数据指针指向的数据实体将被释放,m和n将会共享同一块内存,这块内存的引用指针会增加一个引用计数,m变成了新的数据实体的头,其行、列、数据类型信息等会依据新的数据实体进行更新!

例子

	cv::Mat A, C; //仅创建信息头部分
	A = cv::imread(file_path);
	C = A; //赋值运算符,A和C的数据指针指向同一个数据实体,对一个对象的操作会影响到另一个
	cv::Mat B(C); //拷贝构造
	cv::Mat D = A.clone(); //复制操作

	cv::rectangle(C, cv::Point(100, 200), cv::Point(300, 400), cv::Scalar(0, 0, 255), 1, 1, 0); //对C对象进行操作
	cv::rectangle(B, cv::Point(200, 300), cv::Point(400, 500), cv::Scalar(255, 0, 255), 1, 1, 0); //对B对象进行操作
	cv::rectangle(D, cv::Point(250, 350), cv::Point(450, 550), cv::Scalar(0, 255, 0), 1, 1, 0); //对D对象进行操作

	cv::imshow("A", A); //显示A对象图像
	cv::imshow("B", B); //显示B对象图像
	cv::imshow("C", C); //显示C对象图像
	cv::imshow("D", D); //显示D对象图像

	cout << &A << "," << &C;

结果表明对A、B、C对象的操作是在操作同一个对象,或者严格的说操作的是同一个矩阵(各个对象有自己的信息头),说明C和B对象创建时发生了浅拷贝,矩阵部分没有进行复制。对D的操作不再是同一个矩阵,说明clone()接口被调用时发生了深拷贝,矩阵的内容也被复制了一份到D对象自己的地址空间。

1、cv::Mat初始化

二维三通道的32位浮点型数组,数组元素就是占用12字节的32位浮点型数据(4字节*3通道)。n维单通道和(n-1)维多通道数组有非常接近的存储结构,区别在于,后者数组元素中的通道是按照顺序进行存储的。

通过cv::Mat的默认构造可以创建一个没有大小和数据类型的数组,之后可以通过create成员函数来进行内存空间的申请—通过指定行数、列数和数据类型(type)来配置数组。数组的类型(type)决定了它包含什么样的元素,一个有效的数据类型包含数据的类型和通道数,例如CV_32FC3表示三通道的32位浮点型数据。

S--代表---signed int---有符号整形

U--代表--unsigned int--无符号整形

F--代表--float--单精度浮点型

CV_{8U,16S,16U,32S,32F,64F}C{1,2,3}

注意:你可以创建超过3通道的数组,但是没有既定的宏用于创建,但是可以通过CV_8UC(7)创建一个7通道的8U数据类型数组。

cv::Mat m;
m.create(3,10,CV_32FC3);
m.setTo(cv::Scalar(1.0f, 0.0f, 1.0f));//将第1通道数据设为1.0,第2通道数据设为0.0,第三通道设为1.0
cv::Mat(3, 10, CV_32FC3, cv::Scalar(1.0f, 0.0f, 1.0f));

上面两种初始化的方式是等价的。

  cv::Mat test;
  test.create(2, 3, CV_32FC3);
  test.setTo(cv::Scalar(1.0, 2.0, 3.0));
  LOG(INFO) << "channel0: " << test.at(1, 1)[0];
  LOG(INFO) << "channel1: " << test.at(1, 1)[1];
  LOG(INFO) << "channel2: " << test.at(1, 1)[2];

  float priors_[] = {1.0, 10.0};
  cv::Mat priors(1, 2, CV_32F, priors_);
  LOG(INFO) << "channel0: " << priors.at(0, 0)[0];
  LOG(INFO) << "channel1: " << priors.at(0, 0)[1];
  • Mat_对应的是CV_8U
  • Mat_对应的是CV_8S
  • Mat_对应的是CV_32S
  • Mat_对应的是CV_32F
  • Mat_对应的是CV_64F

2、cv::Mat区块访问

m.row(i)

m中第i行数组

m.col(j)

m中第j列数组

m.rowRange( i0, i1)

m中第i0行到第i1-1行所构成的数组

m.rowRange(cv::Range(i0, i1))

m中第i0行到第i1-1行所构成的数组

m.colRange(j0, j1)

m中第j0列到第j1-1列所构成的数组

m.colRange(cv::Range(j0, j1));

m中第j0列到第j1-1列所构成的数组

m.diag( d);

m中偏移为d的对角线所组成的数组

m(cv::Range(i0,i1),cv::Range(j0,j1))

m中从点(i0, j0)到点(i1-1, j1-1)所包含数据构成的数组

m(cv::Rect(i0, i1, w, h))

m中从点(i0, j0)到点(i0+w-1, j0+h-1)所包含数据构成的数组

m(ranges)

m中依据ranges[0]到ranges[ndim-1]所索引区域构成的数组

3、cv::Mat的其他操作

m1= m0.clone()

从m0进行完全复制,该复制将复制所有的数据元素

m0.copyTo(m1)

将m0复制给m1,如果有必要,将给m1重分配内存空间(等同于m1=m0.clone())

m0.copyTo(m1,mask )

和m0.copyTo(m1)一样,但是只复制mask所指示的区域

m0.convertTo(m1,type,scale,offset)

转换m0中元素的类型(比如CV 32F)并且在尺度变换(默认为1)和增加偏置(默认为0)之后赋值给m1

m0.assignTo( m1,type )

只在内部使用(集成在convertTo中)

m0.setTo( s,mask )

设置m0所有元素为s,如果存在mask,则只对mask指示区域进行操作

m0.reshape( chan,rows)

改变二维数组的有效形状,chan和rows变量可能为0,表示不做更改

m0.push_back(s)

在末尾增加一个mx1大小的数组

m0.push_back( m1)

向mxn大小的矩阵m0增加k行并且复制到m1中,m1大小必须是kxn

m0.pop_back( n)

从mxn大小的矩阵移除n行(默认是1)a

m0.locateROI( size,offset )

将m0的全尺寸写入cv::size 变量size 如果m0只是一个大矩阵的一块小区域,还会写入一个Point类型的offset

m0.adjustROI( t,b,l,r )

通过四个值t (最上),b(最下),1(最左),(最右)调整ROI范围

m0.total()

计算数组序列的元素的数目 (不包括通道)

m0.isContinuous()

如果m0的行之间没有空隙,将返回true

m0.elemSize()

返回m0的位长度(比如三通道浮点矩阵将返回12)
m0.elemSize1() 返回m0最基本元素的位长度(比如三通道浮点矩阵将返回4)

m0.type()

返回m0元素的类型(比如CV 32FC3)

m0.depth()

返回m0通道中的元素类型(比如CV 32F)

m0.channels()

返回m0的通道数目

m0.size()

以cv::Size返回m0的大小

m0.empty() 如果数组没有元素,将返回true (比如m0.total==0或者m0.data==NULL)

通过参数传递Mat对象是怎么处理的?

答案:浅拷贝,形参和实参共用矩阵。

void A()
{
    Mat A; //仅创建信息头部分
    A = imread("1.jpg");

    rectangle(A, cvPoint(250, 350), cvPoint(450, 550), Scalar(0, 255, 0), 1, 1, 0); //对A对象进行操作

    imshow("A", A); //显示A对象图像
	
    test(A);
    
    cout << "实参指针地址:" << &A;    
    
    imshow("A", A); //显示A对象图像
    
}

void test(Mat B)
{
    cout << "形参指针地址:" << &B;
    rectangle(B, cvPoint(100, 200), cvPoint(300, 400), Scalar(0, 0, 255), 1, 1, 0); //对B对象进行操作
    imshow("B", B);
}

在函数调用的过程中,对B对象和A对象的操作会彼此影响,说明两个对象的矩阵是同一个。

传参的过程中,传地址和传值还有区别吗?

答案:有区别,传值时对象发生浅拷贝,传地址传递对象地址。

例子

void test(Mat E)
{
    cout << "形参指针地址:" << &E << endl;
    rectangle(E, cvPoint(150, 250), cvPoint(350, 450), Scalar(0, 0, 255), 1, 1, 0); //对E对象进行操作
    imshow("E", E);
}

void test2(Mat & E)
{
    cout << "形参指针地址:" << &E << endl;
    rectangle(E, cvPoint(200, 300), cvPoint(400, 500), Scalar(0, 0, 255), 1, 1, 0); //对E对象进行操作
    imshow("E2", E);
}

void mat()
{
    Mat A; //仅创建信息头部分
    A = imread(MediaPath + "1.jpg");
	
    rectangle(A, cvPoint(100, 200), cvPoint(300, 400), Scalar(0, 0, 255), 1, 1, 0); //对C对象进行操作

    imshow("A", A); //显示A对象图像

    cout << "实参地址:" << &A << endl;

    test2(A);
    test(A);

    imshow("A", A); //显示A对象图像
}

结果

你可能感兴趣的:(opencv,opencv)