原文出处:https://blog.csdn.net/xuexiaokkk/article/details/50250705
OpenCV提供了多种基本数据类型。虽然这些数据类型在C语言中不是基本类型,但结构都很简单,可将它们作为原子类型。可以在“…/OpenCV/cxcore/include”目录下的cxtypes.h文件中查看其详细定义。
在这些数据类型中最简单的就是CvPoint。CvPoint是一个包含integer类型成员x和y的简单结构体。CvPoint有两个变体类型:CvPoint2D32f和CvPoint3D32f。前者同样有两个成员x,y,但它们是浮点类型;而后者却多了一个浮点类型的成员z。
CvSize类型与CvPoint非常相似,但它的数据成员是integer类型的width和height。如果希望使用浮点类型,则选用CvSize的变体类型CvSize2D32f。
CvRect类型派生于CvPoint和CvSize,它包含4个数据成员:x,y,width和height。(正如你所想的那样,该类型是一个复合类型)。
下一个(但不是最后一个)是包含4个整型成员的CvScalar类型,当内存不是问题时,CvScalar经常用来代替1,2或者3个实数成员(在这个情况下,不需要的分量被忽略)。CvScalar有一个单独的成员val,它是一个指向4个双精度浮点数数组的指针。
所有这些数据类型具有以其名称来定义的构造函数,例如cvSize()。(构造函数通常具有与结构类型一样的名称,只是首字母不大写)。记住,这是C而不是C++,所以这些构造函数只是内联函数,它们首先提取参数列表,然后返回被赋予相关值的结构。 【31】
各数据类型的内联构造函数被列在表3-1中:cvPointXXX(),cvSize(),cvRect()和cvScalar()。这些结构都十分有用,因为它们不仅使代码更容易编写,而且也更易于阅读。假设要在(5,10)和(20,30)之间画一个白色矩形,只需简单调用:
cvRectangle(
myImg,
cvPoint(5,10),
cvPoint(20,30),
cvScalar(255,255,255)
);
表3-1:points, size, rectangles和calar三元组的结构
结构 |
成员 |
意义 |
CvPoint |
int x, y |
图像中的点 |
CvPoint2D32f |
float x, y |
二维空间中的点 |
CvPoint3D32f |
float x, y, z |
三维空间中的点 |
CvSize |
int width, height |
图像的尺寸 |
CvRect |
int x, y, width, height |
图像的部分区域 |
CvScalar |
double val[4] |
RGBA 值 |
cvScalar是一个特殊的例子:它有3个构造函数。第一个是cvScalar(),它需要一个、两个、三个或者四个参数并将这些参数传递给数组val[]中的相应元素。第二个构造函数是cvRealScalar(),它需要一个参数,它被传递给给val[0],而val[]数组别的值被赋为0。最后一个有所变化的是cvScalarAll(),它需要一个参数并且val[]中的4个元素都会设置为这个参数。
图3-1为我们展示了三种图像的类或结构层次结构。使用OpenCV时,会频繁遇到IplImage数据类型,第2章已经出现多次。IplImage是我们用来为通常所说的“图像”进行编码的基本结构。这些图像可能是灰度,彩色,4通道的(RGB+alpha),其中每个通道可以包含任意的整数或浮点数。因此,该类型比常见的、易于理解的3通道8位RGB图像更通用。
OpenCV提供了大量实用的图像操作符,包括缩放图像,单通道提取,找出特定通道最大最小值,两个图像求和,对图像进行阈值操作,等等。本章我们将仔细介绍这类操作。 【32】
图3-1:虽然OpenCV是由C语言实现的,但它使用的结构体也是遵循面向对象的思想设计的。实际上,IplImage由CvMat派生,而CvMat由CvArr派生
在开始探讨图像细节之前,我们需要先了解另一种数据类型CvMat,OpenCV的矩阵结构。虽然OpenCV完全由C语言实现,但CvMat和IplImage之间的关系就如同C++中的继承关系。实质上,IplImage可以被视为从CvMat中派生的。因此,在试图了解复杂的派生类之前,最好先了解基本的类。第三个类CvArr,可以被视为一个抽象基类,CvMat由它派生。在函数原型中,会经常看到CvArr(更准确地说,CvArr*),当它出现时,便可以将CvMat*或IplImage*传递到程序。
在开始学习矩阵的相关内容之前,我们需要知道两件事情。第一,在OpenCV中没有向量(vector)结构。任何时候需要向量,都只需要一个列矩阵(如果需要一个转置或者共轭向量,则需要一个行矩阵)。第二,OpenCV矩阵的概念与我们在线性代数课上学习的概念相比,更抽象,尤其是矩阵的元素,并非只能取简单的数值类型。例如,一个用于新建一个二维矩阵的例程具有以下原型:
cvMat* cvCreateMat ( int rows, int cols, int type );
这里type可以是任何预定义类型,预定义类型的结构如下:CV_
实质上,正如例3-1所示,CvMat的结构相当简单,(可以自己打开文件…/opencv/cxcore/include/cxtypes.h查看)。矩阵由宽度(width)、高度(height)、类型(type)、行数据长度(step,行的长度用字节表示而不是整型或者浮点型长度)和一个指向数据的指针构成(现在我们还不能讨论更多的东西)。可以通过一个指向CvMat的指针访问这些成员,或者对于一些普通元素,使用现成的访问方法。例如,为了获得矩阵的大小,可通过调用函数vGetSize(CvMat*),返回一个CvSize结构,便可以获取任何所需信息,或者通过独立访问高度和宽度,结构为matrix->height 和matrix->width。 【33~34】
例3-1:CvMat结构:矩阵头
typedef struct CvMat {
int type;
int step;
int* refcount; // for internal use only
union {
uchar* ptr;
short* s;
int* i;
float* fl;
double* db;
} data;
union {
int rows;
int height;
};
union {
int cols;
int width;
};
} CvMat;
此类信息通常被称作矩阵头。很多程序是区分矩阵头和数据体的,后者是各个data成员所指向的内存位置。
矩阵有多种创建方法。最常见的方法是用cvCreateMat(),它由多个原函数组成,如cvCreateMatHeader()和cvCreateData()。cvCreateMatHeader()函数创建CvMat结构,不为数据分配内存,而cvCreateData()函数只负责数据的内存分配。有时,只需要函数cvCreateMatHeader(),因为已因其他理由分配了存储空间,或因为还不准备分配存储空间。第三种方法是用函数cvCloneMat (CvMat*),它依据一个现有矩阵创建一个新的矩阵。当这个矩阵不再需要时,可以调用函数cvReleaseMat(CvMat*)释放它。 【34】
例3-2概述了这些函数及其密切相关的其他函数。
例3-2:矩阵的创建和释放
//Create a new rows by cols matrix of type 'type'.
//
CvMat* cvCreateMat( int rows, int cols, int type );
//Create only matrix header without allocating data
//
CvMat* cvCreateMatHeader( int rows, int cols, int type );
//Initialize header on existiong CvMat structure
//
CvMat* cvInitMatHeader(
CvMat* mat,
int rows,
int cols,
int type,
void* data = NULL,
int step = CV_AUTOSTEP
);
//Like cvInitMatHeader() but allocates CvMat as well.
//
CvMat cvMat(
int rows,
int cols,
int type,
void* data = NULL
);
//Allocate a new matrix just like the matrix 'mat'.
//
CvMat* cvCloneMat( const cvMat* mat );
// Free the matrix 'mat', both header and data.
//
void cvReleaseMat( CvMat** mat );
与很多OpenCV结构类似,有一种构造函数叫cvMat,它可以创建CvMat结构,但实际上不分配存储空间,仅创建头结构(与cvInitMatHeader()类似)。这些方法对于存取到处散放的数据很有作用,可以将矩阵头指向这些数据,实现对这些数据的打包,并用操作矩阵的函数去处理这些数据,如例3-3所示。
例3-3:用固定数据创建一个OpenCV矩阵
//Create an OpenCV Matrix containing some fixed data.
//
float vals[] = { 0.866025, -0.500000, 0.500000, 0.866025 };
CvMat rotmat;
cvInitMatHeader(
&rotmat,
2,
2,
CV_32FC1,
vals
);
一旦我们创建了一个矩阵,便可用它来完成很多事情。最简单的操作就是查询数组定义和数据访问等。为查询矩阵,我们可以使用函数cvGetElemType(const CvArr* arr),cvGetDims(const CvArr* arr, int* sizes=NULL)和cvGet- DimSize(const CvArr* arr,int index)。第一个返回一个整型常数,表示存储在数组里的元素类型(它可以为CV_8UC1和CV_64FC4等类型)。第二个取出数组以及一个可选择的整型指针,它返回维数(我们当前的实例是二维,但是在后面我们将遇到的N维矩阵对象)。如果整型指针不为空,它将存储对应数组的高度和宽度(或者N维数)。最后的函数通过一个指示维数的整型数简单地返回矩阵在那个维数上矩阵的大小。 【35~36】
访问矩阵中的数据有3种方法:简单的方法、麻烦的方法和恰当的方法。
简单的方法
从矩阵中得到一个元素的最简单的方法是利用宏CV_MAT_ELEM()。这个宏(参见 例3-4)传入矩阵、待提取的元素的类型、行和列数4个参数,返回提取出的元素 的值。
例3-4:利用CV_MAT_ELEM()宏存取矩阵
CvMat* mat = cvCreateMat( 5, 5, CV_32FC1 );
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()为矩阵设置一个数值
CvMat* mat = cvCreateMat( 5, 5, CV_32FC1 );
float element_3_2 = 7.7;
*( (float*)CV_MAT_ELEM_PTR( *mat, 3, 2 ) ) = element_3_2;
【36】
遗撼的是,这些宏在每次调用的时候都重新计算指针。这意味着要查找指向矩阵基本元素数据区的指针、计算目标数据在矩阵中的相对地址,然后将相对位置与基本位置相加。所以,即使这些宏容易使用,但也不是存取矩阵的最佳方法。在计划顺序访问矩阵中的所有元素时,这种方法的缺点尤为突出。下面我们将讲述怎么运用最好的方法完成这个重要任务。
麻烦的方法
在“简单的方法”中讨论的两个宏仅仅适用于访问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:指针访问矩阵结构
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
); 【37~38】
如果仅仅是读取数据,可用另一个函数族cvGet*D。如例3-7所示,该例与例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 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 );
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将自动地将差值与我们添加的数据类型的字节数 相乘)。 【38】
例3-8中的cvSet*D和cvGet*D多少有些相似,它通过一次函数调用为一个矩阵或图像中的元素设置值,函数cvSetReal*D()和函数cvSet*D()可以用来设置矩阵或者图像中元素的数值。
例3-8:为CvMat或者IplImage元素设定值的函数
void cvSetReal1D( CvArr* arr, int idx0, double value );
void cvSetReal2D( CvArr* arr, int idx0, int idx1, double value );
void cvSetReal3D(
CvArr* arr,
int idx0,
int idx1,
int idx2,
double value
);
void cvSetRealND( CvArr* arr, int* idx, double value );
void cvSet1D( CvArr* arr, int idx0, CvScalar value );
void cvSet2D( CvArr* arr, int idx0, int idx1, CvScalar value );
void cvSet3D(
CvArr* arr,
int idx0,
int idx1,
int idx2,
CvScalar value
);
void cvSetND( CvArr* arr, int* idx, CvScalar value );
为了方便,我们也可以使用cvmSet()和cvmGet(),这两个函数用于处理浮点型单通道矩阵,非常简单。
double cvmGet( const CvMat* mat, int row, int col )
void cvmSet( CvMat* mat, int row, int col, double value )
以下函数调用cvmSet():
cvmSet( mat, 2, 2, 0.5000 );
等同于cvSetReal2D函数调用:
cvSetReal2D( mat, 2, 2, 0.5000 );
恰当的方法
从以上所有那些访问函数来看,你可能会想,没有必要再介绍了。实际上,这些set和get函数很少派上用场。大多数时侯,计算机视觉是一种运算密集型的任务,因而你想尽量利用最有效的方法做事。毋庸置疑,通过这些函数接口是不可能做到十分高效的。相反地,应该定义自己的指针计算并且在矩阵中利用自己的方法。如果打算对数组中的每一个元素执行一些操作,使用自己的指针是尤为重要的(假设没有可以为你执行任务的OpenCV函数)。
要想直接访问矩阵,其实只需要知道一点,即数据是按光栅扫描顺序存储的,列(“x”)是变化最快的变量。通道是互相交错的,这意味着,对于一个多通道矩阵来说,它们变化的速度仍然比较快。例3-9显示了这一过程。 【39~40】
例3-9:累加一个三通道矩阵中的所有元素
float sum( const CvMat* mat ) {
float s = 0.0f;
for(int row=0; row
const float* ptr=(const float*)(mat->data.ptr + row * mat->step);
for( col=0; col
s += *ptr++;
}
}
return( s );
}
计算指向矩阵的指针时,记住一点:矩阵的元素data是一个联合体。所以,对这个指针解引用的时候,必须指明结构体中的正确的元素以便得到正确的指针类型。然后,为了使指针产生正确的偏移,必须用矩阵的行数据长度(step)元素。我们以前曾提过,行数据元素的是用字节来计算的。为了安全,指针最好用字节计算,然后分配恰当的类型,如浮点型。CvMat结构中为了兼容IplImage结构,有宽度和高度的概念,这个概念已经被最新的行和列取代。最后要注意,我们为每行都重新计算了ptr,而不是简单地从开头开始,尔后每次读的时候累加指针。这看起来好像很繁琐,但是因为CvMat数据指针可以指向一个大型数组中的ROI,所以无法保证数据会逐行连续存取。
有一个经常提到但又必须理解的问题是,包含多维对象的多维数组(或矩阵)和包含一维对象的高维数组之间的不同。例如,假设有n个三维的点,你想将这些点传递到参数类型为CvMat*的一些OpenCV函数中。对此,有四种显而易见的方式,记住,这些方法不一定等价。一是用一个二维数组,数组的类型是CV32FC1,有n行,3列(n×3)。类似地,也可以用一个3行n列(3×n)的二维数组。也可以用一个n行1列(n×1)的数组或者1行n列(1×n)的数组,数组的类型是CV32FC3。这些例子中,有些可以自由转换(这意味着只需传递一个,另一个便可可以计算得到),有的则不能。要想理解原因,可以参考图3-2中的内存布局 情况。
从图中可以看出,在前三种方式中,点集以同样的方式被映射到内存。但最后一种方式则不同。对N维数组的c维点,情况变得更为复杂。需要记住的最关键的一点是,给定点的位置可以由以下公式计算出来。
图3-2:有10个点,每个点由3个浮点数表示,这10个点被放在4个结构稍有不同的数组中。前3种情况下,内存布局情况是相同的,但最后一种情况下,内存布局不同
其中,Ncols 和Nchannels分别表示列数和通道数。总的来说,从这个公式中可以看出一点,一个c维对象的N维数组和一个一维对象的(N+c)维数组不同。至于N=1(即把向量描绘成n×1或者1×n数组),有一个特殊之处(即图3-2显示的等值)值得注意,如果考虑到性能,可以在有些情况下用到它。
关于OpenCV的数据类型,如CvPoint2D和CvPoint2D32f,我们要说明的最后一点是:这些数据类型被定义为C结构,因此有严格定义的内存布局。具体说来,由整型或者浮点型组成的结构是顺序型的通道。那么,对于一维的C语言的对象数组来说,其数组元素都具有相同的内存布局,形如CV32FC2的n×1或者1×n数组。这和申请CvPoint3D32f类型的数组结构也是相同的。 【41】
掌握了前面的知识,再来讨论IplImage数据结构就比较容易了。从本质上讲,它是一个CvMat对象,但它还有其他一些成员变量将矩阵解释为图像。这个结构最初被定义为Intel图像处理库(IPL)的一部分。IplImage结构的准确定义如例3-10所示。
例3-10:IplImage结构
typedef struct _IplImage {
int nSize;
int ID;
int nChannels;
int alphaChannel;
int depth;
char colorModel[4];
char channelSeq[4];
int dataOrder;
int origin;
int align;
int width;
int height;
struct _IplROI* roi;
struct _IplImage* maskROI;
void* imageId;
struct _IplTileInfo* tileInfo;
int imageSize;
char* imageData;
int widthStep;
int BorderMode[4];
int BorderConst[4];
char* imageDataOrigin;
} IplImage;
我们试图讨论这些变量的某些功能。有些变量不是很重要,但是有些变量非常重要,有助于我们理解OpenCV解释和处理图像的方式。
width和height这两个变量很重要,其次是depth和nchannals。depth变量的值取自ipl.h中定义的一组数据,但与在矩阵中看到的对应变量不同。因为在图像中,我们往往将深度和通道数分开处理,而在矩阵中,我们往往同时表示它们。可用的深度值如表3-2所示。 【42】
表3-2:OpenCV图像类型
宏 |
图像像素类型 |
IPL_DEPTH_8U |
无符号8位整数 (8u) |
IPL_DEPTH_8S |
有符号 8位整数(8s) |
IPL_DEPTH_16S |
有符号16位整数(16s) |
IPL_DEPTH_32S |
有符号32位整数(32s) |
IPL_DEPTH_32F |
32位浮点数单精度(32f) |
IPL_DEPTH_64F |
64位浮点数双精度(64f) |
通道数nChannels可取的值是1,2,3或4。
随后两个重要成员是origin和dataOrder。origin变量可以有两种取值:IPL_ORIGIN_TL 或者 IPL_ORIGIN_BL,分别设置坐标原点的位置于图像的左上角或者左下角。在计算机视觉领域,一个重要的错误来源就是原点位置的定义不统一。具体而言,图像的来源、操作系统、编解码器和存储格式等因素都可以影响图像坐标原点的选取。举例来说,你或许认为自己正在从图像上面的脸部附近取样,但实际上却在图像下方的裙子附近取样。避免此类现象发生的最好办法是在最开始的时候检查一下系统,在所操作的图像块的地方画点东西试试。
dataOrder的取值可以是IPL_DATA_ORDER_PIXEL或IPL_DATA_ORDER_PLANE,前者指明数据是将像素点不同通道的值交错排在一起(这是常用的交错排列方式),后者是把所有像素同通道值排在一起,形成通道平面,再把平面排列起来。
参数widthStep与前面讨论过的CvMat中的step参数类似,包括相邻行的同列点之间的字节数。仅凭变量width是不能计算这个值的,因为为了处理过程更高效每行都会用固定的字节数来对齐;因此在第i行末和第i+1行开始处可能会有些冗于字节。参数imageData包含一个指向第一行图像数据的指针。如果图像中有些独立的平面(如当dataOrder = IPL_DATA_ORDER_PLANE)那么把它们作为单独的图像连续摆放,总行数为height和nChannels的乘积。但通常情况下,它们是交错的,使得行数等于高度,而且每一行都有序地包含交错的通道。
最后还有一个实用的重要参数—— 感兴趣的区域(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。
通常,我们需要非常迅速和高效地访问图像中的数据。这意味着我们不应受制于存取函数(如cvSet*D之类)。实际上,我们想要用最直接的方式访问图像内的数据。现在,应用已掌握的IplImage内部结构的知识,我们知道怎样做才是最佳的 方法。
虽然OpenCV中有很多优化函数帮助我们完成大多数的图像处理的任务,但是还有一些任务,库中没有预先包装好的函数可以帮我们解决。例如,如果我们有一个三通道HSV图像[Smith78],在色度保持不变的情况下,我们要设置每个点的饱和度和高度为255(8位图像的最大值),我们可以使用指针遍历图像,类似于例3-9中的矩阵遍历。然而,有一些细微的不同,是源于IplImage和CvMat结构的差异。例3-11演示了最高效的方法。
例3-11:仅最大化HSV 图像“S”和“V”部分
void saturate_sv( IplImage* img ) {
for( int y=0; y
uchar* ptr = (uchar*) (
img->imageData + y * img->widthStep
);
for( int x=0; x
ptr[3*x+1] = 255;
ptr[3*x+2] = 255;
}
}
}
在以上程序中,我们用指针ptr指向第y行的起始位置。接着,我们从指针中析出饱和度和高度在x维的值。因为这是一个三通道图像,所以C通道在x行的位置是3*x+c。
与CvMat的成员data相比,IplImage和CvMat之间的一个重要度别在于imageData。CvMat的data元素类型是联合类型,所以你必须说明需要使用的指针类型。imageData指针是字节类型指针(uchar * )。我们已经知道是种类型的指针指向的数据是uchar类型的,这意味着,在图像上进行指针运算时,你可以简单地增加widthStep (也以字节为单位),而不必关心实际数据类型。在这里重新说明一下:当要处理的是矩阵时,必须对偏移并进行调整,因为数据指针可能是非字节类型;当要处理的是图像时,可以直接使用偏移,因为数据指针总是字节类型,因此当你要用到它的时候要清楚是怎么回事。
ROI和widthStep在实际工作中有很重要的作用,在很多情况下,使用它们会提高计算机视觉代码的执行速度。这是因为它们允许对图像的某一小部分进行操作,而不是对整个图像进行运算。在OpenCV中,普遍支持ROI和widthStep,函数的操作被限于感兴趣区域。要设置或取消ROI,就要使用cvSetImageROI()和cvResetImageROI()函数。如果想设置ROI,可以使用函数cvSetImageROI(),并为其传递一个图像指针和矩形。而取消ROI,只需要为函数cvResetImageROI()传递一个图像指针。
void cvSetImageROI( IplImage* image, CvRect rect );
void cvResetImageROI( IplImage* image );
为了解释ROI的用法,我们假设要加载一幅图像并修改一些区域,如例3-12的代码,读取了一幅图像,并设置了想要的ROI的x,y,width和height的值,最后将ROI区域中像素都加上一个整数。本例程中通过内联的cvRect()构造函数设置ROI。通过cvResetImageROI()函数释放ROI是非常重要的,否则,将忠实地只显示ROI区域。
例3-12:用imageROI来增加某范围的像素
// roi_add
#include
#include
int main(int argc, char** argv)
{
IplImage* src;
if( argc == 7 && ((src=cvLoadImage(argv[1],1)) != 0 ))
{
int x = atoi(argv[2]);
int y = atoi(argv[3]);
int width = atoi(argv[4]);
int height = atoi(argv[5]);
int add = atoi(argv[6]);
cvSetImage ROI(src, cvRect(x,y,width,height));
cvAddS(src, cvScalar(add),src);
cvResetImageROI(src);
cvNamedWindow( "Roi_Add", 1 );
cvShowImage( "Roi_Add", src );
cvWaitKey();
}
return 0;
}
使用例3-12中的代码把ROI集中于一张猫的脸部,并将其蓝色通道增加150后的效果如图3-3所示。 【45~46】
图3-3:在猫脸上用ROI增加150像素的效果
通过巧妙地使用widthStep,我们可以达到同样的效果。要做到这一点,我们创建另一个图像头,让它的width和height的值等于interest_rect的width和height的值。我们还需要按interest_rect起点设置图像起点(左上角或者左下角)。下一步,我们设置子图像的widthStep与较大的interest_img相同。这样,即可在子图像中逐行地步进到大图像里子区域中下一行开始处的合适位置。最后设置子图像的imageDate指针指向兴趣子区域的开始,如例3-13所示。
例3-13:利用其他widthStep方法把interest_img的所有像素值增加1
// Assuming IplImage *interest_img; and
// CvRect interest_rect;
// Use widthStep to get a region of interest
//
// (Alternate method)
//
IplImage *sub_img = cvCreateImageHeader(
cvSize(
interest_rect.width,
interest_rect.height
),
interest_img->depth,
interest_img->nChannels
);
sub_img->origin = interest_img->origin;
sub_img->widthStep = interest_img->widthStep;
sub_img->imageData = interest_img->imageData +
interest_rect.y * interest_img->widthStep +
interest_rect.x * interest_img->nChannels;
cvAddS( sub_img, cvScalar(1), sub_img );
cvReleaseImageHeader(&sub_img);
看起来设置和重置ROI更方便一些,为什么还要使用widthStep?原因在于有些时候在处理的过程中,想在操作过程中设置和保持一幅图像的多个子区域处于活动状态,但是ROI只能串行处理并且必须不断地设置和重置。
最后,我们要在此提到一个词—— 掩码或模板,在代码示例中cvAddS()函数允许第四个参数默认值为空:const CvArr* mask=NULL。这是一个8位单通道数组,它允许把操作限制到任意形状的非0像素的掩码区,如果ROI随着掩码或模板变化,进程将会被限制在ROI和掩码的交集区域。掩码或模板只能在指定了其图像的函数中使用。
表3-3列出了一些操作矩阵图像的函数,其中的大部分对于图像处理非常有效。它们实现了图像处理中的基本操作,例如对角化、矩阵变换以及一些更复杂的诸如计算图像的统计操作。 【47】
表3-3:矩阵和图像基本操作
函数名称 |
描述 |
cvAbs |
计算数组中所有元素的绝对值 |
cvAbsDiff |
计算两个数组差值的绝对值 |
续表
函数名称 |
描述 |
cvAbsDiffS |
计算数组和标量差值的绝对值 |
cvAdd |
两个数组的元素级的加运算 |
cvAddS |
一个数组和一个标量的元素级的相加运算 |
cvAddWeighted |
两个数组的元素级的加权相加运算(alpha融合) |
cvAvg |
计算数组中所有元素的平均值 |
cvAvgSdv |
计算数组中所有元素的绝对值和标准差 |
cvCalcCovarMatrix |
计算一组n维空间向量的协方差 |
cvCmp |
对两个数组中的所有元素运用设置的比较操作 |
cvCmpS |
对数组和标量运用设置的比较操作 |
cvConvertScale |
用可选的缩放值转换数组元素类型 |
cvConvertScaleAbs |
计算可选的缩放值的绝对值之后再转换数组元素的类型 |
cvCopy |
把数组中的值复制到另一个数组中 |
cvCountNonZero |
计算数组中非0值的个数 |
cvCrossProduct |
计算两个三维向量的向量积(叉积) |
cvCvtColor |
将数组的通道从一个颜色空间转换另外一个颜色空间 |
cvDet |
计算方阵的行列式 |
cvDiv |
用另外一个数组对一个数组进行元素级的除法运算 |
cvDotProduct |
计算两个向量的点积 |
cvEigenVV |
计算方阵的特征值和特征向量 |
cvFlip |
围绕选定轴翻转 |
cvGEMM |
矩阵乘法 |
cvGetCol |
从一个数组的列中复制元素 |
cvGetCols |
从数据的相邻的多列中复制元素值 |
cvGetDiag |
复制数组中对角线上的所有元素 |
cvGetDims |
返回数组的维数 |
cvGetDimSize |
返回一个数组的所有维的大小 |
cvGetRow |
从一个数组的行中复制元素值 |
cvGetRows |
从一个数组的多个相邻的行中复制元素值 |
cvGetSize |
得到二维的数组的尺寸,以CvSize返回 |
cvGetSubRect |
从一个数组的子区域复制元素值 |
cvInRange |
检查一个数组的元素是否在另外两个数组中的值的范围内 |
cvInRangeS |
检查一个数组的元素的值是否在另外两个标量的范围内 |
续表
函数 名称 |
描述 |
cvInvert |
求矩阵的转置 |
cvMahalonobis |
计算两个向量间的马氏距离 |
cvMax |
在两个数组中进行元素级的取最大值操作 |
cvMaxS |
在一个数组和一个标量中进行元素级的取最大值操作 |
cvMerge |
把几个单通道图像合并为一个多通道图像 |
cvMin |
在两个数组中进行元素级的取最小值操作 |
cvMinS |
在一个数组和一个标量中进行元素级的取最小值操作 |
cvMinMaxLoc |
寻找数组中的最大最小值 |
cvMul |
计算两个数组的元素级的乘积 |
cvNot |
按位对数组中的每一个元素求反 |
cvNorm |
计算两个数组的正态相关性 |
cvNormalize |
将数组中元素进行规一化 |
cvOr |
对两个数组进行按位或操作 |
cvOrS |
在数组与标量之间进行按位或操作 |
cvReduce |
通过给定的操作符将二维数组约简为向量 |
cvRepeat |
以平铺的方式进行数组复制 |
cvSet |
用给定值初始化数组 |
cvSetZero |
将数组中所有元素初始化为0 |
cvSetIdentity |
将数组中对角线上的元素设为1,其他置0 |
cvSolve |
求出线性方程组的解 |
cvSplit |
将多通道所组分割成多个单通道数组 |
cvSub |
两个数组元素级的相减 |
cvSubS |
元素级的从数组中减去标量 |
cvSubRS |
元素级的从标量中减去数组 |
cvSum |
对数组中的所有元素求和 |
cvSVD |
二维矩阵的奇异值分解 |
cvSVBkSb |
奇异值回代计算 |
cvTrace |
计算矩阵迹 |
cvTranspose |
矩阵的转置运算 |
cvXor |
对两个数组进行按位异或操作 |
cvXorS |
在数组和标量之间进行按位异或操作 |
cvZero |
将所有数组中的元素置为0 |
void cvAbs(
const CvArr* src,
const dst
);
void cvAbsDiff(
const CvArr* src1,
const CvArr* src2,
const dst
);
void cvAbsDiffS(
const CvArr* src,
CvScalar value,
const dst
);
【50】
这些函数计算一个数组的绝对值或数组和其他对象的差值的绝对值,cvAbs()函数计算src里的值的绝对值,然后把结果写到dst;cvAbsDiff()函数会先从src1减去src2,然后将所得差的绝对值写到dst;除了从所有src元素减掉的数是常标量值外,可以看到cvAbsDiffS()函数同cvAbsDiff()函数基本相同。
void cvAdd(
const CvArr* src1,
const CvArr* src2,
CvArr* dst,
const CvArr* mask = NULL
);
void cvAddS(
const CvArr* src,
CvScalar value,
CvArr* dst,
const CvArr* mask = NULL
);
void cvAddWeighted(
const CvArr* src1,
double alpha,
const CvArr* src2,
double beta,
double gamma,
CvArr* dst
);
cvAdd()是一个简单的加法函数,它把src1里的所有元素同src2里的元素对应进行相加,然后把结果放到dst,如果mask没有被设为NULL,那么由mask中非零元素指定的dst元素值在函数执行后不变。cvAddS()与cvAdd()非常相似,惟一不同的是被加的数量标量value。
cvAddWeighted()函数同cvAdd()类似,但是被写入dst的结果是经过下面的公式得出的:
【50】
这个函数可用来实现alpha 融合 [Smith79; Porter84];也就是说,它可以用于一个图像同另一个图像的融合,函数的形式如下:
void cvAddWeighted(
const CvArr* src1,
double alpha,
const CvArr* src2,
double beta,
double gamma,
CvArr* dst
);
在函数cvAddWeighted()中我们有两个源图像,分别是src1和src2。这些图像可以是任何类型的像素,只要它们属于同一类型即可。它们还可以有一个或三个通道(灰度或彩色),同样也要保持类型一致。结果图像dst,也必须同src1和src2是相同的像素类型。这些图像可能是不同尺寸,但是它们的ROI必须统一尺寸,否则OpenCV就会产生错误,参数alpha是src1的融合强度,beta是src2的融合强度,alpha融合公式如下:
可以通过设置α从0到1区间取值,β = 1 – α,γ为0,将前面公式转换为标准alpha融合方程。这就得出下式:
但是,在加权融合图像,以及目标图像的附加偏移参数γ方面, cvAddWeighted()提供了更大的灵活性。一般而言,你或许想让alpha和beta不小于0,并且两者之和不大于1,gamma的设置取决于像素所要调整到的平均或最大值。例3-14展示了alpha融合的用法。
例3-14:src2 中alpha融合ROI以(0,0)开始,src1 中ROI以(x,y)开始
// alphablend
//
#include
#include
int main(int argc, char** argv)
{
IplImage *src1, *src2;
if( argc == 9 && ((src1=cvLoadImage(argv[1],1)) != 0
)&&((src2=cvLoadImage(argv[2],1)) != 0 ))
{
int x = atoi(argv[3]);
int y = atoi(argv[4]);
int width = atoi(argv[5]);
int height = atoi(argv[6]);
double alpha = (double)atof(argv[7]);
double beta = (double)atof(argv[8]);
cvSetImage ROI(src1, cvRect(x,y,width,height));
cvSetImageROI(src2, cvRect(0,0,width,height));
cvAddWeighted(src1, alpha, src2, beta,0.0,src1);
cvResetImageROI(src1);
cvNamedWindow( "Alpha_blend", 1 );
cvShowImage( "Alpha_blend", src1 );
cvWaitKey();
}
return 0;
} 【51~52】
例3-14中的代码用两个源图像:初始的(src1)和待融合的(src2)。它从矩形的ROI中读取src1,然后将同样大小的ROI应用到src2中,这一次设在原始位置,它从命令行读入alpha和beta的级别但是把gamma设为0。Alpha融合使用函数cvAddWeighted(),结果被放到src1并显示,例子输出如图3-4所示,一个小孩的脸同一个猫的脸和身体被融合到了一起,值得注意的是,代码采用相同的ROI,像图3-3的例子一样。这次我们使用了ROI作为目标融合区域。
图3-4:一个小孩的脸被alpha融合到一只猫的脸上
void cvAnd(
const CvArr* src1,
const CvArr* src2,
CvArr* dst,
const CvArr* mask = NULL
);
void cvAndS(
const CvArr* src1,
CvScalar value,
CvArr* dst,
const CvArr* mask = NULL
);
这两个函数在src1数组上做按位与运算,在cvAnd()中每个dst元素都是由相应的src1和src2两个元素进行位与运算得出的。在cvAndS()中,位与运算由常标量value得出。同一般函数一样,如果mask是非空,就只计算非0 mask元素所对应的dst元素。
尽管支持所有的数据类型,但是对于cvAnd()来说,src1和src2要保持相同的数据类型。如果元素都是浮点型的,则使用该浮点数的按位表示。 【52】
CvScalar cvAvg(
const CvArr* arr,
const CvArr* mask = NULL
);
cvAvg()计算数组arr的平均像素值,如果mask为非空,那么平均值仅由那些mask值为非0的元素相对应的像素算出。
此函数还有别名cvMean(),但不推荐使用。
cvAvgSdv(
const CvArr* arr,
CvScalar* mean,
CvScalar* std_dev,
const CvArr* mask = NULL
); 【53】
此函数同cvAvg()类似,但除了求平均,还可以计算像素的标准差。
函数现在有不再使用的别名cvMean_StdDev()。
void cvCalcCovarMatrix(
const CvArr** vects,
int count,
CvArr* cov_mat,
CvArr* avg,
int flags
);
给定一些向量,假定这些向量表示的点是高斯分布,cvCalcCovarMatrix()将计算这些点的均值和协方差矩阵。这当然可以运用到很多方面,并且OpenCV有很多附加的flags值,在特定的环境下会起作用(参见表3-4)。这些标志可以用标准的布尔或操作组合到一起。
表3-4:cvCalcCovarMatrix()可能用到的标志参数的值
标志参数的具体标志值 |
意义 |
CV_COVAR_NORMAL |
计算均值和协方差 |
CV_COVAR_SCRAMBLED |
快速PCA“Scrambled”协方差 |
CV_COVAR_USE_AVERAGE |
输入均值而不是计算均值 |
CV_COVAR_SCALE |
重新缩放输出的协方差矩阵 |
在所有情况下,在vects中是OpenCV指针数组(即一个指向指针数组的指针),并有一个指示多少数组的参数count。在所有情况下,结果将被置于cov_mat,但是avg的确切含义取决于标志的值(参见表3-4)。
标识CV_COVAR_NORMAL和CV_COVAR_SCRAMBLED是相互排斥的;只能使用其中一种,不能两者同时使用。如果为CV_COVAR_NORMAL,函数便只计算该点的均值和协方差。
因此,标准的协方差由长度为n的m个向量计算,其中被定义为平均向量的第n个元素,由此产生的协方差矩阵是一个n × n矩阵, 比例z是一个可选的缩放比例,除非使用CV_COVAR_SCALE标志,否则它将被设置为1。 【54】
如果是CV_COVAR_SCRAMBLED标志,cvCalcCovarMatrix ()将如下计算:
这种矩阵不是通常的协方差矩阵(注意转置运算符的位置),这种矩阵的计算来自同样长度为n的m个向量,但由此而来的协方差矩阵是一个m×m矩阵。这种矩阵是用在一些特定的算法中,如针对非常大的向量的快速PCA分析法(人脸识别可能会用到此运算)。
如果已知平均向量,则使用标志CV_COVAR_USE_AVG,在这种情况下,参数avg用来作为输入而不是输出,从而减少计算时间。
最后,标志CV_COVAR_SCALE用于对计算得到的协方差矩阵进行均匀缩放。这是前述方程的比例z,同标志CV_COVAR_NORMAL一起使用时,应用的缩放比例将是1.0 /m(或等效于1.0/count)。如果不使用CV_COVAR_SCRAMBLED,那么z的值将会是1.0/n(向量长度的倒数),cvCalcCovarMatrix()的输入输出矩阵都应该是浮点型,结果矩阵cov_mat的大小应当是n×n 或者 m×m,这取决于计算的是标准协方差还是scrambled的协方差。应当指出的是,在vects中输入的“向量”并不一定要是一维的;它们也可以是两维对象(例如图像)。
void cvCmp(
const CvArr* src1,
const CvArr* src2,
CvArr* dst,
int cmp_op
);
void cvCmpS(
const CvArr* src,
double value,
CvArr* dst,
int cmp_op
);
这两个函数都是进行比较操作,比较两幅图像相应的像素值或将给定图像的像素值与某常标量值进行比较。cvCmp()和cvCmpS()的最后一个参数的比较操作符可以是表3-5所列出的任意一个。 【55】
表3-5:cvCmp()和cvCmpS()使用的cmp_op值以及由此产生的比较操作
cmp_op的值 |
比较方法 |
CV_CMP_EQ |
(src1i == src2i) |
CV_CMP_GT |
(src1i > src2i) |
CV_CMP_GE |
(src1i >= src2i) |
CV_CMP_LT |
(src1i < src2i) |
CV_CMP_LE |
(src1i <= src2i) |
CV_CMP_NE |
(src1i != src2i) |
表3-5列出的比较操作都是通过相同的函数实现的,只需传递合适的参数来说明你想怎么做,这些特殊的功能操作只能应用于单通道的图像。
这些比较功能适用于这样的应用程序,当你使用某些版本的背景减法并想对结果进行掩码处理但又只从图像中提取变化区域信息时(如从安全监控摄像机看一段视 频流)。
void cvConvertScale(
const CvArr* src,
CvArr* dst,
double scale = 1.0,
double shift = 0.0
);
cvConvertScale()函数实际上融多种功能于一体,它能执行几个功能中的任意之一,如果需要,也可以一起执行多个功能。第一个功能是将源图像的数据类型转变成目标图像的数据类型。例如,如果我们有一个8位的RGB灰度图像并想把它变为16位有符号的图像,就可以调用函数cvConvertScale()来做这个工作。
cvConvertScale()的第二个功能是对图像数据执行线性变换。在转换成新的数据类型之后,每个像素值将乘以scale值,然后将shift值加到每个像素上。
至关重要的是要记住,尽管在函数名称中“Convert”在“Scale”之前,但执行这些操作的顺序实际上是相反的。具体来说,在数据类型转变之前,与scale相乘和shift的相加已经发生了。 【56】
如果只是传递默认值(scale = 1.0和shift = 0.0),则不必担心性能; OpenCV足够聪明,能意识到这种情况而不会在无用的操作上浪费处理器的时间。澄清一下(如果你想添加一些),OpenCV还提供了宏指令cvConvert(),该指令同cvConvertScale()一样,但是通常只适用于scale 和 shift 参数设为默认 值时。
对于所有数据类型和任何数量通道cvConvertScale ()都适用,但是源图像和目标图像的通道数量必须相同。(如果你想实现彩色图像与灰度图的相互转换,可以使用cvCvtColor(),之后我们将会提到。) 【56~57】
void cvConvertScaleAbs(
const CvArr* src,
CvArr* dst,
double scale = 1.0,
double shift = 0.0
);
cvConvertScaleAbs()与cvConvertScale()基本相同,区别是dst图像元素是结果数据的绝对值。具体说来,cvConvertScaleAbs()先缩放和平移,然后算出绝对值,最后进行数据类型的转换。
void cvCopy(
const CvArr* src,
CvArr* dst,
const CvArr* mask = NULL
);
用于将一个图像复制到另一个图像。cvCopy()函数要求两个数组具有相同的数据类型、相同的大小和相同的维数。可以使用它来复制稀疏矩阵,但这样做时,不支持mask。对于非稀疏矩阵和图像,mask如果为非空,则只对与mask中与非0值相对应的dst中的像素赋值。
int cvCountNonZero( const CvArr* arr );
cvCountNonZero()返回数组arr中非0像素的个数。
void cvCrossProduct(
const CvArr* src1,
const CvArr* src2,
CvArr* dst
);
这个函数的主要功能是计算两个三维向量的叉积[Lagrange1773]。无论向量是行或者列的形式函数都支持。(实际上对于单通道矩阵,行向量和列向量的数据在内存中的排列方式完全相同)。src1和src2都必须是单道数组,同时dst也必须是单道的,并且长度应精确为3。所有这些阵列的数据类型都要一致。 【57】
void cvCvtColor(
const CvArr* src,
CvArr* dst,
int code
);
此前介绍的几个函数用于把一个数据类型转换成另一个数据类型,原始图像和目标图像的通道数目应保持一致。另外一个函数是cvCvtColor(),当数据类型一致时,它将图像从一个颜色空间(通道的数值)转换到另一个[Wharton71]。具体转换操作由参数code来指定,表3-6列出了此参数可能的值。
表3-6:cvCvtColor()的转换
转换代码 解释 |
|
CV_BGR2RGB CV_RGB2BGR CV_RGBA2BGRA CV_BGRA2RGBA |
在RGB或BGR色彩空间之间转换(包括或者不包括alpha 通道) |
CV_RGB2RGBA CV_BGR2BGRA |
在 RGB或BGR图像中加入alpha 通道 |
CV_RGBA2RGB CV_BGRA2BGR |
从 RGB或BGR图像中删除alpha 通道 |
CV_RGB2BGRA CV_RGBA2BGR CV_BGRA2RGB CV_BGR2RGBA |
加入或者移除alpha通道时,转换RGB到BGR 色彩空间 |
CV_RGB2GRAY CV_BGR2GRAY |
转换RGB或者BGR色彩空间为灰度空间 |
CV_GRAY2RGB CV_GRAY2BGR CV_RGBA2GRAY CV_BGRA2GRAY |
转换灰度为RGB或者BGR色彩空间(在进程中选择移除alpha通道) |
CV_GRAY2RGBA CV_GRAY2BGRA |
转换灰度为RGB或者BGR色彩空间并且加入alpha通道 |
CV_RGB2BGR565 CV_BGR2BGR565 CV_BGR5652RGB CV_BGR5652BGR CV_RGBA2BGR565 CV_BGRA2BGR565 CV_BGR5652RGBA CV_BGR5652BGRA |
在从RGB或者BGR色彩空间转换到BGR565彩色图画时,选择加入或者移除 alpha通道 (16位图) |
CV_GRAY2BGR565 CV_BGR5652GRAY |
转换灰度为BGR565彩色图像或者反变换(16位图) |
续表
转换代码 解释 |
|
CV_RGB2BGR555 CV_BGR2BGR555 CV_BGR5552RGB CV_BGR5552BGR CV_RGBA2BGR555 CV_BGRA2BGR555 |
在从RGB或者BGR色彩空间转换到BGR555色彩空间时,选择加入或者移除 alpha通道(16位图) |
CV_BGR5552RGBA CV_BGR5552BGRA |
|
CV_GRAY2BGR555 CV_BGR5552GRAY |
转换灰度到BGR555色彩空间或者反变换(16位图) |
CV_RGB2XYZ CV_BGR2XYZ CV_XYZ2RGB CV_XYZ2BGR |
转换RGB或者BGR色彩空间到CIE XYZ色彩空间或者反变换(Rec 709和D65 白点) |
CV_RGB2YCrCb CV_BGR2YCrCb CV_YCrCb2RGB CV_YCrCb2BGR |
转换RGB 或者BGR色彩空间到luma-chroma (aka YCC)色彩空间 |
CV_RGB2HSV CV_BGR2HSV CV_HSV2RGB CV_HSV2BGR |
转换RGB或者BGR色彩空间到HSV(hue,saturation,value)色彩空间或反变换 |
CV_RGB2HLS CV_BGR2HLS CV_HLS2RGB CV_HLS2BGR |
转换RGB或者BGR色彩空间到HLS(hue,Lightness,saturation)色彩空间或反变换 |
CV_RGB2Lab CV_BGR2Lab CV_Lab2RGB CV_Lab2BGR |
转换RGB或者BGR色彩空间到CIE LAB色彩空间或反变换 |
续表
转换代码 解释 |
|
CV_RGB2Luv CV_BGR2Luv CV_Luv2RGB CV_Luv2BGR |
转换RGB或者BGR色彩空间到CIE Luv色彩空间 |
CV_BayerBG2RGB CV_BayerGB2RGB |
转换Bayer模式(单通道)到RGB或者BGR色彩空间 |
CV_BayerRG2RGB CV_BayerGR2RGB CV_BayerBG2BGR CV_BayerGB2BGR CV_BayerRG2BGR CV_BayerGR2BGR |
这里不再进一步阐述CIE色彩空间中Bayer模式的细节,但许多这样的转换是很有意义的。我们的目的是,了解OpenCV能够在哪些色彩空间进行转换,这对用户来说很重要。
色彩空间转换都用到以下约定:8位图像范围是0~255,16位图像范围是0~65536,浮点数的范围是0.0~1.0。黑白图像转换为彩色图像时,最终图像的所有通道都是相同的;但是逆变换(例如RGB或BGR到灰度),灰度值的计算使用加权公式:
Y=(0.299)R+(0.587)G+(0.114)B
就HSV色彩模式或者HLS色彩模式来说,色调通常是在0~360之间。在8位图中,这可能出现问题,因此,转换到HSV色彩模式,并以8位图的形式输出时,色调应该除以2。
double cvDet(const CvArr* mat);
cvDet()用于计算一个方阵的行列式。这个数组可以是任何数据类型,但它必须是单通道的,如果是小的矩阵,则直接用标准公式计算。然而对于大型矩阵,这样就不是很有效,行列式的计算使用高斯消去法。
值得指出的是,如果已知一个矩阵是对称正定的,也可以通过奇异值分解的策略来解决。欲了解更多信息,请参阅“cvSVD”一节。但这个策略是将U和V设置为NULL,然后矩阵W的乘积就是所求正定矩阵。
void cvDiv(
const CvArr* src1,
const CvArr* src2,
CvArr* dst,
double scale = 1
);
cvDiv是一个实现除法的简单函数;它用src2除以src1中对应元素,然后把最终的结果存到dst中。如果mask非空,那么dst中的任何与mask中0元素相对应的元素都不改变。如果对数组中所有元素求倒数,则可以设置src1为NULL,函数将假定src1是一个元素全为1的数组。
double cvDotProduct(
const CvArr* src1,
const CvArr* src2
);
【58~60】
这个函数主要计算两个N维向量的点积[Lagrange1773]。与叉积函数相同,点积函数也不太关注向量是行或者是列的形式。src1和src2都应该是单通道的数组,并且数组的数据类型应该一致。
double cvEigenVV(
CvArr* mat,
CvArr* evects,
CvArr* evals,
double eps = 0
);
对对称矩阵mat,cvEigenVV()会计算出该矩阵的特征值和相应的特征向量。函数实现的是雅可比方法[Bronshtein97],对于小的矩阵是非常高效的,雅可比方法需要一个停止参数,它是最终矩阵中偏离对角线元素最大尺寸。可选参数eps用于设置这个值。在计算的过程中,所提供的矩阵mat的数据空间被用于计算,所以,它的值会在调用函数后改变。函数返回时,你会在evects中找到以行顺序保存的特征向量。对应的特征值被存储到evals中。特征向量的次序以对应特征值的重要性按降序排列。该cvEigenVV()函数要求所有三个矩阵具有浮点 类型。
正如cvDet()(前述),如果被讨论的向量是已知的对称和正定矩阵,那么最好使用SVD计算mat的特征值和特征向量。
void cvFlip(
const CvArr* src,
CvArr* dst = NULL,
int flip_mode = 0
);
本函数是将图像绕着在X轴或Y轴或者绕着X轴或Y轴上同时旋转。当参数flip_mode被设置为0的时候,图像只会绕X轴旋转。 【61】
flip_mode被设置为正值时(例如,+1),图像会围绕Y轴旋转,如果被设置成负值(例如,-1),图像会围绕X轴和Y轴旋转。
在Win32运行视频处理系统时,你会发现自己经常使用此功能来进行图像格式变换,也就是坐标原点在左上角和左下角的变换。
double cvGEMM(
const CvArr* src1,
const CvArr* src2,
double alpha,
const CvArr* src3,
double beta,
CvArr* dst,
int tABC = 0
);
广义矩阵乘法(generalized matrix multiplicatipm,GEMM)在OpenCV中是由cvGEMM()来实现的,可实现矩阵乘法、转置后相乘、比例缩放等。最常见的情况下,cvGEMM()计算如下:
其中A,B和C分别是矩阵src1,src2和src3,α和β是数值系数,op()是附在矩阵上的可选转置。参数src3可以设置为空。在这种情况下,不会参与计算。转置将由可选参数tABC来控制,它的值可以是0或者(通过布尔OR操作)CV_GEMM_A_T、CV_GEMM_B_T和CV_GEMM_C_T的任何组合(每一个标志都有一个矩阵转换相对应)。
过去的OpenCV包含cvMatMul()和cvMatMulAdd()方法,但是,它们很容易和cvMul()混淆,其实它们的功能是完全不一样的(即两个数组的元素与元素相乘)。这个函数以宏的形式继续存在,它们直接调用cvGEMM()。两者对应关系如表3-7所示。
表3-7:cvGEMM()一般用法的宏别名
cvMatMul(A,B, D) |
cvGEMM(A,B,1,NULL,0,D,0) |
cvMatMulAdd(A,B,C,D) |
cvGEMM(A,B,1,C,1,D,0) |
只有大小符合约束的矩阵才能进行乘法运算,并且所有的数据类型都应该是浮点型。cvGEMM()函数支持双通道矩阵,在这种情况下,它将双通道视为一个复数的两个部分。
CvMat* cvGetCol(
const CvArr* arr,
CvMat* submat,
int col
);
CvMat* cvGetCols(
const CvArr* arr,
CvMat* submat,
int start_col,
int end_col
);
cvGetCol()函数被用作提取矩阵中的某一列,并把它以向量的形式返回(即只有一列的矩阵)。在这种情况下,矩阵指针submat将被修改为指向arr中的特定列,必须指出的是,该指针在修改过程中并未涉及内存的分配或数据的复制;submat的内容仅仅是作了简单修改以使它正确地指出arr中所选择的列。它支持所有数据类型。
cvGetCols()函数的工作原理与cvGetCols完全一致,区别只在于前者将选择从start_col到end_col之间的所有列。这两个函数都返回一个与被调用的特定列或者多列(即,submat)相对应的头指针。
CvMat* cvGetDiag(
const CvArr* arr,
CvMat*submat,
int diag= 0
);
cvGetDiag ()类似于cvGetCol();它能从一个矩阵选择某一条对角线并将其作为向量返回。submat是一个矩阵类型的头指针。函数cvGetDiag()将填充该向量头指针中的各分量,以使用指向arr中的正确信息。注意,调用cvGetDiag()会修改输入的头指针,将数据指针指向arr对角线上的数据,实际上,并没有复制arr的数据。可选参数diag表明submat指向哪一条对角线的。如果diag被设置为默认值0,主对角线将被选中。如果diag大于0,则始于(diag,0)的对角线将被选中,如果diag小于0,则始于(0,-diag)的对角线将被选中。cvGetDiag()并不要求矩阵arr是方阵,但是数组submat长度必须与输入数组的尺寸相匹配。当该函数被调用时,最终的返回结果与输入的submat相同。
int cvGetDims(
const CvArr* arr,
int* sizes=NULL
);
int cvGetDimSize(
const CvArr* arr,
int index
); 【63】
您一定还记得OpenCV中的矩阵维数可以远远大于2。函数cvGetDims()返回指定数组的维数并可返回每一个维数的大小。如果数组sizes非空,那么大小将被写入sizes。如果使用了参数sizes,它应该是一个指向n个整数的指针,这里的n指维数。如果无法事先获知维数,为了安全起见,可以把sizes大小指定为CV_MAX_DIM。
函数cvGetDimSize()返回一个由index参数指定的某一维大小。如果这个数组是矩阵或者图像,那么cvGetDims()将一直返回为2。对于矩阵和图像,由cvGetDims()返回的sizes的次序将总是先是行数然后是列数。
CvMat* cvGetRow(
const CvArr* arr,
CvMat* submat,
int row
);
CvMat* cvGetRows(
const CvArr* arr,
CvMat*submat,
int start_row,
int end_row
);
cvGetRow()获取矩阵中的一行让它作为向量(仅有一行的矩阵)返回。跟cvGetCol()类似,矩阵头指针submat将被修改为指向arr中的某个特定行,并且对该头指针的修改不涉及内存的分配和数据的复制;submat的内容仅是作为适当的修改以使它正确地指向arr中所选择的行。该指针所有数据类型。
cvGetRows()函数的工作原理与cvGetRow()完全一致,区别只在于前者将选择从start_row到end_row之间的所有行。这两个函数都返回一个的头指针,指向特定行或者多个行。
CvSize cvGetSize( const CvArr* arr );
它与cvGetDims()密切相关,cvGetDims()返回一个数组的大小。主要的不同是cvGetSize()是专为矩阵和图像设计的,这两种对象的维数总是2。其尺寸可以以CvSize结构的形式返回,例如当创建一个新的大小相同的矩阵或图像时,使用此函数就很方便。 【64】
cvGetSubRect
CvSize cvGetSubRect(
const CvArr* arr,
CvArr* submat,
CvRect rect
);
cvGetSubRect()与cvGetColumns()或cvGetRows()非常类似,区别在于cvGetSubRect()通过参数rect在数组中选择一个任意的子矩阵。与其他选择数组子区域的函数的函数一样,submat仅仅是一个被cvGetSubRect()函数填充的头,它将指向用户期望的子矩阵数据,这里并不涉及内存分配和数据的复制。
void cvInRange(const CvArr* src,
const CvArr* lower,
const CvArr* upper,
CvArr* dst
);
void cvInRangeS(
const CvArr* src,
CvScalar lower,
CvScalar upper,
CvArr* dst
);
这两个函数可用于检查图像中像素的灰度是否属于某一指定范围。cvInRange()检查,src的每一个像素点是否落在lower和upper范围中。如果src的值大于或者等于lower值,并且小于upper值,那么dst中对应的对应值将被设置为0xff;否则,dst的值将被设置为0。
cvInRangeS()的原理与之完全相同,但src是与lower和upper中的一组常量值(类型CvScala)进行比较。对于这两个函数,图像src可以是任意类型;如果图像有多个通道,那么每一种通道都将被分别处理。注意,dst的尺寸和通道数必须与src一致,且必须为8位的图像。
double cvInvert(
const CvArr* src,
CvArr* dst,
Int method = CV_LU
);
cvInvert()求取保存在src中的矩阵的逆并把结果保存在dst中。这个函数支持使用多种方法来计算矩阵的逆(见表3-8),但默认采取的是高斯消去法。该函数的返回值与所选用的方法有关。 【65】
表3-8:cvInvert()函数中指定方法的参数值
方法的参数值 |
含义 |
CV_LU |
高斯消去法 (LU 分解) |
CV_SVD |
奇异值分解(SVD) |
CV_SVD_SYM |
对称矩阵的SVD |
就高斯消去法(method=CV_LU)来说,当函数执行完毕,src的行列式将被返回。如果行列式是0,那么事实上不进行求逆操作,并且数组dst将被设置为全0。
就CV_SVD或者CV_SVD_SYM,来说,返回值是矩阵的逆条件数(最小特征值跟最大特征值的比例)。如果src是奇异的,那么cvInvert()在SVD模式中将进行伪逆计算。
cvMahalonobis
CvSize cvMahalonobis(
const CvArr* vec1,
const CvArr* vec2,
CvArr* mat
);
Mahalonobis距离(Mahal)被定义为一点和高斯分布中心之间的向量距离,该距离使用给定分布的协方差矩阵的逆作为归一化标准。参见图3-5。直观上,这是与基础统计学中的标准分数(Z-score)类似,某一点到分布中心的距离是以该分布的方差作为单位。马氏距离则是该思路在高维空间中的推广。
cvMahalonobis()计算的公式如下:
假设向量vec1对应x点,向量vec2是分布的均值。mat是协方差矩阵的逆。
实际上,这个协方差矩阵通常用cvCalcCovarMatrix()(前面所述)来进行计算,然后用cvInvert()来求逆。使用SV_SVD方法求逆是良好的程序设计习惯,因为其中一个特征值为0的分布这种情况在所难免!
图3-5:数据在2D空间分布,3个叠加在一起的椭圆分别对应到分布中心的马氏距离为1.0,2.0和3.0所有点
void cvMax(
const CvArr* src1,
const CvArr* src2,
CvArr* dst
);
void cvMaxS(
const CvArr* src,
double value,
CvArr* dst
); 【66】
cvMax()计算数组src1和src2中相对应的每像素一对中的最大值。而cvMaxS(),将数组src与常量参数value进行比较。通常,如果mask非空,那么只有与非0参数相对应的dst中的元素参与计算。
void cvMerge(
const CvArr* src0,
const CvArr* src1,
const CvArr* src2,
const CvArr* src3,
CvArr* dst
); 【67】
cvMerge()是cvSplit()的逆运算。数组src0,src1,src2,和src3将被合并到数组dst中。当然,dst应该与源数组具有相同的数据类型和尺寸,但它可以有两个,三个或四个道道。未使用的源图像参数可设置为NULL。
void cvMin(
const CvArr* src1,
const CvArr* src2,
CvArr* dst
);
void cvMinS(
const CvArr* src,
double value,
CvArr* dst
);
cvMin()计算数组src1和src2中相对应的每一对像素中的最小值。而cvMaxS(),将数组src与常量标量value进行比较。同样的,如果mask非空的话,那么只有与mask的非0参数相对应的dst中的元素进行计算。
void cvMinMaxLoc(
const CvArr* arr,
double* min_val,
double* max_val,
CvPoint* min_loc = NULL,
CvPoint* max_loc = NULL,
const CvArr* mask = NULL
);
该例程找出数组arr中的最大值和最小值,并且(有选择性地)返回它们的地址。计算出的最大值和最小值赋值给max_val和min_val。或者,如果极值的位置参数非空,那极值的位置便会写入min_loc和max_loc。
通常,如果参数mask非空,那么只有图像arr中与参数mask中的非零的像素相对应的部分才被考虑。cvMinMaxLoc()例程仅仅处理单通道数组,如果有一个多通道的数组,则应该使用cvSetCOI()来对某个特定通道进行设置。
void cvMul(
const CvArr* src1,
const CvArr* src2,
CvArr* dst,
double scale=1
); 【68】
cvMul()是一个简单的乘法函数。它将src1中的元素与src2中相对应的元素相乘,然后把结果赋给dst。如果mask是空,那么与其中0元素相对应的dst元素都不会因此操作而改变。OpenCV中没有函数cvMulS(),因为该功能已经由函数cvScale()或cvCvtScale()提供。
除此之外,有一件事情要记住:cvMul()执行的是元素之间的乘法。有时候,在进行矩阵相乘时,可能会错误使用cvMul(),但这无法奏效;记住,cvGEMM()才是处理矩阵乘法的函数,而不是cvMul()。
void cvNot(
const CvArr* src,
CvArr* dst
);
函数cvNot()会将src中的每一个元素的每一位取反,然后把结果赋给dst。因此,一个值为0x00的8位图像将被映射到0xff,而值为0x83的图像将被映射到0x7c。
double cvNorm(
const CvArr* arr1,
const CvArr* arr2 = NULL,
int norm_type = CV_L2,
const CvArr* mask = NULL
);
这一函数可于计算一个数组的各种范数,当为该函数提供了两个数组作为参数时,可选用各种不同的公式来计算相对的距离。在前一种情况下,计算的范数如表3-9所示。
表3-9:当arr2 =NULL时,对于不同的norm_type由cvNorm()计算范数的公式
norm_type |
结果 |
CV_C |
|
CV_L1 |
|
CV_L2 |
如果第二个数组参数arr2非空,那么范数的计算将使用不同的公式,就像两个数组之间的距离。前三种情况的计算公式如表3-10所示,这些范数是绝对范数;在后三个情况下,将会根据第二个数组arr2的幅度进行重新调整。 【69~70】
表3-10:arr2非空,且norm_type不同值时函数cvNorm()计算范数的计算
公式
norm_type |
结果 |
CV_C |
|
CV_L1 |
|
CV_L2 |
|
CV_RELATIVE_C |
|
CV_ RELATIVE_L1 |
|
CV_ RELATIVE_L2 |
在所有情况下,arr1和arr2必须具有相同的大小和通道数。当通道数大于1时,将会对所有通道一起计算范数(即是说,在表3-9和表3-10中,不仅是针对x和y,也针对通道数求和)。
cvNormalize(
const CvArr* src,
CvArr* dst,
double a = 1.0,
double b = 0.0,
int norm_type = CV_L2,
const CvArr* mask = NULL
);
与许多OpenCV函数一样,cvNormalize()的功能比表面看更多。根据norm_type的不同值,图像src将被规范化,或者以其他方式映射到dst的一个特定的范围内。表3-11列举了norm_type可能出现的值。
表3-11:函数cvNormalize()的参数norm_type可能的值
norm_type |
结果 |
CV_C |
|
CV_L1 |
|
续表
norm_type |
结果 |
CV_L2 |
|
CV_MINMAX |
映射到[a, b]的范围上 |
【70】
计算C范数时,数组src将被进行比例变标,使其中绝对值最大的值等于a。当计算L1范数成L2范数时,该数组也将被缩放,如使其范数为a。如果norm_type的值设置为CV_MINMAX,那么将会对数组的所有的值进行转化,使它们线性映射到a和b之间(包括a和b)。
与以前一样,如果参数mask非空,那么只有与掩码非0值对应的像素会对范数的计算有贡献,并且只有那些像素会被cvNormalize()改变。
void cvOr(
const CvArr* src1,
const CvArr* src2,
CvArr* dst,
const CvArr* mask=NULL
);
void cvOrS(
const CvArr* src,
CvScalar value,
CvArr* dst,
const CvArr* mask = NULL
);
这两个函数将对数组src1进行按位或计算。在函数cvOr()中,dst中的每一个元素都是是由src1和src2中相对应的元素按位做或运算的结果。在cvOrS()函数中,将对src和常量value进行或运算。像往常一样,如果mask非空,则只计算dst中与mask中非0元素对应的元素。
该函数支持所有的数据类型,但在cvOr()中,src1和src2必须有相同的数据类型。如果数组元素是浮点类型,则使用浮点按位表示形式。
CvSize cvReduce(
const CvArr* src,
CvArr* dst,
int dim,
int op = CV_REDUCE_SUM
);