本文参照《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
- #include
- #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 中是通过宏来定义的,具体的可以参见
上面第三行是多维矩阵的建立,先是一个多维矩阵每个维度上的大小,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;
上面第一行是一个创建函数,为了开辟一个指定大小的数据空间,其中的值由 205 初始化,通道数影响的是每一行的长度,比如这里实际开辟的是 4×8 的矩阵大小。然后随之直接输出结果,记得这里只有二维矩阵才有这待遇使用 cout<<;
- Mat E = Mat::eye(4, 4, CV_64F);
- Mat O = Mat::ones(2, 2, CV_32F);
- Mat Z = Mat::zeros(3,3, CV_8UC1);
接下来的三行就是因为 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 的矩阵,可以直接顺序访问。
通过两个循环,如果是连续的,那么第一个 for 只执行一回,如果不是,那么按照正常的访问过程。I.ptr
- 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*/;
- }
- }
template
{
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:迭代器的方式访问
分单通道和三通道两部分:MatIterator_
- 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*/
- ;
- }
- }
- }
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); 来访问,记得多通道的图像是列数上增加的,行数不变。
来源: http://www.bubuko.com/infodetail-1982125.html