codeBook背景建模

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建立的模型相比变化不大。

猜你喜欢

转载自blog.csdn.net/weijiancheng999/article/details/88196877