OpenCV(开源计算机视觉库:http://opencv.org)是一个开源库,包含数百种计算机视觉算法。该文件描述了所谓的 OpenCV 2.x API,它本质上是一个 C++ API,而不是基于 C 的 OpenCV 1.x API(C API 已被弃用,自 OpenCV 2.4 发布以来,已不再使用 "C "编译器进行测试)。
OpenCV 采用模块化结构,这意味着软件包包含多个共享库或静态库。可使用以下模块:
- 核心功能(core)–定义基本数据结构的紧凑型模块,包括密集多维数组 Mat 和所有其他模块使用的基本功能。
- 图像处理(imgproc)–图像处理模块,包括线性和非线性图像过滤、几何图像变换(调整大小、仿射和透视扭曲、基于通用表的重映射)、色彩空间转换、直方图等。
- 视频分析(video)–视频分析模块,包括运动估计、背景减除和物体跟踪算法。
- 相机校准和三维重建(calib3d)–基本的多视角几何算法、单相机和立体相机校准、物体姿态估计、立体对应算法和三维重建元素。
- 二维特征框架 (features2d) - 突出特征检测器、描述符和描述符匹配器。
- 对象检测(objdetect)–检测对象和预定义类别的实例(例如,人脸、眼睛、杯子、人、汽车等)。
- 高级图形用户界面 (highgui) - 简单用户界面功能的易用界面。
- 视频 I/O (videoio) - 视频捕捉和视频编解码器的易用界面。
… 其他一些辅助模块,如 FLANN 和 Google 测试包装器、Python 绑定等。
本文档的后续章节将介绍每个模块的功能。但首先,请务必全面熟悉库中常用的 API 概念。
API 概念
cv 命名空间
所有 OpenCV 类和函数都放在 cv
命名空间中。因此,要从代码中访问这些功能,请使用 cv::
指定符或 using namespace cv;
指令(这里特指C++环境,Java和Python会另有文章描述访问方法):
#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 或其他库发生冲突。在这种情况下,请使用显式命名空间规范解决名称冲突问题:
Mat a(100, 100, CV_32F);
randu(a, Scalar::all(1), Scalar::all(std::rand()));
cv::log(a, a);
a /= std::log(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 行复制到 B 的第 3 行。
// 复制到 A 的第 3 行。
B.row(5).copyTo(C);
// 现在让 A 和 D 共享数据;之后,修改后的 A
// A 的修改版本仍被 B 和 C 引用。
A = D;
// 现在让 B 成为空矩阵(不引用内存缓冲区)、
// 但修改后的 A 仍将被 C 引用、
// 尽管 C 只是原始 A 的一行
B.release();
// 最后,复制一份完整的 C。
// 矩阵将被取消分配,因为它不再被任何人引用
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;
}
由于视频捕捉模块已知视频帧的分辨率和位深度,因此数组帧由 >>
操作符自动分配。数组边缘由 cvtColor 函数自动分配。它的大小和位深度与输入数组相同。通道数为 1,是因为传递了颜色转换代码 cv::COLOR_BGR2GRAY,这意味着颜色到灰度的转换。请注意,帧和边仅在第一次执行循环体时分配一次,因为接下来的所有视频帧都具有相同的分辨率。如果以某种方式改变视频分辨率,数组会自动重新分配。
这项技术的关键组件是 cv::Mat::create 方法。该方法获取所需的数组大小和类型。如果数组已经具有指定的大小和类型,该方法不会做任何操作。否则,它会释放先前分配的数据(如果有的话)(这部分包括递减引用计数器并与零比较),然后分配一个所需大小的新缓冲区。大多数函数会为每个输出数组调用 cv::Mat::create 方法,因此自动输出数据分配得以实现。
但 cv::mixChannels、cv::RNG::fill 以及其他一些函数和方法是这一方案的显著例外。这些函数和方法无法分配输出数组,因此必须提前进行分配。
饱和运算
作为一个计算机视觉库,OpenCV 需要处理大量图像像素,这些像素通常以每通道 8 位或 16 位的紧凑形式编码,因此取值范围有限。此外,图像上的某些操作,如色彩空间转换、亮度/对比度调整、锐化、复杂插值(双立方、Lanczos)等,会产生超出可用范围的值。如果只存储结果的最低 8(16)位,就会产生视觉假象,并可能影响进一步的图像分析。为了解决这个问题,我们采用了所谓的饱和运算法。例如,要将操作结果 r 存储到 8 位图像中,需要在 0…255 的范围内找到最接近的值:
I
(
x
,
y
)
=
m
i
n
(
m
a
x
(
r
o
u
n
d
(
r
)
,
0
)
,
255
)
I\left( x,y \right) =min\left( max\left( round\left( r \right) ,0 \right) ,255 \right)
I(x,y)=min(max(round(r),0),255)
类似的规则也适用于 8 位有符号、16 位有符号和无符号类型。这种语义在库中随处可见。在 C++ 代码中,可以使用 cv::saturate_cast<>
函数来实现,该函数类似于标准的 C++ 转置操作。下面是上述公式的实现:
I.at<uchar>(y, x) = saturate_cast<uchar>(r);
其中 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 };
可使用以下选项指定多通道(n 通道)类型:
CV_8UC1 … CV_64FC4 常量(适用于 1 至 4 的通道数)
CV_8UC(n) … CV_64FC(n) 或 CV_MAKETYPE(CV_8U, n) … 当通道数超过 4 或编译时通道数未知时,使用 CV_MAKETYPE(CV_64F, n) 宏。
注意事项
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 元素复数向量
// 矩阵(10 元素复数向量)
Mat img(Size(1920, 1080), CV_8UC3); // 生成三通道(彩色)图像
// 1920 列和 1080 行的三通道(彩色)图像。
Mat grayscale(img.size(), CV_MAKETYPE(img.depth(), 1)); // 制作一幅单通道图像,大小和行数相同。
// 的单通道图像。
// 通道类型相同的单通道图像
使用 OpenCV 无法构建或处理具有更复杂元素的数组。此外,每个函数或方法只能处理所有可能数组类型中的一个子集。通常,算法越复杂,支持的格式子集就越小。请参阅以下此类限制的典型示例:
- 人脸检测算法只能处理 8 位灰度或彩色图像。
- 线性代数函数和大多数机器学习算法只能使用浮点数组。
- 基本函数,如 cv::add,支持所有类型。
- 色彩空间转换函数支持 8 位无符号、16 位无符号和 32 位浮点类型。
每个函数支持的类型子集都是根据实际需要定义的,将来可以根据用户要求进行扩展。
输入数组和输出数组
许多 OpenCV 函数都处理密集的二维或多维数值阵列。通常,此类函数使用 cv::Mat
作为参数,但在某些情况下,使用 std::vector<>
(例如用于点集)或 cv::Matx<>
(用于 3x3 同构矩阵等)更为方便。为了避免 API 中的许多重复,我们引入了特殊的 "代理 "类。基本 "代理 "类是 cv::InputArray。它用于在函数输入中传递只读数组。从 InputArray 派生的 cv::OutputArray 类用于指定函数的输出数组。通常情况下,您不应该关心这些中间类型(也不应该显式声明这些类型的变量)–它们都会自动运行。您可以使用 cv::Mat
、std::vector<>
、cv::Matx<>
、cv::Vec<>
或 cv::Scalar
来代替 InputArray/OutputArray。当一个函数有一个可选的输入或输出数组,而你没有或不需要它时,请传递 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) 只保留在调试配置中。由于采用了自动内存管理,所有中间缓冲区都会在突然出错时自动去分配。如果需要,只需添加 try 语句捕获异常即可:
try
{
... // 调用 OpenCV
}
catch (const cv::Exception& e)
{
const char* err_msg = e.what();
std::cout << "exception caught: " << err_msg << std::endl;
}
多线程和可重复输入性
当前的 OpenCV 实现是完全可重入的。也就是说,可以在不同的线程中调用不同类实例的相同函数或相同方法。此外,由于引用计数操作使用了特定架构的原子指令,因此同一个 Mat 可以在不同的线程中使用。
这一篇暂看不出来和4.7.0的区别。。。