一、学习目标
我们有多种方法从现实世界中获取数字图像:如数码相机、扫描仪、计算机断层摄影术(CT)和磁共振成像等。在任何情况下,我们(人类)看到的都是图像。然而,当将其转换到我们的数字设备时,我们所使用的是图像中每个点的数值(像素值)。
例如,在上面的图像中,你可以看到汽车的后视镜只是一个包含所有像素点的强度值(像素值)的矩阵。我们获取和存储像素值的方式可能会根据我们的需要而有所不同,但最终计算机世界中的所有图像都可能被简化为数字矩阵和描述矩阵本身的其他信息。OpenCV是一个计算机视觉库,它的主要关注处理和操作这些信息。因此,我们首先需要熟悉的是OpenCV如何存储和处理图像。
二、Mat 介绍(建议细读)
OpenCV从2001年就出现了。当时,OpenCV的库是围绕C接口构建的,为了将图像存储在内存中,Opencv使用了一个称为IplImage的C语言结构体。在大多数较老的教程和教学资料中看到的就是这种结构体。这样做有很多的弊端,它把C语言的所有缺点都展现了出来,其中最大的问题是手动内存管理,用户必须负责内存的分配和回收。虽然这对较小的程序来说不是问题,但一旦你的代码库增长得比较大,那么进行内存管理就会相当复杂,其难度不亚于解决真正得图像处理问题,这样就有点舍本逐末了。
幸运的是c++引入了类的概念,通过自动内存管理机制(或多或少)使用户更容易使用类。幸运的是c++与C是完全兼容的,所以修改后不会出现兼容性问题。因此,OpenCV 2.0引入了一个新的c++接口,它提供了一种新的处理方式,这意味着你不需要手动处理内存管理,这会使你的代码更简洁。c++接口的主要缺点是,目前许多嵌入式开发系统只支持C。因此,除非你的目标是嵌入式平台,否则使用老接口是没有意义的(除非你是一个受虐狂程序员,你在自找麻烦)。
关于Mat,我们需要知道的第一件事是,我们不再需要手动分配它的内存并在不需要时立即释放它。虽然我们也可以自行分配和管理内存,但大多数OpenCV函数将自动分配其数据所需的内存空间。如果您传递一个已经存在的Mat对象,它已经为矩阵分配了所需的空间,那么它将被重用。换句话说,我们在任何时候都只使用执行任务所需的内存。
Mat是一个类,其包含两个数据部分:Mat头部(包含矩阵的大小,存储方法,和存储地址等信息)和一个指向包含像素值矩阵的指针(根据选择的存储方法选择任意维度)。Mat头部的大小是恒定的,然而存储像素值的矩阵本身的大小可能因图像而异,通常要大几个数量级。
OpenCV是一个图像处理库。它包含了大量的图像处理功能。为了解决一个计算机视觉问题,大多数时候您将使用库的多个函数。正因为如此,将图像传递给函数是一种常见的做法。我们不要忘了,我们正在讨论的是图像处理算法,它往往计算量很大。我们最不希望做的就是对大尺寸的图像进行不必要的复制,这会进一步降低程序的速度。
为了解决这个问题,OpenCV使用了一个引用计数系统。每个Mat对象都有自己的头部,但是两个Mat对象之间可以共享一个矩阵(像素矩阵),即让它们的矩阵指针指向相同的地址。此外,复制操作只复制Mat头部信息和指向数据矩阵的指针,而不复制数据本身(浅拷贝)。
Mat A, C; // 只创建Mat的矩阵头信息
A = imread(argv[1], IMREAD_COLOR); // 这里我们将知道所使用的方法(三通道彩色图,分配矩阵)
Mat B(A); // 使用拷贝构造函数
C = A;
所有上述对象最终都指向同一个数据矩阵,使用其中任何一个对象进行修改也会影响到其他所有对象。实际上,不同的对象只是对相同的底层数据提供了不同的访问方法。然而,它们的Mat头部是不同的。真正有趣的是,您可以创建只引用完整数据一部分的Mat对象。例如,要在一个图像中创建一个感兴趣的区域(ROI),你只需创建一个带有新的边界的Mat头部:
Mat D (A, Rect(10, 10, 100, 100) ); // 使用矩阵定义感兴趣区域,后面会讲Rect对象
Mat E = A(Range::all(), Range(1,3)); // 使用行和列的边界定义感兴趣区域
现在你可能会问:如果矩阵本身属于多个Mat对象,那么当这个Mat对象不再使用的时候谁负责清理它?答案很简单:最后一个使用它的对象。这可以通过使用引用计数机制来处理。每当有人复制一个Mat对象的头部时,矩阵的计数器就会增加。每当清理Mat头部时,此计数器将减少。当计数器达到0时,释放矩阵。当然,有时你也想真正地复制矩阵本身,所以OpenCV提供了cv::Mat::clone() 和cv::Mat::copyTo() 函数。
Mat F = A.clone();
Mat G;
A.copyTo(G);
现在修改F或G将不会影响由A的 Mat头部指向的矩阵。现在,你需要记住的是:
- OpenCV函数的输出图像内存分配是自动的(除非另有说明)。
- 您不需要考虑使用OpenCV的c++接口进行内存管理。
- 赋值操作和拷贝构造函数只复制Mat头部。
- 图像的底层矩阵可以使用cv::Mat::clone() 和cv::Mat::copyTo() 函数进行复制。
三、Mat的存储方法
这是关于如何存储像素值。您可以选择使用的颜色空间和数据类型。颜色空间指的是我们如何组合颜色部分以编码给定的颜色。最简单的是灰度图,我们使用的颜色是黑色和白色。这些组合可以让我们创造出许多灰色的阴影。
对于彩色的方式,我们有更多的方法可供选择。每一个像素值都将其分解为三到四个基本部分,我们可以使用这些部分的组合来创建其他部分。最流行的一种是RGB,主要是因为这也是我们的眼睛构建颜色的方式。它的基色是红、绿、蓝。为了编码颜色的透明度,有时会添加第四个元素:alpha (a)。
然而,还有许多其他的颜色系统,它们都有自己的优势:
- RGB是最常见的,因为我们的眼睛使用的是类似的东西,但是请记住,OpenCV标准显示系统使用BGR颜色空间组成颜色(红色和蓝色通道交换位置)。
- HSV和HLS将颜色分解为色相、饱和度和值/亮度分量,这是我们描述颜色的一种更自然的方式。例如,你可以去掉最后一个组件,这样你的算法对输入图像的光照条件就不那么敏感了。
- YCrCb是常用的JPEG图像格式。
- CIE Lab*是一个感知上一致的颜色空间,如果你需要测量一种给定颜色到另一种颜色的距离,它就派上用场了。
每个颜色空间的组成部分都有自己的有效域。这就引出了所使用的数据类型。我们如何存储一个组件定义了我们在它的域上拥有的控件。最小的数据类型可能是char,这意味着一个字节或8位。这可能是无符号的(因此可以存储0到255的值)或有符号的(值从-127到+127)。尽管在三个组件的情况下,这已经给出了1600万种可能的颜色来表示(就像RGB的情况),我们可以通过使用float(4字节= 32位)或double(8字节= 64位)的数据类型来获得更精细的控制。然而,请记住,增加组件的大小(存储字节大小)也会增加图像所占内存的大小。
四、显示地创建 Mat 对象
在加载、修改和保存图像教程中,您已经学习了如何使用cv::imwrite() 函数将Mat对象写入图像文件。但是,出于调试目的,查看实际值要方便得多。你可以使用Mat的 << 操作符来实现这一点。请注意,这只对二维矩阵有效。
尽管Mat作为图像容器工作得非常好,但它也是一个通用的矩阵类。因此,可以创建和操作多维矩阵。你可以用多种方式创建Mat对象:
- cv::Mat::Mat构造函数
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
对于二维地单通道或多通道图像,我们首先定义它们的大小:行和列。
然后,我们需要指定用于存储元素的数据类型以及每个矩阵点的通道数量。为了做到这一点,我们可以使用如下方法定义:
CV_[每个数值的位数][有符号或者无符号][类型前缀]C[通道数目]
例如,CV_8UC3意味着我们使用长度为8位的unsigned char类型,每个像素有三个unsigned char类型来形成三个通道。有最多四个通道预定义的类型。cv::Scalar为四元短向量。指定它,您就可以用自定义值初始化所有矩阵的像素值。如果需要更多,可以使用上面的宏创建类型,在括号中设置通道号,下面会有示例。
- 使用C/ c++数组并通过构造函数进行初始化
int sz[3] = {2,2,2};
Mat L(3,sz, CV_8UC(1), Scalar::all(0));
上面的例子展示了如何创建一个超过二维的矩阵。指定它的维度,然后传递一个包含每个维度大小的指针,其余部分保持不变。
- cv::Mat::create函数
M.create(4,4, CV_8UC(2));
cout << "M = "<< endl << " " << M << endl << endl;
不能使用此构造初始化矩阵值。如果新的矩阵大小不适合旧的,它只会重新分配它的矩阵数据内存。
- MATLAB式初始器:cv::Mat::zeros, cv::Mat::ones, cv::Mat::eye。指定使用的大小和数据类型:
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;
- 对于较小的矩阵,你可以使用逗号分隔的初始化式或初始化列表(最后一种情况需要c++ 11支持):
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对象使用cv::Mat::clone或cv::Mat::copyTo创建一个新的头部。
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));
五、格式化输出 Mat 对象
在上面的例子中,您可以看到默认的格式选项。然而,OpenCV允许你格式化你的矩阵输出:
- 默认方式
cout << "R (default) = " << 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 (c) = " << endl << format(R, Formatter::FMT_C ) << endl << endl;
六、其他常见元素的输出
OpenCV也通过 << 操作符提供了对其他常见OpenCV数据结构输出的支持:
- 2D 点
Point2f P(5, 1);
cout << "Point (2D) = " << P << endl << endl;
- 3D 点
Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;
- std::vector 对比 cv::Mat
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;
- std::vector 的点
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;
七、致谢
1、常规谢天谢地谢父母,感谢各位好友
2、特别感谢OpenCV的官方文档
3、感兴趣的小伙伴欢迎入群一起学习和探讨。飞机票: 入群链接