codeBook背景建模
(1)codeBook算法:
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),否则结束。
(2)代码:
#include <opencv2\core\core.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\video.hpp>
#include <opencv2\video\tracking.hpp>
#include <opencv2\videoio.hpp>
#include <iostream>
#include <vector>
#include <conio.h>
#include <sstream>
using namespace cv;
using namespace std;
Ptr <BackgroundSubtractorMOG2> PMOG2;//背景减法对象
int frameCount = 0; //存储当前帧数
#define CHANNELS 3 //通道数
typedef struct ce {
uchar learnHigh[CHANNELS]; //学习高侧阈
uchar learnLow[CHANNELS]; //学习低端门槛
uchar max[CHANNELS]; //框边界的高边
uchar min[CHANNELS]; //框边界的低边
int t_last_update; //让我们删除旧有的条目
int stale; //最大负运行(最长不活动时间)
} code_element;
//YUV颜色空间下的codebook(编码本)结构
typedef struct code_book {
code_element **cb;
int numEntries;
int t; //记录第一次或最后一次清除操作之间累积的像素点数量
} codeBook;
//update_codebook:捕获背景中相关变化的图像
/*
//使用新数据点更新代码簿条目
//
// p: 指向YUV像素的指针
// c: 此像素的编码本
// cbBounds: 学习码本的范围(经验法则:10)
// numChannels: 我们正在学习的颜色通道数
//
//注意:cvBounds的长度必须等于numChannels
//
//返回:编码本索引
*/
int update_codebook(uchar* p, codeBook& c, unsigned* cbBounds, int numChannels)
{
if (c.numEntries == 0) c.t = 0;//编码本中码元为0时初始化为0
c.t++;//每次调用加一,即每帧图像加一
unsigned int high[3], low[3];
int n, i;
for (n = 0; n < numChannels; n++)
{
high[n] = *(p + n) + *(cbBounds + n);
if (high[n] > 255) high[n] = 255;
low[n] = *(p + n) - *(cbBounds + n);
if (low[n] < 0) low[n] = 0;
//用p 所指像素通道数据,加减cbBonds中数值,作为此像素阀值的上下限
}
int matchChannel;
// SEE IF THIS FITS AN EXISTING CODEWORD
for (i = 0; i < c.numEntries; i++) {
matchChannel = 0;
//// 遍历此编码本每个码元,测试p像素是否满足其中之一
for (n = 0; n < numChannels; n++) {
if ((c.cb[i]->learnLow[n] <= *(p + n)) &&
//如果p 像素通道数据在该码元阀值上下限之间
(*(p + n) <= c.cb[i]->learnHigh[n]))
{
matchChannel++;
}
}
if (matchChannel == numChannels) //如果p 像素各通道都满足上面条件
{
c.cb[i]->t_last_update = c.t;
//更新该码元时间为当前时间
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;
}
}
// OVERHEAD TO TRACK POTENTIAL STALE ENTRIES
for (int s = 0; s < c.numEntries; s++) {
// 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;
}
// ENTER A NEW CODEWORD IF NEEDED
if (i == c.numEntries) //if no existing codeword found, make one
{
code_element **foo = new code_element*[c.numEntries + 1];
for (int ii = 0; ii < c.numEntries; ii++) {
foo[ii] = c.cb[ii];
}
foo[c.numEntries] = new code_element;
if (c.numEntries) delete[] c.cb;
c.cb = foo;
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;
}
// 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);
}
///////////////////////////////////////////////////////////////////
//clear_stale_entries:训练有移动的前景目标(数目很小)的背景
/*
//学习期间,当你学习了一段时间后,
//定期调用此方法清除陈旧的代码簿条目
// c: 要清理的密码本
//
//返回:清除的条目数
*/
int clear_stale_entries(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];
int k = 0;
for (int ii = 0; ii < c.numEntries; ii++) {
if (keep[ii])
{
foo[k] = c.cb[ii];
//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;
int numCleared = c.numEntries - keepCnt;
c.numEntries = keepCnt;
return(numCleared);
}
///////////////////////////////////////////////////
//背景减法:寻找前景目标
/*
//给定像素和码本,确定像素是否为
//由码本覆盖
//
// p: 像素指针(YUV交错)
// c: 代码簿参考
// numChannels: 我们正在测试的通道数
// maxMod: 从中加上这个(可能为负数)数字,以确定新像素是否为前景时的最大级别
// minMod: 从中减去这个(可能是负数)数字,以确定新像素是否为前景时的最小水平
//
//注意: minMod和maxMod的长度必须为numChannels,
//例如 3个通道=> minMod [3],maxMod [3]。 有一分钟和
//每个频道一个最大阈值。
//
//返回: 0 =>背景,255 =>前景
*/
uchar background_diff(uchar* p, codeBook& c, int numChannels, int* minMod, int* maxMod)
{
int matchChannel, i;
// SEE IF THIS FITS AN EXISTING CODEWORD
//
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) return(255);
return(0);
}
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];
//构造各种尺寸的元素以用于形态学变换
cv::Mat structuringElement2x2 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2, 2));
cv::Mat structuringElement3x3 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::Mat structuringElement5x5 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
cv::Mat structuringElement7x7 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(7, 7));
//////////////////////////////////////////////////////////////////////////
// 初始化各变量
//cvNamedWindow("Raw");
cvNamedWindow("CodeBook");
char* file_path = "..//Data//shake.avi";
capture = cvCreateFileCapture(file_path);
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] = 50; // 用于背景差分函数中
maxMod[i] = 50; // 调整其值以达到最好的分割
}
//////////////////////////////////////////////////////////////////////////
Ptr<BackgroundSubtractorMOG2> pMOG2 = createBackgroundSubtractorMOG2(200, 36.0, false);
// 开始处理视频每一帧图像
for (int i = 0;; i++)
{
cvCvtColor(rawImage, yuvImage, CV_BGR2YCrCb);
// 色彩空间转换,将rawImage 转换到YUV色彩空间,输出到yuvImage
// 即使不转换效果依然很好
// yuvImage = cvCloneImage(rawImage);
cvShowImage("frame", rawImage);
cv::Mat frame2 = cv::cvarrToMat(rawImage), fgMaskMOG2, bgImg;
pMOG2->apply(frame2, fgMaskMOG2);
pMOG2->getBackgroundImage(bgImg);
medianBlur(fgMaskMOG2, fgMaskMOG2, 5);
// imshow("medianBlur", fgMaskMOG2);
// Fill black holes
morphologyEx(fgMaskMOG2, fgMaskMOG2, MORPH_CLOSE, getStructuringElement(MORPH_RECT, Size(5, 5)));
// Fill white holes
morphologyEx(fgMaskMOG2, fgMaskMOG2, MORPH_OPEN, getStructuringElement(MORPH_RECT, Size(5, 5)));
imshow("morphologyEx", fgMaskMOG2);
if (i <= 30)
// 30帧内进行背景学习
{
pColor = (uchar *)(yuvImage->imageData);
// 指向yuvImage 图像的通道数据
for (int c = 0; c < imageLen; c++)
{
update_codebook(pColor, cB[c], cbBounds, nChannels);
// 对每个像素,调用此函数,捕捉背景中相关变化图像
pColor += 3;
// 3 通道图像, 指向下一个像素通道数据
}
if (i == 30)
// 到30 帧时调用下面函数,删除码本中陈旧的码元
{
for (int c = 0; c < imageLen; c++)
clear_stale_entries(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 = background_diff(pColor, cB[c], nChannels, minMod, maxMod);
// 我看到这儿时豁然开朗,开始理解了codeBook 呵呵
*pMask++ = maskPixelCodeBook;
pColor += 3;
// pColor 指向的是3通道图像
}
}
if (!(rawImage = cvQueryFrame(capture)))
break;
cvShowImage("CodeBook", ImaskCodeBook);
cv::Mat imgDifference = cv::cvarrToMat(ImaskCodeBook);
medianBlur(imgDifference, imgDifference, 5);
cv::Mat imgThresh;
erode(imgDifference, imgDifference, structuringElement2x2); //两次侵蚀处理,以消除噪音
//erode(imgDifference, imgDifference, structuringElement2x2);
cv::threshold(imgDifference, imgThresh, 20, 255.0, CV_THRESH_BINARY); //执行阈值处理并获得阈值掩码
cv::dilate(imgThresh, imgThresh, structuringElement2x2);
//dilate(imgThresh, imgThresh, structuringElement3x3);
cv::imshow("imgTresh", imgThresh);
//cvShowImage("Raw", rawImage);
if (cvWaitKey(30) == 27)
break;
}
cvReleaseCapture(&capture);
if (yuvImage)
cvReleaseImage(&yuvImage);
if (ImaskCodeBook)
cvReleaseImage(&ImaskCodeBook);
cvDestroyAllWindows();
delete[] cB;
return 0;
}
(3)感官
这个codeBook建模方法个人感觉适合于一些摇晃的树荫场景,用了感觉变化不大,与BackgroundSubtractorMOG2建立的模型相比变化不大。