我们有多种方法从现实世界中获取数字图像:数码相机、扫描仪、计算机断层扫描和磁共振成像等等。在任何情况下,我们(人类)看到的都是图像。然而,当将其转换为数字设备时,我们记录的是图像中每个点的数值。
例如,在上图中,你可以看到汽车的后视镜只不过是一个包含所有像素点强度值的矩阵。我们获取和存储像素值的方式可能会根据我们的需要而变化,但最终计算机世界中的所有图像都可能被简化为数字矩阵和描述矩阵本身的其他信息。OpenCV是一个计算机视觉库,其主要重点是处理和操作这些信息。因此,我们需要熟悉的第一件事是OpenCV如何存储和处理图像。
OpenCV自2001年以来一直存在。在那个时候,库是围绕C接口构建的,为了在内存中存储图像,他们使用了一个名为IplImage的C结构。这是你会在大多数旧的教程和教育材料中看到的。这样做的问题是,它暴露了C语言的所有缺点。最大的问题是手动内存管理。它建立在用户负责处理内存分配和回收的假设之上。虽然这对于较小的程序来说不是问题,但一旦代码库增长,处理所有这些问题将更加困难,而不是专注于解决我们的开发目标。
幸运的是,c++出现并引入了类的概念,通过自动内存管理(或多或少)为用户提供了方便。好消息是c++与C完全兼容,因此更改不会产生兼容性问题。因此,OpenCV 2.0引入了一个新的c++接口,它提供了一种新的做事方式,这意味着你不需要摆弄内存管理,使你的代码简洁(写得少,实现得多)。c++接口的主要缺点是目前许多嵌入式开发系统只支持C语言。因此,除非我们的目标是嵌入式平台,否则使用旧方法是没有意义的(除非我们是一个受虐狂程序员,并且正在自找麻烦)。
关于Mat,我们需要知道的第一件事是,我们不再需要手动分配它的内存并在不需要它时立即释放它。虽然这样做仍然是可能的,但大多数OpenCV函数将自动分配其输出数据。如果我们传递一个已经存在的Mat对象,它已经为矩阵分配了所需的空间,那么它将被重用。换句话说,我们在任何时候都只使用执行任务所需的内存。
Mat基本上是一个包含两个数据部分的类:矩阵头 (包含诸如矩阵大小、用于存储的方法、存储矩阵的地址等信息)和指向包含像素值的矩阵的指针(根据所选择的存储方法取任意维度)。
矩阵头的大小是恒定的,但是矩阵本身的大小可能因图像而异,并且通常要大几个数量级。
其实就相当于一个微细的数据结构库而已。
OpenCV是一个图像处理库。它包含了大量的图像处理功能。为了解决计算难题,大多数情况下,我们最终会使用库的多个函数。因此,将图像传递给函数是一种常见的做法。我们不应该忘记,我们正在讨论的是图像处理算法,它往往需要大量的计算量。我们最不希望做的事情是通过对可能较大的图像进行不必要的复制来进一步降低程序的速度。
为了解决这个问题,OpenCV使用了一个引用计数系统。这个想法是每个Mat对象都有自己的头,但是一个矩阵可以在两个Mat对象之间共享,通过让它们的矩阵指针指向相同的地址。此外,复制操作符只复制头文件和指向大矩阵的指针,而不复制数据本身。很多的编程框都使用了这个原理,就像Qt库也是一样,是写时拷贝。
Mat A, C; // creates just the header parts
A = imread(argv[1], IMREAD_COLOR); // here we'll know the method used (allocate matrix)
Mat B(A); // Use the copy constructor
C = A; // Assignment operator
最后,上述所有对象都指向相同的单个数据矩阵,使用其中任何一个对象进行修改也会影响所有其他对象。实际上,不同的对象只是为相同的底层数据提供不同的访问方法。然而,它们的标题部分是不同的。真正有趣的部分是,我们可以创建仅引用完整数据的一部分的头。例如,要在图像中创建感兴趣的区域(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
现在你可能会问,如果矩阵本身可能属于多个Mat对象,当不再需要它时,谁负责清理它。简短的回答是:最后一个使用它的对象。这是通过使用引用计数机制处理的。每当有人复制Mat对象的标头时,矩阵的计数器就会增加。每当清除标头时,该计数器就会减少。当计数器达到零时,矩阵被释放。有时候你也想复制矩阵本身,所以OpenCV提供了cv::Mat::clone()和cv::Mat::copyTo()函数。
Mat F = A.clone();
Mat G;
A.copyTo(G);
现在修改F或G不会影响由A的头指向的矩阵。你需要记住的是:
这是关于如何存储像素值。我们可以选择颜色空间和使用的数据类型。颜色空间指的是我们如何组合颜色组件来编码给定的颜色。最简单的一种是灰度,我们可以使用的颜色是黑色和白色。这些组合使我们能够创造出许多灰色阴影。
对于丰富多彩的方式,我们有更多的方法来选择。它们中的每一个都将其分解为三到四个基本组件,我们可以使用这些组件的组合来创建其他组件。最流行的一种是RGB,主要是因为这也是我们的眼睛构建颜色的方式。它的基色是红、绿、蓝。为了编码颜色的透明度,有时会添加第四个元素:alpha (a)。
然而,还有许多其他的颜色系统,每个都有自己的优势:
每个构建组件都有自己的有效域。这导致使用的数据类型。如何存储组件定义了我们对其域的控制。最小的数据类型可能是char,这意味着一个字节或8位。它可以是无符号的(因此可以存储从0到255的值)或有符号的(从-127到+127的值)。虽然在三个组件的情况下,这已经提供了1600万种可能的颜色来表示(就像在RGB的情况下),我们可以通过为每个组件使用float(4字节= 32位)或double(8字节= 64位)数据类型来获得更精细的控制。然而,请记住,增加组件的大小也会增加内存中整个图像的大小。
在加载、修改和保存图像教程中,我们已经学习了如何使用cv::imwrite()函数将矩阵写入图像文件。但是,为了调试目的,查看实际值要方便得多。我们可以使用Mat的<<操作符来做到这一点。请注意,这只适用于二维矩阵。
虽然Mat作为一个图像容器工作得很好,但它也是一个一般的矩阵类。因此,可以创建和操作多维矩阵。我们可以通过多种方式创建Mat对象:
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
对于二维和多通道图像,我们首先定义它们的大小:行数和列数。
然后,我们需要指定用于存储元素的数据类型和每个矩阵点的通道数。为此,我们根据以下约定构造了多个定义:
例如,CV_8UC3意味着我们使用8位长的unsigned char类型,每个像素有三个这样的类型来形成三个通道。有预定义的类型,最多四个通道。标量是一个四元素短向量。指定它,我们可以用自定义值初始化所有矩阵点。如果需要更多类型,可以使用上面的宏创建类型,在括号中设置通道号,如下所示。
int sz[3] = {2,2,2};
Mat L(3,sz, CV_8UC(1), Scalar::all(0));
上面的例子展示了如何创建一个二维以上的矩阵。指定它的维度,然后传递一个包含每个维度大小的指针,其余部分保持不变。
M.create(4,4, CV_8UC(2));
cout << "M = "<< endl << " " << M << endl << endl;
不能用这种构造初始化矩阵值。只有当新的大小不适合旧的大小时,它才会重新分配矩阵数据内存。
指定使用的大小和数据类型:
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;
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;
Mat RowClone = C.row(1).clone();
cout << "RowClone = " << endl << " " << RowClone << endl << endl;
请注意
我们可以使用cv::randu()函数用随机值填充矩阵。你需要给随机值一个下限和上限:
Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));
在上面的示例中,我们可以看到默认的格式化选项。然而,OpenCV允许你格式化你的矩阵输出:
默认的
cout << "R(默认)= " << endl << R << endl << endl;
Python
cout << "R (python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;
逗号分隔值(CSV)
cout << "R (csv) = " << endl << format(R, Formatter::FMT_CSV) << endl << endl;
Numpy
cout << "R (numpy) = " << endl << format(R, Formatter::FMT_NUMPY) << endl << endl;
C
cout << "R © = " << endl << format(R, Formatter::FMT_C) << endl << endl;
OpenCV还通过<<操作符提供了对其他常见OpenCV数据结构输出的支持:
Point2f P(5, 1);
cout << "Point (2D) = " << P << endl << endl;
Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;
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;
4. std::vector of 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;