【OpenCV C++20 学习笔记】基本图像容器------Mat
概述
电子设备中储存的图像本质是图像在每个像素点上的数值,这些数据形成一个矩阵,除此之外还包括一些描述这个数值矩阵的信息。OpenCV作为一个计算机视觉库,同样也是要处理这样的信息。所以,要学习OpenCV,首要的事情是了解在OpenCV中是如何储存图像的数值矩阵以及描述信息的。
(本文较长,详细介绍了Mat对象的原理、创建方式和输出方式,读者可根据目录跳转至相关章节)
Mat
2001年OpenCV诞生的时候,是在C语言的接口上创建的,并将图像储存在一个称作IplImage的C语言数据结构中。这种方式最大的一个缺点就是需要用户自己管理内存。如果是小型的项目尚可,如果数据量变大,管理内存就会使人很头疼。
OpenCV2.0 引入了C++接口,实现了内存的自动管理。Mat
成为了OpenCV储存图片信息的数据结构。
Mat
不需要手动分配或释放内存,大部分的OpenCV方法都会自动为输出的Mat
对象分配内存。如果你已经为一个Mat
对象分配了它需要的内存,那么你在传输它的时候,这个内存会被重复利用。也就是说,在执行任务的时候,不会使用多余的内存。
内部结构
Mat
实质上是一个包含了两个部分的类:
- 矩阵头(matrix header):它包含了矩阵的大小、存储方式、存储地址等信息
- 指向矩阵的指针:
Mat
对象只是储存了矩阵的指针,并没有储存矩阵本身,而矩阵中包含了像素值(像素值矩阵的维度由存储方式决定)
Mat
对象的大小是固定的,但是矩阵本身的大小是跟随图像变化的。
在函数间传递图像是OpenCV中非常常见的操作,而且某些图像处理算法很复杂。为了提高程序的运行速度,OpenCV使用了"引用计数机制"。每个Mat
对象都有自己独立的矩阵头,但是同一个矩阵可能会被多个Mat
对象共享,即多个Mat
对象的指针可能会指向内存中的同一个矩阵。而复制操作只会复制Mat
对象的矩阵头以及指向矩阵的指针,并不会直接复制矩阵的数值!
下面的代码详细展示了Mat
对象在实际应用中的内存分配问题:
cpp
Mat A, C; //创建Mat对象的时候只是创建了矩阵头的部分
A = imread(argv[1], IMREAD_COLOR); //读取图片,分配内存存储图片的数值矩阵,并将A的指针指向这个矩阵的内存地址
Mat B(A); //调用复制构造函数创建B,但仅仅是将A中的指向图片矩阵的指针复制到B中,并没有复制图片的数值矩阵
C = A; //赋值操作也只是将A中的指针复制到C中
上面的代码最终使A、B、C3个Mat对象中的指针都指向同一个图片的数值矩阵,虽然进行了复制和赋值操作,但内存中始终只有一个数值矩阵,如下图:
因为3个Mat
对象的指针都是指向同一个数据矩阵,所以在任何一个Mat
对象中对数据矩阵进行修改都会影响到其他Mat
对象。实际上,不同的Mat
对象只是为处理同一个数据矩阵提供了不同的使用方法。但是这些Mat
对象的矩阵头部分是不同的,你甚至可以创建一个只指向数据矩阵的其中一部分的Mat
对象。例如,要想在图像中创建一个感兴趣区域(region of interest,ROI),你可以新建一个Mat
对象:
cpp
Mat D(A, Rect(10, 10, 100, 100); //使用矩形区域
Mat E = A(Range::all(), Range(1, 3)); //使用行和列
引用计数机制
如果像上面的例子一样,同一个数据矩阵属于不同的Mat
对象,那到底谁来负责释放它的内存呢?答案是:最后一个使用它的Mat
对象。这就是通过上面所说的"引用计数机制"来实现的。当有指向数据矩阵A的Mat
对象被复制的时候,矩阵A的引用计数就会增加;当有指向矩阵A的Mat
对象被销毁的时候,矩阵A的引用计数就会减少。当计数为0的时候,矩阵A就会被释放。
OpenCV还提供了深度复制数据矩阵的方法,当你不想只是复制指针,而是想复制矩阵的值的时候,可以使用cv::Mat::clone()
和cv::Mat::copyTo()
方法。
cpp
Mat F = A.clone(); //将A指向的数据矩阵复制给F
Mat G;
A.copyTo(G); //将A指向的数据矩阵复制到G
这样,修改F和G的时候就不会影响A指向的数据矩阵了。
总结一下:
- OpenCV中函数导出的图像数据是自动分配内存的(除非特别指定不自动分配)
- 使用OpenCV的C++接口的时候不用考虑内存管理的问题
- 赋值运算符和复制构造函数只是复制Mat对象的头部信息和指针
- 可以用
cv::Mat::clone()
和cv::Mat::copyTo()
方法实现底层的图片数据矩阵的复制
颜色数据格式
对于如何储存像素的值,通常从两个方面考虑:颜色空间和数据类型。
颜色空间是指利用基本的颜色组合成特定的颜色的方式。有多种方式可以选择:
- RGB:这是最常用的,因为它与人眼编码颜色的方式相似;由红、绿、蓝3中基本颜色的值,加上透明度alpha,来确定最终颜色;注意,OpenCV中的标准颜色显示系统为BGR,红色和蓝色的值调换了位置
- HSV和HLS:将颜色分解为色调、饱和度和亮度;这种方式能更方便地处理图片的亮度
- YCrCb:这是JPEG格式的图片常用的颜色编码方式
- CIT Lab*:这种编码方式能够方便测量两种颜色之间的差距
- 灰度:只有黑色和白色两种基本颜色
显式创建Mat
对象
使用cv::Mat::Mat
构造函数
cpp
Mat M(2,2, CV_8U3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
这里使用了Mat
类的其中一个构造函数。该构造函数一共包括4个参数:
- 行数:定义矩阵行数
- 列数:定义矩阵列数
- 数据类型:定义每个数据项的类型,下文详述
Scalar
常量:用来定义每个数据项的值的向量数组
矩阵的数据项
矩阵数据项的数据类型的定义遵循以下语法规则:
CV_[每个数据项的比特数][有符号或无符号][类型前缀]C[通道数量]
- 比特数:确定每个数据项,即像素点,的数值的长度,如8比特;比特数越高,每个像素点的值域就越大,比如,32比特的浮点类型比8比特的char类型能够储存更多的颜色值
- 有符号或无符号:确定每个数据项的值是否是有符号的(可省略,默认为无)
- 类型前缀:如果是
char
类型,则为C,如果是float
类型,则为F...... - 通道数量:确定每个数据项中包含的颜色通道数量;比如,RGB颜色空间可以有4个通道,分别是红色值、绿色值、蓝色值和透明度值;通道数量可以加上括号,如
CV_8UC(3)
(可省略,默认为1)
上面代码中的CV_8U3
就代表每个数据项的是具有3个通道的8比特无符号的值,输出结果如下:
可以看到矩阵中每个项有3个数值,代表3个颜色通道;共有2*2个项;每个项中的3个颜色通道的值都与Scalar
中定义的相同。
使用数组进行初始化的构造函数
除了2维的矩阵,也可以创建3维矩阵的Mat
对象
cpp
int sz[3]{ 2,2,2 };
Mat L(3, sz, CV_8UC1, Scalar::all(0));
这个构造函数也使用4个参数:
- 维度:确定矩阵的维度
- 大小:一个数组,用来确定每个维度的大小
- 数据项的数据类型:同上一个构造函数
Scalar
常量:同上一个构造函数
所以,这里创建了一个3维的矩阵,每个维度都只有2个数据项,即222;每个数据项使用的都是只有1个颜色通道的8比特无符号数值;每个数据项的值都为0。
cv::Mat::create
函数
这个函数看起来像是在创建一个Mat
对象,但其实它只能修改已有的Mat
对象。
比如,对上面创建的M对象进行修改:
cpp
M.create(4, 4, CV_8UC2);
cout << "M = "<< endl << " " << M << endl << endl;
cv::Mat::create
函数使用了3个参数:
- 行数:修改后的行数
- 列数:修改后的列数
- 数据项类型:修改后的数据项类型
输出结果为:
可以看到原本22的3颜色通道的矩阵变成了4 4的2颜色通道矩阵。cv::Mat::create
函数为M对象重新分配了内存,使其能储存修改之后的更大的矩阵。
MATLAB风格的初始化
cv::Mat::zeros
, cv::Mat::ones
, cv::Mat::eye
等与MATLAB语言类似的函数也可以用来初始化OpenCV中的Mat
对象
zeros
函数用来创建全为0值的矩阵;ones
函数用来创建全为1值的矩阵;eye
函数用来创建对角线为1,其他值为0的矩阵
cpp
Mat E {Mat::eye(4, 4, CV_64F)};
cout << "E = " << endl << " " << E << endl << endl;
Mat O {Mat::ones(2, 2, CV_32F)};
cout << "O = " << endl << " " << O << endl << endl;
Mat Z {Mat::zeros(3,3, CV_8UC1)};
cout << "Z = " << endl << " " << Z << endl << endl;
这些函数都使用相同的参数列表:
- 行数
- 列数
- 数据项类型
输出结果如下:
小型矩阵
如果要构造小型矩阵,可以直接以逗号为间隔,用<<
运算符将每个值一行一行依次输入;
在C++11之后,也可以使用{}风格的初始化列表
cpp
//<<运算符
Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C = " << endl << " " << C << endl << endl;
//初始化列表
C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3); //reshape函数将矩阵中的数据项变成3通道类型
cout << "C = " << endl << " " << C << endl << endl;
输出结果如下:
通过复制创建Mat
对象
要复制Mat
对象,需要第二节讲的使用cv::Mat::clone
或 cv::Mat::copyTo
函数
Mat
对象的输出
上面的例子中的输出使用的都是默认格式,但还有几种其他的输出格式
首先使用随机数创建一个3通道的3*2矩阵
cpp
Mat R {Mat(3, 2, CV_8UC3)};
randu(R, Scalar::all(0), Scalar::all(255));
cv::randu()
为随机数生成函数,使用3个参数:
Mat
对象:用来储存随机值的Mat对象- 最低值:
Scalar
常量类型,确定随机数的最小值 - 最高值:
Scalar
常量类型,确定随机数的最大值
接下来使用format
函数定义输出格式,该函数使用两个参数: Mat
对象:需要输出的Mat
对象- 格式定义:在
Formatter
中定义的枚举类型
详见以下代码:
cpp
cout << "R (default) = " << endl << " " << R << endl << endl;
cout << "R (Python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;
cout << "R (csv) = " << endl << format(R, Formatter::FMT_CSV) << endl << endl;
cout << "R (numpy) = " << endl << format(R, Formatter::FMT_NUMPY) << endl << endl;
cout << "R (C) = " << endl << format(R, Formatter::FMT_C) << endl << endl;
输出结果如下:
其他普通数据项的输出
OpenCV中的大部分数据结构都支持<<
运算符
以下代码展示了如何运用<<
运算符输出点、向量类型的对象
cpp
Point2f P(5, 1);
cout << "Point (2D)= " << P << endl << endl;
Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;
vector<float> v;
v.push_back((float)CV_PI); v.push_back(2); v.push_back(3.01f);
cout << "Vector of floats via Mat = " << Mat(v) << endl << endl;
vector<Point2f> vPoints(20);
for (size_t i = 0; i < vPoints.size(); ++i)
vPoints[i] = Point2f((float)(i * 5), (float)(i % 7));
cout << "A vector of 2D Points = " << vPoints << endl << endl;
输出结果如下: