1 简介
OpenCV 具有模块化结构,这意味着该包包括多个共享或静态库。以下是一些常用的模块:
- 核心功能(
core
):一个定义基本数据结构的紧凑模块,包括密集的多维数组Mat
和所有其他模块使用的基本函数。 - 图像处理(
imgproc
):一个图像处理模块,包括线性和非线性图像滤波、几何图像变换(调整大小、仿射和透视扭曲、基于通用表的重新映射)、颜色空间转换、直方图等。 - 视频分析(
video
):一个视频分析模块,包括运动估计、背景减除和对象跟踪算法。 - 相机校准和3D重建(
calib3d
):基本的多视图几何算法、单视图和立体相机校准、物体姿态估计、立体对应算法和 3D 重建元素。 - 2D 特征框架(
features2d
):显著特征检测器、描述符和描述符匹配器。 - 目标检测(
objdetect
):检测对象和预定义类的实例。 - 高级 GUI(
highgui
):简单 UI 功能的易于使用的界面。 - 视频输入/输出(
videoio
):一个易于使用的视频捕获和视频编解码器接口。
2 API 概念
2.1 cv
命名空间
所有 OpenCV 类和函数都放置在cv
命名空间中。一些当前或未来的 OpenCV 外部名称可能与 STL 或其他库冲突。在这种情况下,必须使用显式命名空间说明符来解决名称冲突。
2.2 自动内存管理
OpenCV 自动处理所有内存。
首先,函数和方法使用的std::vector
、cv::Mat
和其他数据结构都有析构函数,可以在需要时释放底层内存缓冲区。这意味着析构函数并不总是像Mat
那样释放缓冲区。它们考虑了可能的数据共享。析构函数会递减与矩阵数据缓冲区相关的引用计数器。当且仅当引用计数器达到零时,即当没有其他结构引用同一缓冲区时,缓冲区才会被释放。同样,当复制Mat
实例时,实际上并没有复制实际数据。相反,引用计数器会递增,以记住同一数据还有另一个所有者。还有cv::Mat::clone
方法,用于创建矩阵数据的完整副本。请参阅下面的示例:
// 创建一个 8Mb 的矩阵
Mat A(1000, 1000, CV_64F);
// 为同一矩阵创建另一个标题;无论矩阵大小如何,这都是一个即时操作。
Mat B = A;
// 为 A 的第 3 行创建另一个标题;也不复制任何数据
Mat C = B.row(3);
// 现在创建矩阵的独立副本
Mat D = B.clone();
// 将 B 的第 5 行复制到 C,也就是说,将 A 的第 5 行将复制到 A 的第 3 行
B.row(5).copyTo(C);
// 现在让 A 和 D 共享数据;之后,B 和 C 仍然引用 A 的修改版本
A = D;
// 现在,将 B 设置为空矩阵(不引用内存缓冲区),但修改后的 A 版本仍将被 C 引用,尽管 C 只是原始 A 的一行
B.release();
// 最后,制作一个 C 的完整副本。因此,修改后的大矩阵将被释放,因为它没有被任何人引用
C = C.clone();
你可以看到,Mat
和其他基本结构的使用很简单。但是,在不考虑自动内存管理的情况下创建的高级类甚至用户数据类型呢?对他们来说,OpenCV 提供了类似于std::shared_ptr
的cv::Ptr
模板类。因此,与其使用普通指针:
T* ptr = new T(...);
你可以使用:
Ptr<T> ptr(new T(...));
或
Ptr<T> ptr = makePtr<T>(...);
Ptr<T>
封装了一个指向T
实例的指针和一个与指针关联的引用计数器。
2.3 输出数据的自动分配
OpenCV 会自动释放内存,并且在大多数情况下会自动为输出函数参数分配内存。因此,如果一个函数有一个或多个输入数组(cv::Mat
实例)和一些输出数组,则输出数组会自动分配或重新分配。输出数组的大小和类型由输入数组的大小与类型决定。如果需要,这些函数会采用额外的参数来帮助计算输出数组属性。例如:
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
using namespace cv;
int main(int, char**)
{
VideoCapture cap(0);
if(!cap.isOpened()) return -1;
Mat frame, edges;
namedWindow("edges", WINDOW_AUTOSIZE);
for(;;)
{
cap >> frame;
cvtColor(frame, edges, COLOR_BGR2GRAY);
GaussianBlur(edges, edges, Size(7,7), 1.5, 1.5);
Canny(edges, edges, 0, 30, 3);
imshow("edges", edges);
if(waitKey(30) >= 0) break;
}
return 0;
}
数组frame
由>>
运算符自动分配,因为视频捕获模块已知视频帧分辨率和比特深度。数组edges
由cvtColor
函数自动分配。它的大小和位深度与输入数组相同,通道数转换为了 1。请注意,在循环体的第一次执行期间,frame
和edges
只分配一次,因为所有下一个视频帧具有相同的分辨率。如果以某种方式更改视频分辨率,数组将自动重新分配。
这项技术的关键组成部分是cv::Mat::create
方法。它需要所需的数组大小和类型。如果数组已经具有指定的大小和类型,则该方法不执行任何操作。否则,它会释放之前分配的数据(如果有的话)(这部分涉及递减引用计数器并将其与零进行比较),然后分配所需大小的新缓冲区。大多数函数为每个输出数组调用cv::Mat::create
方法,因此实现了自动输出数据分配。
2.4 饱和算术
作为一个计算机视觉库,OpenCV 处理了大量图像像素,这些像素通常以紧凑的、每个通道 8 位或 16 位的形式编码,因此具有有限的值范围。此外,对图像的某些操作,如颜色空间转换、亮度/对比度调整、锐化、复杂插值(双三次、Lanczos),可能会产生超出可用范围的值。如果只存储结果的最低 8(16)位,则会导致视觉伪影,并可能影响进一步的图像分析。为了解决这个问题,使用了所谓的饱和算术。例如,要将运算结果 r 存储到 8 位图像中,您可以在 0…255 范围内找到最接近的值:
I(x,y)=min(max(round(r),0),255) I(x, y) = \min(\max(round(r), 0), 255) I(x,y)=min(max(round(r),0),255)
类似的规则适用于 8 位有符号、16 位有符号和无符号类型。这种语义在库中无处不在。在 C++ 代码中,它是使用类似于标准 C++ 强制转换操作的cv::saturate_cast<>
函数完成的:
I.at<uchar>(y, x) = saturate_cast<uchar>(r);
其中cv::uchar
是 OpenCV 8 位无符号整数类型。在优化的 SIMD 代码中,使用了 paddusb、packuswb 等 SSE2 指令。它们有助于实现与 C++ 代码完全相同的行为。注意,当结果为 32 位整数时,不应用饱和算术。
2.5 固定像素类型。模板的有限使用
模板是 C++ 的一大特性,它能够实现非常强大、高效且安全的数据结构和算法。然而,模板的广泛使用可能会大大增加编译时间和代码大小。此外,当模板被专门使用时,很难将接口和实现分开。这对于基本算法来说可能很好,但对于计算机视觉库来说则不好,因为单个算法可能跨越数千行代码。因此,为了简化其他语言的绑定开发,当前的 OpenCV 实现基于模板的多态性和运行时调度。在运行时调度太慢(如像素访问运算符)、不可能(通用cv::Ptr<>
实现)或非常不方便(cv::saturate_cast<>()
)的地方,当前的实现引入了小的模板类、方法和函数。在当前 OpenCV 版本的其他任何地方,模板的使用都是有限的。
因此,库可以操作的原始数据类型的固定集合是有限的。也就是说,数组元素应该具有以下类型之一:
- 8 位无符号整数(
uchar
) - 8 位有符号整数(
schar
) - 16 位无符号整数(
ushort
) - 16 位带符号整数(
short
) - 32 位带符号整数(
int
) - 32 位浮点数(
float
) - 64 位浮点数(
double
) - 由几个元素组成的元组,其中所有元素都具有相同的类型(上述类型之一)。元素为这种元组的数组称为多通道数组,与元素为标量值的单通道数组相反。通道的最大可能数量由
CV_CN_MAX
常数定义,该常数当前设置为 512。
对于这些基本类型,应用了以下枚举:
enum { CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6 };
可以使用以下方法指定多通道类型:
CV_8UC1 ... CV_64FC4
常数(适用于从1到4的多个通道)。CV_8UC(n) ... CV_64FC(n)
或CV_MAKETYPE(CV_8U, n) ... CV_MAKETYPE(CV_64F, n)
宏,当通道数量超过 4 或编译时未知时。
示例:
Mat mtx(3, 3, CV_32F); // 创建一个 3x3 浮点数矩阵
Mat cmtx(10, 1, CV_64FC2); // 创建一个 10x1 的 2 通道浮点数矩阵(10 元素复数向量)
Mat img(Size(1920, 1080), CV_8UC3); // 创建一个 1920x1080 的 3 通道(颜色)图像
Mat grayscale(img.size(), CV_MAKETYPE(img.depth(), 1)); // 创建一个和 img 相同大小和通道类型的 1 通道图像
使用 OpenCV 无法构造或处理具有更复杂元素的数组。此外,每个函数或方法只能处理所有可能数组类型的子集。通常,算法越复杂,支持的格式子集就越小。