4.2.1 定义
Mat表示一个n维密集的数值单通道或多通道阵列。它可以用来存储实数或复数的向量和矩阵、灰度或彩色图像、体素体积、向量场、点云、张量、直方图(但是,非常高维的直方图可以更好地存储在SparseMat中)。数组的数据布局由数组M定义M.step[],因此元素的地址(i0,...,iM.dims−1)0≤ik<M.size[k]
对于二维数组,将上述公式简化为:
请注意M.step[i]>=M.step[i+1](事实上M.step[i]>= M.step[i+1]*M.size[i+1])。这意味着逐行存储二维矩阵,逐行存储三维矩阵,等等。M.step[M.dims-1]是最小的并且总是等于元素大小M.elemSize()。
因此,Mat中的数据布局与OpenCV 1.x中的CvMat,IplImage和CvMatND类型完全兼容。它还与标准工具包和SDK中的大多数密集数组类型兼容,例如:Numpy (ndarray)、Win32(独立设备位图)和其他类型,即任何使用steps (or strides)计算像素位置的数组。由于这种兼容性,可以为用户分配的数据制作一个Mat头,并使用OpenCV函数对其进行处理。
创建Mat对象有许多不同的方法。最佳选项如下:
(1)使用create(nrows, ncols, type)方法或类似的Mat(nrows, ncols, type[, fillValue])构造函数。分配指定大小和类型的新数组。类型具有与cvCreateMat方法中相同的含义。例如,CV_8UC1表示8位单通道数组,CV_32FC2表示2通道(复杂)浮点数组,等等。
// 创建一个填充1 + 3j的7x7复杂矩阵
Mat M(7,7,CV_32FC2,Scalar(1,3));
// 现在将M转换为100x60 15通道8位矩阵。
// 旧内容将被取消分配
M.create(100,60,CV_8UC(15));
create()在当前数组的形状或类型与指定数组不同时分配一个新数组。
(2)创建一个多维数组
// 创建一个100x100x100 8位阵列
int sz[] = {100, 100, 100};
Mat bigCube(3, sz, CV_8U, Scalar::all(0));
它将1维数传递给Mat构造函数,并创建二维的数组将,列数设置为1。因此,Mat::dims总是>= 2(当数组为空时是0)。
(3)使用拷贝构造函数或赋值运算符,其中可以在右侧有数组或表达式(参见下面)。数组赋值是一个O(1)操作,因为它只复制头并增加引用计数器。clone()方法可用于在需要时获取数组的完整(深度)副本。
(4)为另一个数组的一部分构造一个标头。它可以是单行、单列、多行、多列、数组中的矩形区域(在代数中称为次要区域)或对角线。这些操作也是O(1),因为新头引用了相同的数据。你可以使用这个功能修改数组的一部分,例如:
// 第三行修改为加上第五行乘以3
M.row(3) = M.row(3) + M.row(5)*3;
//将第7列复制到第1列
// M.col(1) = M.col(7); //无效果
Mat M1 = M.col(1);
M.col(7).copyTo(M1);
// 创建一个新的320x240的img
Mat img(Size(320,240),CV_8UC3);
// 从图中选择指定的区域
Mat roi(img, Rect(10,10,100,100));
// 用(0,255,0)(RGB空间为绿色)填充ROI;
// 将修改原始的320x240图像
roi = Scalar(0,255,0);
由于添加了datastart和dataend成员,可以使用locateROI()计算主容器数组中的相对子数组位置:
Mat A = Mat::eye(10, 10, CV_32S);
// 提取A列, 包括第1列,不包括第3列
Mat B = A(Range::all(), Range(1, 3));
// 提取B行, 包括第5行,不包括第9行
Mat C = B(Range(5, 9), Range::all());
Size size;
Point ofs;
C.locateROI(size, ofs);
// size(width = 10,height = 10),ofs将是(x = 1,y = 5)
对于整个矩阵,如果需要深度复制,可以使用提取的子矩阵的clone()方法。
(5)根据用户分配的数据创建头。
a、使用OpenCV处理“外部”数据(例如,当您实现DirectShow*过滤器或gstreamer的处理模块时,等等)。例如:
void process_video_frame(const unsigned char* pixels, int width, int height, int step)
{
Mat img(height, width, CV_8UC3, pixels, step);
GaussianBlur(img, img, Size(7,7), 1.5, 1.5);
}
b、快速初始化小矩阵和/或获得超快元素访问。
double m[3][3] = {{a, b, c}, {d, e, f}, {g, h, i}};
Mat M = Mat(3, 3, CV_64F, m).inv();
常见的情况是将用户分配的数据从CvMat和IplImage转换到Mat,函数cv::cvarrToMat将指针指向CvMat或IplImage,并使用可选标志指示是否复制数据。
(6)使用matlab样式的数组初始化器,zeros(), ones(), eye(), 例如:
//创建一个双精度单位矩阵,并与M相加.
M += Mat::eye(M.rows, M.cols, CV_64F);
(7)使用逗号分隔的初始化器:
// 创建一个3x3双精度单位矩阵
Mat M = (Mat_<double>(3,3) << 1, 0, 0, 0, 1, 0, 0, 0, 1);
使用这种方法,首先使用适当的参数调用Mat类的构造函数,然后只需输入<<操作符,后跟逗号分隔的值,这些值可以是常量、变量、表达式等等。另外,请注意避免编译错误所需的额外括号。
创建阵列后,它将通过引用计数机制自动管理。如果数组头是在用户分配的数据之上构建的,那么您应该自己处理数据。当没有人指向它时,数组数据被释放。如果要在调用数组析构函数之前释放数组头指向的数据,请使用Mat::release()。
关于数组类要学习的下一个重要内容是元素访问。Opencv手册已经描述了如何计算每个数组元素的地址。通常,我们不需要在代码中直接使用该公式。如果知道数组元素类型(可以使用Mat::type()方法检索),则可以访问二维数组的元素Mij:
M.at<double>(i,j) += 1.f;
假设这M是一个双精度浮点数组。对于不同数量的维度,该方法有几种变体。
如果需要处理整个2D数组,最有效的方法是首先获取指向行的指针,然后只使用普通的C运算符[]:
//计算正矩阵元素的总和
// (假设M是双精度矩阵)
double sum=0;
for(int i = 0; i < M.rows; i++)
{
const double* Mi = M.ptr<double>(i);
for(int j = 0; j < M.cols; j++)
sum += std::max(Mi[j], 0.);
}
有些操作,如上面的,实际上并不依赖于数组的形状。它们只是逐个处理一个数组的元素(或者来自多个具有相同坐标的数组的元素,例如数组加法)。这样的操作称为元素智能操作。检查所有输入/输出数组是否连续是有意义的,即每行末尾没有空格。如果是,将它们处理为一长行:
//计算正矩阵元素之和
double sum=0;
int cols = M.cols, rows = M.rows;
if(M.isContinuous())
{
cols *= rows;
rows = 1;
}
for(int i = 0; i < rows; i++)
{
const double* Mi = M.ptr<double>(i);
for(int j = 0; j < cols; j++)
sum += std::max(Mi[j], 0.);
}
对于连续矩阵,外环体只执行一次。因此,开销更小,这在小矩阵的情况下尤其明显。
最后,还有一些stl风格的迭代器,它们足够聪明,可以跳过连续行之间的间隙:
// 计算正矩阵元素的和,基于迭代器的变量
double sum=0;
MatConstIterator_ it = M.begin(), it_end = M.end();
for(; it != it_end; ++it)
sum += std::max(*it, 0.);
矩阵迭代器是随机访问迭代器,因此可以将它们传递给任何STL算法,包括std::sort()。