OpenCV 介绍 v4.8.0

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::vectorcv::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::mixChannelscv::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_8UC1CV_64FC4 常量(适用于 1 至 4 的通道数)
CV_8UC(n)CV_64FC(n)CV_MAKETYPE(CV_8U, n) … 当通道数超过 4 或编译时通道数未知时,使用 CV_MAKETYPE(CV_64F, n) 宏。

注意事项
CV_32FC1 == CV_32FCV_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::Matstd::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的区别。。。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值