《學習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;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章