摘要
codebook的建模效果比平均背景法好很多,建模过程中可以适应运动。CodeBook算法的基本思想是得到每个像素的时间序列模型。这种模型能很好地处理时间起伏,缺点是需要消耗大量的内存。
导读
《Learning OpenCV》一书当中介绍的第二种背景建模方法是codebook。直接通过书本来理解codebook算法有点困难,可以按照下面的顺序来理解codebook算法,首先看看百度百科上对这个算法的基本原理的阐述,我认为百度百科上的描述已经比较直观,但当中有很多细节的东西还需要看具体的代码,所以可以通过细读下面转载的代码来理解codebook算法,理解代码的过程需要有点耐心,先看main函数,理解程序大致的流程,再仔细看看cvupdateCodeBook()、cvclearStaleEntries()、cvbackgroundDiff()这三个函数,看懂了代码之后就应该能够理解这个算法了 。下面阐述的基本原理部分来自于百度百科,已经阐述得比较直观。代码来自于网友的博文http://blog.youkuaiyun.com/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.youkuaiyun.com/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 | } |