《学习OpenCV》codebook法+连通域法(page319)

来源:互联网 发布:手机阿里和淘宝的区别 编辑:程序博客网 时间:2024/05/20 06:06

codebook能够通过学习,消除轻微移动的背景(如摇摆的树叶)的影响;而连通域法能够消除背景建模产生的少量噪声,从而产生一个相对精确的目标轮廓。另外通过测试,codebook一个可能的最大的缺点是对光线非常敏感。

#include "cv.h"#include "highgui.h"#include "cxcore.h"/**********************************************************************************///设置处理的图像通道数,要求小于等于图像本身的通道数#define CHANNELS 3//某些颜色的宏定义#define CV_CVX_WHITECV_RGB(0xff,0xff,0xff)#define CV_CVX_BLACKCV_RGB(0x00,0x00,0x00)//For connected components:int CVCONTOUR_APPROX_LEVEL = 2;     // Approx.threshold - the bigger it is, the simpler is the boundaryint CVCLOSE_ITR = 1;// How many iterations of erosion and/or dialation there should be/**********************************************************************************//**********************************************************************************///下面为码本码元的数据结构//处理图像时每个像素对应一个码本code_book,每个码本中可有若干个码元code_elementtypedef struct ce {uchar learnHigh[CHANNELS];// High side threshold for learning// 此码元各通道的阀值上限(学习界限)uchar learnLow[CHANNELS];// Low side threshold for learning// 此码元各通道的阀值下限// 学习过程中如果一个新像素各通道值x[i],均有 learnLow[i]<=x[i]<=learnHigh[i],则该像素可合并于此码元uchar max[CHANNELS];// High side of box boundary// 属于此码元的像素中各通道的最大值uchar min[CHANNELS];// Low side of box boundary// 属于此码元的像素中各通道的最小值intt_last_update;// This is book keeping to allow us to kill stale entries// 此码元最后一次更新的时间,每一帧为一个单位时间,用于计算staleintstale;// max negative run (biggest period of inactivity)// 此码元最长不更新时间,用于删除规定时间不更新的码元,精简码本} code_element;// 码元的数据结构typedef struct code_book {code_element **cb;// 码元的二维指针,理解为指向码元指针数组的指针,使得添加码元时不需要来回复制码元,只需要简单的指针赋值即可intnumEntries;// 此码本中码元的数目intt;// count every access// 此码本现在的时间,一帧为一个时间单位;记录从开始或最后一次清除操作之间累积的像素点的数目} codeBook;// 码本的数据结构/**********************************************************************************//**********************************************************************************/// int updateCodeBook( uchar* p, codeBook &c, unsigned* cbBounds, int numChannels )// Updates the codebook entry with a new data point//// pPointer to a YUV pixel// cCodebook for this pixel// cbBoundsLearning bounds for codebook (Rule of thumb: 10)// numChannelsNumber of color channels we're learning//// NOTEScbBounds must be of size cbBounds[numChannels]//// RETURNcodebook index/**********************************************************************************/int cvupdateCodeBook( uchar* p, codeBook &c, unsigned* cbBounds, int numChannels ){if (c.numEntries == 0)// 码本中码元为零时,在main函数中一开始为零c.t = 0;// 初始化时间为0c.t += 1;// Record learning event// 码本时间,记录学习的次数,每调用一次加一,即每一帧图像加一/*SET HIGH AND LOW BOUNDS*/int n;unsigned int high[3], low[3];for (n=0; n<numChannels; n++)//遍历三通道{high[n] = *(p+n) + *(cbBounds+n);// *(p+n) 和 p[n] 结果等价,经试验*(p+n)速度更快if (high[n]>255)// high[n] = p[n] + cbBounds[n],上限阈值high[n] = 255;low[n] = *(p+n)- *(cbBounds+n);// low[n] = p[n] - cbBounds[n],下限阈值if (low[n]<0) low[n] = 0;// 用p 所指像素通道数据,加减cbBonds中数值,作为此像素阀值的上下限}/*SEE IF THIS FITS AN EXISTING CODEWORD*/int matchChannel;// 像素p符合码元的通道数int i;// 码本中码元的序数for (i=0; i<c.numEntries; i++)// 遍历此码本每个码元,测试p像素是否满足其中之一{matchChannel = 0;for (n=0; n<numChannels; n++)// 遍历每个通道{if ((c.cb[i]->learnLow[n]<=*(p+n)) && (*(p+n)<=c.cb[i]->learnHigh[n]))// Found an entry for this channel// 码本c的第i个码元的learnlow[n] <= p[n] <= 码本c的第i个码元的learnhigh[n]// 即如果p像素通道数据在该码元阀值上下限之间matchChannel++;// 如果每个通道都符合,则matchChannel = numChannels}if (matchChannel == numChannels)// If an entry was found over all channels// 如果p 像素各通道都满足上面条件{c.cb[i]->t_last_update = c.t;// 更新该码元时间为码本时间,即当前时间for (n=0; n<numChannels; n++)//对每一通道,调整该码元最大最小值{if (c.cb[i]->max[n] < *(p+n))//如果像素p大于码元的max,则码元的max赋值为pc.cb[i]->max[n] = *(p+n);else if (c.cb[i]->min[n] > *(p+n))//如果像素p小于码元的min,则码元的min赋值为pc.cb[i]->min[n] = *(p+n);}break;// 跳出“遍历此码本每个码元”这个循环,即像素p三通道都符合码本中某一码元,则不用遍历以下的码元}// 此时,i<c.numEntries}/*ENTER A NEW CODE WORD IF NEEDED*/if (i==c.numEntries)// No existing code word found, make a new one// p 像素不满足此码本中任何一个码元,下面创建一个新码元{code_element **foo = new code_element* [c.numEntries+1];// 为c.numEntries+1个指向码元数组的指针分配空间,比原码本的码元个数多1个for (int ii=0; ii<c.numEntries; ii++)foo[ii] = c.cb[ii];// 将原码本的码元赋给新码元,即前c.numEntries个指针指向新分配的每个码元foo[c.numEntries] = new code_element;// 为最后一个新码元申请空间if (c.numEntries) delete [] c.cb;// 删除c.cb 指针数组(注意:delete[]与new[]相对应使用)c.cb = foo;// 把foo 头指针赋给c.cbfor (n=0; n<numChannels; n++)// 更新新码元各通道数据{c.cb[c.numEntries]->learnHigh[n] = high[n];// 新码元的learnhigh为上限阈值c.cb[c.numEntries]->learnLow[n] = low[n];// learnlow为下限阈值c.cb[c.numEntries]->max[n] = *(p+n);// max与min为像素p的值c.cb[c.numEntries]->min[n] = *(p+n);}c.cb[c.numEntries]->t_last_update = c.t;// 将码元时间设置为码本时间c.cb[c.numEntries]->stale = 0;c.numEntries += 1;// 在这里改变码元个数}/*OVERHEAD TO TRACK POTENTIAL STALE ENTRIES*/for (int s=0; s<c.numEntries; s++){int negRun = c.t-c.cb[s]->t_last_update;// This garbage is to track which codebook entries are going stale// 计算该码元的不更新时间if (c.cb[s]->stale < negRun) c.cb[s]->stale = negRun;}/*SLOWLY ADJUST LEARNING BOUNDS*/for (n=0; n<numChannels; n++)// 如果像素通道数据在高低阀值范围内,但在码元阀值之外,则缓慢调整此码元学习界限{if (c.cb[i]->learnHigh[n] < high[n]) c.cb[i]->learnHigh[n] += 1;if (c.cb[i]->learnLow[n] > low[n]) c.cb[i]->learnLow[n] -= 1;}return(i);}/**********************************************************************************/// uchar cvbackgroundDiff( uchar* p, codeBook &c, int minMod, int maxMod )// Given a pixel and a code book, determine if the pixel is covered by the codebook//// ppixel pointer (YUV interleaved)// ccodebook reference// numChannels  Number of channels we are testing// maxModAdd this (possibly negative) number onto max level when code_element determining if new pixel is foreground// minModSubract this (possible negative) number from min level code_element when determining if pixel is foreground//// NOTESminMod and maxMod must have length numChannels, e.g. 3 channels => minMod[3], maxMod[3].// // Return0 => background, 255 => foreground/**********************************************************************************/uchar cvbackgroundDiff( uchar* p, codeBook &c, int numChannels, int* minMod, int* maxMod ){int matchChannel;// 下面步骤和背景学习中查找码元如出一辙/*SEE IF THIS FITS AN EXISTING CODEWORD*/int i;for (i=0; i<c.numEntries; i++){matchChannel = 0;for (int n=0; n<numChannels; n++){if ((c.cb[i]->min[n]-minMod[n]<= *(p+n)) && (*(p+n)<=c.cb[i]->max[n]+maxMod[n]))matchChannel++;// Found an entry for this channelelsebreak;// 如果有一通道不符合,则跳出for循环}if (matchChannel == numChannels)// 如果第i个码元所有通道都符合(后面的码元不用检测了),则跳出for循环,此时i<c.numEntriesbreak;// Found an entry that matched all channels}if (i == c.numEntries)// 此时没有一个码元符合,即证明是前景,返回255(白色)return(255);return(0);}//UTILITES///////////////////////////////////////////////////////////////////////////**********************************************************************************/// int clearStaleEntries( codeBook &c )// After you've learned for some period of time, periodically call this to clear // out stale codebook entries//// cCodebook to clean up//// Returnnumber of entries cleared/**********************************************************************************/int cvclearStaleEntries( codeBook &c ){int staleThresh = c.t >> 1;// 设定刷新时间int* keep = new int [c.numEntries];// 申请一个标记数组,数组元素数目为码本中码元的个数int keepCnt = 0;// 记录不删除码元数目// SEE WHICH CODEBOOK ENTRIES ARE TOO STALEfor (int i=0; i<c.numEntries; i++)// 遍历码本中每个码元{if (c.cb[i]->stale > staleThresh)// 如码元中的不更新时间大于设定的刷新时间,则标记为删除keep[i] = 0;// Mark for destruction,标记else{keep[i] = 1;// Mark to keepkeepCnt += 1;// 记录不删除码元数目}}/*KEEP ONLY THE GOOD*/c.t = 0;// Full reset on stale tracking// 码本时间清零code_element **foo = new code_element* [keepCnt];// 申请大小为keepCnt 的码元指针数组int k=0;for (int ii=0; ii<c.numEntries; ii++){if (keep[ii])// 如果keep[ii] = 0则不进入,对应要删除的码元{foo[k] = c.cb[ii];foo[k]->stale = 0;// We have to refresh these entries for next clearStalefoo[k]->t_last_update = 0;k++;}}/*CLEAN UP*/delete [] keep;delete [] c.cb;c.cb = foo;// 把foo 头指针地址赋给c.cb int numCleared = c.numEntries - keepCnt;// 被清理的码元个数c.numEntries = keepCnt;// 剩余的码元个数return(numCleared);// 返回被清理的码元个数}/**********************************************************************************/// void cvconnectedComponents( IplImage* mask, int poly1_hull0, float perimScale, int* num, CvRect* bbs, CvPoint* centers )// This cleans up the forground segmentation mask derived from calls to cvbackgroundDiff//// maskIs a grayscale (8 bit depth) "raw" mask image which will be cleaned up//// OPTIONAL PARAMETERS:// poly1_hull0If set, approximate connected component by (DEFAULT) polygon, or else convex hull (0)// perimScale Len = image (width+height)/perimScale.  If contour len < this, delete that contour (DEFAULT: 4)// numMaximum number of rectangles and/or centers to return, on return, will contain number filled (DEFAULT: NULL)// bbsPointer to bounding box rectangle vector of length num.  (DEFAULT SETTING: NULL)// centersPointer to contour centers vectore of length num (DEFULT: NULL)/**********************************************************************************/void cvconnectedComponents( IplImage* mask, int poly1_hull0, float perimScale, int* num, CvRect* bbs, CvPoint* centers ){static CvMemStorage* mem_storage = NULL;static CvSeq* contours = NULL;/*CLEAN UP RAW MASK*/cvMorphologyEx( mask, mask, NULL, NULL, CV_MOP_OPEN, CVCLOSE_ITR );// 对mask进行开运算(消除高亮的孤立点)cvMorphologyEx( mask, mask, NULL, NULL, CV_MOP_CLOSE, CVCLOSE_ITR );// 对mask进行闭运算(消除低亮的孤立点)/*FIND CONTOURS AROUND ONLY BIGGER REGIONS*/if (mem_storage==NULL) mem_storage = cvCreateMemStorage(0);    else cvClearMemStorage(mem_storage);CvContourScanner scanner = cvStartFindContours( mask, mem_storage, sizeof(CvContour), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE );// 该函数每次返回一个轮廓CvSeq* c;int numCont = 0;while ((c=cvFindNextContour(scanner)) != NULL)// 查找剩余轮廓,一直循环,直至为空{double len = cvContourPerimeter(c);// 返回轮廓的周长double q = (mask->height + mask->width)/perimScale;// calculate perimeter len threshold// 计算轮廓周长的阈值if (len<q)// Get rid of blob if it's perimeter is too smallcvSubstituteContour( scanner, NULL );// 舍弃轮廓周长过小的轮廓else// Smooth it's edges if it's large enough{CvSeq* c_new;if( poly1_hull0 )// Polygonal approximation of the segmentation            c_new = cvApproxPoly( c,// 若poly1_hull0为1,则进行多边形逼近  sizeof(CvContour),   mem_storage,   CV_POLY_APPROX_DP,   CVCONTOUR_APPROX_LEVEL,// 计算多边形逼近的精度  0 );else// Convex Hull of the segmentationc_new = cvConvexHull2(c,mem_storage,CV_CLOCKWISE,1);// 若为0,则进行hull矩操作            cvSubstituteContour( scanner, c_new );// 新处理后的序列取代原序列numCont++;        }}contours = cvEndFindContours( &scanner );/*PAINT THE FOUND REGIONS BACK INTO THE IMAGE*/cvZero(mask);IplImage* maskTemp;/*CALC CENTER OF MASS AND OR BOUNDING RECTANGLES,如果num非空就计算某些参数*/if (num!=NULL){int N =* num, numFilled=0, i=0;CvMoments moments;double M00, M01, M10;maskTemp = cvCloneImage(mask);for (i=0,c=contours; c!=NULL; c=c->h_next,i++){if (i<N)// Only process up to *num of them{cvDrawContours( maskTemp, c, CV_CVX_WHITE, CV_CVX_WHITE, -1, CV_FILLED, 8 );/*Find the center of each contour,如果center非空就计算图像重心*/if (centers!=NULL){cvMoments( maskTemp, &moments, 1 );M00 = cvGetSpatialMoment( &moments, 0, 0 );M10 = cvGetSpatialMoment( &moments, 1, 0 );M01 = cvGetSpatialMoment( &moments, 0, 1 );centers[i].x = (int)(M10/M00);// 通过中心矩计算图像的重心centers[i].y = (int)(M01/M00);}/*Bounding rectangles around blobs,如果bbs非空就计算轮廓的边界框*/if (bbs!=NULL){bbs[i] = cvBoundingRect(c);// 计算边界框}cvZero(maskTemp);numFilled++;}/*Draw filled contours into mask*/cvDrawContours( mask, c, CV_CVX_WHITE, CV_CVX_WHITE, -1, CV_FILLED, 8 ); //draw to central mask}//end looping over contours*num = numFilled;cvReleaseImage( &maskTemp );}/*ELSE JUST DRAW PROCESSED CONTOURS INTO THE MASK,如果num为空则只画轮廓就可以了*/else{for (c=contours; c!=NULL; c=c->h_next){cvDrawContours( mask, c, CV_CVX_WHITE, CV_CVX_BLACK, -1, CV_FILLED, 8 );}}}int main(){/*需要使用的变量*/CvCapture*capture;IplImage*rawImage;IplImage*yuvImage;IplImage*ImaskCodeBook;codeBook*cB;unsignedcbBounds[CHANNELS];uchar*pColor;//YUV pointerintimageLen;intnChannels = CHANNELS;intminMod[CHANNELS];intmaxMod[CHANNELS];/*初始化变量,从摄像头载入影像*/cvNamedWindow( "Raw" );cvNamedWindow( "CodeBook" );capture = cvCreateCameraCapture(0);if (!capture){printf("Couldn't open the capture!");return -1;}rawImage = cvQueryFrame(capture);// 从影像中获取每一帧的图像yuvImage = cvCreateImage( cvGetSize(rawImage), 8, 3 );// 给yuvImage 分配一个和rawImage 尺寸相同,8位3通道图像ImaskCodeBook = cvCreateImage( cvGetSize(rawImage), IPL_DEPTH_8U, 1 );// 为ImaskCodeBook 分配一个和rawImage 尺寸相同,8位单通道图像cvSet( ImaskCodeBook, cvScalar(255));// 设置单通道数组所有元素为255,即初始化为白色图像imageLen = rawImage->width * rawImage->height;// 源图像的面积,亦即像素个数cB = new codeBook[imageLen];// 得到与图像像素数目长度一样的一组码本,以便对每个像素进行处理for (int i=0; i<imageLen; i++)cB[i].numEntries = 0;// 初始化每个码本的码元数目为0,共imageLen个码本,每一个像素对应一个码本for (int i=0; i<nChannels; i++){cbBounds[i] = 10;// 用于确定码元各通道的阀值minMod[i]= 20;// 用于背景差分函数中maxMod[i]= 20;// 调整其值以达到最好的分割}/*开始处理视频每一帧图像*/for (int i=0; ; i++)// 没有跳出循环条件,死循环{cvCvtColor( rawImage, yuvImage, CV_BGR2YCrCb );// 色彩空间转换,将rawImage 转换到YUV色彩空间,输出到yuvImage// 即使不转换效果依然很好//yuvImage = cvCloneImage(rawImage);if (i<=30)// 30帧内进行背景学习{pColor = (uchar*)(yuvImage->imageData);// pColor指向指向yuvImage图像首地址for (int c=0; c<imageLen; c++){cvupdateCodeBook( pColor, cB[c], cbBounds, nChannels );// 对图像的每个像素,调用此函数,捕捉背景中相关变化图像// 对每一像素pColor,设置对应的码本cB[c]pColor += 3;// 3通道图像, 指向下一个像素的第一通道数据,在函数中对n通道进行处理}if (i==30)// 到30帧时调用下面函数,删除码本中陈旧的码元{for (int c=0; c<imageLen; c++)cvclearStaleEntries(cB[c]);// 遍历所有码本,删除每一个码本中陈旧的码元}}else{uchar maskPixelCodeBook;// 30帧过后pColor = (uchar*)((yuvImage)->imageData);// 3 channel yuv imageuchar* pMask = (uchar*)((ImaskCodeBook)->imageData);// 1 channel image// pMask指向ImaskCodeBook图像的首地址for (int c=0; c<imageLen; c++){maskPixelCodeBook = cvbackgroundDiff( pColor, cB[c], nChannels, minMod, maxMod );// 背景处理,对每一个像素判断是否为前景(白色)、背景(黑色)*pMask++ = maskPixelCodeBook;// pMask指针指向的元素,先自加,再赋值// 即将maskPixelCodeBook的值赋给ImaskCodeBook图像(单通道)pColor += 3;// pColor 指向的是3通道图像}}if (!(rawImage = cvQueryFrame(capture)))// 影像播放完毕,跳出for循环break;cvconnectedComponents( ImaskCodeBook, 1, 4, NULL, NULL, NULL );// 连通域法消除噪声cvShowImage( "Raw", rawImage );// 循环显示图片,即播放影像cvShowImage( "CodeBook", ImaskCodeBook );if (cvWaitKey(30) == 27)// 按ESC键退出break;}/*释放内存,销毁窗口*/cvReleaseCapture( &capture );if (yuvImage)cvReleaseImage( &yuvImage );if(ImaskCodeBook) cvReleaseImage( &ImaskCodeBook );cvDestroyAllWindows();delete [] cB;return 0;}

0 0