OpenCV(一) 基础知识

OpenCV是一个开源的BSD许可库,其中包含数百种计算机视觉算法。该文档描述了所谓的OpenCV 2.x API,它基本上是C ++ API,与基于C的OpenCV 1.x API相反。后者在opencv1x.pdf中描述。

OpenCV具有模块化结构,这意味着该软件包包含多个共享或静态库。以下模块可用:

  • 核心 - 一个定义基本数据结构的最小模块,包括所有被其他所有模块使用的密集多维数组Mat和基本函数。
  • imgproc - 图像处理模块,包括线性和非线性图像滤波器,几何图像变换(调整大小,仿射和透视变形,通用基于表格的重新映射),色彩空间转换,直方图等。
  • 视频 - 视频分析模块,包括运动估计,背景扣除和对象跟踪算法。
  • calib3d - 基本多视图几何算法,单个和立体相机校准,对象姿态估计,立体匹配算法以及三维重建元素。
  • features2d - 显着特征检点检测,特征描述子和特征点匹配。
  • objdetect - 检测对象和预定义类的实例(例如,脸部,眼睛,杯子,人物,汽车等)。
  • highgui - 一个易于使用的视频捕捉界面,图像和视频编解码器,以及简单的UI功能。
  • gpu - 来自不同OpenCV模块的GPU加速算法。
  • ……其他一些辅助模块,如FLANN和Google测试包装器,Python绑定等。
    本文档的其他章节描述了每个模块的功能。但首先,确保熟悉库中常用的普通API概念。

API概念

cv命名空间

所有的OpenCV类和函数都被放置在cv命名空间中。因此,要从您的代码访问这些函数,请使用说明符cv::或指令:using 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 );
...

一些当前或未来的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自动管理所有内存。

OpenCV 的内存处理是完全自动化的。

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

// 创建一个大的 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();

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

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

你可以使用:

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

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

输出数据的自动分配

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;
}

一旦视频捕获模块解析到视频帧以及位深度,那么帧数组会自动通过 >> 操作符进行分配。数组的边界是通过 cvtColor 函数自动分配的。它跟输入的数组具有相同的大小和位深度。因为传递了 CV_BGR2GRAY 参数给色彩转换代码,因此通道数是 1。需要注意的是帧和边界只在首次执行时候分配一次,因此紧接着的所有视频帧都具有相同的分辨率。如果你以某种方式改变了视频分辨率,那么数组就会自动重新分配
此技术的关键组件是 Mat::create 方法,需要指定数组的大小和类型。如果数组已经有指定的大小和类型了,那么该方法什么都不做。否则它会释放之前已分配的数据,如果有的话(这一部分设计到涉及计数减一并判断是否为0)然后分配一个新的缓冲区以满足数据要求。大多数函数为每个输出的数组调用 Mat::create 方法来创建,因此实现了输出数据分配的自动化。

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

饱和算法

作为一个计算机视觉库,OpenCV 处理大量的图像像素,这些像素通常以每个通道8位或16位的紧凑格式编码,因此其值范围有限。此外图像上的特定操作,例如色彩空间转换、亮度对比度调整、锐化以及复杂插值(双立方,Lanczos)会产生超出范围的值。如果你只是存储最低的 8或者16位值,这会导致视觉上的伪影,从而影响进一步的图像分析。为了解决这个问题,我们可以使用所谓的“饱和算法”。例如,为了存储操作的结果 r 到一个 8 位的图像,你可以查找 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位整数时,不应用饱和度。

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

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

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.

对于这些基本类型,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) 比特.
示例:

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)); //生成一个单通道的相同大小和通道类型的图像

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

  • 人脸识别算法只支持 8 位灰度图以及彩色图.
  • 线性代数函数以及大多数的机器学习算法只支持浮点数数组.
  • 基本的函数,例如 cv:add 支持所有类型.
  • 色彩空间转换函数支持 8位无符号、16位无符号以及32位浮点数类型.
    每个函数所支持的类型的子集都是为了满足实际的需要,并且可根据用户的需求在将来进行扩展。

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() 。

错误处理

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 异常即可。

多线程和可重入

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

猜你喜欢

转载自blog.csdn.net/m0_37565736/article/details/79608178
今日推荐