访问图象数据
当处理图象数据时,通常需要快速高效。使用如cvSet*D或和它等效的函数会造成调用时的开销。我们应该尽可能使用直接存取图象内部数据的方法。有了IplImage内部结构的知识,我们现在可以理解最好的方法。
尽管通常OpenCV提供很多对图象的操作优化良好的例程,但是经常会有些任务是库里找不到包装好的例程的。下面我们考虑一个例子,我们想要把三通道HSV图象的饱合度调整为255(8位图象的最大值)而保持色调不变。要完成这个任务最好是我们自己处理图象的每个象素。这和我们前边对矩阵的操作相似,但在IplImage和CvMat之间也有一些主要的不同。例3-11演示了高效的方式。
例3-11,把HSV图象的S、V分量最大化:
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+1] = 255;
ptr[3*x+2] = 255;
}
}
}
我们只是简单的直接计算相关行y最左边的象素的指针ptr(We simply compute the pointer ptr directly as the head of the relevant row y)/*唉,有时候翻译出来的总没原文生动形象*/。从那里为参考,我们引用第x列的饱合度数值。因为图象是三通道的,第c通道的地址为3*x+c。
在OpenCV中,一副HSV图象和RGB图象,除了对通道的翻译有所不同,其它是没有区别的。因此由一副HSV图象构建一副RGB图象实际所有的操作完全只在“数据”区域。在图象头中,没有任何成员用来表明数据通道的意义。
IplImage和CvMat相比一个重要的不同是imageData的行为。CvMat的数据元素是一个联合体,所以必须说明你想要的指针类型;imageData是一个byte型(uchar*)。 我们已经知道被指向的数据不一定是uchar类型,这意味着当对指针作算术运算时,你可以简单的加上widthSetp(同样是以字节数为度量的)而不用担心实际的数据类型,直需在做完加法后,把你计算所得的指针转换成你想要的数据类型。总结:当对矩阵操作时,你必须对偏移量进行缩减,因为数据指针可能不是byte型;而当对图象操作时,你可以使用“看上去”那么多的偏移量,因为数据指针永远是byte型,因此在你准备使用它时,只需把整部分做类型转换。
访问图象数据的补充资料
资料来源:OpenCV中文网站
(1) 假设你要访问第k通道、第i行、第j列的像素。
(2) 间接访问: (通用,但效率低,可访问任意格式的图像)
对于单通道字节型图像:
IplImage* img=cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,1);
CvScalar s;
s=cvGet2D(img,i,j); // get the (i,j) pixel value
printf("intensity=%f/n",s.val[0]);
s.val[0]=111;
cvSet2D(img,i,j,s); // set the (i,j) pixel value
对于多通道字节型/浮点型图像:
IplImage* img=cvCreateImage(cvSize(640,480),IPL_DEPTH_32F,3);
CvScalar s;
s=cvGet2D(img,i,j); // get the (i,j) pixel value
printf("B=%f, G=%f, R=%f/n",s.val[0],s.val[1],s.val[2]);
s.val[0]=111;
s.val[1]=111;
s.val[2]=111;
cvSet2D(img,i,j,s); // set the (i,j) pixel value
(3) 直接访问: (效率高,但容易出错)
对于单通道字节型图像:
IplImage* img=cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,1);
((uchar *)(img->imageData + i*img->widthStep))[j]=111;
对于多通道字节型图像:
IplImage* img=cvCreateImage(cvSize(640,480),IPL_DEPTH_8U,3);
((uchar *)(img->imageData + i*img->widthStep))[j*img->nChannels + 0]=111; // B
((uchar *)(img->imageData + i*img->widthStep))[j*img->nChannels + 1]=112; // G
((uchar *)(img->imageData + i*img->widthStep))[j*img->nChannels + 2]=113; // R
对于多通道浮点型图像:
IplImage* img=cvCreateImage(cvSize(640,480),IPL_DEPTH_32F,3);
((float *)(img->imageData + i*img->widthStep))[j*img->nChannels + 0]=111; // B
((float *)(img->imageData + i*img->widthStep))[j*img->nChannels + 1]=112; // G
((float *)(img->imageData + i*img->widthStep))[j*img->nChannels + 2]=113; // R
(4) 基于指针的直接访问: (简单高效)
对于单通道字节型图像:
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;
对于多通道字节型图像:
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;
对于多通道浮点型图像(假设图像数据采用4字节(32位)行对齐方式):
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;
ROI和widthStep更多的信息
ROI和widthStep有非常重要的实用价值,因为它允许代码只处理图象的一部分子区域,从而在很多场和加速计算机视觉的处理。OpenCV对ROI和widthSetp的支持是普遍的:每一个函数都充许把操作只限定在一个子区域。使用cvSetImageROI( )和cvResetImageROI( )可以开启或关闭ROI。
void cvSetImageROI( IplImage* image, CvRect rect );
void cvResetImageROI( IplImage* image );
为了看ROI是怎么使用的,我们载入一张图片,然后变更图象的一部分。在显示之前用cvResetImageROI( )释放ROI是很重要的,否则只会忠实的显示ROI区域。
#include <cv.h>
#include <highgui.h>
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区域好像更方便,那么为什么还想要用widthStep耍花招呢。这样做的原因是有时候在图像处理过程中需要保持多个子区域都是活动的,但是ROI只能顺序的进行,必须不断的设定和重置ROI区域(The reason is that there are times when you want to set and perhaps keep multiple subregions of an image active during processing, but ROI can only be done serially and must be set and reset constantly.)。