1. ROI简介
ROI(Region of Interest)是指图像中的一个矩形区域,可能你后续的程序需要单独处理这一个小区域,如图所示:
图1 ROI的解释
ROI在实际工作中有很重要的作用,在很多情况下,使用它们会提高计算机视觉代码的执行速度。这是因为它们允许对图像的某一小部分进行操作,而不是对整个图像进行运算。在OpenCV中,所有的对图像操作的函数都支持ROI,如果你想打开ROI,可以使用函数cvSetImageROI(),并给函数传递一个矩形子窗口。而cvResetImageROI()是用于关闭ROI的。注意,在程序中,一旦使用了ROI做完相应的运算,就一定要用cvResetImageROI()来关闭ROI,否则,其他操作执行时还会使用ROI的定义。
IplImage 结构解读:
typedef struct _IplImage { intnSize; int ID; intnChannels; intalphaChannel; int depth; charcolorModel[4]; charchannelSeq[4]; intdataOrder; int origin; int align; int width; int height; struct _IplROI *roi; struct_IplImage *maskROI; void*imageId; struct_IplTileInfo *tileInfo; intimageSize; char*imageData; intwidthStep; intBorderMode[4]; intBorderConst[4]; char*imageDataOrigin; }IplImage;
origin变量可以有两个取值:IPL_ORIGIN_TL或者IPL_ORIGIN_BL,分别代表图像坐标系原点在左上角或是左下角。相应的,在计算机视觉领域,一个重要的错误来源就是原点位置的定义不统一。例如,图像的来源不同,操作系统不同,视频解码codec不同,存储方式不同等等,都可以造成原点位置的变化。
IplROI结构体包含了xOffset,yOffset,height,width,coi成员变量,其中xOffset,yOffset是x,y坐标,coi代表channel of interest(感兴趣的通道)。有时候,OpenCV图像函数不是作用于整个图像,而是作用于图像的某一个部分。这是,我们就可以使用roi成员变量了。如果IplImage变量中设置了roi,则OpenCV函数就会使用该roi变量。如果coi被设置成非零值,则对该图像的操作就只作用于被coi指定的通道上了。不幸的是,许多OpenCV函数忽略了coi的值。
【注意】
(1)ROI只是原始图像的一个子区域,没有自己的内存空间,对ROI的操作会影响原始图像。
(2)对ROI区域的遍历还是要根据矩形框在原始图像中的相对位置遍历。笔者曾经犯了利用原始图像src的src->imageData做基地址,用src->height、src->width做边界条件,用src->widthStep计算步长来遍历ROI图像的错误。这是因为抱有IplImage结构体中的成员变量数值会随ROI图像而改变的错误思想。
2. 对应的相关函数
SetImageROI
基于给定的矩形设置感兴趣区域
void cvSetImageROI( IplImage* image, CvRectrect );
【参数介绍】
image
图像头.
rect
ROI矩形.
函数 cvSetImageROI 基于给定的矩形设置图像的ROI(感兴趣区域)。如果ROI是NULL并且参数RECT的值不等于整个图像, ROI被分配。不像 COI,大多数的 OpenCV 函数支持 ROI 并且处理它就像它是一个分离的图像 (例如,所有的像素坐标从ROI的左上角或左下角(基于图像的结构)计算。
ResetImageROI
释放图像的ROI
voidcvResetImageROI( IplImage* image );
【参数介绍】
image
图像头.
函数 cvResetImageROI 释放图像ROI。释放之后整个图像被认为是全部被选中的。相似的结果可以通过下述办法
cvSetImageROI(image, cvRect( 0, 0, image->width, image->height ));
cvSetImageCOI(image, 0 );
但是后者的变量不分配 image->roi
3. 利用ROI剪切图像并批量保存
图像的剪切有多种方法,其中一种是使用ROI的方法:
第一步:将需要剪切的图像图像不部分设置为ROI
cvSetImageROI(src,cvRect(x,y,width,height));
第二步:新建一个与需要剪切的图像部分同样大小的新图像
cvCreateImage(cvSize(width,height),IPL_DEPTH,nchannels);
第三步:将源图像复制到新建的图像中
cvCopy(src,dst,0);
第四步:释放ROI区域
cvResetImageROI(src);
笔者在做火焰检测时需要提取火焰疑似区域进行进一步的判断,需要在以上方法的基础上批量存储ROI区域图像到硬盘,这里有一个技巧是使用字符串格式化sprintf把要保存的路径及文件名写入字符缓冲区。如:
sprintf(imageName,"%s_%d-%04d年%02d月%02d日%02d时%02分%02d秒.jpg","E:\\Test\\ROI\\fire",countROI, tm_ptr.tm_year-100+2000,tm_ptr.tm_mon + 1, tm_ptr.tm_mday, tm_ptr.tm_hour,tm_ptr.tm_min, tm_ptr.tm_sec);
此语句把当前时间与编号都写入字符数组中以备cvSaveImage( const char*filename, const CvArr* image )函数使用。
以下是源码的主函数:
int main(){ IplImage *img = NULL; //输入图像,8bit 3通道 IplImage *colTemp = NULL; //颜色分割后(有内部空洞)的火焰图片 IplImage *gray = NULL; //灰度图 IplImage *mask = NULL; //二值图,用于复制图像的掩膜 IplImage *dst = NULL; //输出火焰疑似图像,8bit、3通道 IplImage *ROI[5] = {NULL}; //定义ROI图像头的数组 char imageName[50]; //存储图像的名称 CvMemStorage *stor; //存储轮廓序列 CvSeq *cont; //可动态增长元素序列 time_t the_time; //时间 stor=cvCreateMemStorage(0); //创建一内存块并返回指向块首的指针,默认64K cont=cvCreateSeq(CV_SEQ_ELTYPE_POINT,sizeof(CvSeq),sizeof(CvPoint),stor); //创建一序列 img = cvLoadImage("E:\\Test\\SegTest\\fire40.JPG"); //载入原始图片 colTemp = cvCreateImage(cvGetSize(img),img->depth,img->nChannels); //经过颜色分割后(有内部空洞)的火焰图片 gray = cvCreateImage(cvGetSize(img),img->depth,1); mask = cvCreateImage(cvGetSize(img),img->depth,1); dst = cvCreateImage(cvGetSize(img),img->depth,img->nChannels); //保存经过填补后的火焰图片 cvZero(dst); colorModel(img,colTemp); //使用颜色模型对火焰图像分割 cvCvtColor(colTemp,gray,CV_BGR2GRAY); //使用cvFindContours函数与cvFillPoly填充连通区内部空洞 fillSeg(gray,mask); cvCopy(img,dst,mask); //使用cvFindContours根据火焰疑似区域(已填充内部空洞)的掩膜(单通道)寻找轮廓; //随之使用cvRectangle找到轮廓的外接矩形,从而可以得到ROI。 cvFindContours(mask,stor,&cont,sizeof(CvContour),CV_RETR_EXTERNAL ,CV_CHAIN_APPROX_SIMPLE,cvPoint(0,0)); cvDrawContours(img,cont,cvScalar(255,255,255),cvScalar(0,0,0),1,1); int countROI = 0; //ROI编号 for(;cont;cont = cont->h_next){ double area = fabs(cvContourArea(cont,CV_WHOLE_SEQ)); double len = cvArcLength(cont, CV_WHOLE_SEQ,-1); double circularity = (4*PI*area)/(len*len); //计算疑似火焰区域的圆形度 CvRect r = ((CvContour*)cont)->rect;//子类转换为父类例子 if(r.height * r.width > CONTOUR_MAX_AERA && circularity < 0.4 && circularity > 0.07 ) //面积小的外接矩形以及圆形度过大或过小的轮廓抛弃掉 { printf("\n圆形度:%f\n",circularity); cvRectangle(img, cvPoint(r.x,r.y),cvPoint(r.x + r.width, r.y + r.height),CV_RGB(255,0,0), 1, CV_AA,0); cvSetImageROI(img,r); ROI[countROI] = cvCreateImage(cvGetSize(img),img->depth,img->nChannels); cvCopy(img,ROI[countROI],0); cvResetImageROI(img); (void) time(&the_time); //获得从纪元(1970年1月1日)开始至今的秒数 tm tm_ptr = *localtime(&the_time); //从time_t格式的时间里返回本地时间 //保存的图片名 sprintf(imageName,"%s_%d-%04d年%02d月%02d日%02d时%02分%02d秒.jpg","E:\\Test\\ROI\\fire",countROI, tm_ptr.tm_year-100+2000,tm_ptr.tm_mon + 1, tm_ptr.tm_mday, tm_ptr.tm_hour,tm_ptr.tm_min, tm_ptr.tm_sec); //sprintf(imageName, "%s%d%s", "E:\\Test\\ROI\\image", countROI, ".jpg");//保存的图片名 cvSaveImage(imageName,ROI[countROI]); //保存一帧图片 countROI++; memset(imageName,0,sizeof(imageName)); } } for(int i = 0;i<countROI;i++){ char showImg[16]; sprintf(showImg,"ROI%d",i); cvShowImage(showImg,ROI[i]); } cvShowImage("原始图片",img); cvShowImage("颜色分割处理",colTemp); cvShowImage("填充处理图片",dst); cvShowImage("掩膜",mask); cvWaitKey(); //销毁窗口 cvDestroyAllWindows(); //释放图像 for(int i = 0;i<countROI;i++){ cvReleaseImage(&ROI[i]); } cvReleaseImage(&img); cvReleaseImage(&colTemp); cvReleaseImage(&gray); cvReleaseImage( &mask ); }
4. 附录
工程源码
#include<opencv/cv.h> #include<opencv/highgui.h> #include <math.h> #define min(x,y) (x<y?x:y) #define R_THRESHHOLD 125 #define S_THRESHHOLD 60 #define CONTOUR_MAX_AERA 200 #define PI 3.1416 //RGB+HSI颜色模型 void colorModel(IplImage *src,IplImage * dst){ int step = NULL; int rows = src->height; int cols = src->width; for(int i = 0;i < rows;i++){ //uchar* dataS = src.ptr<uchar>(i); //uchar* dataD = dst.ptr<uchar>(i); uchar *dataS = (uchar*)src->imageData; uchar *dataD= (uchar*)dst->imageData; for(int j = 0;j < cols; j++){ step = i*src->widthStep+j*src->nChannels;; float S; float b = dataS[step]/255.0; float g = dataS[step+1]/255.0; float r = dataS[step+2]/255.0; float minRGB = min(min(r,g),b); float den = r+g+b; if(den == 0) //分母不能为0 S = 0; else S = (1 - 3*minRGB/den)*100; if(dataS[step+2] <= R_THRESHHOLD || dataS[step+2] < 165){ dataD[step] = 0; dataD[step+1] = 0; dataD[step+2] = 0; } else if(dataS[step+2] <= dataS[step+1] || dataS[step+1] <= dataS[step] ){ dataD[step] = 0; dataD[step+1] = 0; dataD[step+2] = 0; } else if(S <= (float)(S_THRESHHOLD*(255 - dataS [step+2]))/R_THRESHHOLD){ dataD[step] = 0; dataD[step+1] = 0; dataD[step+2] = 0; } else{ dataD[step] = dataS[step]; dataD[step+1] = dataS[step+1]; dataD[step+2] = dataS[step+2]; } } } } //根据分割结果确定轮廓并填充 void fillSeg(IplImage *src,IplImage *tempdst) { CvSeq * contour = NULL; CvMemStorage * storage = cvCreateMemStorage(); //在二值图像中寻找轮廓,CV_CHAIN_APPROX_SIMPLE - 压缩水平、垂直和对角分割,即函数只保留末端的象素点 cvFindContours(src,storage,&contour,sizeof(CvContour),CV_RETR_CCOMP ,CV_CHAIN_APPROX_SIMPLE); cvZero(tempdst); for( contour; contour != 0; contour = contour->h_next) { //轮廓的方向影响面积的符号。因此函数也许会返回负的结果。应用函数 fabs() 得到面积的绝对值。 double area = cvContourArea( contour,CV_WHOLE_SEQ ); //计算整个轮廓或部分轮廓的面积 if(fabs(area) < 10) { continue; } // CvScalar color = CV_RGB( 255, 255, 255 ); CvPoint *point = new CvPoint[contour->total]; CvPoint *Point; //printf("图像分割contour->total\t%d\n",contour->total); for (int i = 0;i<contour->total;i++) { Point = (CvPoint*)cvGetSeqElem(contour,i); point[i].x =Point->x; point[i].y = Point->y; } int pts[1] = {contour->total}; cvFillPoly(tempdst,&point,pts,1,CV_RGB(255,255,255));//填充多边形内部 } cvReleaseMemStorage(&storage); } int main(){ IplImage *img = NULL; //输入图像,8bit 3通道 IplImage *colTemp = NULL; //颜色分割后(有内部空洞)的火焰图片 IplImage *gray = NULL; //灰度图 IplImage *mask = NULL; //二值图,用于复制图像的掩膜 IplImage *dst = NULL; //输出火焰疑似图像,8bit、3通道 IplImage *ROI[5] = {NULL}; //定义ROI图像头的数组 char imageName[50]; //存储图像的名称 CvMemStorage *stor; //存储轮廓序列 CvSeq *cont; //可动态增长元素序列 time_t the_time; //时间 stor=cvCreateMemStorage(0); //创建一内存块并返回指向块首的指针,默认64K cont=cvCreateSeq(CV_SEQ_ELTYPE_POINT,sizeof(CvSeq),sizeof(CvPoint),stor); //创建一序列 img = cvLoadImage("E:\\Test\\SegTest\\fire40.JPG"); //载入原始图片 colTemp = cvCreateImage(cvGetSize(img),img->depth,img->nChannels); //经过颜色分割后(有内部空洞)的火焰图片 gray = cvCreateImage(cvGetSize(img),img->depth,1); mask = cvCreateImage(cvGetSize(img),img->depth,1); dst = cvCreateImage(cvGetSize(img),img->depth,img->nChannels); //保存经过填补后的火焰图片 cvZero(dst); colorModel(img,colTemp); //使用颜色模型对火焰图像分割 cvCvtColor(colTemp,gray,CV_BGR2GRAY); //使用cvFindContours函数与cvFillPoly填充连通区内部空洞 fillSeg(gray,mask); cvCopy(img,dst,mask); //使用cvFindContours根据火焰疑似区域(已填充内部空洞)的掩膜(单通道)寻找轮廓; //随之使用cvRectangle找到轮廓的外接矩形,从而可以得到ROI。 cvFindContours(mask,stor,&cont,sizeof(CvContour),CV_RETR_EXTERNAL ,CV_CHAIN_APPROX_SIMPLE,cvPoint(0,0)); cvDrawContours(img,cont,cvScalar(255,255,255),cvScalar(0,0,0),1,1); int countROI = 0; //ROI编号 for(;cont;cont = cont->h_next){ double area = fabs(cvContourArea(cont,CV_WHOLE_SEQ)); double len = cvArcLength(cont, CV_WHOLE_SEQ,-1); double circularity = (4*PI*area)/(len*len); //计算疑似火焰区域的圆形度 CvRect r = ((CvContour*)cont)->rect;//子类转换为父类例子 if(r.height * r.width > CONTOUR_MAX_AERA && circularity < 0.4 && circularity > 0.07 ) //面积小的外接矩形以及圆形度过大或过小的轮廓抛弃掉 { printf("\n圆形度:%f\n",circularity); cvRectangle(img, cvPoint(r.x,r.y),cvPoint(r.x + r.width, r.y + r.height),CV_RGB(255,0,0), 1, CV_AA,0); cvSetImageROI(img,r); ROI[countROI] = cvCreateImage(cvGetSize(img),img->depth,img->nChannels); cvCopy(img,ROI[countROI],0); cvResetImageROI(img); (void) time(&the_time); //获得从纪元(1970年1月1日)开始至今的秒数 tm tm_ptr = *localtime(&the_time); //从time_t格式的时间里返回本地时间 //保存的图片名 sprintf(imageName,"%s_%d-%04d年%02d月%02d日%02d时%02分%02d秒.jpg","E:\\Test\\ROI\\fire",countROI, tm_ptr.tm_year-100+2000,tm_ptr.tm_mon + 1, tm_ptr.tm_mday, tm_ptr.tm_hour,tm_ptr.tm_min, tm_ptr.tm_sec); //sprintf(imageName, "%s%d%s", "E:\\Test\\ROI\\image", countROI, ".jpg");//保存的图片名 cvSaveImage(imageName,ROI[countROI]); //保存一帧图片 countROI++; memset(imageName,0,sizeof(imageName)); } } for(int i = 0;i<countROI;i++){ char showImg[16]; sprintf(showImg,"ROI%d",i); cvShowImage(showImg,ROI[i]); } cvShowImage("原始图片",img); cvShowImage("颜色分割处理",colTemp); cvShowImage("填充处理图片",dst); cvShowImage("掩膜",mask); cvWaitKey(); //销毁窗口 cvDestroyAllWindows(); //释放图像 for(int i = 0;i<countROI;i++){ cvReleaseImage(&ROI[i]); } cvReleaseImage(&img); cvReleaseImage(&colTemp); cvReleaseImage(&gray); cvReleaseImage( &mask ); }