OpenCV计算机视觉开发实践:基于Qt C++ - 商品搜索 - 京东
3.1.1 矩阵类Mat
Mat类是Core模块中常用的一个矩阵类,该类的声明在头文件opencv2\core\core.hpp中,所以使用Mat类时要引入该头文件。类Mat是OpenCV新定义的数据类型,类似于传统的数据类型int、float或String。Mat类用于在内存中存储图像,图像都是二维数组。所以OpenCV定义了处理图像的矩阵类别Matrix,取英文的前3个字母Mat,就如同int取integer的前3个字母一样。我们在处理一块数据的时候,如果使用Mat类,得到的好处是:不需要手动申请一块内存,在不需要时不用再手动释放内存,可以通过类的封装方便地获取数据的相关信息。
Mat利用了类的特性,将内存管理和数据信息封装在类的内部,用户只需要对Mat类对象进行面向对象操作即可。
该类用来保存图像及其他矩阵数据结构(向量场、直方图、张量、点云等),是从OpenCV 2.0以后才使用的,之前一直用C风格的IplImage。使用IplImage最大的问题就是容易造成内存泄露,管理内存相当麻烦,而Mat类的出现不需要我们手动为其开辟空间,也不需要在不用它时立即释放。补充说明一下,很多OpenCV函数仍然手动地管理内存空间,这样不浪费内存,比如传递一个已存在的Mat对象时,开辟好了的那个空间会被再次使用。但手动管理内存不再是必需的,对于初学者来说,完全不用考虑这些。
Mat类由两部分组成:矩阵头和指向存储所有像素值的矩阵的指针。如何理解矩阵头呢?矩阵头相当于矩阵的说明书,它描述了矩阵的尺寸、存储方法、存储地址以及引用次数。何为引用次数?是这样的,矩阵头的尺寸是一个常数,不会随图像的变化而变化,但是存储图像的矩阵可以随图像大小而变化,一般来说,比矩阵头大好几个数量级。而在处理图像时,经常会遇到复制图像、传递图像的操作,此时如果复制整个矩阵,不仅耗费内存,还影响运行效率。所以,OpenCV中的“引用次数”即“计数机制”,让每一个Mat对象都有一个矩阵头,但是它们共享一个矩阵。这是通过让矩阵指针指向同一个地址实现的,比如:
Mat A; //仅仅创建了矩阵头
A = imread("1.jpg",1); //为矩阵开辟一段内存,及创建矩阵
Mat B(A); //拷贝构造函数
Mat C = A; //赋值
这段代码中,A、B和C都是Mat类,它们指向同一个也是唯一一个数据矩阵。虽然它们的信息头不同,但通过任何一个对象所做的改变都会影响其他对象。
通过clone()或者copyTo()复制一个图像,就包括了矩阵本身,因此,改变复制对象的内容并不会改变源矩阵,例如frame1显然是复制frame,因此对frame1的操作并不会改变frame。
当利用 Mat 类定义的时候,只是创建了矩阵头。在使用拷贝构造或者赋值的时候,其实是新创建了不同的信息头和矩阵指针,它们共享一个矩阵。
有人说了,我就想复制整个矩阵,可以吗?当然可以,此时可以使用clone ()函数或者copyTo函数来实现。
Mat D = A.clone() //D等于A的复制品
Mat E;
A.copyTo(E); //把A复制到E
要记住这个结论:Mat是一个类,由两个数据部分组成:矩阵头(包含矩阵尺寸、存储方法、存储地址等信息)和一个指向存储所有像素值的矩阵的指针。矩阵头的尺寸是常数值,但矩阵本身的尺寸会因图像的不同而不同,通常比矩阵头的尺寸大数个数量级。复制矩阵数据往往会花费较多时间,因此除非有必要,不要复制大的矩阵。
Mat类的两个数据部分可以用图3-1来表示。
图3-1
要使用Mat类,必须先对Mat对象进行初始化。初始化就是构造Mat对象,创建Mat对象的数据区,并根据需要赋予初值或不赋值(不赋值的话,则保留乱码值,比如205)。
初始化Mat对象通常有这几种方法:构造法、直接赋值法、数组法、create函数法和定义特殊矩阵。
3.1.2 构造法
构造法就是利用Mat的构造函数,但要注意的是,不是所有的构造函数都会创建数据区,有些构造函数只会创建一个Mat信息头,比如:
Mat mymat; //只创建一个Mat信息头,并不会创建数据区
我们可以通过Mat::data指针是否为NULL来判断数据区是否创建,如果为NULL,就说明没有创建数据区。Mat类的常用构造函数如下:
1)Mat::Mat()
无参构造方法,这是默认的构造函数。
2)Mat::Mat(int rows, int cols, int type)
创建行数为rows、列数为col、类型为type的图像(图像元素类型,如CV_8UC3等)。
3)Mat::Mat(Size size, int type)
创建大小为size、类型为type的图像。
4)Mat::Mat(int rows, int cols, int type, const Scalar& s)
创建行数为rows、列数为col、类型为type的图像,并将所有元素初始化为值s。
5)Mat::Mat(Size size, int type, const Scalar& s)
创建大小为size、类型为type的图像,并将所有元素初始化为值s。
6)Mat::Mat(const Mat& m)
将m赋值给新创建的对象,此处不会对图像数据进行复制,m和新对象共用图像数据。
7)Mat::Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP)
创建行数为rows、列数为cols(像素的列数,对于多通道,一列像素可能对应多列矩阵元素)、类型为type的图像,此构造函数不创建图像数据所需的内存,而是直接使用data指定内存,图像的步长由step指定。
8)Mat::Mat(Size size, int type, void* data, size_t step=AUTO_STEP)
创建大小为size、类型为type的图像,此构造函数不创建图像数据所需的内存,而是直接使用data指定内存,图像的行步长由step指定。
9)Mat::Mat(const Mat& m, const Range& rowRange, const Range& colRange)
创建的新图像为m的一部分,具体的范围由rowRange和colRange指定,此构造函数不进行图像数据的复制操作,新图像与m共用图像数据。
10)Mat::Mat(const Mat& m, const Rect& roi)
创建的新图像为m的一部分,具体的范围由roi指定,此构造函数不进行图像数据的复制操作,新图像与m共用图像数据。
11)Mat::Mat (int ndims, const int *sizes, int type, const Scalar &s)
创建维数为ndims、类型为type的矩阵,并将所有元素初始化为值s,每一维数的数量由数组sizes确定,比如3行3列,sizes= { 3, 3 }。
12)Mat (int rows, int cols, int type, void *data, size_t step=AUTO_STEP)
创建行数为rows、列数为cols(像素的列数,对于多通道,一列像素可能对应多列矩阵元素)、类型为type的图像,并且用数组data初始化元素值。
13)Mat (Size size, int type, void *data, size_t step=AUTO_STEP)
创建大小为size、类型为type的图像,并且用数组data初始化元素值。
其中,type表示图像元素类型,其定义形式为:
CV_<bit_depth>(S|U|F)C<number_of_channels>
中文含义为CV_(位数)+(数据类型)+(通道数),位数也叫深度,比如现在创建一个存储灰度图片的Mat对象,这个图像的大小为宽100、高100,现在这张灰度图片中有10000个像素点,它每一个像素点在内存空间所占的空间大小是8bit,所以它对应的就是CV_8。不同的图像有不同的像素类型,对于不同的像素类型,需要在模板参数传入不同的值。像素的数据类型包括CV_32U、CV_32S、CV_32F、CV_8U、CV_8UC3等,这些类型都是什么含义呢?CV_后面的第一个数字表示比特数,第二个数字表示C++中的数据类型,如果还有后面两个字符,这两个字符就表示通道数。例如,对于CV_32U,表示具有32比特的unsigned int类型;对于CV_8UC3,表示具有8比特,并且有三个通道的unsigned char类型,C1、C2、C3、C4则表示通道数是1、2、3、4。例如CV_16UC2,表示元素类型是一个16位的无符号整数,通道为2。OpenCV中具体可选的数据类型如表3-1所示。
表3-1
数据类型 |
含 义 |
CV_8UC1, CV_8UC2, CV_8UC3, CV_8UC4 |
Unsigned 8bits uchar 0~255 |
CV_8SC1,CV_8SC2,CV_8SC3,CV_8SC4 |
Signed 8bits char -128~127 |
CV_16UC1,CV_16UC2,CV_16UC3,CV_16UC4 |
Unsigned 16bits ushort 0~65535 |
CV_16SC1,CV_16SC2,CV_16SC3,CV_16SC4 |
Signed 16bits short -32768~32767 |
CV_32SC1,CV_32SC2,CV_32SC3,CV_32SC4 |
Signed 32bits int -2147483648~2147483647 |
CV_32FC1,CV_32FC2,CV_32FC3,CV_32FC4 |
Float 32bits float -1.18*10-38~3.40*10-38 |
CV_64FC1,CV_64FC2,CV_64FC3,CV_64FC4 |
Double 64bits double |
有时会遇到不带通道数的类型,如CV_32S、CV_8U等,通常不带通道数的类型默认通道数为1,例如CV_8U就等同于CV_8UC1,CV_32S就等同于CV_32SC1。
Scalar是一个可以用来存放4个double数值的数组,没有提供的值默认是0,一般用来存放像素值(不一定是灰度值),最多可以存放4个通道,其定义如下:
typedef struct Scalar
{
double val[4];
}Scalar;
比如,Mat M(7,7,CV_32FC2,Scalar(1,3));表示一个二通道,且每个通道的值都为(1,3),深度为32,7行7列的图像矩阵。
另外,要注意多通道矩阵的表示,比如RGB的图,假设分辨率为40×40像素,则每个像素由R、G、B三个通道构成,一般行的排列方式为BGR依次交错排列(特殊情况是每个通道排列一行),则在OpenCV中每行的长度为40×3个数(120列),行数依旧是40。简单地说,就是每个像素点都是由1×3的小矩阵构成的。比如,我们定义一个5×5的3通道矩阵,并赋初值为4,5,6。
Mat r5(Size(5,5), CV_8UC3, Scalar(4, 5,6));
每一行的依次3列的3个元素表示一个像素(这三列的每一列就是一个通道,3列就是3通道,3列元素的值分别为4,5,6),全部矩阵元素表示如下:
[ 4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6;
4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6;
4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6;
4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6;
4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 6]
我们看第一行,每3列就是4,5,6,表示一个像素的3个通道,合起来就表示一个像素;第二行也是如此。
还要注意的是,在OpenCV中,Size_是一个模板类,有成员函数Size_(_Tp _width, _Tp _height);,注意宽在前、高在后(列在前、行在后),而Size只不过是Size_的重命名:
typedef Size_<int> Size2i; //此时_Tp相当于int
typedef Size_<int64> Size2l;
typedef Size_<float> Size2f;
typedef Size_<double> Size2d;
typedef Size2i Size; //定义Size
而宽度就是矩阵像素的列数,高度就是矩阵像素的行数,所以Size(m,n)表示矩阵像素有n行,m列、不要弄反了。
比如,指定矩阵的行和列,并表示为4通道的矩阵,每个点的颜色值为(0, 0, 0, 255),代码可以这样写:
cv::Mat M1(3, 3, CV_8UC4, cv::Scalar(0, 0, 0, 255));
std::cout << "M1 = " << std::endl << M1 << std::endl;
输出结果如下:
M1 =
[ 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255;
0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255;
0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255]
下面我们利用多种形式构造函数来创建Mat类对象。
【例3.1】多方法构造Mat类对象
(1)打开Qt Creator,新建一个控制台工程。
(2)在IDE中打开main.cpp,并输入代码如下:
#include <iostream>
#include "opencv2/imgcodecs.hpp"
using namespace cv; //所有OpenCV类都在命名空间cv下
using namespace std;
int main(void)
{
Mat r1; //构造无参数矩阵
Mat r2(2, 2, CV_8UC1); //构造两行两列,深度是8位比特的单通道矩阵
Mat r3(Size(3, 2), CV_8UC3); //构造行数是2,列数是3*通道数,深度是8位比特的3通道矩阵
//构造4行4列像素,深度是8位比特的2通道矩阵,且每个像素的通道初始值是1和3
Mat r4(4, 4, CV_8UC2, Scalar(1, 3));
//构造5行3列像素矩阵,深度是8位比特的3通道矩阵,且每个像素的通道初始值是4,5,6
Mat r5(Size(3,5), CV_8UC3, Scalar(4, 5,6));
//将r5赋值给r6,公用数据对象
Mat r6(r5);
//通过数组初始化矩阵维数
int sz[2] = { 3, 3 };
cv::Mat r7(2, sz, CV_8UC1, cv::Scalar::all(1));
//通过数组初始化矩阵数据
int a[2][3] = { 1, 2, 3, 4, 5, 6};
Mat r8(2,3,CV_32S,a); //float 对应的是CV_32F,double对应的是CV_64F,默认为单通道
cout << r1 << endl<<r2<<endl<<r3<<endl << r4 << endl << r5 << endl << r6 << endl << r7 <<endl << r8;
}
图3-2
上述代码中,我们利用Mat的不同构造函数创建了Mat对象。Size表示大小时,第一个参数是列数,第二个参数是行数。另外要注意的是,对于多通道矩阵,一列像素对应多列通道值。比如3通道,某一行3列矩阵元素表示一个像素值。构造了6个矩阵后,我们最后用cout输出每个矩阵的所有元素。
(3)保存工程并按Ctrl+R键运行,运行结果如图3-2所示。
3.1.3 直接赋值法
Mat矩阵比较小时,可以使用直接赋值法。直接赋值法就是利用Mat_。Mat_也是一个类,该类是对Mat类的一个包装,其定义如下:
template<typename _Tp> class Mat_ : public Mat
{
public:
//只定义了几个方法
//没有定义新的属性
};
如果要让每个像素取不同的值,可以直接用Mat_赋值,代码如下:
Mat r8 = (Mat_<double>(3, 3) <<1, 2, 3, 4, 5, 6, 7, 8,9);
cout << "r8 total matrix:\n" << r1 << endl;
输出结果如下:
[1, 2, 3;
4, 5, 6;
7, 8, 9]
3.1.4 数组法
这个方法就是使用数组或指针传入Mat构造函数。这个构造函数是:
Mat (int rows, int cols, int type, void *data, size_t step=AUTO_STEP)
创建行数为rows、列数为cols(像素的列数,对于多通道,一列像素可能对应多列矩阵元素)、类型为 type 的图像,并且用数组data初始化元素值。
Mat (Size size, int type, void *data, size_t step=AUTO_STEP)
创建大小为size、类型为type的图像,并且用数组data初始化元素值。data就是要传入数据的指针,比如:
int a[2][3] = { 1, 2, 3, 4, 5, 6}; //定义2行3列二维数组
Mat m1(2,3,CV_32S,a); //float对应的是CV_32F,double对应的是CV_64F,若不带通道数,则默认通道数是1
cout << m1 << endl;
输出结果如下:
[1, 2, 3;
4, 5, 6]
数组适合操作数据量大的情况,比如可以通过for循环来构造二维数组,然后给Mat赋值。
3.1.5 create函数法
成员函数create可以分配新的矩阵数据,即重新创建矩阵元素数据,函数声明如下:
void create (int rows, int cols, int type);
其中rows表示要创建的矩阵行数;cols表示要创建的矩阵列数;type表示图像矩阵元素的类型,比如CV_8UC3。
以下代码实现4×4的二维单通道矩阵,矩阵中的数据为乱值:
Mat M3;
M3.create(4, 4, CV_8UC1);
std::cout << "M3 = " << std::endl << M3 << std::endl;
输出结果如下:
[205, 205, 205, 205;
205, 205, 205, 205;
205, 205, 205, 205;
205, 205, 205, 205]