OpenCV 简介

2017-11-16   1  295

简介

OpenCV(Open Source Computer Vision Library:http://opencv.org)是一个开源的基于BSD许可的库,它包括数百种计算机视觉算法。文档OpenCV 2.x API描述的是C++ API,相对还有一个基于C语言的OpenCV 1.x API,后者的描述在文档opencv1.x.pdf中。

OpenCV具有模块化结构,这就意味着开发包里面包含多个共享库或者静态库。下面是可使用的模块:

  • 核心功能(Core functionality) - 一个紧凑的模块,定义了基本的数据结构,包括密集的多维Mat数组和被其他模块使用的基本功能。
  • 图像处理(Image processing) - 一个图像处理模块,它包括线性和非线性图像滤波,几何图形转化(重置大小,放射和透视变形,通用基本表格重置映射),色彩空间转换,直方图等。
  • 影像分析(video) - 一个影像分析模块,它包括动作判断,背景弱化和目标跟踪算法。
  • 3D校准(calib3d) - 基于多视图的几何算法,平面和立体摄像机校准,对象姿势判断,立体匹配算法,和3D元素的重建。
  • 平面特征(features2d) - 突出的特征判断,特征描述和对特征描述的对比。
  • 对象侦查(objdetect) - 目标和预定义类别实例化的侦查(例如:脸、眼睛、杯子、人、汽车等等)。
  • highgui - 一个容易使用的用户功能界面。
  • 视频输入输出(videoio) - 一个容易使用的视频采集和视频解码器。
  • GPU - 来自不同OpenCV模块的GPU加速算法。
  • … 一些其他的辅助模块,比如FLANN和谷歌的测试封装,Python绑定和其他。

接下来的章节将描述每个模块的功能。但首先一定要熟悉在整个库中使用的通用API概念。


API概念

cv命名空间

所有的OpenCV类和函数都放在cv的命名空间。因此,你的代码去访问这些功能,需要使用cv::字符或者使用命名空间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, 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自动处理所有的内存。

首先需要了解的是std:vector、Mat等,这些被函数和方法调用的数据结构中都具有析构器(destructors),析构器可以在需要的时候去除内存缓冲。这就意味这析构器并不总会去除缓冲,如在Mat中。他们尽可能的考虑到了数据共享。析构器会对矩形数据缓冲区的引用计数做减法,如果当引用计数减少为零,没有其他结构在引用这同一个缓冲区的时候,缓冲区将会被去除。类似的,当Mat实例对象被复制后,真正复制的并不是现存的数据,反倒缓冲区的引用计数做加法,这样另一个人也拥有了这个缓冲区的数据。另外这里有Mat::clone方法,该方法会创建一个完整的矩形缓冲区数据的拷贝。来看下面的例子:


 //创建一个8M的矩阵
 Mat A(1000, 1000, CV_64F);

 //为相同矩阵创建另一个头;//这个是立即的操作,和矩阵大小无关。
 Mat B = A;
 //使用A中的第三行数据创建另一个头,他们之间不存在数据复制。
 Mat C = B.row(3);
 //现在创建一个独立的矩阵副本
 Mat D = B.clone();
 //复制B的第五行到C,实质上是拷贝了A的第五行到A的第三行。 
 B.row(5).copyTo(C);
//现在让A和D共享数据;刚刚修改的那个版本依旧被B和C所引用。
A = D;
//现在让B成为一个空矩阵(不再引用内存缓冲区),
//但刚刚修改的版本依旧被C所引用着,尽管C仅仅是A原始数据中的单独一行。
 B.release();

 //最终,对C做一个完整的复制,结果刚刚被修改过的那个矩阵被释放,//因为已经没有任何人引用它了。

 C = C.clone();


如你所见Mat和其他基本数据结构的使用是如此的简单。但是为什么高层类甚至用户数据类型却不考虑内存的自动管理了呢?对于他们OpenCV提供源自C++11的Ptr模板类,类似于std::shared_ptr,替换目前使用中的普通指针:


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


你可以这样使用:


 Ptr ptr(new T(...));


或者:


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


Ptr封装了一个指向T实例的指针和指针相关的引用计数器。详细参考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",1);
     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函数自动的进行了分配。它和输入队列一样具有相同的大小和色深。通道的个数只有一个,因为传入的颜色转变编码是 COLOR_BGR2GRAY。这个就是一个彩色图到灰度图的转换。注意,帧数据和队列的内存边界仅仅在第一次运行的时候进行了内存空间的分配,在循环体内所有的视频帧具有相同的分辨率。如果你以某种方式改变视频的分辨率,队列会自动的重新分配内存空间。

这个技术的关键部分是Mat::create方法。它会获取需要的队列大小和类型。如果队列已经有了特定的大小和类型,该方法将什么也不做。否则它会释放先前分配到的数据,如果真的释放了(这部分涉及到内存引用计数的自减法和引用是否到0的对比),它会同时分配一个所需大小的内存缓冲。大多数函数都会为输出队列调用Mat::create方法,因此输出数据的自动内存分配就是这样实现的。

这个方案中存在明显的例外:cv::mixChannels, cv::RNG:fill,与其他的几个函数和方法。他们不能被自动分配输出队列,所以你不得不提前做这些事情。


饱和算法(Saturation Arithmetics)

作为一个计算机视觉库,OpenCV需要处理大量的图像像素,他们通常会以8-16位的紧凑方式编入每个通道,形成具有最大值和最小值的区间范围。此外还有一些必要的图形操作,例如色彩空间转换,亮度、对比度、锐度调节,复杂修改(bicubic,Lanczos)能产生超出有效范围的数值。如果你仅存储结果的低8(16)位。这会导致视觉伪影,并可能影响更深层次的图像分析。为了解决这个问题,叫做饱和算法(saturation arithmetics)算法被使用。例如存储数值R,操作的结果是转化成8bit的图像,你能找到0-255范围内最接近的数值:


I(x,y)=min(max(round(r),0),255)


相似的规则应用在有符号8位、无符号16位、有符合16位中。此语义在库中的任何位置都在使用。在C++代码它被使用saturate_cast<>函数,类似于标准C++操作符。见下文所提供的公式实现:


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


这里的cv::uchar是一个OpenCV 8位的无符号类型。在优化的SIMD代码里,如SSE2的操作指南paddusb、packuswb等等都在使用。他们能帮助实现与C++行为完全一样的代码。


注意:饱和算法不适用于32位的整型结果。


固定的像素类型和对模板的限制使用

模板是C++的一个重要特性它可以实现强大的功能,高效而安全的数据结构算法。然而大量使用模板会显著的增加编译时间和代码空间占用。当模板被使用时很难将接口与实现进行分离。这对一些基础算法还好,但对于单个算法就需要数千行代码的计算机视觉库来说并不算好。正因如此,为了简化开发同时也绑定了其他的计算机语言,例如: Python、Java、Matlab,这些语言不支持模板或者支持的很有限。当前OpenCV的实现是在模板之上基于多态的运行时调用。在这个位置运行时调度将会很慢(像素数据的接入操作),不可能(generic Ptr<> implementation)也非常的不方便(saturate_cast<>())当前提出了使用小的模板类、方法和函数。当前版本的OpenCV使用模板是被限制的。

因此,这里是库可以操作的有限的数据类型。也就是数组元素类型应该是以下的类型之一:

  • 8-bit unsigned integer (uchar)
  • 8-bit signed integer (schar)
  • 16-bit unsigned integer (ushort)
  • 16-bit signed integer (short)
  • 32-bit signed integer (int)
  • 32-bit floating-point number (float)
  • 64-bit floating-point number (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 constants (从1-4的通道数据)

- CV_8UC(n) … CV_64FC(n) or CV_MAKETYPE(CV_8U, n) … CV_MAKETYPE(CV_64F, n) 宏(macros)表示在编译时通道的数量多余4个或者未知。


CV_32FC1 == CV_32F, CV_32FC2 == CV_32FC(2) == CV_MAKETYPE(CV_32F, 2), and 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); //做一个3通道(彩色)图片 //1920列1080行.
Mat grayscale(image.size(), CV_MAKETYPE(image.depth(), 1)); //做一个单通道图片,带有相同的大小和相同的通道类型。


使用OpenCV队列不能构建和处理更复杂的元素类型。此外每个函数以及方法仅可以执行所有队列类型中的一个类型。通常越复杂的算法对类型格式支持的越小。

见下面被限制的典型列子:

  • 面部检测算法仅支持8位的灰度或者色彩图像。
  • 线性代数算法和大多数的机器学习算法仅支持浮点数组。
  • 基础函数,例如:cv::add,支持所有类型。
  • 色彩空间转换函数支持8位、16位无符号类型和32位浮点类型。

每个功能所支持的类型,已经根据来自实际需求和基于可扩展的用户需求进行了定义。


输入、输出队列

很多OpenCV函数处理密集2维或多维数字阵列。通常这些函数会以cppMat作为参数,但有一些情况使用std::vector<>(例如一个点集合)或Matx<>(3x3矩阵等)更方便。为了避免在API的命名重复,特殊的“代理”类也被引入。基本“代理”类就是InputArray。它用于在函数输入上传递只读数组。得到来自InputArray,OutputArray类被用于指定一个输出队列函数。通常你不需要关心中间类型(你不应该声明这些明确类型的变量)-它完全可以自动工作。你可以假设一直使用Mat,、std::vector<>、Matx<>、Vec<>或者Scalar替换InputArray/OutputArray。当一个函数有一个可选的输入或输出队列,并且你没有,也不需要通过cv::noArray().


错误处理

OpenCV使用异常(exceptions)示意一个危险的错误。当输入的数据有正确的格式并属于指定的范围,但计算程序却不能成功的获得结果(例如:最后算法未收敛),它返回一个特殊的错误代码(通常,仅是一个布尔型量)。

异常可以被cv::CV_Error(errcode, description)或者它的派生类进行实例化。反过来cv::Exception是通过std::exception派生得来的。所以它可以优雅的执行其他标准C++库的代码。

异常通常会抛出CV_Error(errcode, description)宏或者打印CV_Error(errcode, printf-spec,(打印参数)),或者使用CV_Assert(condition)宏检测,当不能满足的条件的情况下抛出一个异常。对于性能关键代码,有CV_DbgAssert(condition)只能保留在Debug配置模式下。由于内存自动管理的存在,突然发生错误的时候所有的内部缓冲区将被自动释放。您仅需要添加一个声明来捕捉异常,如果需要的话:


try
 {
     ... // call OpenCV
}
catch( cv::Exception& e )
{
    const char* err_msg = e.what();
    std::cout << "exception caught: " << err_msg << std::endl;
 }


多线程重入

当前的OpenCV完全支持重入,这就意味着,在类实例里面相同的常量方法,或者相同的非常量方法,可以被来自不同的线程进行调用。所以类似于cv::Mat可以使用在不同的线程之中,因为因为引用技术操作使用的是原子操作。

评论 ( {{ comments.total }} )
{{ o.content }}
赞 {{ o.likes_count ? o.likes_count : '' }} 回复 {{ o.created_at }}
作者信息