OpenCV(开源计算机视觉库: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) - 一个视频分析模块,包括运动估计、背景减除和目标跟踪算法。
相机校准与三维重建 (calib3d) - 基本的多视图几何算法、单目和立体相机校准、物体姿态估计、立体匹配算法以及三维重建元素。
二维特征框架 (features2d) - 显著特征检测器、描述符和描述符匹配器。
目标检测 (objdetect) - 检测预定义类别中的物体和实例(例如,人脸、眼睛、马克杯、人物、汽车等)。
深度神经网络模块 (dnn) - 深度神经网络模块。
机器学习 (ml) - 机器学习模块包含一组用于数据统计分类、回归和聚类的类和函数。
计算摄影 (photo) - 高级照片处理技术,如去噪、图像修复。
图像拼接 (stitching) - 用于图像拼接和全景图创建的函数。
... 其他辅助模块,例如 FLANN 和 Google 测试包装器、Python 绑定等。
文档的后续章节将描述每个模块的功能。但首先,请务必熟悉库中广泛使用的通用 API 概念。
API 概念
cv 命名空间
所有 OpenCV 类和函数都位于 cv 命名空间中。因此,要在代码中访问此功能,请使用 cv:: 限定符或 using namespace cv; 指令
#include "opencv2/core.hpp"
...
cv::Mat H = cv::findHomography(points1, points2, cv::RANSAC, 5);
...
core.hpp
cv::findHomographyMat findHomography(InputArray srcPoints, InputArray dstPoints, int method=0, double ransacReprojThreshold=3, OutputArray mask=noArray(), const int maxIters=2000, const double confidence=0.995)在两个平面之间找到透视变换。
cv::RANSAC@ RANSACRANSAC 算法。Definition calib3d.hpp:552
或者
#include "opencv2/core.hpp"
using namespace cv;
...
Mat H = findHomography(points1, points2, RANSAC, 5 );
...
cv::Matn 维密集数组类定义 mat.hpp:830
cv定义 core.hpp:107
一些当前或未来的 OpenCV 外部名称可能与 STL 或其他库冲突。在这种情况下,请使用显式命名空间限定符来解决名称冲突。
Mat a(100, 100, CV_32F);
randu(a, Scalar::all(1), Scalar::all(std::rand()));
cv::log(a, a);
a /= std::log(2.);
cv::logvoid log(InputArray src, OutputArray dst)计算每个数组元素的自然对数。
CV_32F#define CV_32FDefinition interface.h:78
自动内存管理
OpenCV 自动处理所有内存。
首先,函数和方法使用的 std::vector、cv::Mat 以及其他数据结构都具有析构函数,这些析构函数在需要时会释放底层内存缓冲区。这意味着析构函数并不总是像 Mat 那样立即释放缓冲区。它们会考虑可能的数据共享。析构函数会递减与矩阵数据缓冲区关联的引用计数。只有当引用计数达到零时,即没有其他结构引用同一缓冲区时,才会释放缓冲区。类似地,当复制一个 Mat 实例时,并没有实际数据被复制。相反,引用计数会增加,以记录存在另一个相同数据的所有者。还有 cv::Mat::clone 方法,它会创建一个矩阵数据的完整副本。请看下面的例子:
// 创建一个大的 8Mb 矩阵
Mat A(1000, 1000, CV_64F);
// 为同一矩阵创建另一个头部;
// 这是一个即时操作,与矩阵大小无关。
Mat B = A;
// 为 A 的第三行创建另一个头部;也没有数据被复制
Mat C = B.row(3);
// 现在创建一个矩阵的独立副本
Mat D = B.clone();
// 将 B 的第五行复制到 C,即,将 A 的第五行复制
// 到 A 的第三行。
B.row(5).copyTo(C);
// 现在让 A 和 D 共享数据;之后修改后的版本
// 的 A 仍被 B 和 C 引用。
A = D;
// 现在将 B 设置为空矩阵(不引用任何内存缓冲区),
// 但修改后的 A 版本仍将被 C 引用,
// 尽管 C 只是原始 A 的单行。
B.release();
// 最后,对 C 进行完整复制。结果是,大的修改后的
// 矩阵将被释放,因为它不再被任何人引用。
C = C.clone();
cv::Mat::cloneCV_NODISCARD_STD Mat clone() const创建数组及其底层数据的完整副本。
cv::Mat::copyTovoid copyTo(OutputArray m) const将矩阵复制到另一个矩阵。
cv::Mat::rowMat row(int y) const为指定的矩阵行创建矩阵头。
cv::Mat::releasevoid release()递减引用计数并在需要时释放矩阵。
CV_64F#define CV_64FDefinition interface.h:79
您可以看到 Mat 和其他基本结构的使用非常简单。但是,对于没有考虑自动内存管理而创建的高级类甚至用户数据类型呢?对于它们,OpenCV 提供了类似于 C++11 中 std::shared_ptr 的 cv::Ptr 模板类。因此,与其使用普通指针:
T* ptr = new T(...);
您可以使用:
Ptr
cv::Ptrstd::shared_ptr< _Tp > PtrDefinition cvstd_wrapper.hpp:23
或者
Ptr
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;
}
cv::Size_用于指定图像或矩形大小的模板类。Definition types.hpp:335
cv::VideoCapture用于从视频文件、图像序列或相机捕获视频的类。Definition videoio.hpp:772
highgui.hpp
mainint main(int argc, char *argv[])定义 highgui_qt.cpp:3
imgproc.hpp
数组 frame 由 >> 运算符自动分配,因为视频帧分辨率和位深度对于视频捕获模块是已知的。数组 edges 由 cvtColor 函数自动分配。它与输入数组具有相同的大小和位深度。通道数为 1,因为传递了颜色转换代码 cv::COLOR_BGR2GRAY,这意味着从彩色到灰度的转换。请注意,frame 和 edges 在循环体首次执行时只分配一次,因为所有后续视频帧都具有相同的分辨率。如果您以某种方式更改视频分辨率,数组将自动重新分配。
这项技术的关键组件是 cv::Mat::create 方法。它接受所需的数组大小和类型。如果数组已经具有指定的大小和类型,该方法不执行任何操作。否则,它会释放之前分配的数据(如果有)(这部分涉及递减引用计数并与零进行比较),然后分配所需大小的新缓冲区。大多数函数都会为每个输出数组调用 cv::Mat::create 方法,从而实现了自动输出数据分配。
该方案的一些显著例外是 cv::mixChannels、cv::RNG::fill 以及其他一些函数和方法。它们无法分配输出数组,因此您必须提前进行此操作。
饱和算术
作为计算机视觉库,OpenCV 经常处理图像像素,这些像素通常以紧凑的每通道 8 位或 16 位形式编码,因此值范围有限。此外,某些图像操作,如色彩空间转换、亮度/对比度调整、锐化、复杂插值(双三次、Lanczos),可能会产生超出可用范围的值。如果您只存储结果的最低 8 (16) 位,这会导致视觉伪影,并可能影响进一步的图像分析。为了解决这个问题,采用了所谓的饱和算术。
例如,要将操作结果 r 存储到 8 位图像中,您需要找到 0..255 范围内的最近值:
\[I(x,y)= \min ( \max (\textrm{round}(r), 0), 255)\]
类似的规则适用于 8 位有符号、16 位有符号和无符号类型。这种语义在库中随处可见。在 C++ 代码中,通过使用类似于标准 C++ 类型转换操作的 cv::saturate_cast<> 函数来完成。请看下面提供的公式的实现:
I.at
Definition interface.h:51
注意其中 cv::uchar 是 OpenCV 的 8 位无符号整数类型。在优化的 SIMD 代码中,使用了 paddusb、packuswb 等 SSE2 指令。它们有助于实现与 C++ 代码中完全相同的行为。
当结果是 32 位整数时,不应用饱和处理。
固定像素类型。模板的有限使用
模板是 C++ 的一个强大特性,它能够实现非常强大、高效且安全的数据结构和算法。然而,过度使用模板可能会显著增加编译时间和代码大小。此外,当独占使用模板时,很难分离接口和实现。这对于基本算法可能没问题,但对于计算机视觉库来说就不太好了,因为单个算法可能跨越数千行代码。由于这个原因,也为了简化 Python、Java、Matlab 等不具有模板或模板能力有限的其他语言的绑定开发,当前的 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_8S#define CV_8S
Definition interface.h:74CV_8U#define CV_8U
Definition interface.h:73CV_32S#define CV_32S
Definition interface.h:77CV_16S#define CV_16S
Definition interface.h:76CV_16U#define CV_16U
Definition interface.h:75
多通道(n 通道)类型可以使用以下选项指定:
CV_8UC1 ... CV_64FC4 常量(适用于 1 到 4 个通道数)
注意CV_8UC(n) ... CV_64FC(n) 或 CV_MAKETYPE(CV_8U, n) ... CV_MAKETYPE(CV_64F, n) 宏,当通道数大于 4 或在编译时未知时使用。
示例
#CV_32FC1 == #CV_32F, #CV_32FC2 == #CV_32FC(2) == #CV_MAKETYPE(CV_32F, 2),并且 #CV_MAKETYPE(depth, n) == ((depth&7) + ((n-1)<<3)。这意味着常量类型由深度(取最低 3 位)和通道数减 1(取接下来的 log2(CV_CN_MAX) 位)组成。
Mat mtx(3, 3, CV_32F); // 创建一个 3x3 浮点矩阵
Mat cmtx(10, 1, CV_64FC2); // 创建一个 10x1 双通道浮点
// 矩阵(10 元素复向量)
Mat img(Size(1920, 1080), CV_8UC3); // 创建一个 3 通道(彩色)图像,
// 宽 1920 像素,高 1080 像素。
Mat grayscale(img.size(), CV_MAKETYPE(img.depth(), 1)); // 创建一个单通道图像,
// 具有与 img 相同的尺寸和
// 相同的通道类型。CV_8UC3#define CV_8UC3
Definition interface.h:90CV_64FC2#define CV_64FC2
Definition interface.h:125CV_MAKETYPE#define CV_MAKETYPE(depth, cn)
Definition interface.h:85
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++ 库组件在代码中优雅地处理它。
try
{
异常通常通过 #CV_Error(errcode, description) 宏抛出,或其类似 printf 的 #CV_Error_(errcode, (printf-spec, printf-args)) 变体,或者使用 CV_Assert(condition) 宏,该宏检查条件并在不满足时抛出异常。对于性能关键的代码,有 CV_DbgAssert(condition),它只在 Debug 配置中保留。由于自动内存管理,所有中间缓冲区在发生意外错误时都会自动释放。如果需要,您只需添加 try 语句来捕获异常:
}
... // 调用 OpenCV
{
catch (const cv::Exception& e)
const char* err_msg = e.what();
}
cv::Exception传递给错误的类。定义 core.hpp:120
cv::Exception::whatvirtual const char * what() const noexcept override
std::cout << "捕获到异常: " << err_msg << std::endl;
多线程与可重入性