OpenCV 简介(cxcore, ml)
这里简单的记录一些 OpenCV 这个库的使用。这是用 C/C++ 写的一个和 computer vision 相关的库,一共含有 5 个组件:
- CXCORE 是 OpenCV 里面使用的常用数据结构,以及处理这些数据结构的函数。
- CV 是常用的 computer vision 相关的函数,比如计算 histogram、目标检测、跟踪的程序。
- ML 是常用的 machine learning 相关的函数,如做分类的 naive Bayes、SVM 等等。
- highgui 是一些比较高层函数,主要是做用户界面。
- 另外还有一个从摄像头获取数据的 CVCAM 库,但是 1.1 里面似乎并进 highgui 了。
最新的是 1.1pre1,但是 debian 里面还是两年前的 1.0 版本的,这就需要自己编译一个了,后面有空学着 debian 的方式做个 deb 包好了。
OpenCV 的文档也算是还不错的了,除了函数的解释,部分功能还有例程、例子可以参考,这对对函数功能不是很清楚的人是非常有用的,比如我 这里不打算详细的记录 OpenCV 的每个功能,感兴趣的看文档吧。
先从 CXCORE 的基本结构讲起,OpenCV 里面提供了:
点 CvPoint(分 int 和 float 类型的,一般 int 的都没有后缀,float 会写 32f,double 是 64f;有 2D 和 3D 的版本),
大小 CvSize、
矩形 CvRect(用左下角的顶点坐标和宽、高表示)、
向量 CvScalar(4 个 double 那么大)。
更重要的就是存储矩阵用的 CvMat、CvMatND(多维矩阵,一般图片就是几个 channel,每个 channel 是一个 CvMat),稀疏矩阵 CvSparseMat。注意这里面有一个比较重要的成员,就是 int* refcount,这是允许多个“矩阵”(其实是 CvMatHeader 或者 CvMatNDHeader)对同一个矩阵进行引用,这时释放的时候涉及到矩阵元素的属主(ownership),只有当 refcount 为 0 的时候,释放的时候才会释放这些元素。数据的入口都是匿名 union,data 可以当作指针用。
图片一般存放在一个 IplImage 结构里面,这个结构里面有很多参数决定图片数据的类型,比如是 UINT8 还是 float 等。创建这些数据最基本的函数就是 cvCreate*,这是产生一个完整的结构,并且分配数据的内存,另有一种是仅仅分配一个 header,函数就是 cvCreate*Header,之后可以用 cvSet*Data 指定数据,释放这些结构使用 cvRelease* 和 cvRelease*Header。另外可以通过 cvInit* 和 cvInit*Header 初始化这些数据结构。我们发现 OpenCV 的函数都是以 cv 开头,而结构都是 Cv 开头。另外,进行深层 copy 使用 cvClone* 函数。如果需要手工增加/减少对数据的 refcount,可以使用 cvIncRefData、cvDecRefData。OpenCV 里面的数据用 CvArr,这是一个 void 类型,因此可以自由转换成为别的类型,如果需要访问 CvArr 类型的数据,可以用 cvGetRawData,下面是一个例子:
cvGetRawData( array, (uchar**)&data, &step, &size );
step /= sizeof(data[0]);
for( y = 0; y < size.height; y++, data += step )
for( x = 0; x < size.width; x++ )
data[x] = (float)fabs(data[x]);
这里的 data 是个 float*,因此每次递增量是 cvGetRawData 返回的 step 除以 sizeof(float)。类似的,cvGet* 命令将会返回对应的 header,如 cvGetMat、cvGetImage 等。
对于一个 CvMat,可以用 cvGetSubMat、cvGetRow(s),cvGetCol(s)、cvGetDiag 获得矩阵的部分,cvGetSize 获得矩阵的大小。遍历稀疏矩阵一般用 cvInitSparseMatIterator、cvGetNextSparseNode。另外可以用 cvGetElemType 获得元素种类,cvGetDim(s) 获得多维矩阵的大小。如果需要访问某些元素,可以用 cvPtr?D 获得指针,cvGet?D 获得元素值,另外有快速版本 cvmGet(对 2D 的矩阵)。对应有 Set 版本,用于赋值,特别的有 cvSetZero 和 cvSetIdentity 用于产生 0 矩阵和单位矩阵。另外和 matlab 类似有个 cvRange,cvReshape 改变矩阵大小,cvRepeat 和 repmat 类似,cvFlip 和 flip* 类似,cvSplit 切分矩阵,cvMerge 是逆操作。针对图片提供有 cvMixChannels。和 randperm 类似有一个 cvRandShuffle。
cvLUT 进行查表(某些图片是 indexed color),cvConvertScale 可以将不同的类型的数据(尤其是图片,有的用 float 的 0-1 表示,有的用 UINT8 表示)互相转化,它的一个特殊情况就是 cvConvertScaleAbs,cvAdd 将两个 CvArr 相加,cvAddS 加的是 scalar,cvAddWeighted 是加权和,cvSub 是相减,cvSubS 减标量,cvSubRS 是用标量减 CvArr,cvMul 是相乘,,cvDiv 是除,另外有 cvAnd(S)、cvOr(S)、cvXor(S)、cvNot、cvCmp(S);cvInRange(S) 可以检查元素是不是位于上下界中,cvMax(S)、cvMin(S) 求最大最小,cvAbsDiff(S) 求差的绝对值,cvCountNonZero 数非零元素个数,cvSum 求和,cvAvg 求平均,cvAvgSdv 计算均值和标准差,cvMinMaxLoc 返回最小最大的位置,cvNorm 计算范数(包括 、 和 )。cvReduce 将矩阵变成向量。
OpenCV 也实现了一些代数运算,比如 cvDotProduct 计算内积,cvNormalize 将向量依照某种范数归一化,cvCrossProduct 计算叉积,cvScaleAdd 将标量相加,cvGEMM 是一个广义的矩阵乘积,包括每个矩阵是不是转置,然后相乘相加(用它定义了 cvMatMulAdd 和 cvMatMul),矩阵产生的变换 cvTransform、cvPerspectiveTransform,转置相乘 cvMulTransposed,迹 cvTrace,转置 cvTranspose、行列式 cvDet,cvInvert 求逆,cvSolve 求解线性方程组,cvSVD 计算奇异值分解,cvSVBkSb 用奇异值产生原来的矩阵,求对称矩阵的特征值 cvEigenVV,cvCovarMat 计算协方差矩阵,cvMahanobis 计算马氏距离,cvCalcPCA 计算主成分分解,cvProjectPCA 进行投影,cvBackProjectPCA 重构,。
下面是 OpenCV 里面常用的数学函数,如舍入的 cvRound、cvFloor 和 cvCeil,开方系列 cvSqrt、cvInvSqrt、cvCbrt,反正切 cvFastArctan、判断 cvIsNaN、cvIsInf,坐标转换 cvCartToPolar 和 cvPolarToCart,幂函数 cvPow,指数 cvExp,对数 cvLog,求解三次方程 cvSolveCubic,一般多项式 cvSolvePoly。
另外有一些随机数相关的函数,比如 cvRNG 产生一个 random number generator,cvRandArr 产生一个随机 CvArr,cvRandInt 返回一个随机整数,cvRandReal 产生 0-1 之间的随机实数。
另外有一些和 discrete Fourier transform 相关的东西,比如 cvDFT,cvDCT 等。
OpenCV 提供了一些动态存储的数据结构,这在很多操作中非常重要,比如需要进行检测某些特征点,那么用户并不知道将会发现多少个,所以最好的方法是使用 OpenCV 的动态数据结构,不过很遗憾的是,似乎这些结构不仅不能和 STL 的容器互相转换,接口也并不统一。
比较重要的类结构是 CvMemStorage,一般说来我们使用 cvCreateMemStorage 创建一个,并最后用 cvReleaseMemStorage 释放,OpenCV 里面很多函数需要 workspace 往往就是一个 CvMemStorage,如果需要自己进行更细致的操作,就需要了解一些比较基本的知识,比如它的结构是一个一个的 block 用双向链表链接(CvMemBlock 结构)而成,通过 CvMemStoragePos 记录位置,通过 cvCreateChildMemStorage 可以创建一个和 parent 相对独立的存储空间,临时存放数据非常方便。通过 cvClearMemStorage 将该结构管理的内存减到最少。CvMemStorage 提供了一个较大尺度的内存管理机制,而 CvSeq 就成为了一个容器,这个容器是一个双向链表(另有 CvSeqBlock 是个循环链表)。这个容器可以用 cvCreateSeq 创建(有一些内部支持的类型),cvSeqPush/cvSeqPop 是当作一个 stack 来进行入栈操作,类似的操作是 cvSeqPush/PopFront(队列常用操作),还可以一次插入多个 cvPush/PopMulti,插入 cvSeqInsert、删除 cvSeqRemove、删除所有元素 cvClearSeq、获得其中一个元素 cvGetSeqElem、获得一个元素的指标 cvGetElemIdx、转换成 array cvCvtSeqToArray、为 array 创建 Seq 的 header cvMakeSeqHeaderForArray,创建一个 CvSeq 的子序列 cvSlice、复制 cvCloneSeq、子序列的删除插入 cvRemove/InsertSlice、反转 cvSeqInvert、排序 cvSeqSort、搜索 cvSeqSearch。和 CvSeq 的输入输出相关的就是 CvSeqWriter 和 CvSeqReader 了,分别调用 cvStart/EndWriter/Reader 即可。
另外一个有用的数据结构是 CvSet,它是 CvGraph、SvSparseMat 和 CvSubdiv2D 的父类,其实现依赖于 CvSeq,创建一个 CvSet 和 CvSeq 类似为 cvCreateSet,添加、删除元素使用 cvSetAdd(cvSetNew 是一个 inline 版本)和 cvSetRemove(cvSetRemoveByPtr 使用指针删除),cvGetSetElem 通过一个索引获得元素,cvClearSet 清除里面的元素。
CvGraph 是用来表示一个带权有向或者无向图的数据结构,用 cvCreateGraph 创建,cvGraphAdd/RemoveVxt 添加顶点(另有一个ByPtr 版本),可以用 cvGetGraphVxt 获得顶点内容,cvGraphVxtIdx 获得元素的索引,cvGraphAdd/RemoveEdge 添加/删除边(也有 ByPtr 版本),另外有搜索边的 cvFindGraphEdge 以及 ByPtr 版本。通过 cvGraphVxtDegree 获得顶点的 degree,清除一个 graph 可以用 cvClearGraph,复制用 cvCloneGraph。遍历一个 graph 可以用 CvGraphScanner 这个数据结构,使用 cvCreateGraphScanner 创建深度优先便利的 scanner(用 Release 释放),这个 scanner 可以用一个 mask 来决定哪些地方是值得停下来供程序员处理的,有了这个 scanner 后用 cvGraphNextItem 就可以迭代了。
OpenCV 里面还有不少作图函数,就是在图片上画一些简单的几何形状,比如 Line、Rectangle、Circle、Ellipse、EllipseBox、FillPoly、FillConvexPoly、PolyLine。另外为了渲染字体,有 cvInitFont,cvPutText、cvGetTextSize 供使用。另外提供了 cv 里面 contour 的绘制函数(一般使用 cvFindContours 获得等高线,然后)cvDrawContours。通过 cvInitLineIterator 可以获得一条直线(指定起点和终点)上面的那些 pixel。cvClipLine 是将一条直线用一个矩形 clip。cvEllipse2Poly 将椭圆转换成为 polyline。
另外 cxcore 里面还提供了一个一致的存储数据的方式,这是通过 XML(或者 YAML)和 CvFileStorage 结构实现的,XML 本身是一个 tree 结构的东西,每个节点用 CvFileNode 表示其类型,用 CvAttrList 表示一个对象含有的属性列表。创建/释放一个 CvFileStorage 对象用 cvOpen/ReleaseFileStorage 函数。前者创建的 CvFileStorage 可用 cvStart/EndWriteStruct 或者 cvWriteInt/String/Real/Comment/RawData 进行写入,另外有 cvWrite 适合写入 CvSeq 等对象。读取使用比较笨的就是直接 cvRead*ByName,或者 cvGetFileNodeByName,比较快的是创建 CvStringHashNode,使用它查询 cvGetFileNode,然后从 CvFileNode 里面获得数据。另外,也有类似的 cvReadInt/Real/等等,以及 cvRead。
OpenCV 提供了一个基本的 RTTI 实现,这是利用 CvTypeInfo 实现的,利用 cvTypeOf 可以获得对应的类型,如果不确定有没有某个类型,可以用 cvFindType 搜索,通过 cvRegisterType 和 cvUnregisterType 注册、注销新类型。这样我们可以直接用 cvRelease 释放某个已知类型的对象、cvClone 复制一个,cvSave 和 cvLoad 实现该对象的存储。
cxcore 里面还有一些非常有用的功能,比如 cvCheckArr 检查有没有越界或者 NaN 类型。cvKMeans2 可以进行 kmeans 聚类,cvSeqPartition 用于将序列通过一个等价函数分成等价类。另外有一些用于出错处理的函数,比如每个 OpenCV 的函数源代码里面写的 CV_FUNCNAME( Name ) 就是用于报告出错函数的宏,等价于声明了一个静态的字符串,常用 CV_ERROR 报错,另有 CV_CHECK 检查出错代码,CV_CALL 提供统一的调用 cxcore 函数方法(自动 CV_CHECK),CV_ASSERT 判断条件(和 assert 类似)。另外有 cvGetErrStatus、cvSetErrStatus,设置出错模式 cvGet/SetErrMode(分 leaf 出错就退出,parent 调用 handler 后向上 unwind,silent 不调用 handler 直接向上 unwind),产生错误 cvError,可以用 cvErrorStr 获得解释。
和 OS 相关的函数,如 cvAlloc、cvFree,计算用时的 cvGetTickCount 和 cvgetTickFrequency,编写模块用的 cvRegisterModule、cvGetModuleInfo,另外还有获得线程、以及并行运算信息的函数,这里不加叙述了。
ml 这个库的结构相对简单,实现的 ML 的算法也仅仅那么几个最经典的,并不具有很好的扩展性。几乎所有的 ml 的模型都是继承 CvStatModel 这个类(其实是一个接口),主要有 clear() 清除模型,train() 训练,predict() 预测,以及 IO 相关的 save/load() 或者 read/write()。实现的算法有朴素贝叶斯分类器 CvNormalBayesClassifier,k-近邻 CvKNearest(不知道用了 kdtree 这些结构没),支持向量机 CvSVM(不支持用户自定义的核函数似乎,可以用 k-fold 交叉验证选择参数),决策树 CART CvDTree,集成算法 AdaBoost CvBoost 以及 boosted tree CvBoostTree、随机树 CvRTree、EM 算法 CvEM(仅仅对某些特定模型可以用),神经网络(两层前馈神经网络)CvANN,感觉就是可以用来做些简单的问题,但是不要指望能用到最新的一些模型。