OpenCV中的矩阵和图像类型
任务刚刚做完,就迫不及待的来写写在OpenCV中常见的几类数据类型:
在使用OpenCV时我们时常会碰到IplImage这个数据类型,IplImage就是我们通常说的“图像”进行编码的基本结构,这些图像可能是灰度,彩色,四通道的,其中每个通道可以包含任意的整数或者浮点数。因此,该类型比常见易于理解,比三通道的RGB彩色图像更为通用(这与计算机的取值有关)。
虽然OpenCV是由C语言实现的,但是它使用的结构体也是遵循面向对象的思想设计的。实际上,IplImage是由CvMat派生的,而CvMat是由CvArr派生的。在开始讨论图像在内存的分布之前,我问去熟悉一下其他的更为基本的数据类型:CvMat
CvMat的矩阵结构:
在开始学习前,我们要知道两件事情:
1.在OpenCV中没有向量(vector)这个结构,在任何时候需要向量,都只需要一个列矩阵(如果需要一个转置或者是共轭向量,则需要一个行矩阵)
2.在OpenCV中矩阵的概念与我们在线性代数课上学的概念相比,更为抽像,尤其是矩阵元素,并非只是能取简单的数值类型
typedef struct CvMat { int type; int step; /* for internal use only */ int* refcount; int hdr_refcount; union { uchar* ptr; short* s; int* i; float* fl; double* db; } data; #ifdef __cplusplus union { int rows; int height; }; union { int cols; int width; }; #else int rows; int cols; #endif } CvMat;
此类信息被称为是矩阵头,很多程序是区分矩阵头和数据体的,后者是各个data成员指向的内存位置。其中有个部分是/* for internal use only */,这部分是该数据在程序运行中记录该内存的数据被引用多少次,何时进行内存回收
我们用一下段代码看看内存中的数据分布:
CvMat*m_Mat=cvCreateMat(10,10,CV_8S);
对于如何进行矩阵的创建,初始化,赋值,释放等函数的学习,可以自行查找响应函数。
在这里我们可以借鉴一个好的程序设计的思路:就是,为大型的变量构建一个表示头,然后在其他的内存空间进行具体数据的分配,可以将表示头中的数据指针指向那个内存空间,实现对数据的打包,并用操作矩阵的函数去处理这些数据,通过这样的方法,可以提高我们程序的执行效率。因为,在进行参数传递时,进行表示头的复制传递,一定是比复制全部的数据块的多,而且,便于使用指针对数据的修改。
矩阵数据的存取:
一旦我们创建了一个矩阵,便可用他来完成很多事情,最简单的操作就是对矩阵的查询,元素的访问:
我们可以使用如下的函数进行矩阵数据的访问:
cvGetElemType (const CvArr *arr); cvGetDims (const CvArr *arr, int *sizes); cvGetDimSize (const CvArr *arr, int index);
第一个返回一个整形常数,表示存储在数组里的元素类型(CV_8UC1之类的类型)
第二个取出数组以及一个可选择的整形指针,他返回维数(我们当前的实例时二维,但是在后面我们将取到N维的数据),如果整形指针不为空的话,它将存储对应数组的高度和宽度。
第三个函数通过一个指示维数的整形简单返回矩阵在那个维数上的矩阵的大小
在《学习OPenCV》一书中,我们学到可以使用三种方法来进行数据的存取:
简单的方法:
利用宏进行数据的存取
#define CV_MAT_ELEM_PTR_FAST( mat, row, col, pix_size ) \ (assert( (unsigned)(row) < (unsigned)(mat).rows && \ (unsigned)(col) < (unsigned)(mat).cols ), \ (mat).data.ptr + (size_t)(mat).step*(row) + (pix_size)*(col)) #define CV_MAT_ELEM( mat, elemtype, row, col ) \ (*(elemtype*)CV_MAT_ELEM_PTR_FAST( mat, row, col, sizeof(elemtype)))
short m_SData[] = {1,2,3,4,5,6,7,8,9}; CvMat m_Mat = cvMat(3,3,CV_8S,m_SData); short a = CV_MAT_ELEM(*m_Mat,short,2,2);
更进一步,还有另一个与此宏相类似的宏:
#define CV_CN_SHIFT 3 #define CV_DEPTH_MAX (1 << CV_CN_SHIFT) #define CV_MAT_DEPTH_MASK (CV_DEPTH_MAX - 1) #define CV_MAT_DEPTH(flags) ((flags) & CV_MAT_DEPTH_MASK) /* 0x3a50 = 11 10 10 01 01 00 00 ~ array of log2(sizeof(arr_type_elem)) */ #define CV_ELEM_SIZE(type) \ (CV_MAT_CN(type) << ((((sizeof(size_t)/4+1)*16384|0x3a50) >> CV_MAT_DEPTH(type)*2) & 3)) #define CV_MAT_ELEM_PTR( mat, row, col ) \ CV_MAT_ELEM_PTR_FAST( mat, row, col, CV_ELEM_SIZE((mat).type) )
CV_MAT_ELEM_PTR()传入矩阵,待返回元素的行和列号这三个参数,返回指向这个元素的指针。该宏和CV_MAT_ELEM()宏的最重要的区别是后者在指针解引用之前将其转换成指定的类型。如果需要同时读取数据和设置数据,可以直接调用CV_MAT_ELEM_PTR()。但是在这种情况下,必须将指针转成恰当的类型。
CvMat* mat = cvCreateMat (3,3,CV_32FC1); float ele = 0.1; *((float*)CV_MAT_ELEM_PTR(*mat,2,1)) = ele;
但遗憾的是,这些宏在每次调用的时候都需要重新计算新的指针。这就意味着要查找直线矩阵基本元素数据区的指针,计算目标数据在矩阵中的相对地址,然后将相对的位置与基本位置相加。所以,即使这些宏容易使用,但是不是存取矩阵的最佳方法。在计划顺序访问矩阵中的所有元素时,这种方法的缺点尤为突出。
麻烦的方法:
在“简单的方法”中讨论的两个宏仅仅适用于访问1维或者2维的数组。OpenCV提供了多维数组的机制。实时上,OpenCV可以支持普通的N为的数组,这个N可以去任何值(但是要看你的内存空间有多大!!!)
cvGetReal*D()函数返回的是整形的,另外四个(cvGet*D())的返回值是CvScalar类型的。这就意味着在使用这些函数的时候,会有很大的浪费空间。所以,只是在你认为用这些函数比较方便和高效的时候才用它,否则,最好使用cvPtr*D()。使用cvPtr*D()的另外一个原因,既可以用这些指针函数访问矩阵中的特定的点,然后由这个点出发,用指针的算术运算得到指向矩阵中的其他数据的指针。在多通道的矩阵中,务必牢记这点:通道是连续的!所以,要将指向该数据类型的指针移动到下一通道,我们只需要将其增加1,;如果想访问下一个像素或者是像素集,我们只需要增加一定的偏移量,使其与通道数相等。
另一个需要知道的技巧是矩阵数组的step元素,step是矩阵中行的长度,单位是字节。在那些结构中,仅靠cols或者width是无法在矩阵的不同行之间移动指针的,处于效率的原因,矩阵或者图像的内存分配都是四个字节,的整数倍,因此,三个字节宽的矩阵将被分配为4个字节,最右一个字节被忽略。因此,如果我们得到一个字节的指针,该指针指向数据元素,那么我们可以用step和之歌指针相加以使指针指向正好在我们的点的下一行元素,如果我们有一个整型或者浮点型的矩阵,对应的有整型的和浮点型的指针执行数据区域,我们将让step/4与这个指针相加来移到下一行,对双精度型的,我们让指针step/8与指针相加(这里仅仅考虑了C将自动地将差值与我们添加的数据类型的字节数相乘)。
恰当的方法:
从以上所有那些访问函数来看,你可能会想,没有必要在介绍了,如果要提高访问速度,运行速度,在要求效率的程序上,基本不使用set和get函数很少排上用场。大多数的时候,计算机视觉是一种运算密集型的任务,因而你想尽快利用有效的方法做事,毋庸置疑,通过这些函数接口是不可能做到十分高效。相反的,应该定义自己的指针计算并且在矩阵中利用自己的方法。如果打算对数组中的每一个元素执行一些操作,使用自己的指针才是最有效的。
例子:累加一个三通道矩阵中的所有元素:<来自《学习OpenCV》>
double my_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++; } } return double(s); }
计算指向矩阵的指针时,记住一点:矩阵的元素data是一个联合体。所以,对这个指针解引用的时候,必须指明结构体中的正确的元素以便得到正确的指针类型。然后,为了使指针产生正确的偏移,必须使用矩阵的行数据的长度(step)元素。