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;
}