OpenCv常用图像和矩阵操作 目录 学习资料
书籍Learning
OpenCV(影印版) 作者:Gary
Bradski, Adrian Kaehler 出版社:东南大学出版社 学习OpenCV(中文版) 作者:Gary
Bradski, Adrian Kaehler 译者:于仕琪 刘瑞祯 出版社:清华大学出版社 OpenCV中文教程 作者:刘瑞祯 于仕琪 网站:http://www./index.php/%E9%A6%96%E9%A1%B5 http://book.51cto.com/art/200912/172349.htm 本地安装目录在安装目录 OpenCV1.0\docs 下有各种学习资料 只用在本地安装目录下面就可以查询到大部分需要的信息,当然也可以直接百度,google 图像IplImage
Structure IplImageOpenCv中图像的结构体为IplImage,位于头文件cxcore.h中,IplImage
结构体的定义如下: ///////////////////////////////////////////////////////////////////////////// typedef struct _IplImage { int nSize; /* IplImage大小 */ int ID; /* 版本 (=0)*/ int nChannels; /* 大多数OPENCV函数支持1,2,3 或 4 个通道 */ int alphaChannel; /* 被OpenCV忽略 */ int depth; /* 像素的位深度,主要有以下支持格式:
IPL_DEPTH_8U, IPL_DEPTH_8S, IPL_DEPTH_16U,IPL_DEPTH_16S, IPL_DEPTH_32S, IPL_DEPTH_ char colorModel[4]; /* 被OpenCV忽略 */ char channelSeq[4]; /* 同上 */ int dataOrder; /* 0 - 交叉存取颜色通道, 1 - 分开的颜色通道. 只有cvCreateImage可以创建交叉存取图像 */ int origin; /*图像原点位置: 0表示顶-左结构,1表示底-左结构 */ int align; /* 图像行排列方式 (4 or 8),在 OpenCV 被忽略,使用
widthStep 代替 */ int width; /* 图像宽像素数 */ int height; /* 图像高像素数*/ struct _IplROI *roi; /* 图像感兴趣区域,当该值非空时, 只对该区域进行处理 */ struct _IplImage *maskROI; /* 在 OpenCV中必须为NULL */ void *imageId; /* 同上*/ struct _IplTileInfo *tileInfo; /*同上*/ int imageSize; /* 图像数据大小(在交叉存取格式下ImageSize=image->height*image->widthStep),单位字节*/ char *imageData; /* 指向排列的图像数据 */ int widthStep; /* 排列的图像行大小,以字节为单位 */ int BorderMode[4]; /* 边际结束模式, 在 OpenCV 被忽略*/ int BorderConst[4]; /* 同上 */ char *imageDataOrigin; /* 指针指向一个不同的图像数据结构(不是必须排列的),是为了纠正图像内存分配准备的 */ } IplImage; } IplImage;
///////////////////////////////////////////////////////////////////////////// 主要的成员变量有 nChannels : 图像的通道数目,即灰度图像:nChannels
= 1; RGB图像nChannels = 3 depth: 每个像素值的数据类型和所占的存储空间 origin变量可以有两种取值:IPL_ORIGIN_TL 或者 IPL_ORIGIN_BL,分别设置坐标原点的位置于图像的左上角或者左下角。在计算机视觉领域,一个重要的错误来源就是原点位置的定义不统一。具体而言,图像的来源、操作系统、编解码器和存储格式等因素都可以影响图像坐标原点的选取。举例来说,你或许认为自己正在从图像上面的脸部附近取样,但实际上却在图像下方的裙子附近取样。避免此类现象发生的最好办法是在最开始的时候检查一下系统,在所操作的图像块的地方画点东西试试。 dataOrder: 多通道的数据存储方式,dataOrder=0是交叉通道存储方式,即BGRBGRBGRBGR的方式存储;dataOrder=1是采用独立通道方式存储,即RRRRRRR。。。,GGGGGGG…,BBBBBB…,一般都是BGRBGRBGR的这种交叉存储方式,cvCreateImage生成的图像也是这种存储方式。 width: 图像的宽度 height: 图像的高度 imageData: 图像的像素矩阵 widthStep:
每一行像素所占的字节数目. 参数widthStep包括相邻行的同列点之间的字节数。仅凭变量width是不能计算这个值的,因为为了处理过程更高效每行都会用固定的字节数来对齐;因此在第i行末和第i+1行开始处可能会有些冗于字节。参数imageData包含一个指向第一行图像数据的指针。如果图像中有些独立的平面(如当dataOrder = IPL_DATA_ORDER_PLANE)那么把它们作为单独的图像连续摆放,总行数为height和nChannels的乘积。但通常情况下,它们是交错的,使得行数等于高度,而且每一行都有序地包含交错的通道。 ROI-- 感兴趣的区域(ROI),实际上它是另一个IPL/IPP 结构IplROI的实例。IplROI包含xOffset,yOffset,height,width和coi成员变量,其中COI代表channel of interest(感兴趣的通道)。ROI的思想是: 一旦设定ROI,通常作用于整幅图像的函数便会只对ROI所表示的子图像进行操作。如果IplImage变量中设置了ROI,则所有的OpenCV函数就会使用该ROI变量。如果COI被设置成非0值,则对该图像的操作就只作用于被指定的通道上了 。不幸的是,许多OpenCV函数都忽略参数COI。 图像的常用操作图像载入函数
函数cvLoadImage载入指定图像文件,并返回指向该文件的IplImage指针。函数支持bmp、jpg、 png、 tiff等格式的图像。其函数原型如下: IplImage* cvLoadImage( const char* filename,
int iscolor); 其中,filename 是待载入图像的名称,包括图像的扩展名;iscolor是一个辅助参数项,可选正数、零和负数三种值,正数表示作为三通道图像载入,零表示该图像作为单通道图像,负数表示载入图像的通道数由图像文件自身决定。 窗口定义函数
函数cvNamedWindow定义一个窗口,用于显示图像。其函数原型如下: int cvNamedWindow( const char* name, unsigned
long flags ); 其中,name是窗口名,flags是窗口属性指标值,可以选择CV_WINDOW_AUTOSIZE和0两种值。CV_WINDOW_AUTOSIZE表示窗口尺寸与图像原始尺寸相同,0表示以固定的窗口尺寸显示图像。 图像显示函数
函数cvShowImage是在指定的窗口中显示图像,其函数原型如下: void cvShowImage( const char* name, const
CvArr* image ); 其中,name是窗口名称,image是图像类型指针,一般是IplImage指针。 图像保存函数
函数cvSaveImage以指定的文件名保存IplImage类型的指针变量,其函数原型如下: int cvSaveImage( const char* filename, const
CvArr* image ); 其中,filename是图像保存路径和名称,image是IplImage指针变量。 Trick: 如果要保存一组图像到result文件夹,图像个数为n,保存名称按照一定的序号递增,假设为imgTmp0.jpg,imgTmp1.jpg,imgTmp2.jpg,imgTmp3.jpg,…,
imgTmpn.jpg,则 操作为: char * f[30]; for(int i=0; i<n;
i++) { sprintf(f,”result/imgTmp%d.jpg”,i); cvSaveImage(f,img); } 借用sprintf函数即可以完成依次命名的功能。 图像销毁函数
函数cvReleaseImage销毁已定义的IplImage指针变量,释放占用内存空间。其函数原型如下: void cvReleaseImage( IplImage** image ); 其中,image为已定义的IplImage指针。 存取图像像素包括获取像素值和对像素值赋值 l
直接获取 假设图像为 IplImage * img,图像的depth= IPL_DEPTH_8U(每个像素用8 bits表示),获取像素坐标(x,y)的操作为 1.
灰度图像(单通道 img->nChannels = 1) 对像素赋值: ((char*)(img->imageData +
y*imge->widthStep))[x] = 255; 获取像素值: int grayValue = ((char*)(img->imageData +
y*imge->widthStep))[x]; 2.彩色图像(单通道
img->nChannels = 1) 对像素赋值: ((char*)(img->imageData +
y*imge->widthStep))[3*x] = 255; ((char*)(img->imageData
+ y*imge->widthStep))[3*x+1] = 255; ((char*)(img->imageData
+ y*imge->widthStep))[3*x+2] = 255; 获取像素值: uchar b = ((char*)(img->imageData +
y*imge->widthStep))[x]; uchar g =
((char*)(img->imageData + y*imge->widthStep))[x]; uchar r =
((char*)(img->imageData + y*imge->widthStep))[x]; 注意: 注意(char*)这个指针的强制转换是针对img->imageData
+ y*imge->widthStep的,也就是针对图像的行指针进行的转换,注意括弧的范围。 当image->depth为其他值时,则可能每个像素的数据类型需要进行(int*),(float*),(double*)等转换。 参数widthStep是相邻行的同列点之间的字节数。仅凭变量width是不能计算这个值的,因为为了处理过程更高效每行都会用固定的字节数来对齐;因此在第i行末和第i+1行开始处可能会有些冗于字节。一次在进行行切换时,一定要widthStep来进行内存的偏移,而不是用depth*width. 一般的情况下,假设有 N-通道,类型为 T 的图像: I(x,y)c ~
((T*)(img->imageData + img->widthStep*y))[x*N + c] //注意x.,y的位置 例子: void saturate_sv( IplImage* img ) { for( int y=0; y < img->height; y++ ) { uchar* ptr = (uchar*) (
Img->imageData + y * img->widthStep );
for( int x=0; x < img->width; x++ ) { ptr[3*x] = 255; ptr[3*x+1] = 255; ptr[3*x+2] = 255;
}
} } 在以上程序中,我们用指针ptr指向第y行的起始位置。接着,我们从指针中析出饱和度和高度在x维的值。因为这是一个三通道图像,所以C通道在x行的位置是3*x+c。 该使用方法是受限的 uchar b,g,r; b
= img->imageData[img->widthStep * row + col * 3] l
宏 可以使用宏
CV_IMAGE_ELEM( image_header, elemtype, y, x_Nc ) I(x,y)c ~
CV_IMAGE_ELEM( img, T, y, x*N + c ),其中c为通道的序号 ,如彩色图像c=0,1,2 不过使用该宏是也要小心数据类型elemtype的问题。 也有针对各种图像(包括 4 通道图像)和矩阵的函数(cvGet2D,
cvSet2D), 但是它们非常慢。 注: 该宏在每次调用时,都会重新计算指针的位置。这意味着,先查找数据区中第0个元素的位置,然后,根据参数中的行和列,计算所需要的元素的地址偏移量,然后将地址偏移量与第0个元素的地址相加,获得所需要的元素的地址。 l
cvGet2D()和cvSet2D() 可以通过cvGet2D()和cvSet2D()两个函数加上一个CvScalar结构体做到获取图像的像素点。
矩阵CvMat
Struct CvMatCvMat的结构体定义为: typedef struct CvMat { int type; /* CvMat signature (CV_MAT_MAGIC_VAL), element type and flags */ int step; /* full row length in bytes */
int* refcount; /* underlying data reference counter */
union { uchar* ptr; short* s; int* i; float* fl; double* db; } data; /* data pointers */
#ifdef __cplusplus union { int rows; int height; };
union { int cols; int width; }; #else int rows; /* number of rows */ int cols; /* number of columns */ #endif
} CvMat;
step 是每一行数据的长度,以字节来表示 data 是存放矩阵数据的联合体,如果矩阵pMat是float类型的,那么获取矩阵数据指针的方式为
pMat->data.fl,如果是整型的 pMat->data.i 行数和列数在c和c++中定义略有不同,但是rows和cols是通用的两个变量。 矩阵的创建和初始化矩阵有多种创建方法。最常见的方法是用cvCreateMat(),它由多个原函数组成,如cvCreateMatHeader()和cvCreateData()。cvCreateMatHeader()函数创建CvMat结构,不为数据分配内存,而cvCreateData()函数只负责数据的内存分配。有时,只需要函数cvCreateMatHeader(),因为已因其他理由分配了存储空间,或因为还不准备分配存储空间。第三种方法是用函数cvCloneMat
(CvMat*) ,它依据一个现有矩阵创建一个新的矩阵。当这个矩阵不再需要时,可以调用函数cvReleaseMat(CvMat*)释放它。 l
分配矩阵空间 CvMat* cvCreateMat(int rows, int cols, int type); l
逐点赋值式初始化
CvMat* mat = cvCreateMat( 2, 2, CV_64FC1 ); l
使用现有数组初始化
double a[] = { 1, 2,
3, 4, 释放矩阵CvMat* M = cvCreateMat(4,4,CV_32FC1);
复制矩阵:CvMat* M1 = cvCreateMat(4,4,CV_32FC1);
存取矩阵元素假设需要存取一个2维浮点矩阵的第(i,j)个元素. 简单的方法从矩阵中得到一个元素的最简单的方法是利用宏CV_MAT_ELEM()。这个宏(参见例3-4)传入矩阵、待提取的元素的类型、行和列数4个参数,返回提取出的元素的值。 例3-4:利用CV_MAT_ELEM()宏存取矩阵 1. CvMat* mat = cvCreateMat( 5, 5, CV_32FC1 ); 2. float element_3_2 = CV_MAT_ELEM( *mat, float, 3, 2 ); 更进一步,还有一个与此宏类似的宏,叫CV_MAT_ELEM_PTR()。CV_MAT_ELEM_ PTR()(参见例3-5)传入矩阵、待返回元素的行和列号这3个参数,返回指向这个元素的指针。该宏和CV_MAT_ELEM()宏的最重要的区别是后者在指针解引用之前将其转化成指定的类型。如果需要同时读取数据和设置数据,可以直接调用CV_MAT_ELEM_PTR()。但在这种情况下,必须自己将指针转化成恰当的类型。 例3-5:利用宏CV_MAT_ELEM_PTR()为矩阵设置一个数值 1. CvMat* mat = cvCreateMat( 5, 5, CV_32FC1 ); 2. float element_3_2 = 7.7; 3. *( (float*)CV_MAT_ELEM_PTR( *mat, 3, 2 ) ) = element_3_2; 遗撼的是,这些宏在每次调用的时候都重新计算指针。这意味着要查找指向矩阵基本元素数据区的指针、计算目标数据在矩阵中的相对地址,然后将相对位置与基本位置相加。所以,即使这些宏容易使用,但也不是存取矩阵的最佳方法。在计划顺序访问矩阵中的所有元素时,这种方法的缺点尤为突出。 麻烦的方法在"简单的方法"中讨论的两个宏仅仅适用于访问1维或2维的数组(回忆一下,1维的数组,或者称为"向量"实际只是一个n×1维矩阵)。OpenCV提供了处理多维数组的机制。事实上,OpenCV可以支持普通的N维的数组,这个N值可以取值为任意大的数。 为了访问普通矩阵中的数据,我们可以利用在例3-6和例3-7中列举的cvPtr*D和cvGet*D…等函数族。cvPtr*D家族包括cvPtr1D(), cvPtr2D(), cvPtr3D()和cvPtrND()…。这三个函数都可接收CvArr*类型的矩阵指针参数,紧随其后的参数是表示索引的整数值,最后是一个可选的参数,它表示输出值的类型。函数返回一个指向所需元素的指针。对于cvPtrND()来说,第二个参数是一个指向一个整型数组的指针,这个数组中包含索引的合适数字。后文会再次介绍此函数(在这之后的原型中,也会看到一些可选参数,必要时会有讲解)。 例3-6:指针访问矩阵结构 1. uchar* cvPtr1D( 2. const CvArr* arr, 3. int idx0, 4. int* type = NULL 5. ); 6. 7. uchar* cvPtr2D( 8. const CvArr* arr, 9. int idx0, 10. 11. 12. 13. int idx1, 14. int* type = NULL 15. ); 16. 17. uchar* cvPtr3D( 18. const CvArr* arr, 19. int idx0, 20. int idx1, 21. int idx2, 22. int* type = NULL 23. ); 24. uchar* cvPtrND( 25. const CvArr* arr, 26. int* idx, 27. int* type = NULL, 28. int create_node = 1, 29. unsigned* precalc_hashval = NULL 30. ); 如果仅仅是读取数据,可用另一个函数族cvGet*D。如例3-7所示,该例与例3-6类似,但是返回矩阵元素的实际值。 例3-7:CvMat和IPlImage元素函数 1. double cvGetReal1D( const CvArr* arr, int idx0 ); 2. double cvGetReal2D( const CvArr* arr, int idx0, int idx1 ); 3. double cvGetReal3D( const CvArr* arr, int idx0, int idx1, int idx2 ); 4. double cvGetRealND( const CvArr* arr, int* idx ); 5. 6. CvScalar cvGet1D( const CvArr* arr, int idx0 ); 7. CvScalar cvGet2D( const CvArr* arr, int idx0, int idx1 ); 8. CvScalar cvGet3D( const CvArr* arr, int idx0, int idx1, int idx2 ); 9. CvScalar cvGetND( const CvArr* arr, int* idx ); cvGet*D中有四个函数返回的是整型的,另外四个的返回值是CvScalar类型的。这意味着在使用这些函数的时候,会有很大的空间浪费。所以,只是在你认为用这些函数比较方便和高效率的时候才用它们,否则,最好用cvPtr*D。 用cvPtr*D()函数族还有另外一个原因,即可以用这些指针函数访问矩阵中的特定的点,然后由这个点出发,用指针的算术运算得到指向矩阵中的其他数据的指针。在多通道的矩阵中,务必记住一点:通道是连续的,例如,在一个3通道2维的表示红、绿、蓝(RGB)矩阵中。矩阵数据如下存储rgbrgbrgb . . .。所以,要将指向该数据类型的指针移动到下一通道,我们只需要将其增加1。如果想访问下一个"像素"或者元素集,我们只需要增加一定的偏移量,使其与通道数相等。 另一个需要知道的技巧是矩阵数组的step元素(参见例3-1和例3-3),step是矩阵中行的长度,单位为字节。在那些结构中,仅靠cols或width是无法在矩阵的不同行之间移动指针的,出于效率的考虑,矩阵或图像的内存分配都是4字节的整数倍。所以,三个字节宽度的矩阵将被分配4个字节,最后一个字节被忽略。因此,如果我们得到一个字节指针,该指针指向数据元素,那么我们可以用step和这个指针相加以使指针指向正好在我们的点的下一行元素。如果我们有一个整型或者浮点型的矩阵,对应的有整型和浮点型的指针指向数据区域,我们将让step/4与指针相加来移到下一行,对双精度型的,我们让step/8与指针相加(这里仅仅考虑了C将自动地将差值与我们添加的数据类型的字节数相乘)。 例3-8中的cvSet*D和cvGet*D多少有些相似,它通过一次函数调用为一个矩阵或图像中的元素设置值,函数cvSetReal*D()和函数cvSet*D()可以用来设置矩阵或者图像中元素的数值。 例3-8:为CvMat或者IplImage元素设定值的函数 1. void cvSetReal1D( CvArr* arr, int idx0, double value ); 2. void cvSetReal2D( CvArr* arr, int idx0, int idx1, double value ); 3. void cvSetReal3D( 4. CvArr* arr, 5. int idx0, 6. int idx1, 7. int idx2, 8. double value 9. ); 10. void cvSetRealND( CvArr* arr, int* idx, double value ); 11. 12. void cvSet1D( CvArr* arr, int idx0, CvScalar value ); 13. void cvSet2D( CvArr* arr, int idx0, int idx1, CvScalar value ); 14. void cvSet3D( 15. CvArr* arr, 16. int idx0, 17. 18. int idx1, 19. int idx2, 20. CvScalar value 21. ); 22. void cvSetND( CvArr* arr, int* idx, CvScalar value ); 为了方便,我们也可以使用cvmSet()和cvmGet(),这两个函数用于处理浮点型单通道矩阵,非常简单。 1. double cvmGet( const CvMat* mat, int row, int col ) 2. void cvmSet( CvMat* mat, int row, int col, double value ) 以下函数调用cvmSet(): 1. cvmSet( mat, 2, 2, 0.5000 ); 等同于cvSetReal2D函数调用: 1. cvSetReal2D( mat, 2, 2, 0.5000 ); 恰当的方法从以上所有那些访问函数来看,你可能会想,没有必要再介绍了。实际上,这些set和get函数很少派上用场。大多数时侯,计算机视觉是一种运算密集型的任务,因而你想尽量利用最有效的方法做事。毋庸置疑,通过这些函数接口是不可能做到十分高效的。相反地,应该定义自己的指针计算并且在矩阵中利用自己的方法。如果打算对数组中的每一个元素执行一些操作,使用自己的指针是尤为重要的(假设没有可以为你执行任务的OpenCV函数)。 要想直接访问矩阵,其实只需要知道一点,即数据是按光栅扫描顺序存储的,列("x")是变化最快的变量。通道是互相交错的,这意味着,对于一个多通道矩阵来说,它们变化的速度仍然比较快。例3-9显示了这一过程。 例3-9:累加一个三通道矩阵中的所有元素 1. float sum( const CvMat* mat ) {
2. 3. float s = 4. for(int row=0; row<mat->rows; row++ ) {
5. const float* ptr=(const float*)(mat->data.ptr + row * mat->step);
6. for( col=0; col<mat->cols; col++ ) {
7. s += *ptr++;
8. 9. } 10. } 11. return( s ); 12. } 计算指向矩阵的指针时,记住一点:矩阵的元素data是一个联合体。所以,对这个指针解引用的时候,必须指明结构体中的正确的元素以便得到正确的指针类型。然后,为了使指针产生正确的偏移,必须用矩阵的行数据长度(step)元素。我们以前曾提过,行数据元素的是用字节来计算的。为了安全,指针最好用字节计算,然后分配恰当的类型,如浮点型。CvMat结构中为了兼容IplImage结构,有宽度和高度的概念,这个概念已经被最新的行和列取代。最后要注意,我们为每行都重新计算了ptr,而不是简单地从开头开始,尔后每次读的时候累加指针。这看起来好像很繁琐,但是因为CvMat数据指针可以指向一个大型数组中的ROI,所以无法保证数据会逐行连续存取。 l
间接存取矩阵元素 cvmSet(M,i,j,2.0); // Set M(i,j)
注意:cvmGet()和cvmSet()函数只支持CV_32FC1(float)和CV_64FC1(double)的类型 l
直接存取,假设使用4-字节校正 CvMat* M = cvCreateMat(4,4,CV_32FC1);
l
直接存取,校正字节任意 CvMat* M = cvCreateMat(4,4,CV_32FC1);
l
直接存取一个初始化的矩阵元素 double a[16];
cvmGet()和cvmSet()的局限
cvmGet()和cvmSet()函数只支持CV_32FC1(float)和CV_64FC1(double)的类型 // 源码如下。也就是说只支持CV_32FC1(float)和CV_64FC1(double)的类型 // 对于U类型的数据,可以使用cvSet2D进行读写 CV_INLINE double cvmGet( const CvMat* mat, int row, int col ) type = CV_MAT_TYPE(mat->type); if( type == CV_32FC1 )
if( type == CV_32FC1 ) 矩阵/向量数学操作矩阵-矩阵操作:CvMat *Ma, *Mb, *Mc;
按元素的矩阵操作:CvMat *Ma, *Mb, *Mc;
向量乘积:double va[] = {1, 2, 3}; 注意 Va, Vb, Vc 在向量积中向量元素个数须相同. 单矩阵操作:CvMat *Ma, *Mb; 非齐次线性系统求解:CvMat* A = cvCreateMat(3,3,CV_32FC1);
特征值分析(针对对称矩阵):CvMat* A = cvCreateMat(3,3,CV_32FC1); 奇异值分解SVD:CvMat* A = cvCreateMat(3,3,CV_32FC1);
标号使得 U 和
V 返回时被转置(若没有转置标号,则有问题不成功!!!). 其他
Shell函数显示图片//用默认关联程序打开图片Vp_Line.jpg ShellExecute(NULL,"open","Results\\Vp_Line.jpg",NULL,NULL,SW_SHOWNORMAL); 利用cvShowImage来显示图像,显示窗口是固定的,对窗口的操作很少。而利用shell函数调用相关程序(譬如windows图片查看器,画图板等),可以使用这些图片浏览工具的强大功能。 IplImage 到cvMat的转换方式一、cvGetMat方式: 方式二、cvConvert方式: |
|