在 OpenCV
的很多跟图像有关的东西里,都离不开一样东西 —— Mat
类。读一张图片,返回的是一个 Mat
,显示一张图片,用的也是 Mat
。我们都知道这是一个矩阵类,但它到底是如何工作的呢?如何将一个矩阵跟各种图像、图像间关系、图像间变换等东西联系起来呢?其实我们多多少少都有一点了解,但概念又不是特别清晰。所以十分有必要去深入的探索一下这个 Mat
类,我们才能更有效且高效地写出想要的程序。正如我们要对 STL
有深入的理解才能更有效且高效的写出我们想要的程序。
Mat 类的结构
这里我们主要讲的是 Mat
类的储存结构,即用什么数据来表现一个 Mat
。我们可以通过源码看到它的一些关键变量:
这些我只列出了主要的一些变量,并且分成三个部分:
- 第[1]部分是与矩阵相关的一些信息,
flags
储存了矩阵的标识、是否连续、深度、通道数等信息,dims
代表矩阵的维数,rows
和cols
代表矩阵的行数与列数,data
是指向储存矩阵数据的指针。 - 第[2]部分是与感兴趣区域有关的信息,感兴趣区域即 ROI(Region Of Interset)。
- 第[3]部分是一个指针,指向一个
UMatData
类型,我们查看一下UMatData
的定义,可以知道这是跟引用计数相关的。
UMatData
的定义:
这是很容易理解的:由一张图片储存成的矩阵通常不小,例如一张 1024 * 768 的三通道彩色图,储存的矩阵就占了 1024 * 768 * 3 * 1 = 2359296 Byte = 2304 KB。而 OpenCV
作为计算机视觉库,它就是负责处理一大堆的这些图像信息,所以经常拷贝大的图像,开销是非常大的。自然地,就采用了引用计数。
一个 Mat
的数据主要包含两个部分:矩阵头和指向像素数据的指针,矩阵头主要包含了:矩阵尺寸、存储方式、存储地址、引用计数等。每个 Mat
类,都会有自己的矩阵头,同时,要是通过引用计数,就可以共享同一片矩阵区域,而不用复制两片一模一样的空间啦!
图像的储存方式
一般来说,一个 M x N 的图像可以由一个 M x N 的矩阵来表示。矩阵如何储存这些像素值呢?需要指定颜色空间和数据类型。颜色空间就是对指定颜色的一种编码,比如最简单的灰度空间,只有黑色跟白色,以及他们的组合可以组成不同程度的灰色。还有常见的彩色空间,可能有三种或四种的基本元素,然后由这些基本元素可以组成各种各样的颜色。RGB
颜色空间就是最常用的彩色空间,若再加入第四个元素 Alpha
,就可以用来表示透明度。而数据类型就是指组成颜色的这些元素的大小,最小的数据类型就是 char
,占一个字节,如 RGB
颜色的元素常用 unsigned char
来表示,可以表示范围 0 ~ 255 的数值。当然还可以用更大的数据类型来获得更精确的颜色分辨。
例如,一个灰度图可由一个二维矩阵表示:
P(0,0) | P(0,1) | … | P(0,N-1) |
---|---|---|---|
P(1,0) | P(1,1) | … | P(1,N-1) |
… | … | … | … |
P(M-1,0) | P(M-1,1) | … | P(M-1,N-1) |
P(i,j) 就表示第 i 行 j 列的像素值。
如果是RBG
图像,则每个元素用三个字节表示,在 OpenCV
中,RGB
的储存顺序为 BGR
:
B(0,0) | G(0,0) | R(0,0) | … | B(0,N-1) | G(0,N-1) | R(0,N-1) |
---|---|---|---|---|---|---|
B(1,0) | G(1,0) | R(1,0) | … | B(1,N-1) | G(1,N-1) | R(1,N-1) |
… | … | … | … | … | … | … |
B(M-1,0) | G(M-1,0) | R(M-1,0) | … | B(M-1,N-1) | G(M-1,N-1) | R(M-1,N-1) |
Mat 对象的创建
OpenCV
为创建一个 Mat
类提供了很多方法,可以在 这里 看到完整的构造的方法,我们列出一些常用的:
创建空矩阵
对于默认构造函数,会创建一个矩阵头,它没有像素数据,所以如果你写出下面这样的代码:
是会产生一个运行时的断言错误的,因为 imshow
要求矩阵的维数必须是二维的(构造时如果构造的是一维的,它也会自动扩展为二维的,让第二个维度的大小为 1)。
创建二维矩阵
对于二维的矩阵,就是要指定矩阵的行列以及矩阵数据的类型,初始值是可选的。矩阵类型的定义如下:
其中,CV_[The number of bits per item][Signed or Unsigned][Type Prefix]
这个部分可以使用以下几个值之一:
- CV_8U
- CV_8S
- CV_16U
- CV_16S
- CV_32S
- CV_32F
- CV_64F
而预定义的 [The channel number]
可以是 1、2、3、4,当然啦,如果你需要更多的通道数,也可以使用宏 XXX(n)
来生成:
比如常用的 RGB 颜色类型是 CV_8UC3
,表示每个像素由三个通道组成,而每个元素占 8 个 bit。
我们可以用 cv::Scalar
来指定矩阵的初值,比如 Scalar(0)
是一个单通道的黑色的像素,Scalar(0,0,255)
是一个三通道的红色的像素。于是,我们就可以用下面的代码创建一张全红的图片:
效果:
创建多维矩阵
如果你需要更高维数的矩阵,它也提供了相应的构造函数,首先要指定维数,然后需要一个一维数组的指针,数组里包含了各个维度的尺寸,以及矩阵数据的类型,初始值也是可选的。例如:
以上代码创建了一个 10 x 10 x 10 的三维矩阵,你仍然可以对它做矩阵运算,但是不能使用诸如 imshow
等函数了,因为必须的二维的图像才能显示出来 ^_^
在 这里 的 Detailed Description
有一句话:
It passes the number of dimensions =1 to the Mat constructor but the created array will be 2-dimensional with the number of columns set to 1. So, Mat::dims is always >= 2 (can also be 0 when the array is empty).
其实这就是在上面提到的。
从函数返回值获取
OpenCV
中有很多函数,它们的返回值是 Mat
,我们可以通过复制构造函数或赋值操作符来得到这样的对象,如我们常用的 imread
函数。还有许多常用的函数,现在就举一些例子。
create
这是 Mat
的一个重要的成员函数,它有以下四种签名:
跟构造函数很类似,就是不能指定初始值。为什么说它重要呢?因为有了它,就可以我们就可以很方便、轻易的写一些函数,比如官方的 这个例子 :
你不再需要去关心为输出的对象构造好行与列,因为这些函数都会调用 create
这个函数。就像它所说的一样,调用 create
时会经过以下的步骤:
- If the current array shape and the type match the new ones, return immediately. Otherwise, de-reference the previous data by calling Mat::release.
- Initialize the new header.
- Allocate the new data of total()*elemSize() bytes.
- Allocate the new, associated with the data, reference counter and set it to 1.
当创建的新对象的尺寸与原来的不同时,才会重新分配新的空间。
operator()
Mat
类重载了 operator()
操作符,以便于获取图像的一部分(感兴趣区域)。它具有以下四种签名:
如通过以下代码来获取一个图像的感兴趣区域:
运行效果如下:
复制 Mat
因为 Mat
使用了引用计数,当你使用复制构造或赋值操作符的时候,它们实际上只是把矩阵数据的指针指向了相同的地址,共用同一片内存,所以对任意一个 Mat
对象做修改,也会影响到其它指向相同矩阵数据区域的对象(如果有)。我们运行一下以下这段代码:
运行结果如下:
我们可以看到,我们对 A 进行了模糊操作,而 B 与 C 也同时“变得”模糊了!这里加了个引号,是因为它们仨本身就是共用了同一个矩阵数据呀!当然是相同的啦!注意:即使是用一个矩阵的一部分去构造另一个矩阵,它们的矩阵数据的指针指向的也是同一片区域。也就是说,对原图像的改变,或对部分图像的改变,都会使矩阵本身的数据改变。例如我们对部分进行模糊处理:
运行效果如下:
看到了吧?原图的那个部分最后也变模糊了。
那么如果我们真的想复制一份 Mat
,让他们各自有各自的矩阵数据怎么办呢?很简单,Mat
为我们提供了两个成员函数:clone
和 copyTo
:
clone
1Mat clone () constcopyTo
12void copyTo (OutputArray m) constvoid copyTo (OutputArray m, InputArray mask) const
他们都会将整个 Mat
的信息包括矩阵数据复制一份,所以对复制出来的新对象进行操作,就不会影响到原来的矩阵数据了。
值得一提的是 copyTo
的第二个版本,多了一个 mask
参数,这个有什么用呢?当然有用啦,这就是为了方便我们处理那些感兴趣区域(ROI)呀!官方文档 给的说明是:
Operation mask. Its non-zero elements indicate which matrix elements need to be copied. The mask has to be of type CV_8U and can have 1 or multiple channels.
按照我目前的理解,就是把你需要的颜色抠出来。比如我们把 OpenCV
图标上面那个红色的圈圈抠出来,我们就用一张灰度图,背景是黑色(0),要抠的地方是白色(255),这样要抠的地方就会原封不动的复制出来了。部分代码如下:
运行效果如下:
元素值的读写
前面我们讲的都是整个 Mat
的创建或复制,那如果我们想对 Mat
里的矩阵的元素值进行访问或修改,该怎么做呢?
矩阵的输出
首先,我们简单的了解一下矩阵的输出,这有助于我们直观的感受矩阵中的数据。 Mat
重载了 operator<<
输出操作符,所以我们可以很方便的就得到格式化输出的结果:
输出如下:
OpenCV
还提供了其它很多格式化输出的形式,如 Python
、CSV
、numpy
等,这里不作为重点,如有兴趣可以看 这里 的例子,自己尝试。
使用 at() 函数
Mat
提供了许多的 at
的重载版本对元素值进行读写,就不一一列举了,可以在 这里 查看。需要注意的是,at
需要我们指定一个 _Tp
模板类型,即如文档所说,你需要这样使用:
- If matrix is of type
CV_8U
then useMat.at<uchar>(y,x)
. - If matrix is of type
CV_8S
then useMat.at<schar>(y,x)
. - If matrix is of type
CV_16U
then useMat.at<ushort>(y,x)
. - If matrix is of type
CV_16S
then useMat.at<short>(y,x)
. - If matrix is of type
CV_32S
then useMat.at<int>(y,x)
. - If matrix is of type
CV_32F
then useMat.at<float>(y,x)
. - If matrix is of type
CV_64F
then useMat.at<double>(y,x)
.
我们可以这样来创建一张黑白过渡的图片:
运行效果如下:
这是由上至下过渡的,我们还可以弄成从左到右的,只需要把 r % 255
改成 c % 255
就可以啦!
当然,at
的模板参数可不是只能用上面所说的那些,我们要保证的是,数据类型能够一一对应。比如我们在弄一个三通道的图像:
运行效果如下:
你还可以继续更多有趣的尝试!
使用迭代器
如果你熟悉 STL
,那么你也一定很喜欢迭代器的读写方式——安全,易写,不易出错。Mat
类也向我们提供了它的迭代器类型 MatIterator_
,接下来我们就使用迭代器的方法来生成一张随机色彩的图像吧!
运行效果如下:
有点像电视雪花啊哈哈,每次运行的结果肯定都一样的,不过都是乱七八糟的了。
使用指针
Mat
同样提供了一个 ptr
函数及其的许多重载版本,可以在 这里 查看。使用指针确实是更容易出错,因为 C-style 指针是不带范围检查的。不过我觉得这点小事也怕干脆就别学了。听说用指针的效率会更高,当然我现在还没有进行充足的测试,有兴趣的朋友可以自己测试一下。我就举跟使用 at
一样的例子吧:
上述代码同样是实现了一个黑白过渡的图像生成。
Mat_ 类
有没有觉得上面很多 at<uchar>
, ptr<uchar>
的写法很烦?而且更重要的是,这是不能做编译器检查的,也就算说,你写成以下代码:
编译期是没法检查这个类型错误的,直到运行期才会炸掉。Mat_
类就是来解决这个问题的,Mat_
类就是对 Mat
的一个封装,加上了一个模板参数用于指定类型,这样就可以免去很多麻烦和造成错误的可能。例如以下一个例子:
小结
Mat
是整个 OpenCV
中非常重要的一个数据结构,它还有许多操作,以上只提到了冰山一角,可以从 这里 获取更多的信息。不断的练习与思考,就是进步的唯一途径。