Mat: CV基本的数据结构
1、认识数字图像
我们有多种方法从现实世界中获取数字图像:数码相机、扫描仪、 计算机断层成像和磁共振成像等等。在任何情况下,我们(人类)看到的都是图像。然而,当我们把它转换到数字设备时,记录的是图像中每个点的数值(也就称为数字图像)。
例如,在上面的图像中,可以看到,汽车的镜子只不过是一个包含所有像素点的强度值矩阵。我们获取和存储像素值的方式可能会根据我们的需要而有所不同,但是最终计算机世界中的所有图像可能会被简化为数字矩阵和其他描述矩阵本身的信息。OpenCV 是一个计算机视觉库,其主要目的是处理和操作这些信息。因此,首先需要熟悉的是 OpenCV 如何存储和处理图像。
2、CV的发展历程
OpenCV 从2001年就开始了,最开始的 CV 库是围绕 C 接口构建的。为了在内存中存储图像,使用了一种称为 IplImage 的 C 结构,这将在大多数旧的教程和教育材料中看到。这样做的问题在于包含了 C 语言的所有缺点,其中最大的问题是需要手动内存管理。它建立在用户负责内存分配和释放的假设之上。虽然这对于较小的程序来说不是问题,但是一旦你的代码基数增长了,处理所有这些问题就会变得更加困难,而不是专注于实现你的开发目标。
幸运的是,C + + 出现了,并且引入了类的概念,通过自动内存管理,使用户更加容易使用。好消息是 C + + 与 C 完全兼容,所以不会产生兼容性问题。因此,OpenCV 2.0 引入新的 C + + 接口,它提供了一种新的方式,这意味着你不需要摆弄内存管理,使你的代码简洁(更少的编写,实现更多)。C + + 接口的主要缺点是,目前许多嵌入式开发系统只支持 C。因此,除非你的目标是嵌入式平台,否则使用旧方法毫无意义(除非你是一个受虐狂程序员,并且自找麻烦)。
关于 Mat,您需要了解的第一件事是,您不再需要手动分配它的内存,并在不需要时尽快释放它。虽然这样做仍然是可能的,但大多数 OpenCV 函数将自动分配其输出数据。如果您传递一个已经存在的 Mat 对象(它已经为矩阵分配了所需的空间) ,这将是一个很好的奖励,它将被重用。换句话说,我们在任何时候只使用执行任务所需的内存。
3、Mat的基本结构
Mat 是一个包含两个数据部分的类:矩阵头(matrix header,包含矩阵大小、存储方法、存储矩阵的地址等信息)和指向矩阵的指针(pointer),矩阵包含像素值。矩阵头部的大小是固定的,但矩阵本身的大小可能会因图像大小而异。
OpenCV 是一个图像处理库。它包含了大量的图像处理功能。为了解决计算方面的挑战,大多数情况下您最终将使用库的多个函数。因此,将图像传递给函数是一种常见的操作。我们不应该忘记,我们正在谈论的是图像处理算法,这往往是相当沉重的计算量。通过制作大型图像的副本,会进一步降低程序的速度。
为了解决这个问题,OpenCV 使用了一个引用计数系统。基本思想:每个 Mat 对象都有自己的头,但是一个矩阵可以在两个 Mat 对象之间通过让它们的【矩阵指针】指向相同的地址来共享。此外,复制操作符只复制大矩阵的矩阵头和指针,而不复制图像数据本身。
3.1、Mat—浅拷贝
Mat A, C; // 仅仅创建矩阵头(matrix header)
A = imread(argv[1], IMREAD_COLOR); // 分配内存,存储图像数据
Mat B(A); // 使用复制构造器(copy constructor)
C = A; // 赋值操作符(Assignment operator)
上述所有对象(B,C)都指向同一个数据矩阵,对其中任何一个进行修改都会影响其它所有对象。实际上,不同的对象只是为相同的底层数据提供了不同的访问方法。然而,他们的头部部分(header)是不同的。
真正有趣的部分是,您可以创建只引用完整数据的一部分的矩阵头(matrix header)。例如,要在图像中创建感兴趣区域(ROI) ,只需创建一个带有新边界的新矩阵头,具体如下:
Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle
Mat E = A(Range::all(), Range(1,3)); // using row and column boundaries
值得注意的是:
- 如果修改A的像素,那么D和E都会被影响。
- 这个有个问题?如果矩阵本身属于多个 Mat 对象,当它不再需要的时候,谁来负责清理它?答案是:最后一个使用它的对象。这是通过使用引用计数机制(reference count systerm)来处理的。每当拷贝一次 Mat 对象的头,矩阵的计数器就会加 1。每当清理矩阵头时,计数器就会减 1。当计数器达到零时,矩阵被释放。
- 浅复制可以降低内存的消耗,如果需要频繁修改像素值,则会带来不小的隐患。
3.2、Mat—深拷贝
如果不希望原始图像被修改,可以使用深拷贝的方法,代码片段如下:
Mat F = A.clone();
Mat G;
A.copyTo(G);
使用上述方式,修改F和G的数据,不会影响到A指向的图像数据,有几点需要注意:
- OpenCV 函数的输出图像分配是自动的(除非特别指定);
- 使用 OpenCV 的 C++ 接口,不用特别关注内存的管理;
- 赋值操作和复制构造函数仅仅拷贝矩阵头;
- Mat 深度拷贝可以使用
cv::Mat::clone()
andcv::Mat::copyTo()
;
4、颜色的表示方法
关于如何存储像素值的:我们可以选择颜色空间(color space)和所使用的数据类型(data type)。颜色空间是指如何组合颜色组件来编码给定的颜色。最简单的是灰度图,可以使用的颜色是黑色和白色,这些元素的组合使我们能够创造出许多灰色的阴影。
对于创建更多丰富多彩的图像,我们有更多的方法可供选择。我们通常把它分解成3~4个基本组成部分,使用这些组合来创建其它的颜色。最流行的是 RGB 颜色空间,主要是因为这也是我们的眼睛建立颜色的方式。它的基本颜色是红色,绿色和蓝色。为了编写颜色的透明度代码,有时会添加第四个元素 alpha (A)。
然而,还有许多其他的颜色系统(RGB,HSV/HLS,YCrCb,Lab),每一个都有自己的优势:
- RGB 是最常见的,因为我们的眼睛使用类似的东西。但请记住,OpenCV 标准显示系统组成的颜色使用 BGR 颜色空间(红色和蓝色通道交换的位置)。
- HSV 和 HLS 将颜色分解为它们的色相(hue)、饱和度(suaturation)和值/亮度(value/luminance)等三个部分,这是我们描述颜色的一种更自然的方式。例如,可以忽略最后一个组件,使算法对输入图像的光照条件不太敏感。
- 流行的 JPEG 图像格式使用 YCrCb。
- CIE L * a * b * 是一个感知上均匀的颜色空间,如果您需要测量给定颜色到另一种颜色的距离,它会派上用场。
每个构建组件都有自己的有效域。这将导致所使用的数据类型。如何存储组件定义了我们对其有效域的控制。最小的数据类型可能是 char,这意味着一个字节或8位,可以是无符号的(因此可以存储从0到255的值)或有符号的(值从 -127到 + 127)。虽然这个宽度,在三个组件(如 RGB)的情况下,已经给出了1600万种可能的颜色来表示,我们可以通过使用每个组件的 float (4字节 = 32位)或 double (8字节 = 64位)数据类型来获得更好的控制。然而,请记住,增加组件的大小也会增加内存的占用。
5、创建Mat对象方法集合
虽然 Mat 作为一个图像容器工作得非常好,但它也是一个通用的矩阵类。因此,可以创建和操作多维矩阵。借助【Mat 类】的成员函数,我们可以通过多种方式创建 【Mat 对象】,创建方法如下:
5.1、Mat类的常用成员函数
-
Mat (int rows, int cols, int type)
CV::Mat 第一个重载成员函数,用于创建新的矩阵。
参数解析:- rows:2D 数组的行数;
- cols:2D 数组的列数;
- type:数组类型,比如CV_64FC4,表示每个元素占用64bit(4byte),元素的数据类型为Float,4个通道;
官方API
-
函数功能:重载函数,便于创建矩阵。
参数解析:- size:2D 数组的大小,Size(cols,rows)。在Size() 构造器中,rows和cols是反的顺序;
- type:参考上述的介绍,此处略;
官方API
5.2、Mat类的成员函数举例
-
cv::Mat::Mat Constructor(构造器)
Mat M(2,3, CV_8UC3, Scalar(0,0,255)); cout << "M = " << endl << " " << M << endl << endl; M = [ 0, 0, 255, 0, 0, 255, 0, 0, 255; 0, 0, 255, 0, 0, 255, 0, 0, 255]
- 2,3:指定Mat矩阵的行(2)和列(3);
- CV_8UC3:指定存储数据的类型,每一个矩阵点的通道数,结构如下
- 具体解释:【8】表示存储一个数据的需要8位的长度(一个字节byte),【U】表示无符号类型,【C】表示字符型(char),【3】表示通道数量;
- Scalar(0,0,255):指定创建矩阵的初始化值。
-
C/C++ 数组 和 Mat 构造器初始化
Mat类的重载成员函数,不同的地方在于参数的设定,
- ndims:矩阵的维度
- size:整数类型的多维数组
- type:与上面的定义是一样的
- 官方重载函数如下图所示:
示例:
int sz[3] = {2,3,5}; Mat L(3,sz, CV_8UC(1), Scalar::all(0)); std::cout << "L.DIMS = " << " " << L.dims << std::endl; std::cout << "L.SIZE = " << " " << L.size << std::endl; // 输出如下 L.DIMS = 3 L.SIZE = 2 x 3 x 5
-
cv::Mat::create
该方法的使用规则:- 如果当前数组形状和类型与新的匹配,则立即返回。否则,通过调用 Mat: : release 解除对前面数据的引用;
- 初始化新的矩阵头(header);
- 为新的数据分配空间,空间大小为: t o t a l ( ) ∗ e l e m S i z e ( ) total()*elemSize() total()∗elemSize(),单位是字节(byte);
- 分配与数据关联的新引用计数器,并将其设置为1 ;
补充内容:
- memory size = image.total() x image.elemSize();
- image.total() = image.width x image.height;
- image.elemSize:一个像素值占的字节数大小,比如16SC3,其它大小为3xsizeof(short)
// create function M.create(4,4, CV_8UC(2)); cout << "N = "<< endl << " " << M << endl << endl; std::cout << "M = " << std::endl << " " << &(M.data) << std::endl << std::endl; N = [ 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0; 216, 224, 38, 2, 0, 0, 0, 0; 32, 225, 38, 2, 0, 0, 0, 0] M = 0x7ffd731496f0
-
Matlab 风格的初始化
Mat E = Mat::eye(4, 4, CV_64F); cout << "E = " << endl << " " << E << endl << endl; Mat O = Mat::ones(2, 2, CV_32F); cout << "O = " << endl << " " << O << endl << endl; Mat Z = Mat::zeros(3,3, CV_8UC1); cout << "Z = " << endl << " " << Z << endl << endl; // 输出内容如下 E = [1, 0, 0, 0; 0, 1, 0, 0; 0, 0, 1, 0; 0, 0, 0, 1] O = [1, 1; 1, 1] Z = [ 0, 0, 0; 0, 0, 0; 0, 0, 0]
-
使用 Mat_ 和 短列表初始化 Mat
Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0); cout << "C = " << endl << " " << C << endl << endl; C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3); cout << "C = " << endl << " " << C << endl << endl; // 输出内容 C = [0, -1, 0; -1, 5, -1; 0, -1, 0] C = [0, -1, 0; -1, 5, -1; 0, -1, 0]
-
cv::Mat::clone or cv::Mat::copyTo
在已有Mat 数据 C 的基础上,创建新的Mat// 取C的第2行,创建新的Mat MatRowClone = C.row(1).clone(); cout << "RowClone = " << endl << " " << RowClone << endl << endl; // 输出内容 RowClone = [-1, 5, -1]
-
随机值初始化矩阵
Mat R = Mat(3, 2, CV_8UC3); randu(R, Scalar::all(0), Scalar::all(255)); // 输出内容 R (default) = [ 91, 2, 79, 179, 52, 205; 236, 8, 181, 239, 26, 248; 207, 218, 45, 183, 158, 101]
6、Mat的输出样式
从输出的结构可以看出,默认的风格(C++)与C是很像的,Python和Numpy是非常类似的。
-
Default
cout << "R (default) = " << endl << R << endl << endl; // 输出内容 R (default) = [ 91, 2, 79, 179, 52, 205; 236, 8, 181, 239, 26, 248; 207, 218, 45, 183, 158, 101]
-
Python
cout << "R (python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl; // 输出内容 R (python) = [[[ 91, 2, 79], [179, 52, 205]], [[236, 8, 181], [239, 26, 248]], [[207, 218, 45], [183, 158, 101]]]
-
CSV
cout << "R (csv) = " << endl << format(R, Formatter::FMT_CSV ) << endl << endl; // 输出内容 R (csv) = 91, 2, 79, 179, 52, 205 236, 8, 181, 239, 26, 248 207, 218, 45, 183, 158, 101
-
Numpy
cout << "R (numpy) = " << endl << format(R, Formatter::FMT_NUMPY ) << endl << endl; // 输出内容 R (numpy) = array([[[ 91, 2, 79], [179, 52, 205]], [[236, 8, 181], [239, 26, 248]], [[207, 218, 45], [183, 158, 101]]], dtype='uint8')
-
C
cout << "R (numpy) = " << endl << format(R, Formatter::FMT_NUMPY ) << endl << endl; // 输出内容 R (c) = { 91, 2, 79, 179, 52, 205, 236, 8, 181, 239, 26, 248, 207, 218, 45, 183, 158, 101}
7、Point 数据存储结构
-
Point 的类型
-
示例解析
// 2D Point Point2f P(5, 1); cout << "Point (2D) = " << P << endl << endl; // 3D Point Point3f P3f(2, 6, 7); cout << "Point (3D) = " << P3f << endl << endl; // vector vector<float> v; v.push_back( (float)CV_PI); v.push_back(2); v.push_back(3.01f); cout << "Vector of floats via Mat = " << Mat(v) << endl << endl; // push 2d points vector<Point2f> vPoints(20); for (size_t i = 0; i < vPoints.size(); ++i) vPoints[i] = Point2f((float)(i * 5), (float)(i % 7)); cout << "A vector of 2D Points = " << vPoints << endl << endl; // 输出内容 Point (2D) = [5, 1] Point (3D) = [2, 6, 7] Vector of floats via Mat = [3.1415927; 2; 3.01] A vector of 2D Points = [0, 0; 5, 1; 10, 2; 15, 3; 20, 4; 25, 5; 30, 6; 35, 0; 40, 1; 45, 2; 50, 3; 55, 4; 60, 5; 65, 6; 70, 0; 75, 1; 80, 2; 85, 3; 90, 4; 95, 5]