OpenCV
OpenCV 是一个广受欢迎的开源计算机视觉库,它提供了很多函数,实现了很多计算机视觉算法,算法从最基本的滤波到高级的物体检测皆有涵盖。OpenCV 只是一个算法库,能为我们搭建计算机视觉应用提供“砖头”。我们并不需要完全精通了算法原理之后才去使用 OpenCV,只要了解了“砖头”的功能,就可以动手了。
一个编程的基本流程包括编辑、编译和连接三大步骤。
集成开发环境:(Integrated Development Environment,简称 IDE)可以帮助你对项目进行管理。常用的 IDE 有微软公司的 Visual Studio,里面包含 Visual C++,Visual C#等,其他的还有 Eclipse、NetBeans、Delphi 等。因此我们平时所说的 VC不是一种编程语言,也不是编译器,它只是一个 IDE。IDE 一般包含编辑器。IDE 自带的编辑器一般都针对编程语言进行了定制,实现语法高亮、自动缩进、自动补全等方便的功能。IDE 还提供丰富的菜单和按钮工具。
头文件: 在编程过程中,程序代码往往被拆成很多部分,每部分放在一个独立的源文件中,而不是将所有的代码放在一个源文件中。考虑一个简单的小例子:程序中有两个函数 main()和 foo()。main()函数位于 main.cpp,foo()函数位于 foo.cpp,main()函数中调用 foo()函数。在编译阶段,由于编译是对单个文件进行编译,所以编译 main.cpp 时,编译器不知道是否存在 foo()函数以及 foo()调用是否正确,因此需要头文件辅助。运行时,编译器不知道 foo 的用法是否正确(因为 foo 在另一个文件 foo.cpp中),只有借助头文件中的函数声明来判断。对 main.cpp 进行编译时,不会涉及foo.cpp 文件,只会涉及 main.cpp 和 foo.h(因为 foo.h 被 include)文件。
库文件: 库文件中包含一系列的子程序。我们需要将 foo()函数提供给客户使用,但是不希望客户看到算法源代码。为了达到这一目的,我们可以将 foo.cpp 编译程库文件(图 1.13),库文件是二进制的,在库文件中是看不到原始的源代码的。库和可执行文件的区别是,库不是独立程序,他们是向其他程序提供服务的代码。库文件的好处不仅仅是对源代码进行保密,使用库文件还可以减少重复编译的时间,增强程序的模块化。将库文件连接到程序中,有两种方式,一种是静态连接库,另一种是动态连接库。
OpenCV 是什么
OpenCV 其实就是一堆 C 和 C++语言的源代码文件,这些源代码文件中实现了许多常用的计算机视觉算法。例如 C 接口函数 cvCanny()实现了 Canny 边缘提取算法。可以直接将这些源代码添加到我们自己的软件项目中,而不需要自己再去写代码实现 Canny 算法,也就是不需要重复“造轮子”。由于 OpenCV 中源代码文件巨多,根据算法的功能,将这些源文件分到多个模块中:core、imgproc、highgui 等。将每个模块中的源文件编译成一个库文件(如 opencv_core.lib、opencv_imgproc.lib、opencv_highgui.lib 等),用户在使用时,仅将所需的库文件添加到自己的项目中,与自己的源文件一起连接成可执行程序则可。
什么是命令行参数
在上面代码中,argc 表示命令行输入参数的个数(以空白符分隔),argv 中存储了所有的命令行参数。
OpenCV 介绍
OpenCV 的全称是 Open Source Computer Vision Library,是一个开放源代码的计算机视觉库。OpenCV 是最初由英特尔公司发起并开发,以 BSD 许可证授权发行,可以在商业和研究领域中免费使用,现在美国 Willow Garage 为 OpenCV 提供主要的支持。OpenCV 可用于开发实时的图像处理、计算机视觉以及模式识别程序。
图像的基本操作
1、图像的表示
一副尺寸为 M × N 的图像可以用一个 M × N 的矩阵来表示,矩阵元素的值表示这个位置上的像素的亮度,一般来说像素值越大表示该点越亮。一般来说,灰度图用 2 维矩阵表示,彩色(多通道)图像用 3 维矩阵(M × N × 3)表示。对于图像显示来说,目前大部分设备都是用无符号 8 位整数(类型为 CV_8U)表示像素亮度。图像数据在计算机内存中的存储顺序为以图像最左上点(也可能是最左下点)开始。
Mat 类
早期的 OpenCV 中,使用 IplImage 和 CvMat 数据结构来表示图像。IplImage和 CvMat 都是 C 语言的结构。使用这两个结构的问题是内存需要手动管理,开发者必须清楚的知道何时需要申请内存,何时需要释放内存。新加入的 Mat 类能够自动管理内存。使用 Mat 类,你不再需要花费大量精力在内存管理上。而且你的代码会变得很简洁,代码行数会变少。但 C++接口唯一的不足是当前一些嵌入式开发系统可能只支持 C 语言,如果你的开发平台支持C++,完全没有必要再用 IplImage 和 CvMat。在新版本的 OpenCV 中,开发者依然可以使用 IplImage 和 CvMat,但是一些新增加的函数只提供了 Mat 接口。
Mat 类的定义如下所示,关键的属性如下方代码所示:
class CV_EXPORTS Mat
{
public:
//一系列函数
…
/* flag 参数中包含许多关于矩阵的信息,如:
-Mat 的标识
-数据是否连续
-深度
-通道数目
/
int flags;
//矩阵的维数,取值应该大于或等于 2
int dims;
//矩阵的行数和列数,如果矩阵超过 2 维,这两个变量的值都为-1
int rows, cols;
//指向数据的指针
uchar data;
//指向引用计数的指针
//如果数据是由用户分配的,则为 NULL
int* refcount;24
//其他成员变量和成员函数
…
};
除了在构造函数中可以创建图像,也可以使用 Mat 类的 create()函数创建图像。如果 create()函数指定的参数与图像之前的参数相同,则不进行实质的内存申请操作;如果参数不同,则减少原始数据内存的索引,并重新申请内存。
Mat M(2,2, CV_8UC3);//构造函数创建图像
M.create(3,2, CV_8UC2);//释放内存重新创建图像
需要注意的时,使用 create()函数无法设置图像像素的初始值。
矩阵的基本元素表达
对于单通道图像,其元素类型一般为 8U(即 8 位无符号整数),当然也可以是 16S、32F 等;这些类型可以直接用 uchar、short、float 等 C/C++语言中的基本数据类型表达。
如果多通道图像,如 RGB 彩色图像,需要用三个通道来表示。在这种情况下,如果依然将图像视作一个二维矩阵,那么矩阵的元素不再是基本的数据类型。OpenCV 中有模板类 Vec,可以表示一个向量。OpenCV 中使用 Vec 类预定义了一些小向量,可以将之用于矩阵元素的表达。
对于 Vec 对象,可以使用[]符号如操作数组般读写其元素,如:
Vec3b color; //用 color 变量描述一种 RGB 颜色
color[0]=255; //B 分量
color[1]=0; //G 分量
color[2]=0; //R 分量
像素值的读写
1、函数 at()来实现读去矩阵中的某个像素,或者对某个像素进行赋值操作。下面两行代码演示了 at()函数的使用方法。
uchar value = grayim.at(i,j);//读出第 i 行第 j 列像素值
grayim.at(i,j)=128; //将第 i 行第 j 列像素值设置为 128
创建了两个图像,分别是单通道的 grayim 以及 3 个通道的 colorim,然后对两个图像的所有像素值进行赋值。
include
include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
Mat grayim(600, 800, CV_8UC1);
Mat colorim(600, 800, CV_8UC3);
//遍历所有像素,并设置像素值
for( int i = 0; i < grayim.rows; ++i)
for( int j = 0; j < grayim.cols; ++j )
grayim.at(i,j) = (i+j)%255;
//遍历所有像素,并设置像素值
for( int i = 0; i < colorim.rows; ++i)
for( int j = 0; j < colorim.cols; ++j )
{
Vec3b pixel;
pixel[0] = i%255; //Blue
pixel[1] = j%255; //Green
pixel[2] = 0; //Red
colorim.at(i,j) = pixel;
}
//显示结果
imshow("grayim", grayim);
imshow("colorim", colorim);
waitKey(0);
return 0;
}
2、使用迭代器,
3、通过数据指针
使用 IplImage 结构的时候,我们会经常使用数据指针来直接操作像素。通过指针操作来访问像素是非常高效的,但是你务必十分地小心。C/C++中的指针操作是不进行类型以及越界检查的,如果指针访问出错,程序运行时有时候可能看上去一切正常,有时候却突然弹出“段错误”(segment fault)。当程序规模较大,且逻辑复杂时,查找指针错误十分困难。对于不熟悉指针的编程者来说,指针就如同噩梦。如果你对指针使用没有自信,则不建议直接通过指针操作来访问像素。虽然 at()函数和迭代器也不能保证对像素访问进行充分的检查,但是总是比指针操作要可靠一些。如果你非常注重程序的运行速度,那么遍历像素时,建议使用指针。
4、选取图像局部区域
Mat 类提供了多种方便的方法来选择图像的局部区域。使用这些方法时需要注意,这些方法并不进行内存的复制操作。如果将局部区域赋值给新的 Mat 对象,新对象与原始对象共用相同的数据区域,不新申请内存,因此这些方法的执
行速度都比较快。
1、单行或单列选择
提取矩阵的一行或者一列可以使用函数 row()或 col()。函数的声明如下:
Mat Mat::row(int i) const
Mat Mat::col(int j) const
参数 i 和 j 分别是行标和列标。例如取出 A 矩阵的第 i 行可以使用如下代码:
Mat line = A.row(i);
例如取出 A 矩阵的第 i 行,将这一行的所有元素都乘以 2,然后赋值给第 j行,可以这样写:
A.row(j) = A.row(i)*2;
2、用 Range 选择多行或多列
Range 是 OpenCV 中新增的类,该类有两个关键变量 star 和 end。Range 对象可以用来表示矩阵的多个连续的行或者多个连续的列。其表示的范围为从 start到 end,包含start,但不包含 end。Range 类的定义如下:
class Range
{
public:
...
int start, end;
};
Range 类还提供了一个静态方法 all(),这个方法的作用如同 Matlab 中的“:”,表示所有的行或者所有的列。
//创建一个单位阵
Mat A = Mat::eye(10, 10, CV_32S);
//提取第 1 到 3 列(不包括 3)
Mat B = A(Range::all(), Range(1, 3));
//提取 B 的第 5 至 9 行(不包括 9)
//其实等价于 C = A(Range(5, 9), Range(1, 3))
Mat C = B(Range(5, 9), Range::all());
3、感兴趣区域
从图像中提取感兴趣区域(Region of interest)有两种方法,一种是使用构造函数,如下例所示:
//创建宽度为 320,高度为 240 的 3 通道图像
Mat img(Size(320,240),CV_8UC3);
//roi 是表示 img 中 Rect(10,10,100,100)区域的对象
Mat roi(img, Rect(10,10,100,100));
除了使用构造函数,还可以使用括号运算符,如下:
Mat roi2 = img(Rect(10,10,100,100));
当然也可以使用 Range 对象来定义感兴趣区域,如下:
//使用括号运算符
Mat roi3 = img(Range(10,100),Range(10,100));
3334
//使用构造函数
Mat roi4(img, Range(10,100),Range(10,100));
4、取对角线元素
矩阵的对角线元素可以使用 Mat 类的 diag()函数获取,该函数的定义如下:
Mat Mat::diag(int d) const
参数 d=0 时,表示取主对角线;当参数 d>0 是,表示取主对角线下方的次对
角线,如 d=1 时,表示取主对角线下方,且紧贴主多角线的元素;当参数 d<0 时,
表示取主对角线上方的次对角线。
如同 row()和 col()函数,diag()函数也不进行内存复制操作,其复杂度也是 O(1)。
5、Mat 表达式
利用 C++中的运算符重载,OpenCV 2 中引入了 Mat 运算表达式。这一新特点使得使用 C++进行编程时,就如同写 Matlab 脚本,代码变得简洁易懂,也便于维护。
如果矩阵 A 和 B 大小相同,则可以使用如下表达式:
C = A + B + 1;其执行结果是 A 和 B 的对应元素相加,然后再加 1,并将生成的矩阵赋给 C变量。下面给出 Mat 表达式所支持的运算。下面的列表中使用 A 和 B 表示 Mat 类型的对象,使用 s 表示 Scalar 对象,alpha 表示 double 值。
加法,减法,取负:A+B,A-B,A+s,A-s,s+A,s-A,-A 缩放取值范围:Aalpha
矩阵对应元素的乘法和除法: A.mul(B),A/B,alpha/A
矩阵乘法:AB (注意此处是矩阵乘法,而不是矩阵对应元素相乘)
矩阵转置:A.t()
矩阵求逆和求伪逆:A.inv()
矩阵比较运算:A cmpop B,A cmpop alpha,alpha cmpop A。此处 cmpop
可以是>,>=,==,!=,<=,<。如果条件成立,则结果矩阵(8U 类型矩
阵)的对应元素被置为 255;否则置 0。 矩阵位逻辑运算:A logicop B,A logicop s,s logicop A,~A,此处 logicop
可以是&,|和^。35
矩阵对应元素的最大值和最小值:min(A, B),min(A, alpha),max(A, B),
max(A, alpha)。 矩阵中元素的绝对值:abs(A)
叉积和点积:A.cross(B),
数据获取与存储
将图像文件读入内存,可以使用 imread()函数;将 Mat 对象以图像文件格式写入内存,可以使用 imwrite()函数。imread()函数返回的是 Mat 对象,如果读取文件失败,则会返回一个空矩阵,即 Mat::data 的值是 NULL。