OpenCV中的三种图像数据结构CvMat,IplImage和Mat(一)CvMat
本文为原创,若有错误的地方欢迎批评指正!
先说区别,它们三个都可以代表和显示图像,但是Mat类型侧重于数学计算,出现在opencv2.0版本之后,其用法与Matlab中的操作非常类似,opencv对其数学计算进行了优化。CvMat和IplImage更侧重于图像,opencv对其图像的操作进行了优化。CvMat从基类CvArr派生而来,IplImage又从CvMat派生而来。接下来就它们结构体构成、初始化、操作像素的方法上分别讨论。
本文先介绍CvMat
CvMat可以看作为一个多通道的矩阵,多通道的意思就是矩阵的每个元素可以是一个多维数组。CvMat结构体中比较常用的成员包括宽度(width)、高度(height)、行长度(step,单位为字节)和指向图像块的数据指针(data)。整个CvMat分为矩阵头和数据体,相当于一本书的目录和具体内容,矩阵头给出参数的指针,数据体存放二维图像块和参数数值。
1.初始化
在初始化的时候就包括仅初始化矩阵头、仅初始化数据体和两个都初始化。
//仅初始化矩阵头,不为数据分配内存空间 CvMat* cvCreateMatHeader(int rows, int cols, int type); //仅为数据体分配内存空间 void cvCreateData(CvArr* arr); //既创建cvMat结构又为数据分配内存(最常用) CvMat* cvCreateMat(int rows, int cols, int type);
除此之外还有一些其他的方法,比如在其他场合已经设置好了参数输入了图像,或者有另一个CvMat结构,用这些已有的数据创建cvMat结构。
//根据矩阵mat克隆一个一模一样的矩阵,不仅矩阵头一样,数据也完全复制过去 CvMat* cvCloneMat(const cvMat* mat); //如果不想复制数据,可以根据矩阵mat克隆一个一模一样的矩阵头 CvMat* cvInitMatHeader(CvMat* mat, int rows, int cols, int type, void* data=NULL, int step=CV_AUTOSTEP);
2.矩阵数据存取
访问数据就是要读取像素的亮度等数值,用于进一步图像处理算法,或者将计算出来的数值写入图像。《学习OpenCV》教材中将读写数据的方法分为简单的方法、麻烦的方法和恰当的方法。
/*简单的方法————使用宏*/ //返回数据的值 ElemType data = CV_MAT_ELEM(*mat, ElemType, row, col); //返回数据的指针,适用于同时读取和设置,但注意类型转化 ElemType* ptr = (ElemType*)CV_MAT_ELEM_PTR(*mat, row, col);//读 *((ElemType*)CV_MAT_ELEM_PTR(*mat, row, col)) = data;//写
宏仅适用于访问一维或二维的数组,为了支持普通的N维数组,可以采用函数读取的方式。1D、2D和3D都有独有的函数,索引的坐标个数分别为1、2、3个(idx0~idx2),再高维的矩阵就要用普适的函数,索引的参数为一个指向一个整形数组的指针int* idx。
/*麻烦的方法————使用函数*/ //返回数据的值,可以返回double和CvScalar两种类型 double cvGetReal1D(const CvArr* arr, int idx0); double cvGetReal2D(const CvArr* arr, int idx0, int idx1); double cvGetReal3D(const CvArr* arr, int idx0, int idx1, int idx2); double cvGetRealND(const CvArr* arr, int* idx); CvScalar cvGet1D(const CvArr* arr, int idx0); CvScalar cvGet2D(const CvArr* arr, int idx0, int idx1); CvScalar cvGet3D(const CvArr* arr, int idx0, int idx1, int idx2); CvScalar cvGetND(const CvArr* arr, int* idx); //返回数据的指针 uchar* CvPtr1D(const CvArr* arr, int idx0, int* type=NULL); uchar* CvPtr2D(const CvArr* arr, int idx0, int idx1, int* type=NULL); uchar* CvPtr1D(const CvArr* arr, int idx0, int idx1, int idx2, int* type=NULL); uchar* CvPtr1D(const CvArr* arr, int* idx, int* type=NULL, int create_node=1, unsigned * precalc_hashval=NULL); /*写(Set)的函数与读(Get)的函数形式非常类似, 只是输入参数中增加了一个要写入的值。*/ //此外对于<浮点型单通道矩阵>有两个简便的函数 double cvmGet(const CvMat* mat, int row, int col); void cvmSet(CvMat* mat, int row, int col, double value);
接下来是重点要介绍的恰当的方法,就是应用指针。只要弄清楚图像块存储时时怎样一个结构,就可以利用指针任意读写任意一个位置任意一个通道的值,实现“指哪打哪”。
以上这幅图说明的是三维点在opencv中的储存方式,如果x、y和z分别代表BGR或者HSV的话,最常用的就是n-by-3的这种方式了吧!在获取某个像素点处某个特定通道值的时候,需要首先纵向地用图像的第一个元素的地址得到像素点所在行的第一个元素的地址,再横向地计算这个像素的地址,再加上第几个通道,就是所要的值。假设一幅图的高为rows,宽为cols,行长度为step,通道数为nChannels,整幅图像的首地址是data->ptr,那么要索引第(i,j)个像素的地址,计算公式就是:
pointer = data->ptr + i*step + j*nChannels;
为什么在每次要计算当前行的首地址而不是用图像整个的首地址直接索引呢?为什么在计算的时候行数乘的是step而不是cols*nChannels呢?这里我理解的是,首先要避免设定了ROI的问题,每一行像素个数不一定等于原来图像的cols。再一个就是如果是像bitmap一样,每行不够4个字节的整数倍要凑成4个字节的整数倍存储的话,cols就又不准确了。
接下来贴上一个《学习OpenCV》中的例子,累加一个三通道矩阵中的所有元素:
float sum( const CvMat *mat) { float s = 0.0f; for(int row=0; row < mat->rows; row++) { //计算当前行第一个像素的地址 const float* ptr = (const float*)(mat->data.ptr + row * mat->step); //该行开始累加 for(int col = 0; col < mat->cols; col++) { s += *ptr++; } } }