介绍#

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 或其他库冲突。在这种情况下,请使用明确的命名空间限定符来解决名称冲突

    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 方法可创建矩阵数据的完整副本。请参阅下面的示例

    // 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 范围内最接近的值

\[ I(x,y)= \min ( \max (\textrm{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 实现基于多态和运行时分发(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 也可以在不同线程中使用,因为引用计数操作使用了特定于架构的原子指令。