现在的位置: 首页 > 综合 > 正文

OpenCV学习笔记系列(三)

2018年04月25日 ⁄ 综合 ⁄ 共 5777字 ⁄ 字号 评论关闭

访问矩阵中的数据

有三种方法访问矩阵中的数据:简单的方法、困难的方法和最恰当的方法。

简单的方法(The easy way)

最简单的获取矩阵中数据的方法是使用CV_MAT_ELEM( )宏。这个宏输入矩阵、数据的类型、行、列,然后返回矩阵元素。例如:

CvMat* mat = cvCreateMat( 5, 5, CV_32FC1 );
float element_3_2 = CV_MAT_ELEM( *mat, float, 3, 2 );

从内部来看(under the hood),CV_MAT_ELEM( )只是调用CV_MAT_ELEM_PTR( )这个宏。CV_MAT_ELEM_PTR( )输入矩阵以及所需元素的行列值,返回一个指向元素的指针。

从cxtypes.h中,我们可以看到CV_MAT_ELEM_PTR( )和CV_MAT_ELEM_PTR( )的定义如下:

#define CV_MAT_ELEM( mat, elemtype, row, col ) /
(*(elemtype*)CV_MAT_ELEM_PTR_FAST( mat, row, col, sizeof(elemtype)))

#define CV_MAT_ELEM_PTR( mat, row, col ) /
CV_MAT_ELEM_PTR_FAST( mat, row, col, CV_ELEM_SIZE((mat).type) )

可以看出CV_MAT_ELEM( )只是调用CV_MAT_ELEM_PTR( )这个宏实际调用了CV_MAT_ELEM_PTR_FAST( )这个宏。这可以是版本的问题,上述代码的版本是2.1.0。

CV_MAT_ELEM( )和CV_MAT_ELEM_PTR( )之间一个重要的区别是CV_MAT_ELEM( )在引用指针之前已经把指针转化为指定的类型。如果你不但想读取元素的值而且还要设定它,那么可以直调用CV_MAT_ELEM_PTR( )。然而,这种情况下,你必须把返回的指针转换成适当的类型。下面的例子用CV_MAT_ELEM_PTR( )宏设定矩阵中的一个元素:

CvMat* mat = cvCreateMat( 5, 5, CV_32FC1 );
float element_3_2 = 7.7;
*( (float*)CV_MAT_ELEM_PTR( *mat, 3, 2 ) ) = element_3_2;

不幸的是,这些宏在每次调用时都要重新计算所需的指针。这种方法首先查找矩阵中数据区域中第一个元素的指针,计算一个偏移量以获取感兴趣的信息的地址,然后把这个偏移加到第一个元素的地址。因此虽然这些宏易于使用,但它们不是存取矩阵最佳的方法,尤其是你准备按顺序存取矩阵中所有元素的时候。我们会紧接着讨论完成这个任务最好的方法。

困难的方法(The hard way)

在简单的方法中讨论的两个宏只适于访问一维和二维数组(一维数组,或者说“向量”,实际上是nx1的矩阵)。OpenCV的提供了处理多维数组的机制。实际上,OpenCV充许一般的N维的矩阵,维数可以任意多。

为了访问一般的矩阵,我们使用函数族cvPtr*D和cvGet*D。cvPtr*D族包含cvPtr1D( )、cvPtr2D( )、cvPtr3D( )和cvPtrND( )…. 前三个中的每一个都输入一个矩阵指针参数CvArr*,接着是一个适当的整型数值为指示,返回一个感兴趣元素的指针。对于cvPtrND( ),第二个参数是用以指示数值的整型数组指针。我们稍后还会讨论这个函数。在函数原型中,我们也注意到有一些可选的参数,如果需要时可以给它们赋地址。

例3-6,矩阵结构的存取函数:

uchar* cvPtr1D( const CvArr* arr, int idx0, int* type = NULL);
uchar* cvPtr2D( const CvArr* arr, int idx0, int idx1, int* type = NULL);
uchar* cvPtr3D( const CvArr* arr, int idx0, int idx1, int idx2, int* type = NULL);
uchar* cvPtrND( const CvArr* arr, int* idx, int* type = NULL, int create_node = 1, unsigned* precalc_hashval = NULL);

如果仅仅是读取数据,那么可以使用外有一个函数族cvGet*D,它和例3-6的函数类似,但返回的是矩阵元素的实际值。

例3-7,读取CvMat和IplImage的元素函数:

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 cvGetReal1D( const CvArr* arr, int idx0);
CvScalar cvGetReal2D( const CvArr* arr, int idx0, int idx1);
CvScalar cvGetReal3D( const CvArr* arr, int idx0, int idx1, int idx2);
CvScalar cvGetRealND( const CvArr* arr, int* idx);

cvGet*D的4个例程返回值的类型为double,另外4个的返回值类型为CvScalar。这意味着在使用这些函数时会造成极大的空间浪费。他们只应该在方便有效的时候使用,就好只使用cvPtr*D。

最好使用cvPtr*D的一个原因是,你可以使用函数返回的指针达到访问矩阵指定的点,然后你可以使用算数运算把指针从那里来回移动。在多通道矩阵中通道是相连,记住这一点的是很重要的。(It is important to remember that the channels are contiguous in a multichannel matrix.)例如,在表示红、绿、蓝字节的三通道二维矩阵中,矩阵中的数据是这样存储的:rgbrgb….。因此,要把适当类型的指针移动到下一个通道,我们只把指针加1即可。如果想要把指针移动到下一个“像素”或下一组元素,我们只需加上和通道数相同大小的偏移量即可(这个例子中为3)。

另一个技巧是要知道矩阵数组的步长(step)是矩阵一行长度的字节数(Th e other trick to know is that the step element in the matrix array is the length in bytes of a row in the matrix.)。在矩阵结构中,仅仅知道列或宽度并不足以在矩阵行间移动,因为出于机器效率,矩阵或图像在分配内存是4字节对齐的。因此当一个矩阵的宽度为3个字节时,会被分配4个字节,最后一个字节会被忽略。因此出于这个原因,当我们获得一个byte型的数据元素指针时,我们把指针加上步长(step)就可以达到把指针移动到下边一行元素下面的元素。如果我们有一个整形或浮点型的矩阵,和数据相应元素的整形或浮点型指针,要把指针移动到下一行,那么要加step/4;对于double则需要加step/8(这只是考虑到C会自动把偏移量乘以数据类型的字节数)。

函数cvSet*D和cvSetReal*D用于设定矩阵或图像元素的值。

例3-8,设定CvMat或IplImage的函数:

void cvSetReal1D( const CvArr* arr, int idx0, double val);
void cvSetReal2D( const CvArr* arr, int idx0, int idx1, double val);
void cvSetReal3D( const CvArr* arr, int idx0, int idx1, int idx2, double val);
void cvSetRealND( const CvArr* arr, int* idx, double val);

void cvSet1D( const CvArr* arr, int idx0, CvScalar val);
void cvSet2D( const CvArr* arr, int idx0, int idx1, CvScalar val);
void cvSet3D( const CvArr* arr, int idx0, int idx1, int idx2, CvScalar val);
void cvSetND( const CvArr* arr, int* idx, CvScalar val);

作为一个附加的便利,我们也有cvmGet( )和cvmSet( ),他们可以用来处理单通道浮点型矩阵。他们非常简单:

double cvmGet(const CvMat* mat, int row, int col);
void cvmSet(CvMat* mat, int row, int rol, double value);

因此通常调用cvmSet( mat, 2, 2, 0.500) 和调用cvRealSet2D(mat, 2, 2, 0.500 )是等效的。

最恰当的方法(The right way)

有个所有的那些存取函数,你可能认为那已经足够了。事实上,你可能很少使用上边的set或get函数。大多数时候,机器视觉是对处理器性能很敏感的,你会尽可能的使用最有效率的方法。不用说,使用那些函数接口是没有效率的。你应该是用自己的指针算数运算,并简单的对矩阵进行引用。如果要处理数组中的每一个元素,维护自己的指针是非常重要的(假设OpenCV没有提供完成你的任务的例程)。

为了直接访问矩阵中的数据,所有你实际需要的是要知道数据是按光栅扫描顺序连续排列的,列序数(“x”)是变动最快的变量(For direct access to the innards of a matrix, all you really need to know is that the datais stored sequentially in raster scan order, where columns (“x”) are the fastest-running variable)。通道是交错在一起的,这意味着,在多通道的矩阵中,通道序数也是快速变化的。例3-9演示了怎样使用这种方法。

例3-9,对一个三通道矩阵所有元素求和

float sum( const CvMat * mat){
    float s = 0.0f;
    for (int row = 0; row < mat->rows; row++){
        const float* ptr = (cosnt float*) (mat->data.ptr + row * mat->step);
        for (int col = 0; col < mat->cols; col++){
            s += *ptr++;
        }
    }
    return s;
}

当计算矩阵内部的指针时,要记住矩阵元素“data”是一个联合结构。所以,当你引用这个指针时,必须指明联合中正确的成员以获得正确的指针类型。接着,对指针偏移时必须使用矩阵中“step”这个成员。之前也曾注意到,元素“step”是字节数。安全起见,你的指针最好以字节为单位进行算术运算,然后再把它转换成合适的类型,在上个例子中是float类型。虽然CvMat结构为了和旧的IplImage结构相兼容,提供了“height”和”width”成员,但是我们应该更新的“row”和”col”成员。最后,我们注意到我们为每一行重新计算”ptr”变量,而不是简单的从头开始为每一次读数据而增加指针。这看上去好像做了额外的工作,但是因为CvMat数据指针只指向大数组内的ROI区域,因此不能保证数据在行间是连续的。

点数组(Arrays of Points)

有一个问题经常会出现,而且能够理解这个问题是很重要的,那就是由多维对象构成的多维数组和一维对象构成的高维数组(One issue that will come up oft en—and that is important to understand—is the diff erence between a multidimensional array (or matrix) of multidimensional objects and an array of one higher dimension that contains only one-dimensional objects.)。假设,你有3维的n个点要传给接受CvMat*(或者更像形式cvArr*)参数的OpenCV函数,显然有4中方法可以实现,一定要记住他们之间没必要等效。第一种方法是使用CV32F1类型的n行3列(nx3)的二维数组。相似的,你也可以使用3行n列(3xn)的二维数组。你也可以使用CV32F3类型的n行1列(nx1)或1行n列(1xn)的二维数组。有些情况下这些情况可以自由的相互转换(这意味着你只需传入一个数据指针,其他的是可预计的),但是有些却不能。为了理解原因,考虑如图3-2分布的内存:

clip_image001
图3-2,一组共10个点,每个点都有3个浮点型数据构成,把他们放置到略有不同的4个结构中;有三种情况内存布局是相同的,有一种不同。

从图中你可以看到,在上述的4种情况下,有三种在映射的内存是相同的,但最后一种却不同。对于c-维点构成的N维数组的情况就更复杂。关键要要记住的是对于任何给定的点,它的位置可以用如下公式给出:
clip_image003
这里,Ncol和Nchannels分别是是列数和通道数。从这个公式中我们可以看出,通常由c-维点构成的N维数组和一维对象构成的(N+c)数组是不相同的。在N=1(也就是nx1或1xn的向量)的特殊情况下,有一个特殊的简化(特殊的,如图3-2所示的等效结构),它有时候可以利用它来提高效率。

最后需要详细考虑的是例如CvPoint2D和CvPoint2D32f这样的OpenCV数据类型。这些数据类型被定义为C的结构体,因此他们有严格定义的内存布局。特殊的,由整形和浮点型构成的结构他们是“通道”相连的。因此,由这些对象构成的一维的C样式的数组和有CV32FC2构成的nx1或1xn矩阵有相同的内存布局。相同的道理也可以推广到类型CvPoint3D32f。

抱歉!评论已关闭.