codebook背景建模

摘要 codebook的建模效果比平均背景法好很多,建模過程中可以適應運動。CodeBook算法的基本思想是得到每個像素的時間序列模型。這種模型能很好地處理時間起伏,缺點是需要消耗大量的內存。

導讀

《Learning OpenCV》一書當中介紹的第二種背景建模方法是codebook。直接通過書本來理解codebook算法有點困難,可以按照下面的順序來理解codebook算法,首先看看百度百科上對這個算法的基本原理的闡述,我認爲百度百科上的描述已經比較直觀,但當中有很多細節的東西還需要看具體的代碼,所以可以通過細讀下面轉載的代碼來理解codebook算法,理解代碼的過程需要有點耐心,先看main函數,理解程序大致的流程,再仔細看看cvupdateCodeBook()、cvclearStaleEntries()、cvbackgroundDiff()這三個函數,看懂了代碼之後就應該能夠理解這個算法了 。下面闡述的基本原理部分來自於百度百科,已經闡述得比較直觀。代碼來自於網友的博文http://blog.csdn.net/zcube/article/details/7353941

基本原理

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),否則結束。

參考代碼

代碼來自於網友的博文http://blog.csdn.net/zcube/article/details/7353941

001 /************************************************************************/
002 /*          A few more thoughts on codebook models
003 In general, the codebook method works quite well across a wide number of conditions,
004 and it is relatively quick to train and to run. It doesn’t deal well with varying patterns of
005 light — such as morning, noon, and evening sunshine — or with someone turning lights
006 on or off indoors. This type of global variability can be taken into account by using
007 several different codebook models, one for each condition, and then allowing the condition
008 to control which model is active.                                       */
009 /************************************************************************/
010  
011 #include "stdafx.h"
012 #include <cv.h>          
013 #include <highgui.h>
014 #include <cxcore.h>
015  
016 #define CHANNELS 3     
017 // 設置處理的圖像通道數,要求小於等於圖像本身的通道數
018  
019 ///////////////////////////////////////////////////////////////////////////
020 // 下面爲碼本碼元的數據結構
021 // 處理圖像時每個像素對應一個碼本,每個碼本中可有若干個碼元
022 // 當涉及一個新領域,通常會遇到一些奇怪的名詞,不要被這些名詞嚇壞,其實思路都是簡單的
023 typedef struct ce {
024     uchar   learnHigh[CHANNELS];    // High side threshold for learning
025     // 此碼元各通道的閥值上限(學習界限)
026     uchar   learnLow[CHANNELS];     // Low side threshold for learning
027     // 此碼元各通道的閥值下限
028     // 學習過程中如果一個新像素各通道值x[i],均有 learnLow[i]<=x[i]<=learnHigh[i],則該像素可合併於此碼元
029     uchar   max[CHANNELS];          // High side of box boundary
030     // 屬於此碼元的像素中各通道的最大值
031     uchar   min[CHANNELS];          // Low side of box boundary
032     // 屬於此碼元的像素中各通道的最小值
033     int     t_last_update;          // This is book keeping to allow us to kill stale entries
034     // 此碼元最後一次更新的時間,每一幀爲一個單位時間,用於計算stale
035     int     stale;                  // max negative run (biggest period of inactivity)
036     // 此碼元最長不更新時間,用於刪除規定時間不更新的碼元,精簡碼本
037 } code_element;                     // 碼元的數據結構
038  
039 typedef struct code_book {
040     code_element    **cb;
041     // 碼元的二維指針,理解爲指向碼元指針數組的指針,使得添加碼元時不需要來回複製碼元,只需要簡單的指針賦值即可
042     int             numEntries;
043     // 此碼本中碼元的數目
044     int             t;              // count every access
045     // 此碼本現在的時間,一幀爲一個時間單位
046 } codeBook;                         // 碼本的數據結構
047  
048  
049 ///////////////////////////////////////////////////////////////////////////////////
050 // int updateCodeBook(uchar *p, codeBook &c, unsigned cbBounds)
051 // Updates the codebook entry with a new data point
052 //
053 // p            Pointer to a YUV pixel
054 // c            Codebook for this pixel
055 // cbBounds     Learning bounds for codebook (Rule of thumb: 10)
056 // numChannels  Number of color channels we're learning
057 //
058 // NOTES:
059 //      cvBounds must be of size cvBounds[numChannels]
060 //
061 // RETURN
062 //  codebook index
063 int cvupdateCodeBook(uchar *p, codeBook &c, unsigned *cbBounds, int numChannels)
064 {
065     if(c.numEntries == 0) c.t = 0;
066     // 碼本中碼元爲零時初始化時間爲0
067     c.t += 1;   // Record learning event
068     // 每調用一次加一,即每一幀圖像加一
069      
070     //SET HIGH AND LOW BOUNDS
071     int n;
072     unsigned int high[3],low[3];
073     for (n=0; n<numChannels; n++)
074     {
075         high[n] = *(p+n) + *(cbBounds+n);
076         // *(p+n) 和 p[n] 結果等價,經試驗*(p+n) 速度更快
077         if(high[n] > 255) high[n] = 255;
078         low[n] = *(p+n)-*(cbBounds+n);
079         if(low[n] < 0) low[n] = 0;
080         // 用p 所指像素通道數據,加減cbBonds中數值,作爲此像素閥值的上下限
081     }
082  
083     //SEE IF THIS FITS AN EXISTING CODEWORD
084     int matchChannel;  
085     int i;
086     for (i=0; i<c.numEntries; i++)
087     {
088         // 遍歷此碼本每個碼元,測試p像素是否滿足其中之一
089         matchChannel = 0;
090         for (n=0; n<numChannels; n++)
091             //遍歷每個通道
092         {
093             if((c.cb[i]->learnLow[n] <= *(p+n)) && (*(p+n) <= c.cb[i]->learnHigh[n])) //Found an entry for this channel
094             // 如果p 像素通道數據在該碼元閥值上下限之間
095             {  
096                 matchChannel++;
097             }
098         }
099         if (matchChannel == numChannels)        // If an entry was found over all channels
100             // 如果p 像素各通道都滿足上面條件
101         {
102             c.cb[i]->t_last_update = c.t;
103             // 更新該碼元時間爲當前時間
104             // adjust this codeword for the first channel
105             for (n=0; n<numChannels; n++)
106                 //調整該碼元各通道最大最小值
107             {
108                 if (c.cb[i]->max[n] < *(p+n))
109                     c.cb[i]->max[n] = *(p+n);
110                 else if (c.cb[i]->min[n] > *(p+n))
111                     c.cb[i]->min[n] = *(p+n);
112             }
113             break;
114         }
115     }
116  
117     // ENTER A NEW CODE WORD IF NEEDED
118     if(i == c.numEntries)  // No existing code word found, make a new one
119     // p 像素不滿足此碼本中任何一個碼元,下面創建一個新碼元
120     {
121         code_element **foo = new code_element* [c.numEntries+1];
122         // 申請c.numEntries+1 個指向碼元的指針
123         for(int ii=0; ii<c.numEntries; ii++)
124             // 將前c.numEntries 個指針指向已存在的每個碼元
125             foo[ii] = c.cb[ii];
126          
127         foo[c.numEntries] = new code_element;
128         // 申請一個新的碼元
129         if(c.numEntries) delete [] c.cb;
130         // 刪除c.cb 指針數組
131         c.cb = foo;
132         // 把foo 頭指針賦給c.cb
133         for(n=0; n<numChannels; n++)
134             // 更新新碼元各通道數據
135         {
136             c.cb[c.numEntries]->learnHigh[n] = high[n];
137             c.cb[c.numEntries]->learnLow[n] = low[n];
138             c.cb[c.numEntries]->max[n] = *(p+n);
139             c.cb[c.numEntries]->min[n] = *(p+n);
140         }
141         c.cb[c.numEntries]->t_last_update = c.t;
142         c.cb[c.numEntries]->stale = 0;
143         c.numEntries += 1;
144     }
145  
146     // OVERHEAD TO TRACK POTENTIAL STALE ENTRIES
147     for(int s=0; s<c.numEntries; s++)
148     {
149         // This garbage is to track which codebook entries are going stale
150         int negRun = c.t - c.cb[s]->t_last_update;
151         // 計算該碼元的不更新時間
152         if(c.cb[s]->stale < negRun)
153             c.cb[s]->stale = negRun;
154     }
155  
156     // SLOWLY ADJUST LEARNING BOUNDS
157     for(n=0; n<numChannels; n++)
158         // 如果像素通道數據在高低閥值範圍內,但在碼元閥值之外,則緩慢調整此碼元學習界限
159     {
160         if(c.cb[i]->learnHigh[n] < high[n])
161             c.cb[i]->learnHigh[n] += 1;
162         if(c.cb[i]->learnLow[n] > low[n])
163             c.cb[i]->learnLow[n] -= 1;
164     }
165  
166     return(i);
167 }
168  
169 ///////////////////////////////////////////////////////////////////////////////////
170 // uchar cvbackgroundDiff(uchar *p, codeBook &c, int minMod, int maxMod)
171 // Given a pixel and a code book, determine if the pixel is covered by the codebook
172 //
173 // p        pixel pointer (YUV interleaved)
174 // c        codebook reference
175 // numChannels  Number of channels we are testing
176 // maxMod   Add this (possibly negative) number onto max level when code_element determining if new pixel is foreground
177 // minMod   Subract this (possible negative) number from min level code_element when determining if pixel is foreground
178 //
179 // NOTES:
180 // minMod and maxMod must have length numChannels, e.g. 3 channels => minMod[3], maxMod[3].
181 //
182 // Return
183 // 0 => background, 255 => foreground
184 uchar cvbackgroundDiff(uchar *p, codeBook &c, int numChannels, int *minMod, int *maxMod)
185 {
186     // 下面步驟和背景學習中查找碼元如出一轍
187     int matchChannel;
188     //SEE IF THIS FITS AN EXISTING CODEWORD
189     int i;
190     for (i=0; i<c.numEntries; i++)
191     {
192         matchChannel = 0;
193         for (int n=0; n<numChannels; n++)
194         {
195             if ((c.cb[i]->min[n] - minMod[n] <= *(p+n)) && (*(p+n) <= c.cb[i]->max[n] + maxMod[n]))
196                 matchChannel++; //Found an entry for this channel
197             else
198                 break;
199         }
200         if (matchChannel == numChannels)
201             break//Found an entry that matched all channels
202     }
203     if(i == c.numEntries)
204         return(255);
205         //p像素各通道值滿足碼本中其中一個碼元,則返回黑色
206     return(0);
207 }
208  
209  
210 //UTILITES/////////////////////////////////////////////////////////////////////////////////////
211 /////////////////////////////////////////////////////////////////////////////////
212 //int clearStaleEntries(codeBook &c)
213 // After you've learned for some period of time, periodically call this to clear out stale codebook entries
214 //
215 //c     Codebook to clean up
216 //
217 // Return
218 // number of entries cleared
219 int cvclearStaleEntries(codeBook &c)
220 {
221     int staleThresh = c.t >> 1;           // 設定刷新時間
222     int *keep = new int [c.numEntries]; // 申請一個標記數組
223     int keepCnt = 0;                    // 記錄不刪除碼元數目
224     //SEE WHICH CODEBOOK ENTRIES ARE TOO STALE
225     for (int i=0; i<c.numEntries; i++)
226         // 遍歷碼本中每個碼元
227     {
228         if (c.cb[i]->stale > staleThresh)
229             // 如碼元中的不更新時間大於設定的刷新時間,則標記爲刪除
230             keep[i] = 0; //Mark for destruction
231         else
232         {
233             keep[i] = 1; //Mark to keep
234             keepCnt += 1;
235         }
236     }
237  
238     // KEEP ONLY THE GOOD
239     c.t = 0;                        //Full reset on stale tracking
240     // 碼本時間清零
241     code_element **foo = new code_element* [keepCnt];
242     // 申請大小爲keepCnt 的碼元指針數組
243     int k=0;
244     for(int ii=0; ii<c.numEntries; ii++)
245     {
246         if(keep[ii])
247         {
248             foo[k] = c.cb[ii];
249             foo[k]->stale = 0;       //We have to refresh these entries for next clearStale
250             foo[k]->t_last_update = 0;
251             k++;
252         }
253     }
254     //CLEAN UP
255     delete [] keep;
256     delete [] c.cb;
257     c.cb = foo;
258     // 把foo 頭指針地址賦給c.cb
259     int numCleared = c.numEntries - keepCnt;
260     // 被清理的碼元個數
261     c.numEntries = keepCnt;
262     // 剩餘的碼元地址
263     return(numCleared);
264 }
265  
266  
267  
268 int main()
269 {
270     ///////////////////////////////////////
271     // 需要使用的變量
272     CvCapture*  capture;
273     IplImage*   rawImage;
274     IplImage*   yuvImage;
275     IplImage*   ImaskCodeBook;
276     codeBook*   cB;
277     unsigned    cbBounds[CHANNELS];
278     uchar*      pColor; //YUV pointer
279     int         imageLen;
280     int         nChannels = CHANNELS;
281     int         minMod[CHANNELS];
282     int         maxMod[CHANNELS];
283      
284     //////////////////////////////////////////////////////////////////////////
285     // 初始化各變量
286     cvNamedWindow("Raw");
287     cvNamedWindow("CodeBook");
288  
289     capture = cvCreateFileCapture("tree.avi");
290     if (!capture)
291     {
292         printf("Couldn't open the capture!");
293         return -1;
294     }
295  
296     rawImage = cvQueryFrame(capture);
297     yuvImage = cvCreateImage(cvGetSize(rawImage), 8, 3);   
298     // 給yuvImage 分配一個和rawImage 尺寸相同,8位3通道圖像
299     ImaskCodeBook = cvCreateImage(cvGetSize(rawImage), IPL_DEPTH_8U, 1);
300     // 爲ImaskCodeBook 分配一個和rawImage 尺寸相同,8位單通道圖像
301     cvSet(ImaskCodeBook, cvScalar(255));
302     // 設置單通道數組所有元素爲255,即初始化爲白色圖像
303      
304     imageLen = rawImage->width * rawImage->height;
305     cB = new codeBook[imageLen];
306     // 得到與圖像像素數目長度一樣的一組碼本,以便對每個像素進行處理
307      
308     for (int i=0; i<imageLen; i++)
309         // 初始化每個碼元數目爲0
310         cB[i].numEntries = 0;
311     for (int i=0; i<nChannels; i++)
312     {
313         cbBounds[i] = 10;   // 用於確定碼元各通道的閥值
314  
315         minMod[i]   = 20;   // 用於背景差分函數中
316         maxMod[i]   = 20;   // 調整其值以達到最好的分割
317     }
318          
319      
320     //////////////////////////////////////////////////////////////////////////
321     // 開始處理視頻每一幀圖像
322     for (int i=0;;i++)
323     {
324         cvCvtColor(rawImage, yuvImage, CV_BGR2YCrCb);
325         // 色彩空間轉換,將rawImage 轉換到YUV色彩空間,輸出到yuvImage
326         // 即使不轉換效果依然很好
327         // yuvImage = cvCloneImage(rawImage);
328  
329         if (i <= 30)
330             // 30幀內進行背景學習
331         {
332             pColor = (uchar *)(yuvImage->imageData);
333             // 指向yuvImage 圖像的通道數據
334             for (int c=0; c<imageLen; c++)
335             {
336                 cvupdateCodeBook(pColor, cB[c], cbBounds, nChannels);
337                 // 對每個像素,調用此函數,捕捉背景中相關變化圖像
338                 pColor += 3;
339                 // 3 通道圖像, 指向下一個像素通道數據
340             }
341             if (i == 30)
342                 // 到30 幀時調用下面函數,刪除碼本中陳舊的碼元
343             {
344                 for (int c=0; c<imageLen; c++)
345                     cvclearStaleEntries(cB[c]);
346             }
347         }
348         else
349         {
350             uchar maskPixelCodeBook;
351             pColor = (uchar *)((yuvImage)->imageData); //3 channel yuv image
352             uchar *pMask = (uchar *)((ImaskCodeBook)->imageData); //1 channel image
353             // 指向ImaskCodeBook 通道數據序列的首元素
354             for(int c=0; c<imageLen; c++)
355             {
356                 maskPixelCodeBook = cvbackgroundDiff(pColor, cB[c], nChannels, minMod, maxMod);
357                 // 我看到這兒時豁然開朗,開始理解了codeBook 呵呵
358                 *pMask++ = maskPixelCodeBook;
359                 pColor += 3;
360                 // pColor 指向的是3通道圖像
361             }
362         }
363         if (!(rawImage = cvQueryFrame(capture)))
364             break;
365         cvShowImage("Raw", rawImage);
366         cvShowImage("CodeBook", ImaskCodeBook);
367  
368         if (cvWaitKey(30) == 27)
369             break;
370         if (i == 56 || i == 63)
371             cvWaitKey();
372     }  
373      
374     cvReleaseCapture(&capture);
375     if (yuvImage)
376         cvReleaseImage(&yuvImage);
377     if(ImaskCodeBook)
378         cvReleaseImage(&ImaskCodeBook);
379     cvDestroyAllWindows();
380     delete [] cB;
381  
382     return 0;
383 }

發佈了8 篇原創文章 · 獲贊 3 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章