文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏
参与翻译: coyee (18)

OpenCV (Open Source Computer Vision Library: http://opencv.org) 是一个使用 BSD 许可证的开源库,包含数百个计算机视觉算法。此文档详细的描述了 OpenCV 2.x API,这主要是 C++ API,相对于 OpenCV 1.x API 的 C API。

OpenCV 使用模块化的结构,这表明其包含很多共享或者静态库。OpenCV 提供如下模块:

  • core - 这是一个定义了基本数据结构的紧凑模块,包含大量被其他模块调用的多维的 Mat 和其他基本函数。
  • imgproc - 这是一个图像处理模块,包含线性和非线性的图像过滤,几何图形转换 (大小调整, 仿射和透视扭曲, 通用的基于表的重新映射), 色彩空间转换以及直方图等等。
  • video - 这是一个视频分析模块,包含运动评估、背景提取以及对象跟踪算法。
  • calib3d - 基本的多视图几何算法,实现单个和立体的摄像头校准,对象构成的评估、立体相关算法和一些 3D 构造算法。
  • features2d - 突出特征检测、描述以及描述器的匹配。
  • objdetect - 对象检测以及预定义类的检测,例如人脸、眼睛、杯子、人物、汽车等等。
  • highgui - 一个易用的视频捕获、图像和视频编码接口,同时提供简单的图形化界面。
  • gpu - 来自不同 OpenCV 模块的 GPU 加速算法。
  • ... 其他一些助手模块,例如 FLANN 和 Google 测试封装、Python 包等等。
第 1 段(可获 2 积分)

该文档的其他章节描述了每个模块的功能。但首先,我们需要对一些贯通整个库的通用 API 熟悉。

API 概念

cv 命名空间

所有 OpenCV 的类和函数被放到了 cv 这个命名空间。所以为了在代码中访问这些功能,需要使用 cv:: 说明符或者使用 namespace cv; 指令:

#include "opencv2/core/core.hpp"
...
cv::Mat H = cv::findHomography(points1, points2, CV_RANSAC, 5);
...

或者

#include "opencv2/core/core.hpp"
using namespace cv;
...
Mat H = findHomography(points1, points2, CV_RANSAC, 5 );
...
第 2 段(可获 2 积分)

当前 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, Mat, 以及其他数据结构提供了析构函数,可在需要的时候释放底层占用内存。这意味着好比是 Mat 这个数据结构而言,析构函数并不总是会释放内存,此举是为了便于数据共享。析构函数只是减少了所关联对象的引用计数器而已。如果引用计数器数值为 0 的时候对象才会被释放,因为再没有其他结构引用到该数据。同样的,当一个 Mat 实例被拷贝,实际上并没有发送拷贝数据的操作,只是引用计数值增1来记录使用关系。当然 Mat 还提供了 Mat::clone 方法来强制进行数据拷贝。示例如下:

第 3 段(可获 2 积分)
// 创建一个大的 8Mb 的矩阵
Mat A(1000, 1000, CV_64F);

// 创建该矩阵的另外一个引用
// 真实一个实例操作,无视阵列大小
Mat B = A;
// 为 A 的第三行创建另外一个头,此处没有数据拷贝动作
Mat C = B.row(3);
// 现在创建一个独立的阵列拷贝
Mat D = B.clone();
// 从 B 拷贝第五行数据到 C
// to the 3-rd row of A.
B.row(5).copyTo(C);
// 让 A 和 D 共享修改后数据,注意 A 仍被 B 和 C 引用
A = D;
// 现在让 B 变成空的阵列,释放内存,
// 但是修改好的 A 仍被 C 引用,
// 尽管 C 只是原始 A 的单行数据
B.release();

// 最,我们对 C 做一个全拷贝,这是大的改动
// 阵列将被释放,因为没有任何对象引用到它
C = C.clone();
第 4 段(可获 2 积分)

你会发现 Mat 和其他基础结构的使用时很简单的。但是其他一些高级类和用户自行创建的数据类型如何呢?是否也可以实现自动的内存管理呢?对于这些来说 OpenCV 提供了 Ptr<> 模板类,类似 C++ TR1 的 std::shared_ptr . 因此使用指针来替换的方法如下:

T* ptr = new T(...);

你可以使用:

Ptr<T> ptr = new T(...);

也就是说 Ptr<T> ptr 封装了一个指向 T 实例的指针和关联该指针的引用计数,详情请看 Ptr 的详细描述。

输出数据的自动分配

第 5 段(可获 2 积分)

OpenCV 会自动释放内存,就如同大多数时候为输出函数的参数自动分配内存一样。因此,如果一个函数有一个或者多个输入的数组 (cv::Mat 实例) 和一些输出数组,输出的数组会实现自动的内存分配和释放。输出数组的大小和类型会根据输入数组的大小和类型来自动识别。如果需要的话函数可以提供额外的参数来帮助设定输出数组的属性。

示例代码:

#include "cv.h"
#include "highgui.h"

using namespace cv;

int main(int, char**)
{
    VideoCapture cap(0);
    if(!cap.isOpened()) return -1;

    Mat frame, edges;
    namedWindow("edges",1);
    for(;;)
    {
        cap >> frame;
        cvtColor(frame, edges, CV_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;
}
第 6 段(可获 2 积分)

一旦视频捕获模块解析到视频帧以及位深度,那么帧数组会自动通过 >> 操作符进行分配。数组的边界是通过 cvtColor 函数自动分配的。它跟输入的数组具有相同的大小和位深度。因为传递了 CV_BGR2GRAY 参数给色彩转换代码,因此通道数是 1。需要注意的是帧和边界只在首次执行时候分配一次,因此紧接着的所有视频帧都具有相同的分辨率。如果你以某种方式改变了视频分辨率,那么数组就会自动重新分配。

第 7 段(可获 2 积分)

此技术的关键组件是 Mat::create 方法,需要指定数组的大小和类型。如果数组已经有指定的大小和类型了,那么该方法什么都不做。否则它会释放之前已分配的数据,如果有的话(这一部分设计到引用计数减一并判断是否为0)然后分配一个新的缓冲区以满足数据要求。大多数函数为每个输出的数组调用 Mat::create 方法来创建,因此实现了输出数据分配的自动化。

一些值得注意的例外是 cv::mixChannels 和 cv::RNG::fill ,同时还有其他的函数和方法。他们不会分配输出数组,你必须在调用之前进行分配。

第 8 段(可获 2 积分)

饱和算法

作为一个计算机视觉库,OpenCV 处理大量的图像像素,这些都是使用紧缩的每通道、形式或者值范围的 8或者16位的像素。此外图像上的特定操作,例如色彩空间转换、亮度对比度调整、锐化以及其他复杂的操作 (bi-cubic, Lanczos) 会产生超出范围的值。如果你只是存储最低的 8或者16位值,这会导致视觉上的伪影,从而影响进一步的图像分析。为了解决这个问题,我们可以使用所谓的“饱和算法”。例如,为了存储操作的结果 r 到一个 8 位的图像,你可以查找 0 到 255 中最ji,can produce values out of the available range. If you just store the lowest 8 (16) bits of the result, this results in visual artifacts and may affect a further image analysis. To solve this problem, the so-called saturation arithmetics is used. For example, to store r, the result of an operation, to an 8-bit image, you find the nearest value within the 0..255 range:

第 9 段(可获 2 积分)

I(x,y)= \min ( \max (\textrm{round}(r), 0), 255)

带符号的 8 位、16类型以及不带符号的类型也使用类似的规则,在整个库的 C++ 代码都使用这个语义规则。可使用 saturate_cast<> 函数来实现类似标准 C++ 的 cast 操作。下面一行代码实现了上图中的计算公式:

I.at<uchar>(y, x) = saturate_cast<uchar>(r);

因为 cv::uchar 是一个 OpenCV 8-bit 无符号整数值,因此在优化的 SIMD 代码,例如 SSE2 指令:paddusb, packuswb 等等就会被使用到。这实现了与 C++ 代码类似的相同行为。

注意

当结果是 32位整数时,是无法用到 Saturation 饱和的(译者注:此句该做何解?)。

第 10 段(可获 2 积分)

固定像素类型,模板使用限制

模板是 C++ 一个非常棒的特性之一,可以实现非常强大、高效以及安全的数据结构和算法。但是大量使用模板会戏剧性的增加编译时间以及代码的体积。除此之外,很难分离一个模板的接口以及相应的实现。这用来做一些基本的算法是挺好的,但是对于计算机视觉库这样可能包含数千行代码的复杂工作就不太合适。因为这个同时需要提供其他语言的支持版本,而像 Python、Java、Matlab 等编程语言并没有模板的概念,就会导致功能受限。当前的 OpenCV 实现是基于多态以及模板之上的运行时调度。这会导致运行时调度变得非常慢(例如像素访问操作)以及无法运行(泛型 Ptr<> 实现),以及可能非常不方便(saturate_cast<>()),因此当前实现使用了小的模板类、方法和函数。在当前的 OpenCV 版本中模板的使用都是受限的。

第 11 段(可获 2 积分)

因此,对于一些可操作的基本类型来说是有一些固定的限制。也就是说,数组元素必须是如下罗列的类型中的其中一个:

  • 8-bit 无符号整数 (uchar)
  • 8-bit 有符号整数 (schar)
  • 16-bit 无符号整数 (ushort)
  • 16-bit 有符号整数 (short)
  • 32-bit 有符号整数 (int)
  • 32-bit 浮点数(float)
  • 64-bit 浮点数 (double)
  • 一组多元素的元组,但所有元素的类型必须一致,而且必须是上面几种类型。数组的元素如果是元组,相当于是多通道数组,与单通道数组也跟元组类型相反,这些元素必须是标量类型。最大的通道数是定义为 CV_CN_MAX 的常量值,当前是 512.
第 12 段(可获 2 积分)

对于这些基本类型,OpenCV 提供了如下枚举与之对应:

enum { CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6 };

多通道 (n-channel) 类型可通过如下的选项进行指定:

  • 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) 比特.

第 13 段(可获 2 积分)

示例:

Mat mtx(3, 3, CV_32F); // 生成一个 3x3 浮点阵列
Mat cmtx(10, 1, CV_64FC2); // 生成一个 10x1 2通道浮点
                           // 阵列 (10元素的复杂向量)
Mat img(Size(1920, 1080), CV_8UC3); // 生成一个三通道的彩色图像
                                    // 包含 1920 列和 1080 行.
Mat grayscale(image.size(), CV_MAKETYPE(image.depth(), 1)); // 生成一个单通道的相同大小和通道类型的图像
第 14 段(可获 2 积分)

包含更复杂元素的数组没法使用 OpenCV 进行构建和处理。此外,每个函数或者方法只能处理任何可能数组类型的一个子集。通常,算法越复杂,支持的格式子集就越小。下面是这些限制的一些典型示例:

  • 人脸识别算法只支持 8 位灰度图以及彩色图.
  • 线性代数函数以及大多数的机器学习算法只支持浮点数数组.
  • 基本的函数,例如 cv:add 支持所有类型.
  • 色彩空间转换函数支持 8位无符号、16位无符号以及32位浮点数类型.
第 15 段(可获 2 积分)

每个函数所支持的类型的子集都是为了满足实际的需要,并且可根据用户的需求在将来进行扩展。

InputArray 和 OutputArray

很多 OpenCV 函数密集的处理2维以及多维的数组,例如使用 cpp:class:Mat 作为参数的函数,但是在某些情况下使用 std::vector<> 或者 Matx<> 更方便(例如一个点阵集合)。为了避免 API 中很多重复的代码,OpenCV 专门引入了一个 “proxy”类。一个基本的 “proxy”类就是 InputArray. 它被用来传递只读数组。而 InputArray 的派生类 OutputArray 用来在函数中指定输出数组。一般情况下你不需要关心这些中间类型(你也不能显式的定义这种类型的变量),它们都是自动被处理的。你可以假设在使用 Mat、std::vector<>、Matx<>、Vec<> 以及一些标量类型的时候会自动替换成 InputArray/OutputArray 。当一个函数包含一个可选的输入和输出数组时,你没有这样的参数也不想要有,可以传递 cv::noArray() 。

第 16 段(可获 2 积分)

错误处理

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 语句和 catch 异常即可。

第 17 段(可获 2 积分)
try
{
    ... // call OpenCV
}
catch( cv::Exception& e )
{
    const char* err_msg = e.what();
    std::cout << "exception caught: " << err_msg << std::endl;
}

多线程和可重入

当前的 OpenCV 版本是完全支持可重入的,这就是说相同的函数、类实例的 constant 方法或者是不同类实例的相同 non-constant 方法可以在不同的线程中调用。同时,相同的 cv::Mat 也可以在不同的线程中使用,因为这里有引用计数来实现特定架构的原子操作。

第 18 段(可获 2 积分)

文章评论