IplImage是用来编码“图象”的基本的数据类型,IplImage可以看做是从CvMat中衍生出来的。
CvMat矩阵结构
在OpenCV中没有向量(Vector)的构造方法。当我们需要向量时,我们使用1行的矩阵(或1列的矩阵,如果需要转置或共轭向量)
创建二维矩阵的例程原型为:
cvMat* cvCreateMat (int rows, int cols, int type);
//这里type可以是预定义的长列表中的任何一种形式:CV_(S|U|F)C。因此,矩阵可以包含32位的浮点型数(CV_32FC1),或者三通道8位整形数(CV_8UC3).
CvMat的内部结构:
typedef struct CvMat
{
int type;
int step;
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;
主要包含一个宽度、一个高度、一个元素类型、一个步长以及一个指向数据数组的指针。我们可以使用CvMat指针直接访问这些成员,或者使用已提供的访问函数。例如为了获取矩阵的尺寸信息,你可以调用cvGetSize(CvMat*)返回一个CvSize结构,也可以用matrix->width和matrix->height独立的访问width和height成员。
矩阵数组的步长(step)是矩阵一行长度的字节数,而不是元素个数。矩阵或图像在分配内存是4字节对齐的。因此当一个矩阵的宽度为3个字节时,会被分配4个字节,最后一个字节会被忽略。因此,当我们获得一个byte型的数据元素指针时,我们把指针加上步长(step)就可以达到把指针移动到下边一行元素下面的元素。如果我们有一个整形或浮点型的矩阵,和数据相应元素的整形或浮点型指针,要把指针移动到下一行,那么要加step/4;对于double则需要加step/8(这只是考虑到C会自动把偏移量乘以数据类型的字节数)。
2. 创建:
cvCreateMat( )组合了两个原子函数
(1) cvCreateMatHeader( )(创建一个CvMat结构,但没有给数据分配内存)和cvCreateData( )。
(2) cvCreateData( )处理给数据分配内存。
有三种方法访问矩阵中的数据:
一、最简单的获取矩阵中数据的方法是使用CV_MAT_ELEM( )宏(只适于访问一维和二维数组)。这个宏输入矩阵、数据的类型、行、列,然后返回矩阵元素。
例如:
CvMat* mat = cvCreateMat( 5, 5, CV_32FC1 );
float element_3_2 = CV_MAT_ELEM( *mat, float, 3, 2 );
二、使用函数族cvPtr*D和cvGet*D
(1) cvPtr*D族包含cvPtr1D( )、cvPtr2D( )、cvPtr3D( )和cvPtrND( )…. 前三个中的每一个都输入一个矩阵指针参数CvArr*,接着是一个适当的整型数值为指示,返回一个感兴趣元素的指针。对于cvPtrND( ),第二个参数是用以指示数值的整型数组指针。在函数原型中,我们也注意到有一些可选的参数,如果需要时可以给它们赋地址。
可以使用cvPtr*D函数返回的指针达到访问矩阵指定的点,然后可以使用算数运算把指针从那里来回移动。
在多通道矩阵中通道是相连的。
例如,在表示红、绿、蓝字节的三通道二维矩阵中,矩阵中的数据是这样存储的:rgbrgb….。因此,要把适当类型的指针移动到下一个通道,我们只把指针加1即可。如果想要把指针移动到下一个“像素”或下一组元素,我们只需加上和通道数相同大小的偏移量即可(这个例子中为3)。
(2) 如果仅仅是读取数据,那么可以使用外有一个函数族cvGet*D,返回的是矩阵元素的实际值。
三、最恰当的方法是用自己的指针算数运算,并简单的对矩阵进行引用。如果要处理数组中的每一个元素,维护自己的指针是非常重要的,
例:对一个三通道矩阵所有元素求和
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区域,因此不能保证数据在行间是连续的。
3. IplImage头结构:
typedefstruct _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;
“depth”变量可能的值:
IPL_DEPTH_8U 8位无符号整型
IPL_DEPTH_8S 8位有符号整型
IPL_DEPTH_16S 16位有符号整型
IPL_DEPTH_32S 32位有符号整型
IPL_DEPTH_32F 32位单精度浮点型
IPL_DEPTH_64F 64位双精度浮点型
nChannel的可能的值为1、2、3或4。
“origin”变量取值IPL_ORIGIN_TL或IPL_ORIGIN_BL,分别对应于原点坐标分别在图象的左上角或左下角。
“dataOrder”:指定了数据的包装方式, OpenCV只支持IPL_DATA_ORDER_PIXEL方式,表示交叉存取颜色通道,像素存储顺序为BGR BGR BGR … BGR(IPL_DATA_ORDER_PLANE表示通道聚集在一起构成一个图象的面(Plane),这些面是逐个相连的,像素存储顺序为RRR…R GGG…G BBB…B)。
“WidthStep”:包含了行偏移的字节数(CvMat中step参数相似)。只有变量width不足以计算距离,为了达到更快的处理图象的目的,每一行可能会以一定数目的字节数对齐(内存对齐)。因此两行之间可能会存在一些空隙。
“imageData”:包含了第一行数形数据的指针。
“RIO”(region of interest):是一个特殊的重要的感兴趣的区域,它实际上是一个另外的IPL/IPP结构IplROI。它的定义如下(在cxtype.h中有定义):
typedefstruct _IplROI
{
int coi;
int xOffset;
int yOffset;
int width;
int height;
}IplROI;
```:
ROI背后的理念是:一旦它被设定,通常会对整个图象进行处理的函数就只处理ROI指示的区域。如果ROI被设定了,几乎所有的OpenCV函数都会使用它。如果COI被设定为非零值,表示一些操作就只在指定的通道上起作用。很不幸的是,大部分OpenCV函数都会忽略这个参数。
考虑一个例子,我们想要把三通道HSV图象的饱合度调整为255(8位图象的最大值)而保持色调不变。要完成这个任务最好自己处理图象的每个象素。这和前边对矩阵的操作相似,但在IplImage和CvMat之间也有一些主要的不同。高效的方式:
例,把HSV图象的S、V分量最大化:
<div class="se-preview-section-delimiter">div>
void saturate_sv( IplImage* img )
{
for( int y=0; yheight; y++ )
{
uchar* ptr = (uchar*) (img->imageData + y * img->widthStep);
{
ptr[3*x+1] = 255;
ptr[3*x+2] = 255;
}
}
}
只是简单的直接计算相关行y最左边的象素的指针ptr,引用第x列的饱合度数值。因为图象是三通道的,第c通道的地址为3*x+c。
<div class="se-preview-section-delimiter">div>
在OpenCV中,一副HSV图象和RGB图象,除了对通道的翻译有所不同,其它是没有区别的。因此由一副HSV图象构建一副RGB图象实际所有的操作完全只在“数据”区域。在图象头中,没有任何成员用来表明数据通道的意义。
IplImage和CvMat相比一个重要的不同是imageData的行为。CvMat的数据元素是一个联合体,所以必须说明你想要的指针类型;imageData是一个byte型(uchar*)。但被指向的数据不一定是uchar类型,这意味着当对指针作算术运算时,可以简单的加上widthSetp(同样是以字节数为度量的)而不用担心实际的数据类型,直需在做完加法后,把计算所得的指针转换成想要的数据类型。
总结:当对矩阵操作时,必须对偏移量进行缩减,因为数据指针可能不是byte型;而当对图象操作时,可以使用“看上去”那么多的偏移量,因为数据指针永远是byte型,因此在准备使用它时,只需把整部分做类型转换。
例:假设你要访问第k通道、第i行、第j列的像素。
基于指针的直接访问: (简单高效)
(1)对于单通道字节型图像:
<div class="se-preview-section-delimiter">div>
IplImage* img = cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,1);
int height = img->height;
int width = img->width;
int step = img->widthStep/sizeof(uchar);
uchar* data = (uchar *)img->imageData;
data[i*step+j] = 111;
(2)对于多通道字节型图像:
<div class="se-preview-section-delimiter">div>
IplImage* img = cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,3);
int height = img->height;
int width = img->width;
int step = img->widthStep/sizeof(uchar);
int channels = img->nChannels;
uchar* data = (uchar *)img->imageData;
data[i*step+j*channels+k] = 111;
(3)对于多通道浮点型图像(假设图像数据采用4字节(32位)行对齐方式):
<div class="se-preview-section-delimiter">div>
IplImage* img = cvCreateImage(cvSize(640,480),IPL_DEPTH_32F,3);
int height = img->height;
int width = img->width;
int step = img->widthStep/sizeof(float);
int channels = img->nChannels;
float * data = (float *)img->imageData;
data[i*step+j*channels+k] = 111;
4. OpenCV对ROI和widthSetp的支持是普遍的:每一个函数都充许把操作只限定在一个子区域。使用cvSetImageROI( )和cvResetImageROI( )可以开启或关闭ROI。
<div class="se-preview-section-delimiter">div>
void cvSetImageROI(IplImage*image, CvRect rect);
void cvResetImageROI( IplImage* image );
例:载入一张图片,然后变更图象的一部分。在显示之前用cvResetImageROI( )释放ROI是很重要的,否则只会忠实的显示ROI区域。
<div class="se-preview-section-delimiter">div>
#include
#include
int main()
{
IplImage *src = cvLoadImage("poppy.jpg", 1); //载入图象
cvSetImageROI(src, cvRect(200, 100, 100, 100)); //为图象设置ROI区域
cvAddS(src, cvScalar(100, 100, 100), src); //对图象做与运算
cvResetImageROI(src); //释放ROI区域
cvSaveImage("poppy1.jpg", src); //保存处理后的图象
cvNamedWindow("Roi_add"); //创建一个窗口
cvShowImage("Roi_add", src); //在窗口上显示图象
cvWaitKey(); //延时
return 0;
}
如果我们灵活的使用widthStep,也可以达到相同的效果。
#include<cv.h>
#include<highgui.h>
int main()
{
IplImage *src = cvLoadImage("poppy.jpg", 1); //载入图象
CvRect interest_rect = cvRect(200, 100, 100, 100); //用CvRect结构设定一个感兴趣的区域
IplImage *sub_img= cvCreateImageHeader(cvSize(interest_rect.width,interest_rect.height),
src->depth, src->nChannels ); //创建一个和源图象属性相同的子图象
sub_img->origin=src->origin;//设相同P的原点标准
sub_img->widthStep = src->widthStep;//设定子图象的widthStep,这是此技术中最精妙的一笔
sub_img->imageData = src->imageData +
interest_rect.y * src->widthStep +
interest_rect.x * src->nChannels;//设定子图象的数据区域
cvAddS(sub_img, cvScalar(100, 100, 100), sub_img); //对图象做与运算
cvReleaseImageHeader(&sub_img); //释放子图象头
cvSaveImage("poppy1.jpg", src); //保存处理后的图象
cvNamedWindow("Roi_add"); //创建一个窗口
cvShowImage("Roi_add", src); //显示图象
cvWaitKey(); //延时
return 0;
}
在图像处理过程中有时需要保持多个子区域都是活动的,但是ROI只能顺序的进行,必须不断的设定和重置ROI区域。
参考:《O’Reilly Learning OpenCV》& OpenCV中文网站