OpenCV中的Mat数据类型学习笔记

Aug 24,2019   15288 words   55 min


本篇博客主要介绍OpenCV中一个重要且经常使用的数据类型——Mat,它是OpenCV的C/C++ API里特有的类型,Python中用Numpy的NdArray表示。虽然之前也写过很多C++的OpenCV代码,但一直没系统地总结一下相关用法,因此本篇博客从基础开始,介绍Mat的用法与注意事项。

要想使用Mat,需要在CMake里配置好参数,再在代码中导入基本的头文件opencv2/opencv.hpp。这个可以看之前这篇博客里的内容,此处不再介绍。

0.Mat简介

在早期OpenCV中(OpenCV 1.x)大量使用IplImageCvMat实现数据处理,其需要手动进行内存管理,使用不是很方便。到OpenCV 2.x版本后引入C++面向对象思想,重构了代码,引入Mat。作为升级,Mat存储的数据结构与CvMatIplImage等完全兼容,也和Numpy(ndarray)兼容。

1.Mat创建

创建Mat有多种方法,常见的有以下几种:

(1)使用构造函数或create()成员函数

Mat构造函数很多,经典的构造函数的格式如下:Mat(nrows,ncols,type[,fillValue]),中括号里代表可选参数,表示填入的初始值,如果不写默认为0。nrowsncols一般为int型的整数,但ncols也可以为数组,这样表示建立一个多维Mat,此时传入的nrows表示维度。前两个参数还可以用Size(ncol,nrow)代替(注意Size里行列的顺序和函数参数的顺序是反的),这就是另一种构造函数了。create()成员函数格式如下:Mat(nrows,ncols,type),它只能接收3个参数,与构造函数的区别就是无法赋初值。使用示例如下:

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    Mat m1(4, 3, CV_8UC1);
    cout << m1 << endl << endl;

    Mat m2(3, 5, CV_8UC1, 200);
    cout << m2 << endl << endl;

    Mat m3(2, 4, CV_8UC1, -10);
    cout << m3 << endl << endl;

    Mat m4;
    m4.create(2, 3, CV_8UC1);
    cout << m4 << endl;
    cout << "CV_8UC1" << endl;
    cout << "channels:" << m4.channels() << endl;
    cout << "row & col:" << m4.rows << " & " << m4.cols << endl;
    cout << "size:" << m4.size << endl;
    cout << "dims:" << m4.dims << endl << endl;

    Mat m5(3, 2, CV_8UC3, Scalar(20, 10, 30));
    cout << m5 << endl;
    cout << "CV_8UC3" << endl;
    cout << "channels:" << m5.channels() << endl;
    cout << "row & col:" << m5.rows << " & " << m5.cols << endl;
    cout << "size:" << m5.size << endl;
    cout << "dims:" << m5.dims << endl << endl;

    Mat m6(3, 2, CV_8UC(5));
    cout << m6 << endl;
    cout << "CV_8UC5" << endl;
    cout << "channels:" << m6.channels() << endl;
    cout << "row & col:" << m6.rows << " & " << m6.cols << endl;
    cout << "size:" << m6.size << endl;
    cout << "dims:" << m6.dims << endl << endl;

    int sz[] = {3, 4, 2};
    Mat m7(3, sz, CV_8U, Scalar::all(0));
    cout << "CV_8U" << endl;
    cout << "m7 can't print out,because m7.dims = 3 and it requires m.dims <= 2" << endl;
    cout << "channels:" << m7.channels() << endl;
    cout << "row & col:" << m7.rows << " & " << m7.cols << endl;
    cout << "size:" << m7.size << endl;
    cout << "dims:" << m7.dims << endl << endl;
    return 0;
}

上述代码采用不同方法演示了6次,输出结果如下: 下面结合输出结果,逐一对其进行详细讲解:

m1按照常规构造函数的方式新建且没有设初值,所以建好后元素全为0。

m2也是按常规构造函数新建,不同的是设置了初值200,所以输出结果矩阵元素全为200。

m3采用构造函数方式构建,不同的是初值设置。由于设置的数据类型是8UC1(8bit无符号整形,1个通道),因此取值范围是0-255,而-10越界了,所以结果取最小值0。同理若超过255,所有元素取255,这点需要注意。

m4按照成员函数方式构建,其不能给矩阵设初值。而且有个非常值得注意的问题是如果在它之前新建了其它的Mat,那么它的初始值是随机的,而并非全为0。只有当它是第一个被新建的时候,初始值才全为0。在本段代码中,只有把新建m4之前的代码注释掉后,才全为0。尚不清楚为什么会出现这个情况。在官方文档中有这样一段解释,可能有关:create() allocates only a new array when the shape or type of the current array are different from the specified ones.

m5按照构造函数方式构建,不同的是数据类型为8UC3(8bit无符号整形,3个通道),因此赋初值的时候需要注意,不能再直接写一个数字。如果写了,默认就是对第一个通道赋值。在有多个通道的时候,正确的赋值方法是用Scalar()函数传入数值,传入的数值分别对应各通道的初值。严格来说在只有一个通道的时候最严谨的写法是Scalar(200),但为了方便也可以不这样写。需要特别注意的是Scalar的构造函数最多只支持4个数(通道),如果多于4个数(通道)会报错提示找不到对应的构造函数,即使用Scalar::all()也不行。对于多于4个通道的Mat可以在新建好后,利用循环迭代赋值。另外需要注意的是控制台中对于多通道Mat的输出方式。由于不同通道设置了不同的初值,所以可以比较容易看出它的输出规律。简单来说就是行数与我们设置的相同,每行用封号隔开;在每行内,逐元素按照通道依次输出。例如本代码中输出的结果:第一行第一个元素的通道1的值、通道2的值、通道3的值;第一行第二个元素的通道1的值、通道2的值、通道3的值;第二行第一个元素的通道1的值、通道2的值、通道3的值…以此类推。 这种储存方法与Numpy对于多层数据的表达类似。Numpy对于有多层的数据存储方式是把每一个位置的所有通道的值都存成一个向量,然后再是下一个元素,注意理解。例如如下所示矩阵: 该矩阵共有2行、3列、3个通道,其中[1 2 3]表示第一行第一列的位置在3个通道中的值,同理[4 5 6]表示第一行第二列位置在3个通道中的值,[8 6 2]表示第一行第三列的位置在三个通道中的值,[5 3 1]表示第二行第一列位置对应三通道的值。 可以理解为先构造好了一个2行3列的矩阵,然后获取每个位置在不同波段中对应的值组成一个向量,最后放在这个位置上,用向量替换单一的数字。

m6也是通过构造函数新建了一个数据类型为8U的Mat。这里由于没有赋初值,也出现了随机数问题,解决方法与之前一样。但与之前不同用的是 CV8UC()函数,其括号里的数字表示通道数,可以为任意数字,如20、30等,如CV8UC(3)等价于CV8UC3。这对于处理高光谱影像非常有用。注意由于代码中的Mat其通道数超过了4,所以无法使用Scalar赋初值,可手动循环赋值。

m7依旧采用了构造函数方式创建,但创建的Mat比较特别,是一个五维的矩阵块(?),想象不出来了。第一个参数表示创建的矩阵维度为5维,第二个参数为整型数组表示每一维的大小,第三个参数表示数据类型,最后调用了Scalar赋初值。注意sz[]数组一定要和第一个参数设置的维度对应,不能多也不能少。经过测试,多了不会报错,最终效果是只取数组中前几个维数个数元素,后面不管了。但如果少了,问题比较大,比如数组里为3、4,而设置的还是3维,这样就等于第三维没设置,程序运行的时候就会设置一个非常大的数,如下(为了说明这个问题,电脑差点跑崩溃) 这里再简单解释下OpenCV中对于Mat维度的规定,其规定Mat的维度永远是>=2的,即使是一维的数组,其也认为是nx1的矩阵。所以在这里如果你第一个参数传入的是1,最终得到的矩阵的dims属性还是会为2。

这里细心的同学可能会注意到,这里维度为5了,刚刚Scalar不是最多为4吗?为什么还能赋值。这里就需要注意区别维度(Dimension)和通道(Channel),为了易于观察,在控制台中输出了不同Matchannelsrowscolssizedims信息作为对比。简单来说,在OpenCV中,一个Mat里可以有多个通道,每个通道中的数据维度不限。可以理解为有一层层的抽屉,一个抽屉对应一个channel,抽屉个数对应channel个数。在抽屉中可以放一张纸(二维数据)也可以放一个魔方(三维数据)甚至是更高维的某个神秘物体。但无论如何这些物体都(数据)有维度(对应dims)、大小(对应rowscolssize)信息。所以在OpenCV中一个包含二维数据的三通道Mat仍然是二维的,一个单通道却包含三维数据的Mat是三维的,这个需要理解一下。所以Scalar最多只能到4并不是针对数据的维度(Dimension),而是通道(Channel)的个数。

这里还需要说下size属性,在二维情况下size属性和rowscols等价,但在三维或更高维时rowscols就不适用了(查询返回-1),只能用size。想想也是可以理解的,行列这个概念本来就适用于二维,在三维中行列对应的位置不唯一。

(2)使用拷贝构造函数或赋值运算符

需要注意的是采用拷贝构造函数或赋值运算符新建的Mat是对原对象的浅拷贝,只是拷贝了矩阵头并增加了引用计数,如果想完全复制一份新的(深拷贝),应用成员函数.clone().copyTo()。简单使用示例如下:

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    Mat m1(Size(3, 2), CV_16S, Scalar(-3));
    Mat m2(m1);

    Mat m3 = m1;
    Mat m4 = imread("img.jpg");
    Mat m5 = m1.clone();

    m3.at<short>(1, 1) = 2;
    m5.at<short>(0, 1) = 9;
    cout << "m1:" << endl << m1 << endl << endl;
    cout << "m2:" << endl << m2 << endl << endl;
    cout << "m3:" << endl << m3 << endl << endl;
    cout << "m5:" << endl << m5 << endl;
    return 0;
}

上面代码运行结果如下:

对输出结果解释如下。首先代码中通过构造函数方式新建了一个大小为2x3的矩阵,数据类型为short,并且赋初值为-3。这里有两个需要注意的问题。一是前面提过的Size的参数顺序是先列后行,而构造函数的顺序是先行后列,所以如果用Size,需要修改下行列顺序。二是采用此种方式,Mat的数据类型只能写成OpenCV中定义好的类型,不能传入intfloat等,会不识别,关于这个问题在后面详细讨论。OpenCV中定义好的数据类型有:CV_8UCV_8SCV_16UCV_16SCV_32SCV_32FCV_64F,一看就明白什么意思了。采用拷贝构造的方式新建了m2。正如前面所说,其是m1的浅拷贝。m3采用赋值方法进行构造,同样是浅拷贝。除了将已有的Mat赋给它,还可以通过函数的返回值赋值,如调用imread()函数读取影像,返回Matm4。最后m5依然是通过赋值方式,但调用了.clone()函数,将m1深拷贝给了m5。所以m2m3都是m1的”影子”,它们三个数据共享,修改其中任意一个,其余都会变化。而m5则是m1的副本,修改它不会对m1造成任何影响。在代码中通过at()函数(关于它后面详细说)取出了不同位置上的元素并进行修改。最终输出结果就如上图了。由于修改了m3,而m3m1的软拷贝,所以m1也跟着被修改了。同时又由于m1被修改了,m2m1的软拷贝,所以它也变了。而m5m1互相独立,所以互不影响。

(3)块操作赋值

简单来说就是从一个大的矩阵中取出一小块,作为新的Mat。示例如下:

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    Mat m1 = Mat::eye(3, 5, CV_16S);
    cout << m1 << endl << endl;

    Mat m2(m1, Rect(1, 0, 3, 2));
    cout << m2 << endl << endl;

    m2.at<short>(0, 1) = 5;
    cout << m2 << endl << endl;
    cout << m1 << endl << endl;

    Mat m3 = m1(Range::all(), Range(1, 3)).clone();
    cout << m3 << endl << endl;
    m3.at<short>(1, 1) = 9;
    cout << m3 << endl << endl;
    cout << m1 << endl;
    return 0;
}

运行结果如下: 先简单说一下代码再说注意事项。代码中首先用eye()函数建了一个3x5的short类型单位阵m1m2通过Rect()函数截取了m1中的一块,而且是浅拷贝。然后对m2进行了修改。所以m2m1元素都有变化。而m3则是截取了m1中的所有行、第1-3列所对应部分,并且是深拷贝,所以修改后和m1各自独立互不干扰。

第一个需要注意的是Rect()函数用法,共有四个参数,分别是:起始列号、起始行号、列方向长度(包含起始列)、行方向长度(包含起始行)。参数是列在前、行在后,和前面是相反的。第二个是Range()用法,其两个参数分别是起始与终止索引,左包右不包,如(1,3)实际上取的是1、2两列。Range::all()表示所有,如放在行参数位置上表示所有行、放在列参数上表示所有列。

(4)通过二维数组转换

简单来说就是将C++中的二维数组转换成Mat,这在做一些数据互操作或对外数据接口的时候可能会有用。使用示例如下:

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    double m[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    Mat mat = Mat(3, 3, CV_64F, m);
    cout << mat << endl;
    return 0;
}
(5)通过CvMat转换

这个主要也是为了和旧的数据类型兼容,将老的CvMatIplImage类型转成新的Mat,十分简单,只需要cvarrToMat()函数即可搞定。示例代码如下:

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    Ptr<IplImage> iplimg = cvLoadImage("img.jpg");
    Mat img = cvarrToMat(iplimg);
    return 0;
}
(6)类Eigen赋值法

所谓的类似Eigen的赋值法就是之前在Eigen学习笔记里提到过的“逗号初始化语法”。需要注意的是逗号初始化只对Mat_有,对于Mat是没有的。这个多一个下划线的Mat_是OpenCV对于Mat类的又一层包装,截取一段源码如下:

template<typename _Tp> inline
Mat_<_Tp>::Mat_(int _rows, int _cols)
    : Mat(_rows, _cols, traits::Type<_Tp>::value)
{
}

可以看到,当新建Mat_的时候还是调用了Mat的构造函数。而非要说它们有什么不同的话,使用上最直观的区别就是三点:一是Mat_可以使用逗号初始语法,Mat不行;二是Mat_模板参数是可传入常规intdouble等数据类型的,而Mat构造函数则不行;三是Mat_在调用at()函数时,可以没有模板参数。所以OpenCV考虑到了这一点才又包装了一层吧。更多相关内容可参考这个网页。 示例代码如下:

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    Mat m = (Mat_<double>(3, 3) << 1, 0, 0, 0, 1, 0, 0, 0, 1);
    cout << m << endl;
    cout << m.at<double>(0, 1) << endl;
    return 0;
}

2.Mat元素操作

获取Mat中的元素非常简单,.at<>()函数即可搞定。基本用法是传入一个模板参数,表示数据类型,再传入行、列索引即可返回元素(可修改),也可以只传入行或列,这样返回的就是一整行或一整列。其实在前面的代码中已经或多或少用过了,简单的示例如下:

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    Mat m = Mat::eye(3, 3, CV_64F);
    m.at<double>(1, 1) = 2.5;
    cout << m << endl;
    return 0;
}

这里先给出结论再讨论问题:如果是8bit灰度影像,采用at<uchar>(i,j)获取;如果是8bit3通道RGB影像,采用at<Vec3b>(i,j)[x]获取,x取0,1,2,分别代表BGR。

下面是注意事项。首先是模板参数的数据类型问题,at()函数传入的模板参数数据类型必须与Mat的数据类型严格一致,且只能是intdouble等类型。而Mat构造函数中只能接受OpenCV的数据类型CV_8U等,intdouble等会出错。如果是RGB这种多通道彩色图像,模板参数可以传入Vec3fVec3w等。所以这里就有个OpenCV的数据类型与常规数据类型对应的问题。虽然我们可以通过Mat的成员函数.type()获得数据类型,但其返回的是int类型的代号,还是无法知道对应关系。为了解决这个问题,我查阅了OpenCV相关源码,在opencv2/core/traits.hppopencv2/core/hal/interface.h里找到了答案,部分源码贴出来如下:

首先,在traits.hpp里给出了常规数据类型与OpenCV数据类型的对应关系。DataType后面尖括号里的就是常规类型,枚举大括号里的depth就是对应的OpenCV数据类型。

template<> class DataType<bool>
{
public:
    typedef bool        value_type;
    typedef int         work_type;
    typedef value_type  channel_type;
    typedef value_type  vec_type;
    enum { generic_type = 0,
           depth        = CV_8U,
           channels     = 1,
           fmt          = (int)'u',
           type         = CV_MAKETYPE(depth, channels)
         };
};

template<> class DataType<uchar>
{
public:
    typedef uchar       value_type;
    typedef int         work_type;
    typedef value_type  channel_type;
    typedef value_type  vec_type;
    enum { generic_type = 0,
           depth        = CV_8U,
           channels     = 1,
           fmt          = (int)'u',
           type         = CV_MAKETYPE(depth, channels)
         };
};

template<> class DataType<schar>
{
public:
    typedef schar       value_type;
    typedef int         work_type;
    typedef value_type  channel_type;
    typedef value_type  vec_type;
    enum { generic_type = 0,
           depth        = CV_8S,
           channels     = 1,
           fmt          = (int)'c',
           type         = CV_MAKETYPE(depth, channels)
         };
};

template<> class DataType<char>
{
public:
    typedef schar       value_type;
    typedef int         work_type;
    typedef value_type  channel_type;
    typedef value_type  vec_type;
    enum { generic_type = 0,
           depth        = CV_8S,
           channels     = 1,
           fmt          = (int)'c',
           type         = CV_MAKETYPE(depth, channels)
         };
};

template<> class DataType<ushort>
{
public:
    typedef ushort      value_type;
    typedef int         work_type;
    typedef value_type  channel_type;
    typedef value_type  vec_type;
    enum { generic_type = 0,
           depth        = CV_16U,
           channels     = 1,
           fmt          = (int)'w',
           type         = CV_MAKETYPE(depth, channels)
         };
};

template<> class DataType<short>
{
public:
    typedef short       value_type;
    typedef int         work_type;
    typedef value_type  channel_type;
    typedef value_type  vec_type;
    enum { generic_type = 0,
           depth        = CV_16S,
           channels     = 1,
           fmt          = (int)'s',
           type         = CV_MAKETYPE(depth, channels)
         };
};

template<> class DataType<int>
{
public:
    typedef int         value_type;
    typedef value_type  work_type;
    typedef value_type  channel_type;
    typedef value_type  vec_type;
    enum { generic_type = 0,
           depth        = CV_32S,
           channels     = 1,
           fmt          = (int)'i',
           type         = CV_MAKETYPE(depth, channels)
         };
};

template<> class DataType<float>
{
public:
    typedef float       value_type;
    typedef value_type  work_type;
    typedef value_type  channel_type;
    typedef value_type  vec_type;
    enum { generic_type = 0,
           depth        = CV_32F,
           channels     = 1,
           fmt          = (int)'f',
           type         = CV_MAKETYPE(depth, channels)
         };
};

template<> class DataType<double>
{
public:
    typedef double      value_type;
    typedef value_type  work_type;
    typedef value_type  channel_type;
    typedef value_type  vec_type;
    enum { generic_type = 0,
           depth        = CV_64F,
           channels     = 1,
           fmt          = (int)'d',
           type         = CV_MAKETYPE(depth, channels)
         };
};

而在interface.h头文件里,则给出了数据类型与编号的对应关系。

#define CV_8U   0
#define CV_8S   1
#define CV_16U  2
#define CV_16S  3
#define CV_32S  4
#define CV_32F  5
#define CV_64F  6
#define CV_USRTYPE1 7

此外,在opencv2/core/matx.hpp里也找到了Vec与普通类型的对应关系。

typedef Vec<uchar, 3> Vec3b;
typedef Vec<short, 3> Vec3s;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<int, 3> Vec3i;
typedef Vec<float, 3> Vec3f;
typedef Vec<double, 3> Vec3d;

这样常规数据类型与OpenCV类型的对应关系如下表所示:

常规类型 OpenCV类型 OpenCV索引 Vec类型
bool CV_8U 0 -
uchar CV_8U 0 VecXb
schar CV_8S 1 -
char CV_8S 1 -
ushort CV_16U 2 VecXw
short CV_16S 3 VecXs
int CV_32S 4 VecXi
float CV_32F 5 VecXf
double CV_64F 6 VecXd

上面说了这么多,你可能会好奇,如果类型不一样会怎样。下面这段代码演示了不一样的结果。

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    Mat m = Mat::eye(3, 3, CV_32F);
    m.at<double>(1, 1) = 2.5;
    cout << m << endl;
    return 0;
}

Mat32F也就是float类型,所以应该为at<float>(1,1),正确结果如下: 但这里写成了double,输出结果如下: 可以看到由于数据类型不同导致了完全错误的结果(已经不是floatdouble精度不同的问题了,数值、位置全不一样)。这样的原因是OpenCV是根据元素所在位置(行列号)以及数据类型所占内存空间来计算每个元素所对应的内存地址的。而现在数据类型所占内存空间错了就会导致算出来的元素内存地址有问题,从而出现这种情况。

此外还在interface.h头文件里发现了个好玩的东西。之前说CV_8UC(n)这个函数很好,可以用于高光谱数据。但这个n最大能取到多少,在这个头文件里给了答案。头文件里定义了CV_CN_MAX(最大的通道数)数值是512,也即最多能有512个波段。

[2021-02-25更新]

上面介绍了这么多和数据类型有关的内容,那你可能会好奇,如何进行数据类型转换。比如,一个很常见的转换是从8UC1到32FC1。其实OpenCV也提供了方便的转换函数供我们使用,就是.convertTo()。使用方法也非常简单,一看就明白,如下。

Mat mat1(10, 10, CV_8UC1, 30);
Mat mat2;
// 直接调用convertTo函数进行类型转换
mat1.convertTo(mat2, CV_32FC1);
// 调用type函数查看数据类型
cout << mat1.type() << endl;
cout << mat2.type() << endl;

如果你运行上面的几行代码,不出意外的话,mat1的数据类型是0,mat2的数据类型是5。去上面的查找表里比对一下就能看到,分别对应int和float类型,也就完成了Mat的数据类型转换。

[更新结束]

3.Mat的遍历

前面介绍了Mat的元素操作,所以对其进行遍历也就变得简单了,可以通过ptr()at()函数配合for循环嵌套即可。

(1)ptr()函数

下面这段代码稍微复杂些,演示的是计算矩阵中所有非零元素的和。

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    Mat M = Mat::eye(40, 40, CV_64F);
    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.);
    }
    cout << sum << endl;
    return 0;
}

上面这段代码还显示不出采用指针的优势。假设你有一个高光谱影像,共有32个波段,想用OpenCV进行处理。对于这种情况OpenCV中没有预定义好的类型可用,使用at()函数就有些力不从心了,但指针依然可用。

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    Mat m(100, 100, CV_64FC(32));
    RNG rng;
    int row = m.rows;
    int col = m.cols * m.channels();
    for (int i = 0; i < row; ++i) {
        double *data = m.ptr<double>(i);
        for (int j = 0; j < col; ++j) {
            data[j] = rng.uniform(0, 2048) / 2.0;
        }
    }
    return 0;
}

上面代码中新建了个100×100大小的32个通道的影像。注意这里影像的col不再是直接从cols获取到的,而应该再乘以通道数,原因就是上面说过的OpenCV对于多通道数据的储存方式。然后通过循环嵌套依次获取到每个元素,并通过RNG模块生成随机数赋值给像素。

[2021-02-25更新]

另外,插一句题外话。如何利用OpenCV自动生成一个m×n的随机数矩阵,而不用自己写for循环遍历。其实也非常简单,和上面是类似的,还是需要依靠RNG来实现。具体代码如下。

Mat a(32, 32, CV_32FC3);
// 实例化一个随机数发生器
RNG rng;
// 使用fill方法
rng.fill(a, RNG::UNIFORM, 0.f, 1.f);
// 输出结果
cout << a << endl;

OpenCV的API中,本身并没有一步到位很方便的随机矩阵生成接口,所以需要Mat搭配RNG使用。更详细的介绍可以参考这个网页

[更新结束]

(2)at()函数

当然一般情况下也完全可以调用at()函数依次传入迭代的行列号进行遍历。下面演示读取一个真实RGB彩色图像遍历像素,将蓝色波段灰度大于128的像素进行二值化。

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    Mat img = imread("img.jpg");
    Mat img2 = img.clone();
    for (int i = 0; i < img2.rows; ++i) {
        for (int j = 0; j < img2.cols; ++j) {
            if (img2.at<Vec3b>(i, j)[0] > 128) {
                img2.at<Vec3b>(i, j) = Vec3b(255, img2.at<Vec3b>(i, j)[1], img2.at<Vec3b>(i, j)[2]);
            } else {
            }
        }
    }
    imshow("img", img);
    imshow("img2", img2);
    waitKey(0);
    return 0;
}

原图与处理后的效果对比如下。

3.一些常用属性或函数

其实上面已经用到不少了,这里为了方便查阅,再罗列一下。

  • .channels():返回Mat的通道个数

  • .row():返回Mat的某一行数据(可修改)

  • .col():返回Mat的某一列数据(可修改)

  • .rows:返回Mat的行数,仅对二维Mat有效,高维返回-1

  • .cols:返回Mat的列数,仅对二维Mat有效,高维返回-1

  • .at():返回Mat某行某列的元素(可修改)

  • .ptr():返回指向Mat的行指针

  • .size:返回Mat的尺寸大小(长x宽x高…)

  • .dims:返回Mat中数据的维度,OpenCV中维度永远大于等于2

  • .clone():返回Mat的深拷贝

  • .copyTo():返回Mat的深拷贝

  • .eye():生成单位阵

  • .zeros():生成元素全为0的矩阵

  • .ones():生成元素全为1的矩阵

  • .type():返回Mat的元素类型索引

  • .inv()Mat求逆

  • .mul()Mat矩阵乘法

  • .data():返回指向矩阵数据单元的指针

  • .total():返回矩阵中的元素总数

  • cv::hconcat():水平拼接两个矩阵

  • cv::vconcat():数值拼接两个矩阵

4.参考资料

  • [1]https://docs.opencv.org/3.1.0/d3/d63/classcv_1_1Mat.html
  • [2]https://blog.csdn.net/mao_hui_fei/article/details/78412301

本文作者原创,未经许可不得转载,谢谢配合

返回顶部