Introduction
OpenCV,或称开源计算机视觉库,是一个基于BSD开源协议,包含许多种计算机视觉算法的开源库。此文档所描述的OpenCV 2.x API实质上是C++的API,与基于C语言的OpenCV 1.x不同(C语言的API已被废弃,自OpenCV 2.4之后的发行版便再也没用C编译器做过测试)。
OpenCV拥有模块化结构,也即软件包包括了许多动态或者静态库。例如以下是一些可用模组。
- 核心功能(core):一个紧凑的模块,定义了基础数据结构,包括稠密多维数组
Mat
以及一些可被其它模块用到的函数 - 图像处理(imgproc):一个图像处理模块,包含线性和非线性滤波器,几何图像变换(大小改变
(resize)
,仿射(affine)
以及扭曲透视,基于泛型表的重新映射),色彩空间转换,直方图等等。 - 视频分析(video):视频分析包括了运动估计,背景减法,对象追踪等散发
- 相机标定及三维重建(calib3d):基本的多视图几何算法,单目双目相机标定,对象姿态估计,双目立体匹配算法等,还包括一些用于三维重建的元素。
- 二维特征框架(features2d):显著特征检测器,描述符以及描述符匹配器
- 对象检测(objdetect):对象检测以及预先定义好的类的实例(比如脸、眼睛、被子、行人、车辆等等)
- 高级用户界面和媒体流I/O操作(highgui):简单易用的一个基本UI界面
- 视频输入/输出(videoio):简单易用的视频捕捉和解码模块
- ……以及一些有用的其它模块,比如flann和Google test wrappers,Python binding等。
接下来的文档会详细介绍每个模块的功能及用法。但首先请熟悉本库中最基础的API概念及其用法。
API概念(API Concept)
cv命名空间(cv Namespace)
OpenCV所有的类和函数都放置在cv这个命名空间里的,因此想要在你的代码中使用OpenCV的功能,请使用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 );
...
现有或者在将来可能会出现一些外部名称和STL相同(冲突),因此,请使用明确的cv::
标示符已解决名称冲突:
Mat a(100, 100, CV_32F);
randu(a, Scalar::all(1), Scalar::all(std::rand()));
cv::log(a, a);
a /= std::log(2.);
std::
与cv::
中均有log()
函数
自动内存管理(Automatic Memory Management)
OpenCV对内存实行自动管理
首先,被大多数函数或者方法调用的std::vector
和cv::Mat
以及其它数据结构有析构函数,在需要时释放底层内存缓冲区。这意味着析构函数并需要总是释放Mat的内存缓冲,它们考虑到了可能的数据共享,析构函数与矩阵数据缓存区相关的引用计数器(reference counter)递减。当且仅当引用计数到了0的情况下,缓存被释放,也即是说,没有其它结构引用同一缓冲区。与之相似,当一个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,实质上是拷贝了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提供了cv::Ptr
这样一个模板类,与C++11中的std::shared_ptr有些相像。所以,不使用普通指针:
T* ptr = new T(...);
你可以使用:
Ptr<T> ptr(new T(...));
或者是
Ptr<T> ptr = makePtr<T>(...)
Ptr<T>
封装了一个指向T实例的指针和与指针相关联的引用计数器。
输出数据的自动分配 (Automatic Allocation of the Output Data)
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;
}
由于视频帧分辨率和比特深度对于视频捕获模块是已知的,所以阵列帧由>>
运算符自动分配。数组edge由cvtColor函数自动分配。它具有与输入数组相同的大小和位深度。通道数为1,因为颜色转换代码cv::COLOR_BGR2GRAY
被传递,这意味着颜色将转边为灰度图。请注意,在第一次执行循环体时,frame和edge仅分配一次,因为之后所有的帧(frame)都具有相同的分辨率。如果以某种方式更改视频分辨率,则会自动重新分配阵列。
该技术实现的关键部分是Mat::create
方法。它需要所需的数组大小和类型。如果数组已具有指定的大小和类型,则该方法不执行任何操作。否则,它释放先前分配的数据(如果有的话)(这部分涉及递减引用计数器并将其与零比较),然后分配所需大小的新缓冲区。大多数函数为每个输出数组调用Mat::create
方法,因此实现了自动输出数据分配。
值得一些注意的是cv::mixChannels
,cv::RNG::fill
,以及一些其他函数和方法例外。它们无法分配输出数组,因此必须提前执行此操作。
饱和算法(Saturation Arithmetics)
作为一个计算机视觉库,OpenCV需要处理大量的图像像素,图像通常会以8-16位的紧凑方式编入每个通道,形成具有最大值和最小值的区间范围。此外还有一些必要的图像处理操作,例如色彩空间转换,亮度、对比度、锐度调节,复杂修改(bicubic,Lanczos)能产生超出有效范围的数值。如果仅存储结果的低8(16)位,可能会导致视觉伪影,并可能影响更进一步的图像分析。为了解决这个问题,饱和算法(saturation arithmetics)应运而生。如存储数值R,操作的结果是转化成8bit的图像,以便能在0-255范围内找到最接近的数值:
相似的规则应用在有符号8位、无符号16位、有符号16位类型中。此语义在库中具有通用性,在C++代码中是通过类似于标准C++操作符saturate_cast<>
函数实现的。见下文所提供的实现公式:
I.at<uchar>(y, x) = saturate_cast<uchar>(r);
这里的cv::uchar是一个OpenCV 8bit的无符号类型。在SIMD的优化代码里,以及如SSE2的操作指南中的paddusb、packuswb等等都在使用这种类型。在其帮助下,它们达到了和C++中一样的高效表现。
注意: 饱和算法不适用于32位的整型结果
固定像素类型和对模板的限制使用(Fixed Pixel Types. Limited Use of Templates)
模板是C++中一个非常重要的特性,它可以实现非常强大、高效且安全的数据结构和算法。但是模板的滥用会导致编译时间的显著增加、内存占用过大等问题。而且大量使用模板使会接口和实现很难分离,模板对于一些基础算法还好,但是涉及到计算机视觉单个算法就有成百上千行来说却很累赘。正因如此,同时也为了其它语言接口(如Python、Java、Matlab,这些语言不支持模板或者支持的很有限)开发的简便性,当前OpenCV的实现是基于的多态和模板上的运行时调度。在某些时候,运行时调度很慢(如访问像素的操作)、无法访问(通常是Ptr<>
的实现)或者就是不方便(saturate_cast<>()
),实现时就小的模板类,方法和函数。当前版本中的OpenCV模板使用时受限制的。
故OpenCV库可操作性的数据类型是有限的,为以下类型之一:
- 8-bit 无符号整形(uchar)
- 8-bit 有符号整形(schar)
- 16-bit 无符号整形(ushort)
- 16-bit 有符号整形(short)
- 32-bit 有符号整形(int)
- 32-bit 浮点型(float)
- 64-bit 浮点型(double)
- 所有元素都是同一数据类型(以上7种之一)的由几个元素组成的一个元组(tuple)
- 由以上元组所组成的单位数组(array)就叫做多通道数组。与单位元素都是标量的单通道数组相对立。通道可能的最大数是由
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 常量 (通道数1-4)
- CV_8UC(n) ... CV_64FC(n) or 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); //创建一个3通道(彩色)图片
//1920列1080行。
Mat grayscale(image.size(), CV_MAKETYPE(image.depth(), 1)); //创建一个单通道图片,带有相同的大小和相同的通道类型。
OpenCV不能构建和处理更复杂的元素数组。此外每个函数以及方法仅可以执行所有数组类型中的一个类型,通常算法越复杂,格式可支持性越小。
下面是一些格式使用范围的例子:
- 面部检测算法仅支持8位的灰度或者色彩图像
- 线性代数算法和大多数的机器学习算法仅支持浮点数组。
- 基础函数,例如
cv::add
,支持所有类型。 - 色彩空间转换函数支持8位、16位无符号类型和32位浮点类型。
每个函数支持的类型已经根据实际需要定义好了,且可以在未来根据用户的需求拓展。
输入/输出数组(InputArray and OutputArray)
许多OpenCV函数都能够处理稠密二维或者多维数组。这些函数通常都以cppMat
作为参数,但在某些时候使用std::vector<>
(比如存放点集)和cv::Matx
(类似于3x3的单应矩阵)显得更为方便。为避免API的命名重复,特殊的“代理”类也被引入。最基本的代理类就是cv::InputArray
,其作用是将只读数组作为函数输入。派生自InputArray的类cv::OutputArray
作为函数的输出。通常不需要关注中间类型(不可声明明确类型的变量)——它会自动工作。故除了InputArray和OutputArray不能使用,总能使用的是 Mat
, std::vector
, cv::Matx<>
, cv::Vec<>
和cv::Scalar
。当一个函数有可选的输入和输出数组,而你并没有或者并不想输入输出,传递cv::noArray()
错误处理
OpenCV使用异常(Expection)来示意严重错误。当输入数据类型正确且在规定的范围之内,但是算法因为某些原因无法运作(比如优化算法没有收敛)时会返回一个特殊的错误代码(通常就是布尔值)。
异常可以被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 configuration)过程中保留。在自动内存管理情况下,如果突然发生错误,所有内部缓存会被自动释放。用户只需要添加一个声明即可捕获异常:
try
{
... // call OpenCV
}
catch( cv::Exception& e )
{
const char* err_msg = e.what();
std::cout << "exception caught: " << err_msg << std::endl;
}
多线程和可重入性
当前版本的OpenCV支持重入,也即是说同样的函数或是不同类中的同样方法可以被不同的线程调用。而且相同的Mat
可以在不同线程中调用,因为引用计数使用了特定体系结构的原子指令(atomic instructions)。