一、 旧的数据结构
在老版本的 opencv 中,矩阵主要用 c 的结构体实现,主要的几个结构体为:
cvArr (通用数组)、
cvMat (多通道二维矩阵)、
cvMatND (多通道多维稠密矩阵)、
cvSparseMat (多通道多维稀疏矩阵)、
IplImage (图片,二维矩阵,数据只能是1\2\3\4通道)、
CvSeq (序列)
CvSet (集合,派生于序列CvSet)
很多函数都是以cvArr*作为参数,这时可以将上面提到的各种类型的指针作为实参传递给函数。这里有两个问题需要搞清楚:
1. 为什么各种类型的指针都能作为cvArr*类型的实参传递给函数?
在C++中,通常可以这样解释: 其他的类型都是 cvArr 的派生类,派生类的指针可以代替基类指针传给函数。
虽然很多文章都提到其他各种类型都"派生"于CvArr,但这只是一个类比,C语言本身其实并不支持类、继承和派生。那为什么在C语言中也可以用其他类型的指针代替cvArr*呢?看下cvArr的定义就清楚了 :
typedef void cvArr ;
那么cvArr*其实就是void*,在C语言中可以用任何类型的指针作为void*类型的实参。
2. 调用的函数内部怎么知道传进来的指针到底是什么类型?不同类型的数据有不同的格式,因此必须知道确切的数据类型才能正确解析数据。
cvMat、cvMatND、cvSparseMat这三个结构体的第一个字段(前四个字节)都是type,这个字段包含了一个魔数,这三种类型有不同的魔数;
CvSeq(CvSet)的第一个字段(前四个字节)是flags,它也包含了一个独特的魔数;
IplImage的第一个字段(前四个字节)是nSize,它始终等于sizeof(IplImage),也相当于是一个魔数。
因此,只要检测前四个字节中的魔数就可以判断数组(矩阵)的类型。opencv函数的内部也是这么做的。
举例说明: cvAbsDiff 函数的实现 (代码来自 opencv-2.4.11)
下面的代码来自较新版本的opencv。在新版本的opencv中,虽然保留了传统的 C 风格的接口,但内部实现已都变为C++ (这段代码来自Arithm.cpp,是个C++文件)。
为了能在 C 代码中调用这些 CPP 文件中定义的函数,需要将CPP文件中定义的 C 风格的接口函数都用CV_IMPL声明,CV_IMPL其实就是 extern "C"。
传统的C接口函数大都是以小写的"cv"开头。
/* 接口保持为 C 风格,但内部实现却使用了C++的类,cv::Mat */
CV_IMPL void cvAbsDiff( const CvArr* srcarr1, const CvArr* srcarr2, CvArr* dstarr )
{
// 把srcarr1和dstarr用cvarrToMat函数分别转化为cv::Mat类的对象后再做相应操作。
// cvarrToMat函数内部就是根据魔数判断arr的类型。
cv::Mat src1 = cv::cvarrToMat(srcarr1), dst = cv::cvarrToMat(dstarr);
CV_Assert( src1.size == dst.size && src1.type() == dst.type() );
cv::absdiff( src1, cv::cvarrToMat(srcarr2), dst );
}
void cv::absdiff( InputArray src1, InputArray src2, OutputArray dst )
{
arithm_op(src1, src2, dst, noArray(), -1, getAbsDiffTab());
}
/*
疑问: absdiff函数的参数应是InputArray和OutputArray类型,但cvAbsDiff()里传递给
absdiff的是Mat类型。这个类型的转换是如何被支持的?
答: _InputArray 类重载了一个"转换构造函数" : _InputArray(const Mat& m),
因此可以直接用Mat对象赋值给_InputArray对象,或代替_InputArray对象
作为函数的实参。_OutputArray也一样。
*/
二、 新的数据结构
新版本的opencv中,直接使用C++来实现数据结构和算法。opencv中定义了一个cv命名空间,在该命名空间中又定义了一个 Mat 类,这个类可以取代旧的结构体 cvMat 和 cvMatND。
class CV_EXPORTS Mat
{
public:
// ... a lot of methods ...
...
/*! includes several bit-fields:
- the magic signature
- continuity flag
- depth
- number of channels
*/
int flags; // 记录了魔数、元素类型等。
//! the array dimensionality, >= 2
int dims; // 矩阵的维数
//! the number of rows and columns or (-1, -1) when the array has more than 2 dimensions
int rows, cols; // 矩阵是二维时,记录矩阵的行和列; 矩阵高于二维时无效
//! pointer to the data
uchar* data; // 数据
//! pointer to the reference counter;
// when array points to user-allocated data, the pointer is NULL
int* refcount;
...
MSize size; // 各个维度的 size 。
MStep step; // 各个维度的 步长 ( in bytes )
};
对于一个 n+1 维的矩阵,其下标为 (i0,i1,i2...,in) 的元素的地址为(该元素包含多个通道的数据):
M(i0,i1,i2...,in) = M.data + M.step[0]*i0 + M.step[1]*i1 + M.step[2]*i2
+ .... + M.step[n]*in
更多细节:
http://www.opencv.org.cn/opencvdoc/2.3.2/html/modules/core/doc/basic_structures.html
三、Mat与cvMat、IplImage等之间的转换
Mat类重载的几个"转换构造函数":
//! converts old-style CvMat to the new matrix; the data is not copied by default
Mat(const CvMat* m, bool copyData=false);
//! converts old-style CvMatND to the new matrix; the data is not copied by default
Mat(const CvMatND* m, bool copyData=false);
//! converts old-style IplImage to the new matrix; the data is not copied by default
Mat(const IplImage* img, bool copyData=false);
利用这三个构造函数可以分别将 cvMat、cvMatND、IplImage 类型的矩阵转化为 Mat 类型:
CvMat cvmat ;
Mat mat1=Mat(&cvmat) ;
CvMatND cvmatnd ;
Mat mat2=Mat(&cvmatnd) ;
IplImage iplimage ;
Mat mat3=Mat(&iplimage) ;
这三个转换构造函数默认都是只创建矩阵头信息但并不拷贝矩阵数据的,除非指定缺省参数copyData为true。
由于转换构造函数有隐含类型转换的作用,且上面三个转换构造函数都只有一个非缺省参数,因此上面的几行也可以直接写为:
CvMat cvmat ;
Mat mat1= &cvmat ;
CvMatND cvmatnd ;
Mat mat2= &cvmatnd ;
IplImage iplimage ;
Mat mat3= &iplimage ;
另外,Mat类还重载了几个类型转换符:
//! converts header to CvMat; no data is copied
operator CvMat() const;
//! converts header to CvMatND; no data is copied
operator CvMatND() const;
//! converts header to IplImage; no data is copied
operator IplImage() const;
利用这三个类型转换符可以将Mat 类型分别转化为cvMat、cvMatND、IplImage 类型:
Mat mat ;
CvMat cvmat = mat ; /* 企图将一个Mat对象赋值给一个CvMat对象时,
需要进行类型转换,编译器会自动调用Mat类的类型转换函数
生成一个CvMat临时对象。下同。 */
CvMatND cvmatnd = mat ;
IplImage iplimage = mat ;
这样的转换也只是创建了矩阵头信息,而并没有拷贝矩阵数据。
顺便一提,刚开始我不知道定义了转换构造函数后就可以直接跨类型赋值,即我只认同 " Mat mat = Mat (&iplimage) " 这种写法,而不认同 " Mat mat = &iplimage " , 我原本认为要想实现第二种写法只能通过重载 Mat类的赋值运算符。但后来才查到原来转换构造函数也可以这样使用(与重载赋值运算符效果相同),总之,感觉C++的语法实在有些混乱。
想想,如果同时重载了赋值运算符和转换构造函数,编译器该听谁的?
这篇博文提到了C++中一些混乱的规则: http://www.cnblogs.com/darknightsnow/archive/2012/10/18/2729726.html