
1.3 Mat图像存储容器
人们从现实世界中获取数字图像的途径有数码相机、扫描仪、计算机断层扫描和核磁共振成像等。在任何情况下,人眼所看到的都是图像。然而,实际上数字设备记录的是图像中每个点的数值,如图1-39所示。

图1-39 图像表示
可以看到,在上述图像中,图像的成像只不过是一个包含所有像素点强度值的矩阵。获取和存储像素值的方法可能会根据不同的需要而有所不同,但最终,计算机世界里的所有图像都可以简化为数值矩阵信息。OpenCV是一个计算机视觉库,可以帮助我们处理和操纵这些信息。因此,读者需要熟悉的第一件事是OpenCV如何存储和处理图像。
· 1.3.1 Mat容器简介
Mat容器是早期(2001年)OpenCV基于C语言接口建立的。为了在内存中存放图像,当时采用的是IplImage C语言结构体。然而,采用这种方法用户必须接受C语言所有的不足,其中最大的不足是需要手动进行内存管理,即用户需要对开辟和销毁内存负责。虽然对于小型的程序来说手动管理内存不是问题,但一旦代码变得庞大,用户就会越来越多地纠结于这个问题,而不是着力实现其开发目标。
幸运的是,C++出现了,并且带来了类的概念。同时,由于C++与C语言完全兼容,所以在进行代码更改时不会出现任何兼容性问题。为此,OpenCV在2.0版本中引入了一个新的C++接口,并在OpenCV中使用了Mat类,通过Mat解决了内存管理和运算符重载等问题,利用自动内存管理给出了解决问题的新方法。使用这个方法,用户不需要纠结于如何管理内存,而且代码会变得简洁。C++接口唯一的不足是当前许多嵌入式开发系统只支持C语言,所以当用户的目标不是这种开发平台(嵌入式)时,没有必要使用“旧”方法。
对于Mat类,首先要知道的是用户不必再手动为其开辟空间,也不必在不需要时立即将空间释放(但手动做这些工作还是可以的,大多数OpenCV函数仍会手动为输出数据开辟空间)。当传递一个已经存在的Mat对象时,开辟好的矩阵空间会被重用。也就是说,用户每次都使用大小正好的内存来完成任务。
Mat类由两个数据部分组成:矩阵头(包含矩阵尺寸、存储方法、存储地址等信息)和一个指向存储所有像素值矩阵(根据所选存储方法的不同,矩阵可以是不同的维数)的指针。矩阵头的尺寸是常数值,但矩阵本身的尺寸会因图像的不同而不同,通常比矩阵头的尺寸大好几个数量级。因此,当在程序中传递图像并创建副本时,大的开销是由矩阵造成的,而不是矩阵头。OpenCV是一个图像处理库,其中包含大量的图像处理函数。为了解决问题,通常要使用库中的多个函数,因此经常需要在函数中传递图像。同时,对于那些计算量很大的图像处理算法,除非万不得已,否则不应该复制“大”图像,因为这会降低程序运行速度。
为了解决这个问题,OpenCV使用引用计数机制,其思路是让每个Mat对象有自己的矩阵头,但共享同一个矩阵(通过让矩阵指针指向同一地址来实现)。当使用复制构造函数(又称拷贝构造函数)时,只需复制矩阵头和矩阵指针即可,而不用复制矩阵本身,代码如下。
Mat A,C; // 只创建矩阵头部分
A = imread(argv[1],CV_LOAD_IMAGE_COLOR); // 这里为矩阵开辟内存
Mat B(A); // 使用复制构造函数
C = A; // 赋值运算符
以上代码中的所有Mat对象最终都指向同一个,也是唯一的数据矩阵。虽然它们的矩阵头不同,但通过任何一个对象所做的改变都会影响其他对象。实际上,不同的对象只是访问相同数据的不同途径而已。这里还要提及一个强大的功能:可以创建只引用部分数据的矩阵头。例如用户想要创建一个感兴趣区域(Region of Interest,ROI),只需要创建包含边界信息的矩阵头即可。
Mat D (A,Rect(10,10,100,100) ); // 使用矩阵确定感兴趣区域
Mat E = A(Range:all(),Range(1,3)); // 使用行和列确定边界,这里为A矩阵全部行的第1—3列
如果矩阵属于多个Mat对象,那么当不再需要它时谁来负责清理?简单的回答是最后一个使用它的对象。清理工作可以通过引用计数机制来实现。无论用户什么时候复制一个Mat对象的矩阵头,都会增加矩阵的引用次数;反之,当一个矩阵头被释放之后,这个计数被减1;当计数值为0,矩阵就会被清理。但在某些时候确实需要完全复制矩阵本身(不只是矩阵头和矩阵指针),这时可以使用函数clone()或者copyTo()实现深复制。示例如下。
Mat F = A.clone();
Mat G;
A.copyTo(G);
此时改变F或者G就不会影响Mat矩阵头所指向的矩阵。二者的区别在于,copyTo()是否申请新的内存空间,取决于目标头像矩阵头中的大小信息是否与源图像一致,若一致则是浅复制,不申请新的空间,否则先申请空间后再进行复制;clone ()是完全的深复制,在内存中申请新的空间。
以上内容可以总结如下。
①OpenCV函数中输出图像的内存分配是自动完成的(如果不特别指定的话)。
②使用OpenCV的Mat类时不需要考虑内存释放问题。
③赋值运算符和复制构造函数只复制矩阵头。
④函数clone()或者copyTo()可用来复制表示图像的矩阵。
· 1.3.2 存储方法
这一小节讲述如何存储像素值。存储像素值需要指定颜色空间和数据类型。颜色空间是指对一个给定的颜色,如何组合颜色元素以对其编码。最简单的颜色空间要属灰度级空间,只处理黑色和白色,对它们进行组合可以产生不同程度的灰色。
对于彩色,则有更多种类的颜色空间。但不论哪种存储方法,都是把颜色分成3个或者4个基元素,通过组合基元素来产生所有的颜色。RGB颜色空间是最常用的颜色空间之一,它也是人眼内部构成颜色的方式。它的基色是红色、绿色和蓝色,有时为了表示透明颜色也会加入第四个元素alpha (A)。
不同的颜色空间,各有自身的优势,具体介绍如下。
- RGB(Red:红,Green:绿,Blue:蓝)是最常见的颜色空间之一,这是因为人眼采用相似的工作机制,所以它也被显示设备所采用。
- HSV(Hue:色调或色相,Saturation:饱和度,Value:明度)和HLS(Hue:色调或色相,Lightness:亮度,Saturation:饱和度)把颜色分解成色调、饱和度和明度/亮度。这是描述颜色更自然的方式,比如可以通过抛弃最后一个元素,使算法对输入图像的光照条件不敏感。
- YcrCb(即YUV),“Y”表示明亮度(Luminance或Luma),也就是灰度值;而“U”和“V” 表示色度(Chrominance或Chroma),描述影像色彩及饱和度,用于指定像素的颜色,YcrCb在JPEG图像格式中广泛使用。
- CIE L*a*b*是目前最流行的测色系统之一。以明度L*和色度坐标a*、b*来表示颜色在颜色空间中的位置。L*表示颜色的明度,范围由0到100,表示颜色从深(黑)到浅(白),a*正值表示偏红,负值表示偏绿;b*正值表示偏黄,负值表示偏蓝。
每个组成元素都有自己的定义域,具体取决于其数据类型。如何存储一个元素决定了在该元素定义域上能够控制的精度。最小的数据类型是char,占1字节或者8个二进制位,可以是有符号型(0到255之间)或无符号型(-127到+127之间)。尽管使用3个字符char型元素已经可以表示1600万种可能的颜色(使用RGB颜色空间),但若使用单精度浮点数float(4字节,32位)型或双精度浮点数double(8字节,64位)型元素则能分辨出更加精细的颜色。但增加元素的尺寸也会增加图像所占的内存空间。
OpenCV中图像的通道数可以是1、2、3或4。其中常见的是单通道和三通道,二通道和四通道不常见。
①单通道的是灰度图像。
②三通道的是彩色图像,例如RGB图像。
③四通道的图像是RGBA图像,是RGB加上一个A通道,也叫alpha通道,表示透明度。PNG图像是一种典型的四通道图像。alpha通道可以赋值0到1,或者0到255的数字,表示从透明到不透明。
④二通道的图像是RGB555和RGB565格式的图像。二通道图像在程序处理中会用到,如傅里叶变换。其中,RGB565是16位的,只需要2字节存储每个像素点,其中第一字节的前5位是R(红色),第一字节后3位+第二字节前3位是G(绿色),第二字节后5位是B(蓝色),相对3个字节,对源图像进行了压缩。
- HSI(Hue,Saturation,Intensity),其中H定义颜色的频率,称为色调;S表示颜色的深浅程度,称为饱和度;I表示强度或亮度。
· 1.3.3 创建Mat对象
Mat不仅是很优秀的图像容器类,同时也是通用的矩阵类,可以用来创建和操作多维矩阵。创建一个Mat对象有多种方法,此处将通过项目1-1来介绍这一部分内容。
创建Mat对象的具体操作过程如下。
(1)创建Qt项目1-1,如图1-40、图1-41所示。

图1-40 创建Qt项目1-1

图1-41 选择对应的Kits组件——MSVC2015 32bit
(2)修改1-1.pro文件来配置OpenCV环境。往1-1.pro文件中加入如下代码。
# 导入头文件
INCLUDEPATH+=D:/OpenCV/opencv/build/include
INCLUDEPATH+=D:/opencv/opencv/build/include/opencv2
# 导入库文件
win32:CONFIG(debug, debug|release):{
LIBS+=-LD:/OpenCV/opencv/build/x64/vc14/lib\
-lopencv_world440d
}
else{
LIBS+=-LD:/OpenCV/opencv/build/x64/vc14/lib\
-lopencv_world440
}
至此,环境已经配置完毕。本书后面的环境如无特殊说明均默认使用此处的环境配置方法,不再重复说明,请读者注意。
█ 1.采用Mat()构造函数创建Mat对象
例1-1:使用Mat()构造函数创建Mat对象。
(1)编辑main.cpp文件。
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2 Mat对象
Mat img(2,2,CV_8UC3,Scalar(0,255,255));
cout <<"矩阵元素" << endl << img << endl;
return 0;
}
(2)运行代码,步骤如下。本书后面的运行代码方法如无特殊说明均默认为此处的运行方法,不再重复说明,请读者注意。
(3)编译程序前,需要先对工程进行QMake编译。将鼠标指针移动到左侧的工程名上并右击,在弹出的快捷菜单中选择“qmake”命令进行QMake编译,结果如图1-42所示。

图1-42 QMake编译结果输出
(4)QMake编译通过之后,继续将鼠标指针移动到左侧的工程名上并右击,在弹出的快捷菜单中选择“构建”命令。同样,此时下方的“编译输出”窗口没有出现异常错误提示,表示编译通过,此时才真正生成了可执行的.exe文件,如图1-43所示。

图1-43 编译输出
(5)编译生成.exe文件之后,可以单击Qt Creator窗口左下角的三角形按钮,运行编译通过的测试程序,程序运行结果如图1-44所示。注意其中的行数为2,而列数为2×3=6。

图1-44 例1-1程序运行结果
由此可知,在创建Mat对象时,对于二维多通道图像,首先要定义其尺寸,即行数和列数。然后需要指定存储元素的数据类型,以及每个矩阵点的通道数。为此,依据下面的规则有多种定义方法。
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
例如前面例1-1中的“CV_8UC3”表示使用8个二进制位;U表示Unsigned int,即无符号整型;C代表所存储图像的通道;3代表所存储图像的通道数,每个像素由3个元素组成三通道。预先定义的通道数可以多达4个。Scalar是一个short整型的vector容器,指定这个参数能够使用指定的定制化值来初始化矩阵。
在C/C++中通过构造函数进行初始化,此处基于前面例1-1的方法进行了修改。
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2 Mat对象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout <<"matrix element"<< endl << img << endl;
//创建一个超过二维的矩阵
int sz[3] ={ 2,2,2};
//三维的Mat对象(2×2×2),元素全部为0
Mat array2(3,sz,CV_8UC1,Scalar(0));
//因为是三维的,所以不能用DOS命令行界面显示
return 0;
}
上面演示了如何创建一个超过二维的矩阵:指定维数,然后传递一个指向一个数组的指针,这个数组包含每个维度的尺寸;其余的参数含义参考例1-1,此处为单通道图像,所以是CV_8UC1,Scalar(0)。
█ 2.采用create()函数创建Mat对象
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace cv;
using namespace std;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2 Mat对象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout << "matrix element" << endl << img << endl;
//... 中间注释省略前面介绍的代码
//用create()函数实现对Mat对象的初始化
Mat img;
img.create(4,4,CV_8UC(2));
cout <<"M = " << endl << img << endl;
return 0;
}
程序运行结果如图1-45所示。

图1-45 create()函数创建的Mat对象
注意,这个创建Mat对象的方法不能为矩阵设初值,只是在改变尺寸时重新为矩阵数据开辟内存。
█ 3.采用MATLAB样式初始化器cv::Mat::zeros、cv::Mat::ones、cv::Mat::eye创建Mat对象
用这种方法创建Mat对象时,需要指定要使用的矩阵大小和数据类型。
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2Mat对象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout <<"matrix element"<< endl << img << endl;
//... 中间注释省略前面介绍的代码
Mat array1 = Mat::eye(4,4,CV_64F); // 对角矩阵
Mat array2 = Mat::ones(4,4,CV_32F); //全1矩阵
Mat array3 = Mat::zeros(4,4,CV_8UC1); //全0矩阵
cout <<"Diagonal matrix"<< endl << array1 << endl;
cout << "full one matrix" << endl << array2 << endl;
cout << "full zero matrix" << endl << array3 << endl;
return 0;
}
程序运行结果如图1-46所示。

图1-46 MATLAB样式初始化器创建的Mat对象
█ 4.在小矩阵中可以用逗号分隔的初始化函数
//在小矩阵中可以用逗号分隔的初始化函数
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2 Mat对象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout << "matrix element" << endl << img << endl;
//... 中间注释省略前面介绍的代码
Mat array = (Mat_<double>(3,3) << 0,-1,5,-1,5,-1,0,-1,0);
cout << "matrix" << endl << array << endl;
return 0;
}
程序运行结果如图1-47所示。

图1-47 在小矩阵中使用逗号分隔的初始化函数
█ 5.使用clone()或者copyTo()函数为一个存在的Mat对象创建一个新的信息头
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2 Mat对象
// Mat img(2,2, CV_8UC3,Scalar(0,255,255));
// cout << "matrix element" << endl << img << endl;
//... 中间注释省略前面介绍的代码
//使用clone()或者copyTo()函数为一个存在的Mat对象创建一个新的信息头
Mat srcImage(3,3,CV_8UC3,Scalar(0,0,255));
Mat copyImage;
srcImage.copyTo(copyImage);
Mat newImage = srcImage.row(1).clone();
cout << "matrix"<< endl << newImage << endl;
return 0;
}
程序运行结果如图1-48所示。

图1-48 使用clone()或者copyTo()函数为一个存在的Mat对象创建一个新的信息头