介绍#
OpenCV (Open Source Computer Vision Library: https://opencv.ac.cn) 是一个开源库,包含数百个计算机视觉算法。本文档描述的是所谓的 OpenCV 2.x API,它本质上是一个 C++ API,与基于 C 的 OpenCV 1.x API 不同(自 OpenCV 2.4 版本起,C API 已被弃用且不再使用 “C” 编译器进行测试)
OpenCV 具有模块化结构,这意味着该软件包包含多个共享或静态库。以下模块可用:
核心功能 (core) - 一个定义基础数据结构的紧凑模块,包括稠密
多维数组 Mat 以及所有其他模块使用的基础函数。
图像处理 (imgproc) - 一个图像处理模块,包括线性及非线性图像滤波、
几何图像变换(缩放、仿射和透视变换、基于通用表的重映射)、颜色空间转换、直方图等。
图像文件读写 (imgcodecs) - 包含用于读写各种格式图像文件的函数。
视频 I/O (videoio) - 一个用于视频捕获和视频编解码的易用接口。
高级 GUI (highgui) - 一个用于简单 UI 功能的易用接口。
视频分析 (video) - 一个视频分析模块,包括运动估计、背景减除
和目标跟踪算法。
3D (3d) - 基础多视图几何算法、物体姿态估计和 3D 重建元素。
特征框架 (features) - 显著特征检测器、描述符和描述符匹配器。
目标检测 (objdetect) - 对预定义类别(例如:
人脸、眼睛、杯子、人、汽车等)的目标和实例进行检测).
相机标定 (calib) - 单相机和立体相机标定
立体匹配 (stereo) - 立体匹配算法
高级 GUI (highgui) - 一个用于简单 UI 功能的易用接口。
视频 I/O (videoio) - 一个用于视频捕获和视频编解码的易用接口。
深度神经网络模块 (dnn) - 深度神经网络模块。
计算摄影学 (photo) - 先进的图像处理技术,如去噪、修复(inpainting)。
图像拼接 (stitching) - 用于图像拼接和全景图创建的函数。
… 以及其他一些辅助模块,例如 FLANN 和 Google test 封装、Python 绑定等。
本文档的后续章节将描述每个模块的功能。但首先,请确保熟悉库中广泛使用的通用 API 概念。
API 概念#
cv 命名空间#
所有 OpenCV 类和函数都放置在 cv 命名空间中。因此,要在代码中访问这些功能,请使用 cv:: 限定符或 using namespace cv; 指令
#include "opencv2/core.hpp"
...
cv::Mat H = cv::findHomography(points1, points2, cv::RANSAC, 5);
...
或
#include "opencv2/core.hpp"
using namespace cv;
...
Mat H = findHomography(points1, points2, RANSAC, 5 );
...
某些当前或未来的 OpenCV 外部名称可能会与 STL 或其他库冲突。在这种情况下,请使用明确的命名空间限定符来解决名称冲突
自动内存管理#
OpenCV 自动处理所有内存。
首先,std::vector、cv::Mat 以及函数和方法使用的其他数据结构都具有析构函数,可在需要时释放底层内存缓冲区。这意味着析构函数并不总是像 Mat 的情况那样直接释放缓冲区。它们会考虑可能的数据共享。析构函数会递减与矩阵数据缓冲区相关联的引用计数。当且仅当引用计数达到零(即没有其他结构引用该缓冲区)时,缓冲区才被释放。类似地,当复制一个 Mat 实例时,并不会真正复制实际数据。相反,引用计数会增加,以记录该数据有另一个所有者。此外,还有 cv::Mat::clone 方法可创建矩阵数据的完整副本。请参阅下面的示例
// create a big 8Mb matrix
Mat A(1000, 1000, CV_64F);
// create another header for the same matrix;
// this is an instant operation, regardless of the matrix size.
Mat B = A;
// create another header for the 3-rd row of A; no data is copied either
Mat C = B.row(3);
// now create a separate copy of the matrix
Mat D = B.clone();
// copy the 5-th row of B to C, that is, copy the 5-th row of A
// to the 3-rd row of A.
B.row(5).copyTo(C);
// now let A and D share the data; after that the modified version
// of A is still referenced by B and C.
A = D;
// now make B an empty matrix (which references no memory buffers),
// but the modified version of A will still be referenced by C,
// despite that C is just a single row of the original A
B.release();
// finally, make a full copy of C. As a result, the big modified
// matrix will be deallocated, since it is not referenced by anyone
C = C.clone();
你可以看到使用 Mat 和其他基础结构很简单。但对于高级类,甚至是未考虑到自动内存管理而创建的用户数据类型怎么办?为此,OpenCV 提供了 cv::Ptr 模板类,它类似于 C++11 中的 std::shared_ptr。因此,与其使用普通指针
T* ptr = new T(...);
你可以使用
Ptr<T> ptr(new T(...));
或者
Ptr<T> ptr = makePtr<T>(...);
Ptr<T> 封装了一个指向 T 实例的指针以及与该指针相关联的引用计数。详情请参阅 cv::Ptr 描述。
输出数据的自动分配#
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 函数自动分配。它具有与输入数组相同的大小和位深。通道数由于传递了颜色转换代码 cv::COLOR_BGR2GRAY(意味着从彩色转换为灰度),因此通道数为 1。请注意,由于所有后续视频帧的分辨率都相同,frame 和 edges 仅在循环体第一次执行期间分配一次。如果你以某种方式更改了视频分辨率,数组将被自动重新分配。
这项技术的关键组件是 cv::Mat::create 方法。它接收所需的数组大小和类型。如果数组已经具有指定的大小和类型,该方法不执行任何操作。否则,它将释放之前分配的数据(如果有,这部分涉及递减引用计数并将其与零比较),然后分配一个所需大小的新缓冲区。大多数函数为每个输出数组调用 cv::Mat::create 方法,从而实现了输出数据的自动分配。
该方案的一些显著例外情况是 cv::mixChannels、cv::RNG::fill 以及其他一些函数和方法。它们无法自动分配输出数组,因此你必须提前完成此操作。
饱和运算#
作为一个计算机视觉库,OpenCV 大量处理图像像素,这些像素通常以紧凑的每通道 8 位或 16 位形式编码,因此具有有限的取值范围。此外,某些图像操作(如颜色空间转换、亮度和对比度调整、锐化、复杂插值(双三次、Lanczos))可能会产生超出可用范围的值。如果你仅存储结果的最低 8(16)位,这会导致视觉伪影并可能影响进一步的图像分析。为了解决这个问题,使用了所谓的 饱和 运算。例如,为了将操作结果 r 存储到 8 位图像中,你需要找到 0..255 范围内最接近的值
类似的规则也适用于 8 位有符号、16 位有符号和无符号类型。这种语义在整个库中都被使用。在 C++ 代码中,它是通过 cv::saturate_cast<> 函数实现的,这些函数类似于标准 C++ 的强制转换操作。下面是上述公式的实现
其中 cv::uchar 是 OpenCV 的 8 位无符号整数类型。在优化后的 SIMD 代码中,使用了如 paddusb、packuswb 等 SSE2 指令。它们有助于实现与 C++ 代码完全相同的行为。
注意
当结果为 32 位整数时,不应用饱和运算。
固定像素类型。模板的有限使用#
模板是 C++ 的一项强大特性,它能够实现功能强大、高效且安全的数据结构和算法。然而,过度使用模板可能会显著增加编译时间和代码体积。此外,在完全使用模板时,很难将接口与实现分离。对于基础算法来说这可能没问题,但对于单个算法可能涵盖数千行代码的计算机视觉库而言,这并不理想。基于此,同时也为了简化为其他没有模板或模板能力有限的语言(如 Python、Java、Matlab)开发绑定,当前的 OpenCV 实现基于多态和运行时分发(runtime dispatching)而非模板。在运行时分发速度过慢(如像素访问运算符)、无法实现(泛型 cv::Ptr<> 实现)或极其不便(cv::saturate_cast<>())的地方,当前的实现引入了小型模板类、方法和函数。在当前 OpenCV 版本的其他任何地方,模板的使用都受到限制。
因此,库可以操作的原始数据类型是一个有限的固定集合。也就是说,数组元素应具有以下类型之一:
8 位无符号整数 (uint8_t, uchar)
8 位有符号整数 (int8_t, schar)
16 位无符号整数 (uint16_t, ushort)
16 位有符号整数 (int16_t, short)
32 位无符号整数 (uint32_t, unsigned) *1
32 位有符号整数 (int32_t, int)
64 位无符号整数 (uint64_t, unsigned long) *1
64 位有符号整数 (int64_t, long) *1
16 位脑浮点数 / bfloat16 (bfloat) *1
16 位半精度浮点数 / hfloat16 (hfloat)
32 位单精度浮点数 (float)
64 位双精度浮点数 (double)
布尔值 (boolean) *1
由多个元素组成的元组,其中所有元素具有相同的类型(上述类型之一)。元素为此类元组的数组被称为多通道数组,与元素为标量值的单通道数组相对。最大可能通道数由 #CV_CN_MAX 常量定义,目前设置为 128。
注意
*1) 从 OpenCV 5 开始支持。
对于这些基本类型,适用以下定义
#define CV_8U 0
#define CV_8S 1
#define CV_16U 2
#define CV_16S 3
#define CV_32S 4
#define CV_32F 5
#define CV_64F 6
#define CV_16F 7
#define CV_16BF 8
#define CV_Bool 9
#define CV_64U 10
#define CV_64S 11
#define CV_32U 12
多通道(n通道)类型可以使用以下选项指定
#CV_8UC1 … #CV_32UC4 常量(适用于 1 到 4 个通道)
当通道数超过 4 个或在编译时未知时,使用 CV_8UC(n) … CV_32UC(n) 或 CV_MAKETYPE(CV_8U, n) … CV_MAKETYPE(CV_32U, n) 宏。
注意
#CV_32FC1 == #CV_32F, #CV_32FC2 == #CV_32FC(2) == #CV_MAKETYPE(CV_32F, 2),并且 #CV_MAKETYPE(depth, n) == (CV_MAT_DEPTH(depth) + (((cn)-1) << CV_CN_SHIFT))。这意味着常量类型是由深度(取低 CV_CN_SHIFT 位,目前设置为 5)和通道数减 1(取接下来的 log2(CV_CN_MAX) 位)构成的。
示例
Mat mtx(3, 3, CV_32F); // make a 3x3 floating-point matrix
Mat cmtx(10, 1, CV_64FC2); // make a 10x1 2-channel floating-point
// matrix (10-element complex vector)
Mat img(Size(1920, 1080), CV_8UC3); // make a 3-channel (color) image
// of 1920 columns and 1080 rows.
Mat grayscale(img.size(), CV_MAKETYPE(img.depth(), 1)); // make a 1-channel image of
// the same size and same
// channel type as img
包含更复杂元素的数组无法使用 OpenCV 构建或处理。此外,每个函数或方法只能处理所有可能数组类型的一个子集。通常,算法越复杂,支持的格式子集就越小。以下是此类限制的典型示例:
人脸检测算法仅适用于 8 位灰度或彩色图像。
线性代数函数和大多数机器学习算法仅适用于浮点数组。
基础函数(如 cv::add)支持所有类型。
颜色空间转换函数支持 8 位无符号、16 位无符号和 32 位浮点类型。
每个函数支持的类型子集是根据实际需求定义的,未来可能会根据用户请求进行扩展。
InputArray 和 OutputArray#
许多 OpenCV 函数处理密集的二维或多维数值数组。通常,这类函数接受 cv::Mat 作为参数,但在某些情况下,使用 std::vector<>(例如点集)或 cv::Matx<>(例如 3x3 单应性矩阵)更为方便。为了避免 API 中出现大量重复,引入了特殊的“代理”类。基础“代理”类是 cv::InputArray,用于在函数输入中传递只读数组。派生自 InputArray 的类 cv::OutputArray 用于指定函数的输出数组。通常,您不需要关心这些中间类型(也不应显式声明这些类型的变量)——它们会自动工作。您可以认为在 InputArray/OutputArray 处始终可以使用 cv::Mat, std::vector<>, cv::Matx<>, cv::Vec<> 或 cv::Scalar。当函数具有可选的输入或输出数组,而您没有或不想要一个时,请传递 cv::noArray()。
错误处理#
OpenCV 使用异常来发出严重错误信号。当输入数据格式正确且属于指定值范围,但算法由于某些原因无法成功(例如,优化算法未收敛)时,它会返回一个特殊的错误代码(通常只是一个布尔变量)。
异常可以是 cv::Exception 类或其派生类的实例。而 cv::Exception 本身是 std::exception 的派生类。因此,可以使用其他标准 C++ 库组件在代码中优雅地处理它。
异常通常通过 #CV_Error(errcode, description) 宏,或其类似 printf 的 #CV_Error_(errcode, (printf-spec, printf-args)) 变体,或者使用检查条件且在条件不满足时抛出异常的 #CV_Assert(condition) 宏来抛出。对于性能至关重要的代码,有 #CV_DbgAssert(condition),它仅在 Debug 配置中保留。由于自动内存管理,在发生突然错误时,所有中间缓冲区都会被自动释放。如果需要,您只需添加 try 语句来捕获异常。
try
{
... // call OpenCV
}
catch (const cv::Exception& e)
{
const char* err_msg = e.what();
std::cout << "exception caught: " << err_msg << std::endl;
}
多线程与可重入性#
当前的 OpenCV 实现是完全可重入的。也就是说,同一个函数或不同类实例的同一个方法可以从不同线程调用。同时,同一个 Mat 也可以在不同线程中使用,因为引用计数操作使用了特定于架构的原子指令。