【从零学习OpenCV 4】这4种读取Mat类元素的的方法你都知道么?
重磅干货,第一时间送达
经过几个月的努力,小白终于完成了市面上第一本OpenCV 4入门书籍《从零学习OpenCV 4》。为了更让小伙伴更早的了解最新版的OpenCV 4,小白与出版社沟通,提前在公众号上连载部分内容,请持续关注小白。
对于Mat类矩阵的读取与更改,我们已经在矩阵的循环赋值中见过如何用at方法对矩阵的每一位进行赋值,这只是OpenCV提供的多种读取矩阵元素方式中的一种,本小节将详细介绍如何读取Mat类矩阵中的元素,并对其数值进行修改。在学习如何读取Mat类矩阵元素之前,首先需要知道Mat类变量在计算机中是如何存储的。多通道的Mat类矩阵是一个类似于三维的数据,而计算机的存储空间是一个二维空间,因此Mat类矩阵在计算机存储时是将三维数据变成二维数据,先存储第一个元素每个通道的数据,之后再存储第二个元素每个通道的数据。每一行的元素都按照这种方式进行存储,因此如果我们找到了每个元素的起始位置,便可以找到这个元素中每个通道的数据。图2-5展示了一个三通道的矩阵的存储方式,其中连续的蓝色、绿色和红色的方块分别代表每个元素的三个通道。
图2-5 三通道3*3矩阵存储方式
了解了Mat类变量的存储方式之后,我们来看一下Mat类具有的属性,我们在表2-2中列出了常用的属性,同时详细的介绍了每种属性的作用。
表2-2 Mat类矩阵的常用属性
属性 |
作用 |
cols |
矩阵的列数 |
rows |
矩阵的行数 |
step |
以字节为单位的矩阵的有效宽度 |
elemSize() |
每个元素的字节数 |
total() |
矩阵中元素的个数 |
channels() |
矩阵的通道数 |
这些属性之间互相组合可以得到多数Mat类矩阵的属性,例如step属性与cols属性组合,可以求出每个元素所占据的字节数,而再与channels()属性结合,就可以知道每个通道的字节数,进而知道矩阵中存储的数据量的类型。接下来通过一个例子来具体说明每个属性的用处,用Mat (3, 4, CV_32FC3)定义一个矩阵,这时通道数channels()为3;列数cols为4;行数rows为3;矩阵中元素的个数为3*4,结果为12;每个元素的字节数为32/8*channels(),最后结果为12;以字节为单位的有效长度step为eleSize()*cols,结果为48。
常用的Mat类矩阵的元素读取方式有:通过at方法进行读取、通过指针ptr进行读取、通过迭代器进行读取、通过矩阵元素的地址定位方式进行读取。接下来将详细的介绍这四种读取方式。
1
01
通过at方法读取Mat类矩阵中的元素
通过at方法读取矩阵元素分为针对单通道的读取方法和针对多通道的读取方法,在代码清单2-19中给出了通过at方法读取单通道矩阵元素的代码。
代码清单2-19 at方法读取Mat类单通道矩阵元素
cv::Mat a = (cv::Mat_<uchar>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
int value = (int)a.at<uchar>(0, 0);
通过at方法读取元素需要在后面跟上“<数据类型>”,如果此处的数据类型与矩阵定义时的数据类型不相同,就会出现因数据类型不匹配的报错信息。该方法以坐标的形式给出需要读取的元素坐标(行数,列数)。需要说明的是,如果矩阵定义的是uchar类型的数据,在需要输入数据的时候,需要强制转换成int类型的数据进行输出,否则输出的结果并不是整数。
由于单通道图像是一个二维矩阵,因此在at方法的最后给出二维平面坐标即可访问对应位置元素。而多通道矩阵每一个元素坐标处都是多个数据,因此引入一个变量用于表示同一元素多个数据。在openCV 中,针对3通道矩阵,定义了cv::Vec3b、cv::Vec3s、cv::Vec3w、cv::Vec3d、cv::Vec3f、cv::Vec3i六种类型用于表示同一个元素的三个通道数据。通过这六种数据类型可以总结出其命名规则,其中的数字表示通道的个数,最后一位是数据类型的缩写,b是uchar类型的缩写、s是short类型的缩写、w是ushort类型的缩写、d是double类型的缩写、f是float类型的缩写、i是int类型的缩写。当然OpenCV也为2通道和4通道定义了对应的变量类型,其命名方式也遵循这个命名规则,例如2通道和4通道的uchar类型分别用cv::Vec2b和cv::Vec4b表示。代码清单2-20中给出了通过at方法读取多通道矩阵的实现代码。
代码清单2-20 at方法读取Mat类多通道矩阵元素
cv::Mat b(3, 4, CV_8UC3, cv::Scalar(0, 0, 1));
cv::Vec3b vc3 = b.at<cv::Vec3b>(0, 0);
int first = (int)vc3.val[0];
int second = (int)vc3.val[1];
int third = (int)vc3.val[2];
在使用多通道变量类型时,同样需要注意at方法中数据变量类型与矩阵的数据变量类型相对应,并且cv::Vec3b类型在输入每个通道数据时需要将其变量类型强制转成int类型。不过,如果直接将at方法读取出的数据直接赋值给cv::Vec3i类型变量,就不需要在输出每个通道数据时进行数据类型的强制转换。
1
02
通过指针ptr读取Mat类矩阵中的元素
前面我们分析过Mat类矩阵在内存中的存放方式,矩阵中每一行中的每个元素都是挨着存放,如果找到每一行元素的起始地址位置,那么读取矩阵中每一行不同位置的元素就是将指针在起始位置向后移动若干位即可。在代码清单2-21中给出了通过指针ptr读取Mat类矩阵元素的代码实现。
代码清单2-21 指针ptr读取Mat类矩阵元素
cv::Mat b(3, 4, CV_8UC3, cv::Scalar(0, 0, 1));
for (int i = 0; i < b.rows; i++)
{
uchar* ptr = b.ptr<uchar>(i);
for (int j = 0; j < b.cols*b.channels(); j++)
{
cout << (int)ptr[j] << endl;
}
}
在程序里,首先有一个大循环用来控制矩阵中每一行,之后定义一个uchar类型的指针ptr,在定义时需要声明Mat类矩阵的变量类型,并在定义最后用小括号声明指针指向的Mat类矩阵的哪一行。第二个循环控制用于输出矩阵中每一行所有通道的数据。根据图2-5中所示的存储形式,每一行中存储的数据数量为列数与通道数的乘积,即指针可以向后移动cols*channels()-1位,如第7行代码所示,指针向后移动的位数在中括号给出。程序中给出了循环遍历Mat类矩阵中的每一个数据的方法,当我们能够确定需要访问的数据时,可以直接通过给出行数和指针后移的位数进行访问,例如当读取第2行数据中第3个数据时,可以用a.ptr<uchar>(1)[2]这样的形式来直接访问。
1
03
通过迭代器访问Mat类矩阵中的元素
Mat类变量同时也是一个容器变量,所以Mat类变量拥有迭代器,用于访问Mat类变量中的数据,通过迭代器可以实现对矩阵中每一个元素的遍历,代码实现在代码清单2-22中给出。
代码清单2-22 指针ptr读取Mat类矩阵元素
cv::MatIterator_<uchar> it = a.begin<uchar>();
cv::MatIterator_<uchar> it_end = a.end<uchar>();
for (int i = 0; it != it_end; it++)
{
cout << (int)(*it) << " ";
if ((++i% a.cols) == 0)
{
cout << endl;
}
}
Mat类的迭代器变量类型是cv::MatIterator_< >,在定义时同样需要在括号中声明数据的变量类型。Mat类迭代器的起始是Mat.begin< >(),结束是Mat.end< >(),与其他迭代器用法相同,通过“++”运算实现指针位置向下迭代,数据的读取方式是先读取第一个元素的每一个通道,之后再读取第二个元素的每一个通道,直到最后一个元素的最后一个通道。
1
04
通过矩阵元素地址定位方式访问元素
前面三种读取元素的方式都需要知道Mat类矩阵存储数据的类型,而且在从认知上,我们更希望能够通过声明“第x行第x列第x通道”的方式来读取某个通道内的数据,代码清单2-23中给出的就是这种读取数据的方式。
代码清单2-23 通过矩阵元素地址定位方式访问元素
(int)(*(b.data + b.step[0] * row + b.step[1] * col + channel));
代码中row变量的含义是某个数据所在元素的行数,col变量的含义是某个数据所在元素的列数,channel变量的含义是某个数据所在元素的通道数。这种方式与我们通过指针读取数据的形式类似,都是通过将首个数据的地址指针移动若干位后指向需要读取的数据,只不过这种方式可以通过直接给出行、列和通道数进行读取,不需要用户再进行计算某个数据在这行数据存储空间中的位置。