opencv2-新特性及Mat
转:http://www.cnblogs.com/shouhuxianjian/p/4529192.html
本文参照《opencv_2.4.9tutorial》的core部分完成。因为功力还不足以学习侯捷那种大师一样去深入浅出的解析opencv的源码,也只能先学会怎么用opencv,然后实在觉得不够才会去看源码,了解一个开源项目的源码,其实也有助于提升架构框架的能力,和写出一手大神范的代码。
这里推荐一牛逼opencv的大神:http://blog.csdn.net/poem_qianmo/article/details/19809337 浅墨大神即将要出有关《OpenCV3》的书了。
然后推荐一直战斗在Opencv引入中国 传教士行列的大神于仕琪 http://www.opencv.org.cn/forum.php?mod=viewthread&tid=33549,这里面写了简短的入门教程,对于没有编程经验的来说可以看看,其中一句说的好:“学不好opencv主要是编码能力和模式识别、图像处理上两个方面的欠缺的问题”。
正文:
一、opencv2及其特点
相对于opencv的历史来说,可以看《学习opencv》即《learning opencv》,这里面有详细的介绍。之前的opencv版本是基于C代码写的,因为c/cpp的国际标准化,使得能够在后期移植到各个平台上,所以很多开源项目其实都是基于c/cpp系列的(个人:不过 我还不知道为什么不用java来写原始代码,不过 估计肯定是效率,问题吧?)。之前版本的opencv也叫做opencv1,所以对于有cpp接口的opencv2 来说,才有了我们安装opencv中的一步----需要去包含opencv2这个文件夹。而且相对来说,如果是.h 的 那么一般就是 c 的头文件;如果是 .hpp 的那么就是cpp接口的头文件。而且opencv是以c/cpp系列为源码,然后在基于此加上一些不同的编程语言接口,并使用cmake来跨平台吧(个人:这里的个中缘由 因为知识尚浅,而且都没真正了解cmake的内部原理,信口胡说的。)
相对于cpp接口的源码来说,它就有很多好处了,比如不需要像之前需要用 c 语言来模拟 cpp的面向对象那样,能够更加的模块化,而且对于编程人员来说,新增的cpp接口中增加的智能指针类,可以大大减少编程人员所需的自己申请 自己释放内存的问题。(其实 个人更加喜欢cpp,自然各种好话往上堆啊,哈哈)。
相比较来说,opencv2是基于cpp写的,基于cpp的语言特点更能模块化,它每个不同的模块都包含着与其相关的数据结构,所以如果只需要某个模块,那么就直接包含那个模块就好,例如:
#include <opencv2/core/core.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp>
与cpp的语言特性一样,它使用了命名空间,只要在头上加上using namespace cv;就行,当然也可以使用其中的部分,有关命名空间的原理:参考《c++ 程序设计语言》一书相关章节,这可是cpp发明人写的书额。
第二版与第一版最大的不同在于它不是用IplImage和CvMat这样的数据结构,而是通过使用Mat这样的类来代替,其实在《learning opencv》一书中也说了,在opencv1中的很多地方这两个数据结构是相通的,换句话说也即是冗余了,估计当时只是为了区分图像处理还是矩阵数据处理吧。
关于如何将opencv2 与opencv1 进行混合编程或者兼容 可以参考http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/core/interoperability_with_OpenCV_1/interoperability_with_OpenCV_1.html#interoperabilitywithopencv1 ,这是《opencv_2.4.9tutorial》的对应章节的中文版。
二、opencv2的模块介绍
参考 浅墨的介绍http://blog.csdn.net/poem_qianmo/article/details/19925819。其实不同模块对应不同功能,首要的当然是学习core和highgui部分,和imgproc模块部分。这部分没啥好写的,也就不重复介绍了。
三、Mat类的使用及数据的访问方法
3.1Mat类的初始化
1、考虑到OpenCV中会对大图像进行操作,如果一两幅也就算了,可是总有经常拷贝图像和视频帧的时候,这时候如果频繁的拷贝数据,那么就很慢了,而且大部分的情况下程序员的想法也是想对同一幅图像数据进行操作,所以OpenCV将这个想法作为默认想法,如果真想在复制一副图像的同样的数据,那么就显式的调用函数。在OpenCV中一般会有(opencv1中有图像或者矩阵头,和图像或者矩阵数据部分 两个部分),这里也是一样,只不过将默认拷贝矩阵头和指向数据的指针进行复制(有点像cpp语言特性中的引用)。
Mat A, C; // 只创建信息头部分 A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 这里为矩阵开辟内存 Mat B(A); // 使用拷贝构造函数 C = A; // 赋值运算符
Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle Mat E = A(Range:all(), Range(1,3)); // using row and column boundaries
这里是先声明矩阵A和C,然后进行图像的读取(imread函数的参数这里暂且不说,有兴趣的去看看对应的API介绍,不过照虎画猫 其实都差不多):1、对于初始化式、赋值式、建立感兴趣区域(ROI,不过这里也算是初始化式、赋值式),这三种操作来说,都是类似引用的想法,在Mat类不同对象中只有包含的数据(也就是矩阵的的大小啊,通道啊什么的)是完整复制的,但是它们操作的时候对应的数据却是同一块区域,所以这里可以猜测,Mat类的内部结构其实是一堆信息外带一个指向数据的指针,这样就保证复制的是指针而不是整体的数据(可以这么想 ,不过具体怎样得去看源码,只是这样暂时便于理解)。
如果真想复制数据部分,那么:
Mat F = A.clone(); Mat G; A.copyTo(G);
通过使用类中的clone()和copyTo()函数来完成,这时候操作不同的类对象,那么两个就完全没什么牵连的关系了。
2、上面的是从图像中读取数据,这才有的矩阵,但是如果想平地而起的创建咋办?那就得使用Mat类的其他构造函数了。这里不介绍多,尽量往简单通用的地方靠,等使用opencv多了,那就可以往深的地方看看:
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
或者 Mat R = Mat(3, 2, CV_8UC3); randu(R, Scalar::all(0), Scalar::all(255));
int sz[3] = {2,2,2}; Mat L(3,sz, CV_8UC(1), Scalar::all(0));
上面第一行是建立个二维矩阵:参数为行、列、元素类型、每个元素的初始化值。CV_8UC3,8就是8 bit,U是无符号,C是通道,在OpenCV中是通过宏来定义的,具体的可以参见<types_c.h>的584-625行。ps:对于二维矩阵有个特例就是可以直接cout<<M<<endl; 因为内部实现了对2维矩阵的直接输出。在官方文档的具体输出部分也是只介绍了二维矩阵在不同格式下的显示(不过因为暂时只玩过matlab和cpp,那些python啥的没接触,也就不介绍了 http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/core/mat%20-%20the%20basic%20image%20container/mat%20-%20the%20basic%20image%20container.html#matthebasicimagecontainer 这里的格式化打印部分,有兴趣的看看,个人觉得这个是在黑框显示的,只是给程序员看内部数据的,所以觉得有一个用得着就够了,介绍多了,容易晕)。
上面第二行是先建立矩阵大小,然后不指定初始化值,通过使用randu()函数来随机指定,其中需要有随机数的上下界。
上面第三行是多维矩阵的建立,先是一个多维矩阵每个维度上的大小,CV_8UC(1) 就是指定多少通道,因为默认只有1 2 3 4 ,所以其他维度就得自己指定(这里也算是多此一举,因为可以使用CV_8UC1 代替的)。
3、Matlab类型的初始化
Mat M; M.create(4,4, CV_8UC(2));
cout << "M = "<< endl << " " << M << endl << endl;
Mat E = Mat::eye(4, 4, CV_64F); Mat O = Mat::ones(2, 2, CV_32F); Mat Z = Mat::zeros(3,3, CV_8UC1);
上面第一行是一个创建函数,为了开辟一个指定大小的数据空间,其中的值由205初始化,通道数影响的是每一行的长度,比如这里实际开辟的是4×8的矩阵大小。然后随之直接输出结果,记得这里只有二维矩阵才有这待遇使用cout<<;
接下来的三行就是因为Matlab所属公司和OpenCV小组合作的结果,这样才有这么便捷的初始化(个人:这里的理解在cpp语法上应该是先创建一个临时类对象,然后进行eye等的初始化和数据空间的创建,然后在将临时类对象赋值给矩阵E、 O、 Z,具体的可以看《C++程序设计语言 》或者《C++primer》中的有关章节)。关于CV_64F的定义可以查看<types_c.h>中569-576行。
3.2 Mat类对象的访问方法
这里要说下在OpenCV中的数据表示方法,如下图所示,是每一行中长度也就是矩阵的列数是由 (点×通道数) 决定,而行数是不变的,而且是BGR排序,不是RGB排序,这个排序的顺序,完全是最初的大牛制定的时候卖萌罢了。即:
Mat I = imread("your picture",CV_LOAD_IMAGE_COLOR);
int nCols = I.cols*I.channels();
int nRows =I .rows;
方法1:c分割的指针访问
按照上面的代码接着写
if (I.isContinuous()) { nCols *= nRows; nRows = 1; }
这里的isContinuous()是为了识别该系统的编译器是否将这个矩阵的数据行与行之间连接的存储,还是分开的存储,如果是连接的存储,其实也就是个n×1的矩阵,可以直接顺序访问。
uchar* p; for(size_t i = 0; i < nRows; ++i) { p = I.ptr<uchar>(i); for (size_t j = 0; j < nCols; ++j) { p[j] = /*your operation*/; } }
通过两个循环,如果是连续的,那么第一个for只执行一回,如果不是,那么按照正常的访问过程。I.ptr<uchar>(i)是访问矩阵 I 的 第 i 行开始的地址。
template<typename _Tp> inline _Tp* Mat::ptr(int y)
{
CV_DbgAssert( y == 0 || (data && dims >= 1 && (unsigned)y < (unsigned)size.p[0]) );
return (_Tp*)(data + step.p[0]*y);
}
上面的是在<mat.hpp>中的代码段,可以看出,这个模板就是返回data(data就是矩阵的第一行第一列的地址)加上每行的步长长度乘以第几行。
方法2:迭代器的方式访问
const int channels = I.channels(); switch(channels) { case 1: { MatIterator_<uchar> it, end; for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it) *it = /*your operation*/; break; } case 3: { MatIterator_<Vec3b> it, end; for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it) { (*it)[0] = /*your operation*/; (*it)[1] = /*your operation*/; (*it)[2] = /*your operation*/; } } }
分单通道和三通道两部分:MatIterator_<uchar>it,end。就是Mat的迭代器的声明,其中
uchar:typedef unsigned char uchar;
Vec3b:typedef Vec<uchar, 3> Vec3b;
Vec:template<typename _Tp, int cn> class Vec;
方法3:at()函数访问
switch(channels) { case 1: { for( size_t i = 0; i < nRows; ++i) for( size_t j = 0; j < nCols; ++j ) I.at<uchar>(i,j) = /*your operation*/; break; } case 3: { Mat_<Vec3b> _I = I;
for( size_t i = 0; i < nRows; ++i){ for( size_t j = 0; j < nCols; ++j ) {
_I(i,j)[0] = /*your operation*/;
_I(i,j)[1] = /*your operation*/; _I(i,j)[2] = /*your operation*/;
}
I = _I;
break;
}
}
单通道的时候,采用at<uchar>()函数,进行访问矩阵 I 的对应位置;
而三通道的时候,是先建立个矩阵_I,当然,这里还是创建新的矩阵头,指向的还是同一片数据区域。只是这个矩阵头中,将每个点的三个通道放在一起,作为一个一维的3元素的数组,所以可以很清楚的访问每个像素点的位置。如果是想还是使用at()函数,那么就I.at<uchar>(i,(j-1)*nChannels +nChannels);来访问,记得多通道的图像是列数上增加的,行数不变。