CodeBook算法的基本思想是獲得每一個像素的時間序列模型。這種模型能很好地處理時間起伏,缺點是須要消耗大量的內存。CodeBook算法爲當前圖像的每個像素創建一個CodeBook(CB)結構,每一個CodeBook結構又由多個CodeWord(CW)組成。
CB和CW的形式以下:
CB={CW1,CW2,…CWn,t}
CW={lHigh,lLow,max,min,t_last,stale}
其中n爲一個CB中所包含的CW的數目,當n過小時,退化爲簡單背景,當n較大時能夠對複雜背景進行建模;t爲CB更新的次數。CW是一個6元組,其中IHigh和ILow做爲更新時的學習上下界,max和min記錄當前像素的最大值和最小值。上次更新的時間t_last和陳舊時間stale(記錄該CW多久未被訪問)用來刪除不多使用的CodeWord。
假設當前訓練圖像I中某一像素爲I(x,y),該像素的CB的更新算法以下,另外記背景閾值的增加斷定閾值爲Bounds:
(1) CB的訪問次數加1;
(2) 遍歷CB中的每一個CW,若是存在一個CW中的IHigh,ILow知足ILow≤I(x,y)≤ IHigh,則轉(4);
(3) 建立一個新的碼字CWnew加入到CB中, CWnew的max與min都賦值爲I(x,y), IHigh <- I(x,y) + Bounds,ILow <- I(x,y) – Bounds,而且轉(6);
(4) 更新該碼字的t_last,若當前像素值I(x,y)大於該碼字的max,則max <- I(x,y),若 I(x,y)小於該碼字的min,則min <- I(x,y);
(5) 更新該碼字的學習上下界,以增長背景模型對於複雜背景的適應能力,具體作法是: 若IHigh < I(x,y) + Bounds,則IHigh 增加1,若ILow > I(x,y) – Bounds,則ILow 減小1;
(6) 更新CB中每一個CW的stale。
使用已創建好的CB進行運動目標檢測的方法很簡單,記判斷前景的範圍上下界爲minMod和maxMod,對於當前待檢測圖像上的某一像素I(x,y),遍歷它對應像素背景模型CB中的每個碼字CW,若存在一個CW,使得I(x,y) < max + maxMod而且I(x,y) > min – minMod,則I(x,y)被判斷爲背景,不然被判斷爲前景。
在實際使用CodeBook進行運動檢測時,除了要隔必定的時間對CB進行更新的同時,須要對CB進行一個時間濾波,目的是去除不多被訪問到的CW,其方法是訪問每一個CW的stale,若stale大於一個閾值(一般設置爲總更新次數的一半),移除該CW。
綜上所述,CodeBook算法檢測運動目標的流程以下:
(1) 選擇一幀到多幀使用更新算法創建CodeBook背景模型;
(2) 按上面所述方法檢測前景(運動目標);
(3) 間隔必定時間使用更新算法更新CodeBook模型,並對CodeBook進行時間濾波;
(4) 若檢測繼續,轉(2),不然結束。ios
codebookWithOpenCV2.hc++
#ifndef ___CODEBOOKWITHOPENCV2_H___ #define ___CODEBOOKWITHOPENCV2_H___ #include <iostream> #include <list> #include <opencv2/opencv.hpp> #define CHANNELS 3 using namespace std; using namespace cv; class CodeWord { public: 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) // 此碼元最長不更新時間,用於刪除規定時間不更新的碼元,精簡碼本 }; class CodeBook { public: list<CodeWord> codeElement; //考慮到方便的刪除舊碼元節點,同時訪問碼元的時候只須要順序遍歷,選用STL中的List,避免用指針。 int numEntries; // 此碼本中碼元的數目 int t; // count every access // 此碼本如今的時間,一幀爲一個時間單位 // 碼本的數據結構 }; class BackgroundSubtractorCodeBook { public: BackgroundSubtractorCodeBook(); void updateCodeBook(const Mat &inputImage);//背景建模 void initialize(const Mat &inputImagee,Mat &outputImage);//獲取第一幀,進行初始化 void clearStaleEntries();//清除舊的碼元 void backgroudDiff(const Mat &inputImage,Mat &outputImage);//背景前景判斷 ~BackgroundSubtractorCodeBook(); private: Mat yuvImage; Mat maskImage; CodeBook* codebookVec; //原本想用Vector,不用數組指針,可是codebook定長,不用增減,vector也很差初始化。 unsigned cbBounds[CHANNELS]; //背景建模時,用於創建學習的上下界限 uchar* pColor; //yuvImage pointer 通道指針 uchar* pMask;// maskImage pointer int imageSize; int nChannels; int minMod[CHANNELS]; //判斷背景或者前景所用調節閾值 int maxMod[CHANNELS]; uchar maskPixelCodeBook; void _updateCodeBookPerPixel(int pixelIndex); void _clearStaleEntriesPerPixel(int pixelIndex); uchar _backgroundDiff(int pixelIndex); }; #endif
codebookWithOpenCV2.cpp算法
#include "codebookWithOpenCV2.h" BackgroundSubtractorCodeBook::BackgroundSubtractorCodeBook() {
nChannels=CHANNELS; } //初始化maskImage,codebook數組,將每一個像素的codebook的碼元個數置0,初始化建模閾值和分割閾值 void BackgroundSubtractorCodeBook::initialize(const Mat &inputRGBImage,Mat &outputImage) { if (inputRGBImage.empty()) { return ; } if (yuvImage.empty()) { yuvImage.create(inputRGBImage.size(),inputRGBImage.type()); } if (maskImage.empty()) { maskImage.create(inputRGBImage.size(),CV_8UC1); Mat temp(inputRGBImage.rows,inputRGBImage.cols,CV_8UC1,Scalar::all(255)); maskImage=temp; } imageSize=inputRGBImage.cols*inputRGBImage.rows; codebookVec=new CodeBook[imageSize]; for (int i=0;i<imageSize;++i) { codebookVec[i].numEntries=0; }//初始化碼元個數 for (int i=0; i<nChannels; i++) { cbBounds[i] = 10; // 用於肯定碼元各通道的建模閥值 minMod[i] = 20; // 用於背景差分函數中 maxMod[i] = 20; // 調整其值以達到最好的分割 } outputImage=maskImage; } //逐個像素建模 void BackgroundSubtractorCodeBook::updateCodeBook(const Mat &inputImage) { cvtColor(inputImage,yuvImage,CV_RGB2YCrCb); pColor=yuvImage.data; for (int c=0;c<imageSize;++c) { _updateCodeBookPerPixel(c); pColor+=3; } } //單個像素建模實現函數,遍歷全部碼元,分三通道匹配,若知足,則更新該碼元的時間,最大,最小值 //若不匹配,則建立新的碼元。 void BackgroundSubtractorCodeBook::_updateCodeBookPerPixel(int pixelIndex) { if (codebookVec[pixelIndex].numEntries==0) { codebookVec[pixelIndex].t=0; } codebookVec[pixelIndex].t+=1; int n; unsigned int high[3],low[3]; for (n=0; n<nChannels; n++) //處理三通道的三個像素 { high[n] = *(pColor+n) + *(cbBounds+n); // *(p+n) 和 p[n] 結果等價,經試驗*(p+n) 速度更快 if(high[n] > 255) high[n] = 255; low[n] = *(pColor+n)-*(cbBounds+n); if(low[n] < 0) low[n] = 0; // 用p 所指像素通道數據,加減cbBonds中數值,做爲此像素閥值的上下限 } int matchChannel; list<CodeWord>::iterator jList; list<CodeWord>::iterator jListAfterPush; for (jList=codebookVec[pixelIndex].codeElement.begin();jList!=codebookVec[pixelIndex].codeElement.end();++jList) { // 遍歷此碼本每一個碼元,測試p像素是否知足其中之一 matchChannel = 0; for (n=0; n<nChannels; n++) //遍歷每一個通道 { if(((*jList).learnLow[n]<= *(pColor+n)) && (*(pColor+n) <= (*jList).learnHigh[n])) //Found an entry for this channel // 若是p 像素通道數據在該碼元閥值上下限之間 { matchChannel++; } } if (matchChannel == nChannels) // If an entry was found over all channels // 若是p 像素各通道都知足上面條件 { (*jList).t_last_update = codebookVec[pixelIndex].t; // 更新該碼元時間爲當前時間 // adjust this codeword for the first channel for (n=0; n<nChannels; n++) //調整該碼元各通道最大最小值 { if ((*jList).max[n] < *(pColor+n)) (*jList).max[n] = *(pColor+n); else if ((*jList).min[n] > *(pColor+n)) (*jList).min[n] = *(pColor+n); } break;//若是知足其中一個碼元,則退出循環。 } } // ENTER A NEW CODE WORD IF NEEDED if(jList==codebookVec[pixelIndex].codeElement.end()) // No existing code word found, make a new one // p 像素不知足此碼本中任何一個碼元,下面建立一個新碼元 { CodeWord newElement; for(n=0; n<nChannels; n++) // 更新新碼元各通道數據 { newElement.learnHigh[n] = high[n]; newElement.learnLow[n] = low[n]; newElement.max[n] = *(pColor+n); newElement.min[n] = *(pColor+n); } newElement.t_last_update = codebookVec[pixelIndex].t; newElement.stale = 0; codebookVec[pixelIndex].numEntries += 1; codebookVec[pixelIndex].codeElement.push_back(newElement);//新的碼元加入鏈表的最後。 } // OVERHEAD TO TRACK POTENTIAL STALE ENTRIES for (jListAfterPush=codebookVec[pixelIndex].codeElement.begin(); jListAfterPush!=codebookVec[pixelIndex].codeElement.end();++jListAfterPush) { // This garbage is to track which codebook entries are going stale int negRun = codebookVec[pixelIndex].t - (*jListAfterPush).t_last_update; // 計算該碼元的不更新時間 if((*jListAfterPush).stale < negRun) (*jListAfterPush).stale = negRun; } // SLOWLY ADJUST LEARNING BOUNDS //對符合的碼元進行更新,剛創建的碼元確定不知足條件,不用考慮 for(n=0; n<nChannels; n++) // 若是像素通道數據在高低閥值範圍內,但在碼元閥值以外,則緩慢調整此碼元學習界限 { if((*jList).learnHigh[n] < high[n]) (*jList).learnHigh[n] += 1;//+1是什麼意思?緩慢調整? if((*jList).learnLow[n] > low[n]) (*jList).learnLow[n] -= 1; } return; } void BackgroundSubtractorCodeBook::clearStaleEntries() { for (int i=0;i<imageSize;++i) { _clearStaleEntriesPerPixel(i); } } void BackgroundSubtractorCodeBook::_clearStaleEntriesPerPixel(int pixelIndex) { int staleThresh=codebookVec[pixelIndex].t; for (list<CodeWord>::iterator itor=codebookVec[pixelIndex].codeElement.begin(); itor!=codebookVec[pixelIndex].codeElement.end();) { if ((*itor).stale>staleThresh) { itor=codebookVec[pixelIndex].codeElement.erase(itor);//erase以後返回被刪的下一個元素的位置 } else { (*itor).stale=0; (*itor).t_last_update=0; itor++; } } codebookVec[pixelIndex].t=0;//碼本時間清零; codebookVec[pixelIndex].numEntries=(int)codebookVec[pixelIndex].codeElement.size(); return; } void BackgroundSubtractorCodeBook:: backgroudDiff(const Mat &inputImage,Mat &outputImage) { cvtColor(inputImage,yuvImage,CV_RGB2YCrCb); pColor=yuvImage.data; pMask =maskImage.data; for(int c=0; c<imageSize; c++) { maskPixelCodeBook = _backgroundDiff(c); *pMask++ = maskPixelCodeBook; pColor += 3; // pColor 指向的是3通道圖像 } outputImage=maskImage.clone(); } uchar BackgroundSubtractorCodeBook::_backgroundDiff(int pixelIndex) { int matchChannels; list<CodeWord>::iterator itor; for (itor=codebookVec[pixelIndex].codeElement.begin(); itor!=codebookVec[pixelIndex].codeElement.end();++itor) { matchChannels=0; for (int n=0;n<nChannels;++n) { if (((*itor).min[n] - minMod[n] <= *(pColor+n)) && (*(pColor+n) <=(*itor).max[n] + maxMod[n])) //相對於背景學習,這裏是與碼元中的最大最小值比較,並加入了餘量minMod,maxMod; matchChannels++; //Found an entry for this channel else break;//一個通道沒匹配,直接退出 } if (matchChannels == nChannels) break; //Found an entry that matched all channels,肯定是背景像素 返回0 黑色 } if (itor==codebookVec[pixelIndex].codeElement.end()) { return(255); } return (0); } BackgroundSubtractorCodeBook::~BackgroundSubtractorCodeBook() { delete [] codebookVec; }
codebookTest.cpp數組
#include "codebookWithOpenCV2.h" int main() { VideoCapture cap; cap.open("people.avi"); if( !cap.isOpened() ) { printf("can not open video file\n"); return -1; } namedWindow("image", WINDOW_NORMAL); namedWindow("foreground mask", WINDOW_NORMAL); BackgroundSubtractorCodeBook bgcbModel; Mat inputImage,outputMaskCodebook; for(int i=0;;++i) { cap>>inputImage; if( inputImage.empty() ) break; if(i==0) { bgcbModel.initialize(inputImage,outputMaskCodebook); } else if (i<=30&&i>0) { bgcbModel.updateCodeBook(inputImage); if (i==30) { bgcbModel.clearStaleEntries(); } } else { bgcbModel.backgroudDiff(inputImage,outputMaskCodebook); } imshow("image",inputImage); imshow("foreground mask",outputMaskCodebook); int c = waitKey(30); if (c == 'q' || c == 'Q' || (c & 255) == 27) break; } return 0; }
代碼是參考以下翻譯過來的。數據結構
/************************************************************************/ /* A few more thoughts on codebook models In general, the codebook method works quite well across a wide number of conditions, and it is relatively quick to train and to run. It doesn’t deal well with varying patterns of light — such as morning, noon, and evening sunshine — or with someone turning lights on or off indoors. This type of global variability can be taken into account by using several different codebook models, one for each condition, and then allowing the condition to control which model is active. */ /************************************************************************/ #include "stdafx.h" #include <cv.h> #include <highgui.h> #include <cxcore.h> #define CHANNELS 3 // 設置處理的圖像通道數,要求小於等於圖像自己的通道數 /////////////////////////////////////////////////////////////////////////// // 下面爲碼本碼元的數據結構 // 處理圖像時每一個像素對應一個碼本,每一個碼本中可有若干個碼元 // 當涉及一個新領域,一般會遇到一些奇怪的名詞,不要被這些名詞嚇壞,其實思路都是簡單的 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) // 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: // cvBounds must be of size cvBounds[numChannels] // // RETURN // codebook index int cvupdateCodeBook(uchar *p, codeBook &c, unsigned *cbBounds, int numChannels) { if(c.numEntries == 0) 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] = 255; low[n] = *(p+n)-*(cbBounds+n); if(low[n] < 0) low[n] = 0; // 用p 所指像素通道數據,加減cbBonds中數值,做爲此像素閥值的上下限 } //SEE IF THIS FITS AN EXISTING CODEWORD int matchChannel; 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 // 若是p 像素通道數據在該碼元閥值上下限之間 { matchChannel++; } } if (matchChannel == numChannels) // If an entry was found over all channels // 若是p 像素各通道都知足上面條件 { c.cb[i]->t_last_update = c.t; // 更新該碼元時間爲當前時間 // adjust this codeword for the first channel for (n=0; n<numChannels; n++) //調整該碼元各通道最大最小值 { if (c.cb[i]->max[n] < *(p+n)) c.cb[i]->max[n] = *(p+n); else if (c.cb[i]->min[n] > *(p+n)) c.cb[i]->min[n] = *(p+n); } break; } } // 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 個指向碼元的指針 for(int ii=0; ii<c.numEntries; ii++) // 將前c.numEntries 個指針指向已存在的每一個碼元 foo[ii] = c.cb[ii]; foo[c.numEntries] = new code_element; // 申請一個新的碼元 if(c.numEntries) delete [] c.cb; // 刪除c.cb 指針數組 c.cb = foo; // 把foo 頭指針賦給c.cb for(n=0; n<numChannels; n++) // 更新新碼元各通道數據 { c.cb[c.numEntries]->learnHigh[n] = high[n]; c.cb[c.numEntries]->learnLow[n] = low[n]; c.cb[c.numEntries]->max[n] = *(p+n); 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++) { // This garbage is to track which codebook entries are going stale int negRun = c.t - c.cb[s]->t_last_update; // 計算該碼元的不更新時間 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; } if (matchChannel == numChannels) break; //Found an entry that matched all channels } if(i == c.numEntries) // p像素各通道值知足碼本中其中一個碼元,則返回白色 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]) { 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); } 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 = cvCreateFileCapture("tree.avi"); 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++) // 初始化每一個碼元數目爲0 cB[i].numEntries = 0; 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); // 指向yuvImage 圖像的通道數據 for (int c=0; c<imageLen; c++) { cvupdateCodeBook(pColor, cB[c], cbBounds, nChannels); // 對每一個像素,調用此函數,捕捉背景中相關變化圖像 pColor += 3; // 3 通道圖像, 指向下一個像素通道數據 } if (i == 30) // 到30 幀時調用下面函數,刪除碼本中陳舊的碼元 { for (int c=0; c<imageLen; c++) cvclearStaleEntries(cB[c]); } } else { uchar maskPixelCodeBook; pColor = (uchar *)((yuvImage)->imageData); //3 channel yuv image uchar *pMask = (uchar *)((ImaskCodeBook)->imageData); //1 channel image // 指向ImaskCodeBook 通道數據序列的首元素 for(int c=0; c<imageLen; c++) { maskPixelCodeBook = cvbackgroundDiff(pColor, cB[c], nChannels, minMod, maxMod); // 我看到這兒時豁然開朗,開始理解了codeBook 呵呵 *pMask++ = maskPixelCodeBook; pColor += 3; // pColor 指向的是3通道圖像 } } if (!(rawImage = cvQueryFrame(capture))) break; cvShowImage("Raw", rawImage); cvShowImage("CodeBook", ImaskCodeBook); if (cvWaitKey(30) == 27) break; if (i == 56 || i == 63) cvWaitKey(); } cvReleaseCapture(&capture); if (yuvImage) cvReleaseImage(&yuvImage); if(ImaskCodeBook) cvReleaseImage(&ImaskCodeBook); cvDestroyAllWindows(); delete [] cB; return 0; }
另外,在stackoverflow上找到的代碼,未測試,可參考。ide
bgfg_cb.h函數
#ifndef __bgfg_cb_h__ #define __bgfg_cb_h__ //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.148.9778&rep=rep1&type=pdf #include "opencv2/core/core.hpp" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> #include <time.h> using namespace cv; using namespace std; struct codeword { float min; float max; float f; float l; int first; int last; bool isStale; }; extern int alpha ; extern float beta ; extern int Tdel ,Tadd , Th; void initializeCodebook(int w,int h); void update_cb(Mat& frame); void fg_cb(Mat& frame,Mat& fg); #endif
bgfg_cb.cpp學習
#include "opencv2/core/core.hpp" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> #include <time.h> #include "bgfg_cb.h" using namespace cv; using namespace std; vector<codeword> **cbMain; vector<codeword> **cbCache; int t=0; int alpha = 10;//knob float beta =1; int Tdel = 200,Tadd = 150, Th= 200;//knobs void initializeCodebook(int w,int h) { cbMain = new vector<codeword>*[w]; for(int i = 0; i < w; ++i) cbMain[i] = new vector<codeword>[h]; cbCache = new vector<codeword>*[w]; for(int i = 0; i < w; ++i) cbCache[i] = new vector<codeword>[h]; } void update_cb(Mat& frame) { if(t>10) return; for(int i=0;i<frame.rows;i++) { for(int j=0;j<frame.cols;j++) { int pix = frame.at<uchar>(i,j); vector<codeword>& cm =cbMain[i][j]; bool found = false; for(int k=0;k<cm.size();k++) { if(cm[k].min<=pix && pix<=cm[k].max && !found) { found=true; cm[k].min = ((pix-alpha)+(cm[k].f*cm[k].min))/(cm[k].f+1); cm[k].max = ((pix+alpha)+(cm[k].f*cm[k].max))/(cm[k].f+1); cm[k].l=0; cm[k].last=t; cm[k].f++; }else { cm[k].l++; } } if(!found) { codeword n={}; n.min=max(0,pix-alpha); n.max=min(255,pix+alpha); n.f=1; n.l=0; n.first=t; n.last=t; cm.push_back(n); } } } t++; } void fg_cb(Mat& frame,Mat& fg) { fg=Mat::zeros(frame.size(),CV_8UC1); if(cbMain==0) initializeCodebook(frame.rows,frame.cols); if(t<10) { update_cb(frame); return; } for(int i=0;i<frame.rows;i++) { for(int j=0;j<frame.cols;j++) { int pix = frame.at<uchar>(i,j); vector<codeword>& cm = cbMain[i][j]; bool found = false; for(int k=0;k<cm.size();k++) { if(cm[k].min<=pix && pix<=cm[k].max && !found) { cm[k].min = ((1-beta)*(pix-alpha)) + (beta*cm[k].min); cm[k].max = ((1-beta)*(pix+alpha)) + (beta*cm[k].max); cm[k].l=0; cm[k].first=t; cm[k].f++; found=true; }else { cm[k].l++; } } cm.erase( remove_if(cm.begin(), cm.end(), [](codeword& c) { return c.l>=Tdel;} ), cm.end() ); fg.at<uchar>(i,j) = found?0:255; if(found) continue; found = false; vector<codeword>& cc = cbCache[i][j]; for(int k=0;k<cc.size();k++) { if(cc[k].min<=pix && pix<=cc[k].max && !found) { cc[k].min = ((1-beta)*(pix-alpha)) + (beta*cc[k].min); cc[k].max = ((1-beta)*(pix+alpha)) + (beta*cc[k].max); cc[k].l=0; cc[k].first=t; cc[k].f++; found=true; }else { cc[k].l++; } } if(!found) { codeword n={}; n.min=max(0,pix-alpha); n.max=min(255,pix+alpha); n.f=1; n.l=0; n.first=t; n.last=t; cc.push_back(n); } cc.erase( remove_if(cc.begin(), cc.end(), [](codeword& c) { return c.l>=Th;} ), cc.end() ); for(vector<codeword>::iterator it=cc.begin();it!=cc.end();it++) { if(it->f>Tadd) { cm.push_back(*it); } } cc.erase( remove_if(cc.begin(), cc.end(), [](codeword& c) { return c.f>Tadd;} ), cc.end() ); } } }
main.cpp測試
#include "opencv2/core/core.hpp" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include "bgfg_cb.h" #include <iostream> using namespace cv; using namespace std; void proc() { Mat frame,fg,gray; VideoCapture cap("C:/Downloads/S2_L1.tar/S2_L1/Crowd_PETS09/S2/L1/Time_12-34/View_001/frame_%04d.jpg"); cap >> frame; initializeCodebook(frame.rows,frame.cols); for(;;) { cap>>frame; cvtColor(frame,gray,CV_BGR2GRAY); fg_cb(gray,fg); Mat cc; imshow("fg",fg); waitKey(1); } } int main(int argc, char ** argv) { proc(); cin.ignore(1); return 0; }