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

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

#include "cv.h"	
#include "highgui.h"
#include "cxcore.h"

/**********************************************************************************/
//设置处理的图像通道数,要求小于等于图像本身的通道数
#define CHANNELS 3		
//某些颜色的宏定义
#define CV_CVX_WHITE	CV_RGB(0xff,0xff,0xff)
#define CV_CVX_BLACK	CV_RGB(0x00,0x00,0x00)

//For connected components:
int CVCONTOUR_APPROX_LEVEL = 2;     // Approx.threshold - the bigger it is, the simpler is the boundary
int CVCLOSE_ITR = 1;				// How many iterations of erosion and/or dialation there should be
/**********************************************************************************/


/**********************************************************************************/
//下面为码本码元的数据结构
//处理图像时每个像素对应一个码本code_book,每个码本中可有若干个码元code_element
typedef 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
								// 属于此码元的像素中各通道的最小值
	int	t_last_update;			// This is book keeping to allow us to kill stale entries
								// 此码元最后一次更新的时间,每一帧为一个单位时间,用于计算stale
	int	stale;					// max negative run (biggest period of inactivity)
								// 此码元最长不更新时间,用于删除规定时间不更新的码元,精简码本
} code_element;					// 码元的数据结构

typedef struct code_book 
{
	code_element **cb;			// 码元的二维指针,理解为指向码元指针数组的指针,使得添加码元时不需要来回复制码元,只需要简单的指针赋值即可
	int	numEntries;				// 此码本中码元的数目	
	int	t;						// count every access
								// 此码本现在的时间,一帧为一个时间单位;记录从开始或最后一次清除操作之间累积的像素点的数目
} codeBook;						// 码本的数据结构
/**********************************************************************************/


/**********************************************************************************/
// int updateCodeBook( uchar* p, codeBook &c, unsigned* cbBounds, int numChannels )
// Updates the codebook entry with a new data point
//
// p			Pointer to a YUV pixel
// c			Codebook for this pixel
// cbBounds		Learning bounds for codebook (Rule of thumb: 10)
// numChannels	Number of color channels we're learning
//
// NOTES		cbBounds must be of size cbBounds[numChannels]
//		
// RETURN		codebook index
/**********************************************************************************/
int cvupdateCodeBook( uchar* p, codeBook &c, unsigned* cbBounds, int numChannels )
{
	if (c.numEntries == 0)					// 码本中码元为零时,在main函数中一开始为零
		c.t = 0;							// 初始化时间为0	
	c.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赋值为p
					c.cb[i]->max[n] = *(p+n);
				else if (c.cb[i]->min[n] > *(p+n))	//如果像素p小于码元的min,则码元的min赋值为p
					c.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.cb	
		for (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
//
// p			pixel pointer (YUV interleaved)
// c			codebook reference
// numChannels  Number of channels we are testing
// maxMod		Add this (possibly negative) number onto max level when code_element determining if new pixel is foreground
// minMod		Subract this (possible negative) number from min level code_element when determining if pixel is foreground
//
// NOTES		minMod and maxMod must have length numChannels, e.g. 3 channels => minMod[3], maxMod[3].
// 
// Return		0 => 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 channel
			else
				break;							// 如果有一通道不符合,则跳出for循环
		}
		if (matchChannel == numChannels)		// 如果第i个码元所有通道都符合(后面的码元不用检测了),则跳出for循环,此时i<c.numEntries
			break;								// 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
//
// c			Codebook to clean up
//
// Return		number 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 STALE
	for (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 keep
			keepCnt += 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 clearStale
			foo[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
//
// mask			Is a grayscale (8 bit depth) "raw" mask image which will be cleaned up
//
// OPTIONAL PARAMETERS:
// poly1_hull0	If 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)
// num			Maximum number of rectangles and/or centers to return, on return, will contain number filled (DEFAULT: NULL)
// bbs			Pointer to bounding box rectangle vector of length num.  (DEFAULT SETTING: NULL)
// centers		Pointer 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 small
			cvSubstituteContour( 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 segmentation
				c_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;
	unsigned	cbBounds[CHANNELS];
	uchar*		pColor;					//YUV pointer
	int			imageLen;
	int			nChannels = CHANNELS;
	int			minMod[CHANNELS];
	int			maxMod[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 image
			uchar* 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
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值