摘要 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 |
} |