官方文档链接:https://docs.opencv.org/4.2.0/d6/d6d/tutorial_mat_the_basic_image_container.html
目标 (Goal)
我们有多种方式可以从现实世界中获取数字图像:数字照相机,扫描仪,计算机断层扫描和磁共振成像等等。在任何情况下,我们看到的都是图像。然而,当我们将其转换为数字设备时,我们记录的是图像中每个点的数值。
例如在上图中,你可以看到汽车的后视镜不过是一个包含所有像素点强度值的矩阵。我们获取和存储像素值的方式可能会根据我们的需要而有所不同,但是最终,在计算机中所有图像都可能被简化为数字矩阵和描述矩阵本身的其他信息。OpenCV 是一个计算机视觉库,它的主要功能是处理和操作这些信息。因此,首先需要熟悉的是 OpenCV 如何存储和处理图像。
Mat
OpenCV 从2001年就出现了。在当时,这个库是围绕一个 C 接口构建的,为了将图像存储在内存中,前辈们使用了一个称为 “IplImage” 的 C 结构体。你会在很多旧的教程和教学资料中看到有关 “IplImage” 的论述。“IplImage” 的使用使得 C 语言的所有缺点都暴露出来了。**最大的问题是需要手动管理内存。**它的使用是建立在由用户负责内存分配和释放的假设之上。虽然对于小程序来说这不是问题,但是,一旦代码增长,用户将更难处理所有这些问题,很难专注于解决最初的开发目标。
幸运的是,C++的出现和 “类” 概念的提出,使得程序可以自动管理内存,极大地方便了用户的使用。另外,C++ 完全兼容 C,所以不会产生兼容性问题。因此,OpenCV 2.0 引入了新的 C++ 接口,它提供了一种新的方式,这意味着你不需要考虑内存管理,同时使得代码更加简洁。C++ 接口的主要缺点是目前许多嵌入式开发系统只支持 C。因此,除非你的目标是嵌入式平台,否则不必使用旧的方法。
关于 Mat,第一,你需要知道你不再需要手动分配内存,并且在不需要内存的时候,它会立即自动释放。大多数 OpenCV 函数将自动分配其输出数据。如果你传递一个已经存在的 Mat 对象,它已经为矩阵分配了所需的空间,它就将被重用。换言之,我们在任何时候都只使用执行任务所需的内存。
Mat 基本上是一个包含两个数据部分的类:
- 矩阵头:包含矩阵的大小,存储方法,存储矩阵的地址等信息。
- 指针:指向被包含的像素值的矩阵指针,可根据选择的存储方法取任何维度。
矩阵头大小是恒定的,但是矩阵本身的大小可能因图像而异,并且通常以数量级增大。
OpenCV 是一个图像处理库。它包含了大量的图像处理函数。为了解决计算上的难题,大多数情况下,用户将使用多个库函数。因此,常用的做法是将图像传递给函数。同时,图像处理算法往往意味着相当大的计算量。我们最不想因为不必要的大图像拷贝而降低程序速度。
为了解决这个问题,OpenCV 使用了引用计数系统。其思想是每个 Mat 对象都有自己的头,但是通过让他们的矩阵指针指向同一个地址,矩阵可以在两个 Mat 对象之间分享。此外,复制运算符只复制头和指向大矩阵的指针,而不复制数据本身。
cv::Mat A, C; // creates just the header parts
A = cv::imread(argv[1], IMREAD_COLOR); // here we'll know the method used (allocate matrix)
cv::Mat B(A); // Use the copy constructor
C = A; // Assignment operator
最后,所有上述的对象都指向同一个数据矩阵,使用它们当中的任何一个对象进行修改都会影响到其他的所有对象。实际上,不同的对象只是为相同的底层数据提供不同的访问方法。不过它们的对象头部分是不同的。另外有趣的是,用户可以创建只引用完整数据的头部分。例如,要在图像中创建感兴趣的区域(ROI),只需要创建一个具有新边界的头即可。
cv::Mat D (A, cv::Rect(10, 10, 100, 100) ); // using a rectangle
cv::Mat E = A(cv::Range::all(), cv::Range(1,3)); // using row and column boundaries
代码实现结果:
此处涉及到一个问题,当一个矩阵本身属于多个 Mat 对象,这些对象不再被需要时,它们是否负责清理矩阵空间,释放内存。答案是:最后一个使用矩阵的对象负责这项工作。这个过程是通过 引用计数 机制来处理的。每当有对象复制 Mat 对象的头时,矩阵的计数器会增加。当矩阵的头被释放时,计数器会减少。当计数器变为 0 时,矩阵被释放。有时用户想要复制矩阵本身,这就需要用到 OpenCV 提供的函数
- cv::Mat::clone()
- cv::Mat::copyTo()
cv::Mat F = A.clone();
cv::Mat G;
A.copyTo(G);
此时,修改 F 或者 G 将不会影响 A 的头所指向的矩阵。你需要记住的是:
- OpenCV 函数的输出图像分配是自动的(除非另有说明)。
- 使用 OpenCV 的 C++ 接口,你不需要考虑内存管理。
- 赋值运算符和拷贝构造器只复制头。
- 使用 cv::Mat::clone() 和 cv::Mat::copyTo() 函数可以复制图像的底层矩阵数据。
存储方法 (Storing methods)
这部分内容有关如何存储像素值。用户可以选择 颜色空间 和使用的 数据类型 。颜色空间指的是如何组合颜色组件以编码给定的颜色。最简单的一种方式是灰度,可以使用的颜色是黑色和白色。它们的各种结合可以创造出许多种灰色的阴影。
对于色彩丰富的方式,有许多方法可供选择。每一种都可以分解为三个或四个基本组件,即三种或四种原色,可以使用这些原色的组合来创建出其他颜色。使用最为广泛的是 RGB,主要是因为这也是我们眼睛建立颜色的方式。RGB 的底色是红色、绿色和蓝色。为了编码颜色的透明度,有时会添加第四个元素:alpha 即 (a)。
但是,还有很多其他的颜色系统,每个系统都有它们自己的优势:
- RGB 是最常见的方式。因为我们的眼睛就是使用类似的方式。但是请记住 OpenCV 标准显示系统使用 BGR 颜色空间来合成颜色(即颜色通道的顺序是 B、G、R)。
- HSV 和 HLS 将颜色分解为色调(hue)、饱和度(saturation)和值(value)/亮度分量(luminance components),这是一种更自然的描述颜色的方法。例如,你可能会忽略最后一个组件,使算法对输入图像的光照条件不太敏感。
- YCrCb 被流行的 JPEG 图像格式所使用。
- CIE L*a*b* 是一个感知上均匀的颜色空间,如果需要测量给定颜色到另一种颜色距离,它会很方便。
每个构建图像的通道都有自己的有效域。这将导致使用的数据类型。如何存储组件定义了用户对其域的控制。最小的数据类型可能是 char,即 1 个字节 或 8 位,可以是无符号(可存储 0 到 255 之间的值)或有符号(可存储 -127 到 +127 之间的值)。尽管在三通道的情况下,已经提供了 1600 万种可能的颜色来表示(例如 RGB),但用户可以通过为每个通道使用浮点(4 字节 = 32 位)或双字节(8 字节 = 64 位)数据类型来获得更精细的控制。不过,增加通道数值的精度也会增加内存中整个图片的大小。
创建 Mat 对象 (Creating a Mat object explicitly)
在 “加载、修改和保存图像” 教程中已经学习了如何使用 cv::imwrite() 函数将矩阵写入图像文件。但是,出于调试的目的,查看实际值要方便得多。可以 使用 Mat 的 << 运算符执行操作。注意,这种方法只适用于二维矩阵。
Mat 作为图像容器是一个通用的矩阵类。因此,可以创建和操作多维矩阵。可以通过多种方式创建 Mat 对象:
- cv::Mat::Mat Constructor
cv::Mat M(2, 2, CV_8UC3, cv::Scalar(0, 0, 255));
std::cout << "M = " << std::endl << M << std::endl << std::endl;
对于二维和多通道图像,首先需要定义它们的尺度 size:row 和 column。
然后需要指定用于存储元素的数据类型和每个矩阵点的通道数。为此,根据以下约定构造了多个定义:
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
例如,CV_8UC3 意味着使用 8 位长 的 无符号 字符 类型,每个像素有其中的三个值组成 3 个通道。预定义的类型中最多为 4 个通道。cv::Scalar() 函数适用于指定矩阵点的初始值,包含四个参数,后两个参数默认为 0.0。如果需要更多,可以使用上部宏创建类型,在括号种设置通道号,如下所示。
- 使用 C/C++ 数组通过构造函数初始化 (Use C/C++ arrays and initialize via constructor)
int sz[3] = { 2, 2, 2 };
cv::Mat L(3, sz, CV_8UC(1), cv::Scalar::all(0));
上面的例子展示了如何创建一个二维以上的矩阵。指定其维度,然后传递一个指针,该指针包含每个维度的大小,其余的保持不变。
- cv::Mat::create 函数
cv::Mat M;
M.create(4, 4, CV_8UC(2));
std::cout << "M = " << std::endl << M << std::endl << std::endl;
不能用此构造函数初始化矩阵值。如果新的矩阵大小不适合旧的矩阵,它将只重新分配其矩阵数据内存。
- MATLAB 形式的初始化:cv::Mat::zeros,cv::Mat::ones,cv::Mat::eye。需要指定使用的数据类型和大小:
cv::Mat E = cv::Mat::eye(4, 4, CV_64F);
std::cout << "E = " << std::endl << " " << E << std::endl << std::endl;
cv::Mat O = cv::Mat::ones(2, 2, CV_32F);
std::cout << "O = " << std::endl << " " << O << std::endl << std::endl;
cv::Mat Z = cv::Mat::zeros(3, 3, CV_8UC1);
std::cout << "Z = " << std::endl << " " << Z << std::endl << std::endl;
- 对于小矩阵,可以使用逗号分隔的初始化器或初始化列表 (在最后一种情况下需要 C++ 11 支持):
cv::Mat C = (cv::Mat_<double>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
std::cout << "C = " << std::endl << " " << C << std::endl << std::endl;
cv::Mat C = (cv::Mat_<double>({ 0, -1, 0, -1, 5, -1, 0, -1, 0 })).reshape(3);
std::cout << "C = " << std::endl << " " << C << std::endl << std::endl;
- 为现有的 Mat 对象创建一个新的头,然后使用 cv::Mat::clone 或 cv::Mat::copyTo
cv::Mat RowClone = C.row(1).clone();
std::cout << "RowClone = " << std::endl << " " << RowClone << std::endl << std::endl;
注意
可以使用 cv::randu() 函数用随机值填充矩阵。用户需要给出随机值的下限和上限:
cv::Mat R = cv::Mat(3, 2, CV_8UC3);
cv::randu(R, cv::Scalar::all(0), cv::Scalar::all(255));
std::cout << "R = " << std::endl << " " << R << std::endl << std::endl;
输出格式 (Output formatting)
上面的示例中,可以看到默认的格式化选项。同时,OpenCV 还允许格式化矩阵输出:
- 默认 (Default)
std::cout << "R (default) = " << std::endl << " " << R << std::endl << std::endl;
- Python
std::cout << "R (python) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_PYTHON) << std::endl << std::endl;
- CSV (Comma separated values)
std::cout << "R (csv) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_CSV) << std::endl << std::endl;
- Numpy
std::cout << "R (numpy) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_NUMPY) << std::endl << std::endl;
- C
std::cout << "R (C) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_C) << std::endl << std::endl;
其他一般项目输出 (Output of other common items)
OpenCV 还支持通过 << 运算符输出其他常见的 OpenCV 数据结构:
- 2D Point
cv::Point2f P(5, 1);
std::cout << "Point (2D) = " << P << std::endl << std::endl;
- 3D Point
cv::Point3f P3f(2, 6, 7);
std::cout << "Point (3D) = " << P3f << std::endl << std::endl;
- std::vector via cv::Mat
std::vector<float> v;
v.push_back((float)CV_PI);
v.push_back(2);
v.push_back(3.01f);
std::cout << "Vector of floats via Mat = " << cv::Mat(v) << std::endl << std::endl;
- std::vector of points
std::vector<cv::Point2f> vPoints(20);
for (size_t i = 0; i < vPoints.size(); ++i)
vPoints[i] = cv::Point2f((float)(i * 5), (float)(i % 7));
std::cout << "A vector of 2D Points = " << vPoints << std::endl << std::endl;
完整代码
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#define window "OpenCV"
int main(int argc, char** argv)
{
cv::Mat M(2, 2, CV_8UC3, cv::Scalar(0, 0, 255));
int sz[3] = { 2, 2, 2 };
cv::Mat L(3, sz, CV_8UC(1), cv::Scalar::all(0));
cv::Mat M;
M.create(4, 4, CV_8UC(2));
std::cout << "M = " << std::endl << M << std::endl << std::endl;
cv::Mat E = cv::Mat::eye(4, 4, CV_64F);
std::cout << "E = " << std::endl << " " << E << std::endl << std::endl;
cv::Mat O = cv::Mat::ones(2, 2, CV_32F);
std::cout << "O = " << std::endl << " " << O << std::endl << std::endl;
cv::Mat Z = cv::Mat::zeros(3, 3, CV_8UC1);
std::cout << "Z = " << std::endl << " " << Z << std::endl << std::endl;
cv::Mat C = (cv::Mat_<double>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
std::cout << "C = " << std::endl << " " << C << std::endl << std::endl;
cv::Mat C = (cv::Mat_<double>({ 0, -1, 0, -1, 5, -1, 0, -1, 0 })).reshape(3);
std::cout << "C = " << std::endl << " " << C << std::endl << std::endl;
cv::Mat RowClone = C.row(1).clone();
std::cout << "RowClone = " << std::endl << " " << RowClone << std::endl << std::endl;
cv::Mat R = cv::Mat(3, 2, CV_8UC3);
cv::randu(R, cv::Scalar::all(0), cv::Scalar::all(255));
std::cout << "R (default) = " << std::endl << " " << R << std::endl << std::endl;
std::cout << "R (python) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_PYTHON) << std::endl << std::endl;
std::cout << "R (csv) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_CSV) << std::endl << std::endl;
std::cout << "R (numpy) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_NUMPY) << std::endl << std::endl;
std::cout << "R (C) = " << std::endl << " " << cv::format(R, cv::Formatter::FMT_C) << std::endl << std::endl;
cv::Point2f P(5, 1);
std::cout << "Point (2D) = " << P << std::endl << std::endl;
cv::Point3f P3f(2, 6, 7);
std::cout << "Point (3D) = " << P3f << std::endl << std::endl;
std::vector<float> v;
v.push_back((float)CV_PI);
v.push_back(2);
v.push_back(3.01f);
std::cout << "Vector of floats via Mat = " << cv::Mat(v) << std::endl << std::endl;
std::vector<cv::Point2f> vPoints(20);
for (size_t i = 0; i < vPoints.size(); ++i)
vPoints[i] = cv::Point2f((float)(i * 5), (float)(i % 7));
std::cout << "A vector of 2D Points = " << vPoints << std::endl << std::endl;
return 0;
}