本人在2025年3月开始学习opencv,因此写下所学内容以及实时的思考,以鼓励自己坚持学习和梳理自己的知识结构
1、Mat类型的变量的定义
Mat是opencv中的一个重要的类,它的核心是其中存储的数组,其他元素几乎是围绕这个数组来实现的。在其他语言之中,或者说其他库之中,涉及到图像的基本也都是使用一个和Mat类似的结构来完成所有的功能。毕竟计算机把图像以颜色或亮度的方式来存储的,所以一张图片的核心内容是计算机中表现为一个数组,这一点是很容易就可以理解并接受的。
接下来说一下Mat类型的变量的定义方式。由于是一个类,所以正常肯定是使用构造函数来初始化一个类对象的,例如下面的写法:
Mat img1(5, 5, CV_8UC3, Scalar(0, 1, 2));
这里就定义了一个对象img1,传入的参数依次是行、列、每一个元素的类型、初始化方式。例如这里的img1,它的核心是一个5行5列的数组,数组中的每一个元素是一个CV_8UC3的类型,也就是8位无符号3通道的数据,其实就是3个数。每一个元素是三个数,听起来写起来都怪怪的,更像是一个三维的数组,事实上确实可以从这个角度来理解,因为无论数据多复杂,只要合法,可以用任何方式去解释这一串数据。
因为C++的多态,Mat的构造函数太多了,常见的还有如下的方式:
Mat img1(5, 5, CV_8UC3);
少了最后一个参数,那么img1的数组则由随机数填充。说是随机数,也可能只是那一块内存原来的数值,这个不确定,具体会出现随机数然后呈现循环出现的现象。
除此之外,还有就是通过new的方式来动态地创建一个对象,这个跟上面的大同小异。
常见的方法还有下面这个:
Mat img2;
img2.create(3, 3, CV_8UC3);
先使用默认构造函数,然后再通过create的方式来设置其他参数,具体细节目前未能了解。
最后要说的是opencv例子中使用的imread:
Mat img = imread(path);
这种方法直接使用一张图片来初始化得到Mat对象,算是在后续图像中会常用的方法,其他方法更像是对更底层意义上的矩阵的处理。
2、Mat的复制
先说一下浅拷贝和深拷贝。简单来说,浅拷贝就是对对象中的元素使用简单的=来进行赋值。对于元素中包含指针这种数据就可能导致一些问题,因为两个对象的元素指向同一个地址,其中一个释放可能会导致另一个的使用出现问题。深拷贝就是对这种可能出现问题的情况进行仔细的处理。
Mat的浅拷贝和C++的浅拷贝自然是同一个概念,,但是由于Mat的设计和实现细节,在表现上又呈现出差异。Mat的核心数据是使用指针在堆区开辟空间(没看细节,从现象来看是这样的),然后对于这个核心的数据又使用了引用计数的方式(感觉类似于智能指针),这使得浅拷贝的方式不会导致堆区空间重复释放的问题,也不会影响另一个对象的使用。但是由于核心数据是同一块内存,所以一方的写会导致另一方被修改。因此,如果确实是希望有两个对象就要使用深拷贝。Mat中深拷贝的一种方式是使用copyTo,如下:
Mat img = imread(path);
Mat tmp;
img.copyTo(tmp);
不过要说这种方式叫深拷贝其实不太合适,毕竟它跟以往在C++中见到的方式不太一样。只能说这种方式取得了深拷贝的效果。因为是刚入门学习这个,所以其他深拷贝方式我也不太清楚。
3、获取各个元素
行和列分别是rows和cols,类型则是存在flag中,通过计算得到具体的类型,这里使用type得到,具体通道数使用channels来获得。可以直接使用<<输出核心数据,这里应该是做了输出运算符<<的重载。
Mat img1(5, 5, CV_8UC3, Scalar(0, 1, 2));
cout << img1.rows << endl;
cout << img1.cols << endl;
cout << img1.type() << " " << CV_8UC3 << endl;
cout << img1.channels() << endl;
cout << img1 << endl;
至于如何得到核心的图像数据,这里就比较复杂了。大体分成两种方法。前面说构造函数的时候提到传入CV_8UC3指定数组每一个元素都是由3个8位无符号整型构成的,这里也是按照得到的数据是以单个数的方式还是以单个元素的方式来区分的。
第一种方式是按照单个数的方式,如下:
int height = img.rows;
int weight = img.cols * img.channels();
for (int i = 0; i < height; i++)
{
uchar *data = img.ptr<uchar>(i);
for (int j = 0; j < weight; j++)
{
data[j] = data[j] / 2;
}
}
由于类型是CV_8UC3,每一个元素都实际上有3个数,因此把核心数据当成二维数组来看的话,宽度是原来的三倍,然后对每一行,使用ptr这个函数来获得这一行的数据。由于Mat对象需要指定元素的类型,因此在获得数据的时候也需要指定合适的类型去获取。这是泛型编程的内容,它使得Mat可以灵活地存放不同类型的数据,虽然这样看起来就比较麻烦。
第二种方式就是按照每一个元素的方式的方式,如下:
int height = img.rows;
int weight = img.cols;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < weight; j++)
{
Vec3b &data = tmp.at<Vec3b>(i, j);
data[0] /= 4;
data[1] /= 4;
data[2] /= 4;
}
}
同理,需要指定类型才能正确地获取这个元素。 at返回的是元素的引用。这种方式就是将核心数据当成三维数组来看了。
类比上述两种方法,似乎还可以把核心数据当作一维数组来看,不过图像数据本来就是二维数组,三维的情况也是因为图像一个像素有多个通道。把图像当作一维数组的话,后面使用这个数组还是免不了把他又当作二维或三维来看,这样一来就有点多此一举了。
4、显示图片
最后以一张照片的显示作为第一步学习的结尾。
string path = "xiaohuang.jpg";
Mat img = imread(path);
imshow("xiaohuang", img);
waitKey(0); // 等待任意按键,不加的话图片一闪而过
结果图片太大放不上来。泪目!
至此第一部分结束。第一次写文章,希望可以鼓励自己坚持学习下去。也期待这篇文章能被网络上的大伙带来帮助,虽然这么基础的内容可能对别人也没什么帮助就是了。如果大家看到这觉得有值得交流的地方,欢迎评论区留言,我们互相学习,一起进步。