转自:http://www.cnblogs.com/xrwang/archive/2010/04/12/BackgroundGenerationAndForegroundDetectionPhase3.html
作者:王先荣
在上一篇文章里,我尝试翻译了《Nonparametric Background Generation》,本文主要介绍以下内容:如何实现该论文的算法,如果利用该算法来进行背景建模及前景检测,最后谈谈我的一些体会。为了使描述更加简便,以下将该论文的算法及实现称为NBGModel。
背景建模或前景检测(Background Generation And Foreground Detection) 二(非参数背景生成)的实现代码
1 使用示例
NBGModel在使用上非常的简便,您可以仿照下面的代码来使用它:
//初始化NBGModel对象 NBGModel nbgModel = new NBGModel(320, 240); //训练背景模型 nbgModel.TrainBackgroundModel(historyImages); //前景检测 nbgModel.Update(currentFrame); //利用结果 pbResult.Image = nbgModel.ForegroundMask.Bitmap; //释放对象 nbgModel.Dispose();
下面是更加完整的示例:
更加完整的示例 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Drawing.Imaging; using System.Diagnostics; using System.Runtime.InteropServices; using Emgu.CV; using Emgu.CV.Structure; using Emgu.CV.CvEnum; using System.Threading; namespace ImageProcessLearn { public partial class FormForegroundDetect2 : Form { //成员变量 //public const string ImageFilePathName = @"D:\Users\xrwang\Desktop\背景建模与前景检测\PETS2009\Crowd_PETS09\S0\City_Center\Time_12-34\View_001\frame_{0:D4}.jpg"; public const string ImageFilePathName = @"E:\PETS2009\S0_City_Center\Time_12-34\View_002\frame_{0:D4}.jpg"; //图像文件的路径信息,其中的{xxx}部分需要被实际的索引数字替换 public const int ImageWidth = 768; //图像的宽度 public const int ImageHeight = 576; //图像的高度 public const int MaxImageFileIndexNumber = 794; //最大的图像索引数字 private Action action = Action.Stop; //当前正在执行的动作 private NBGModel nbgModel = null; //非参数背景模型 private Thread threadTrainingBackground = null; //用于训练背景的工作线程 private Thread threadForegroundDetection = null; //用于前景检测的工作线程 public FormForegroundDetect2() { InitializeComponent(); } //开始训练背景 private void btnStartTrainingBackground_Click(object sender, EventArgs e) { threadTrainingBackground = new Thread(new ThreadStart(TrainingBackground)); threadTrainingBackground.Start(); action = Action.StartTrainingBackground; SetButtonState(); } //训练背景 private void TrainingBackground() { //构造非参数背景模型对象 if (nbgModel != null) nbgModel.Dispose(); NBGParameter param = (NBGParameter)this.Invoke(new GetNBGParameterDelegate(GetNBGParameter)); nbgModel = new NBGModel(ImageWidth, ImageHeight, param); //添加用于训练的图像 for (int idx = 0; idx < param.n; idx++) { if (action != Action.StartTrainingBackground) return; Image<Bgr, Byte> image = new Image<Bgr, byte>(string.Format(ImageFilePathName, idx)); nbgModel.AddHistoryImage(image); this.Invoke(new ShowTrainingBackgroundImageDelegate(ShowTrainingBackgroundImage), new object[] { image.Bitmap, idx }); image.Dispose(); } //训练背景 Stopwatch sw = new Stopwatch(); sw.Start(); nbgModel.TrainBackgroundModel(); sw.Stop(); //显示结果 this.Invoke(new ShowTrainingBackgroundResultDelegate(ShowTrainingBackgroundResult), new object[]{ nbgModel.Mrbm.Bitmap,string.Format("NBGModel训练背景用时{0:F04}毫秒,参数(样本数目:{1},典型点数目:{2})。",sw.ElapsedMilliseconds,param.n,param.m)}); } //用于在训练背景工作线程中获取设置参数的委托及方法 private delegate NBGParameter GetNBGParameterDelegate(); private NBGParameter GetNBGParameter() { return new NBGParameter((int)nudNBGModelN.Value, (int)nudNBGModelM.Value, (double)nudNBGModelTheta.Value, (double)nudNBGModelT.Value, new MCvTermCriteria((int)nudNBGModelMaxIter.Value, (double)nudNBGModelEps.Value)); } //用于在训练背景工作线程中显示结果的委托及方法 private delegate void ShowTrainingBackgroundResultDelegate(Bitmap mrbm, string result); private void ShowTrainingBackgroundResult(Bitmap mrbm, string result) { pbResult.Image = mrbm; txtResult.Text += result + "\r\n"; action = Action.Stop; SetButtonState(); } //用于在训练背景工作线程中显示训练图像的委托及方法 private delegate void ShowTrainingBackgroundImageDelegate(Bitmap bitmap, int idx); private void ShowTrainingBackgroundImage(Bitmap bitmap, int idx) { pbSource.Image = bitmap; lblImageIndex.Text = string.Format("{0:D4}", idx); } //开始前景检测 private void btnStartForegroundDetection_Click(object sender, EventArgs e) { threadForegroundDetection = new Thread(new ThreadStart(ForegroundDetection)); threadForegroundDetection.Start(); action = Action.StartForegroundDetection; SetButtonState(); } //检测前景 private void ForegroundDetection() { if (nbgModel == null) return; Stopwatch sw = new Stopwatch(); int idx; for (idx = nbgModel.Param.n; idx < MaxImageFileIndexNumber; idx++) { if (action != Action.StartForegroundDetection) break; if ((idx - nbgModel.Param.n) % 50 == 49) nbgModel.ClearStale(40); Image<Bgr, Byte> image = new Image<Bgr, byte>(string.Format(ImageFilePathName, idx)); sw.Start(); nbgModel.Update(image); sw.Stop(); Image<Bgr, Byte> imageForground = image.Copy(nbgModel.ForegroundMask); this.Invoke(new ShowForegroundDetectionImageDelegate(ShowForegroundDetectionImage), new object[] { image.Bitmap, imageForground.Bitmap, idx }); image.Dispose(); imageForground.Dispose(); } int frames = idx - nbgModel.Param.n; this.Invoke(new ShowForegroundDetectionResultDelegate(ShowForegroundDetectionResult), new object[] { string.Format("NBGModel前景检测,共{0}帧,平均耗时{1:F04}毫秒,参数(样本数目:{2},典型点数目:{3},权重系数:{4},最小差值:{5},最大迭代次数:{6},终止精度:{7})", frames,1d*sw.ElapsedMilliseconds/frames,nbgModel.Param.n,nbgModel.Param.m,nbgModel.Param.theta,nbgModel.Param.t,nbgModel.Param.criteria.max_iter,nbgModel.Param.criteria.epsilon)}); } //用于在前景检测工作线程中显示图像的委托及方法 private delegate void ShowForegroundDetectionImageDelegate(Bitmap image, Bitmap foreground, int idx); private void ShowForegroundDetectionImage(Bitmap image, Bitmap foreground, int idx) { pbSource.Image = image; pbResult.Image = foreground; lblImageIndex.Text = string.Format("{0:D4}", idx); } //用于在前景检测工作线程中显示结果的委托及方法 private delegate void ShowForegroundDetectionResultDelegate(string result); private void ShowForegroundDetectionResult(string result) { txtResult.Text += result; } //停止 private void btnStop_Click(object sender, EventArgs e) { if (action == Action.StartTrainingBackground) { action = Action.Stop; Thread.Sleep(1); if (threadTrainingBackground.ThreadState == System.Threading.ThreadState.Running) threadTrainingBackground.Abort(); nbgModel.Dispose(); nbgModel = null; } action = Action.Stop; SetButtonState(); } /// <summary> /// 设置按钮的状态 /// </summary> private void SetButtonState() { if (action == Action.Stop) { btnStartTrainingBackground.Enabled = true; btnStartForegroundDetection.Enabled = true; btnStop.Enabled = false; } else if (action == Action.StartForegroundDetection || action==Action.StartTrainingBackground) { btnStartTrainingBackground.Enabled = false; btnStartForegroundDetection.Enabled = false; btnStop.Enabled = true; } } //加载窗体时 private void FormForegroundDetect2_Load(object sender, EventArgs e) { toolTip.SetToolTip(nudNBGModelN, "样本数目:需要被保存的历史图像数目"); toolTip.SetToolTip(nudNBGModelM, "典型点数目:历史图像需要被分为多少组"); toolTip.SetToolTip(nudNBGModelTheta, "权重系数:权重大于该值的聚集中心是候选背景"); toolTip.SetToolTip(nudNBGModelT, "最小差值:观测值与聚集中心的最小差值如果大于该值,为前景;否则为背景"); toolTip.SetToolTip(nudNBGModelMaxIter, "最大迭代次数:MeanShift计算过程中的最大迭代次数"); toolTip.SetToolTip(nudNBGModelEps, "终止精度:MeanShift计算过程中,如果矩形窗的位移小于等于该值,则停止计算"); pbSource.Image = Image.FromFile(string.Format(ImageFilePathName, 0)); lblImageIndex.Text = string.Format("{0:D4}", 0); } //当窗体关闭后,释放资源 private void FormForegroundDetect2_FormClosed(object sender, FormClosedEventArgs e) { if (threadTrainingBackground != null && threadTrainingBackground.ThreadState == System.Threading.ThreadState.Running) threadTrainingBackground.Abort(); if (threadForegroundDetection != null && threadForegroundDetection.ThreadState == System.Threading.ThreadState.Running) threadForegroundDetection.Abort(); if (nbgModel != null) nbgModel.Dispose(); } } /// <summary> /// 当前正在执行的动作 /// </summary> public enum Action { StartTrainingBackground, //开始训练背景 StartForegroundDetection, //开始前景检测 Stop //停止 } }
2 实现NBGModel
2.1 我在实现NBGModel的时候基本上跟论文中的方式一样,不过有以下两点区别:
(1)论文中的MeanShift计算使用了Epanechnikov核函数,我使用的是矩形窗形式的MeanShift计算。主要是因为我自己不会实现MeanShift,只能利用OpenCV中提供的cvMeanShift函数。这样做也有一个好处——不再需要计算与保存典型点。
(2)论文中的方法在检测的过程中聚集中心会不断的增加,我模仿CodeBook的实现为其增加了一个清除消极聚集中心的ClearStable方法。这样可以在必要的时候将长期不活跃的聚集中心清除掉。
2.2 NBGModel中用到的数据成员如下所示:
private int width; //图像的宽度
private int height; //图像的高度
private NBGParameter param; //非参数背景模型的参数
private List<Image<Ycc, Byte>> historyImages = null; //历史图像:列表个数为param.n,在更新时如果个数大于等于param.n,删除最早的历史图像,加入最新的历史图像
//由于这里采用矩形窗口方式的MeanShift计算,因此不再需要分组图像的典型点。这跟论文不一样。
//private List<Image<Ycc,Byte>> convergenceImages = null; //收敛图像:列表个数为param.m,仅在背景训练时使用,训练结束即被清空,因此这里不再声明
private Image<Gray, Byte> sampleImage = null; //样本图像:保存历史图像中每个像素在Y通道的值,用于MeanShift计算
private List<ClusterCenter<Ycc>>[,] clusterCenters = null; //聚集中心数据:将收敛点分类之后得到的聚集中心,数组大小为:height x width,列表元素个数不定q(q<=m)。
private Image<Ycc, Byte> mrbm = null; //最可靠背景模型
private Image<Gray, Byte> backgroundMask = null; //背景掩码图像
private double frameCount = 0; //总帧数(不包括训练阶段的帧数n)
其中,NBGParameter结构包含以下成员:
public int n; //样本数目:需要被保留的历史图像数目
public int m; //典型点数目:历史图像需要被分为多少组
public double theta; //权重系数:权重大于该值的聚集中心为候选背景
public double t; //最小差值:观测值与候选背景的最小差值大于该值时,为前景;否则为背景
public MCvTermCriteria criteria; //Mean Shift计算的终止条件:包括最大迭代次数和终止计算的精度
聚集中心ClusterCenter使用类而不是结构,是为了方便更新,它包含以下成员:
public TColor ci; //聚集中心的像素值
public double wi; //聚集中心的权重
public double li; //聚集中心包含的收敛点数目
public double updateFrameNo; //更新该聚集中心时的帧数:用于清除消极的聚集中心
2.3 NBGModel中的关键流程
1.背景建模
(1)将训练用的样本图像添加到历史图像historyImages中;
(2)将历史图像分为m组,以每组所在位置的矩形窗为起点进行MeanShift计算,结果窗的中点为收敛中心,收敛中心的像素值为收敛值,将收敛值添加到收敛图像convergenceImages中;
(3)计算收敛图像的聚集中心:(a)得到收敛中心的最小值Cmin;(b)将[0,Cmin+t]区间中的收敛中心划分为一类;(c)计算已分类收敛中心的平均值,作为聚集中心的值;(d)删除已分类的收敛中心;(e)重复a~d,直到收敛中心全部归类;
(4)得到最可靠背景模型MRBM:在聚集中心中选取wi最大的值作为某个像素的最可靠背景。
2.前景检测
(1)用wi≥theta作为条件选择可能的背景组Cb;
(2)对每个观测值x0,计算x0与Cb的最小差值d;
(3)如果d>t,则该点为前景;否则为背景。
3.背景维持
(1)如果某点为背景,更新最近聚集中心的wi为(li+1)/m;
(2)如果某点为前景:(a)以该点所在的矩形窗为起点进行MeanShift计算,可得到新的收敛中心Cnew(wi=1/m);(b)将Cnew加入到聚集中心clusterCenters;
(3)在必要的时候,清理消极的聚集中心。
NBGModel实现代码 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Drawing; using System.Drawing.Imaging; using System.Diagnostics; using System.Runtime.InteropServices; using Emgu.CV; using Emgu.CV.Structure; using Emgu.CV.CvEnum; namespace ImageProcessLearn { public class NBGModel : IDisposable { //成员变量 private int width; //图像的宽度 private int height; //图像的高度 private NBGParameter param; //非参数背景模型的参数 private List<Image<Ycc, Byte>> historyImages = null; //历史图像:列表个数为param.n,在更新时如果个数大于param.n,删除最早的历史图像,加入最新的历史图像 //由于这里采用矩形窗口方式的MeanShift计算,因此不再需要分组图像的典型点。这跟论文不一样。 //private List<Image<Ycc,Byte>> convergenceImages = null; //收敛图像:列表个数为param.m,仅在背景训练时使用,训练结束即被清空,因此这里不再声明 private Image<Gray, Byte> sampleImage = null; //样本图像:保存历史图像中每个像素在Y通道的值,用于MeanShift计算 private List<ClusterCenter<Ycc>>[,] clusterCenters = null; //聚集中心数据:将收敛点分类之后得到的聚集中心,数组大小为:height x width,列表元素个数不定q(q<=m)。 private Image<Ycc, Byte> mrbm = null; //最可靠背景模型 private Image<Gray, Byte> backgroundMask = null; //背景掩码图像 private double frameCount = 0; //总帧数(不包括训练阶段的帧数n) //属性 /// <summary> /// 图像的宽度 /// </summary> public int Width { get { return width; } } /// <summary> /// 图像的高度 /// </summary> public int Height { get { return height; } } /// <summary> /// 非参数背景模型的参数 /// </summary> public NBGParameter Param { get { return param; } } /// <summary> /// 最可靠背景模型 /// </summary> public Image<Ycc, Byte> Mrbm { get { return mrbm; } } /// <summary> /// 背景掩码 /// </summary> public Image<Gray, Byte> BackgroundMask { get { return backgroundMask; } } /// <summary> /// 前景掩码 /// </summary> public Image<Gray, Byte> ForegroundMask { get { return backgroundMask.Not(); } } /// <summary> /// 总帧数 /// </summary> public double FrameCount { get { return frameCount; } } /// <summary> /// 构造函数 /// </summary> /// <param name="width">图像宽度</param> /// <param name="height">图像高度</param> public NBGModel(int width, int height) : this(width, height, NBGParameter.GetDefaultNBGParameter()) { } /// <summary> /// 构造函数 /// </summary> /// <param name="width">图像宽度</param> /// <param name="height">图像高度</param> ///<param name="param">非参数背景模型的参数</param> public NBGModel(int width, int height, NBGParameter param) { CheckParameters(width, height, param); this.width = width; this.height = height; this.param = param; historyImages = new List<Image<Ycc, byte>>(param.n); sampleImage = new Image<Gray, byte>(param.n, 1); clusterCenters = new List<ClusterCenter<Ycc>>[height, width]; for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) clusterCenters[row, col] = new List<ClusterCenter<Ycc>>(param.m); //聚集中心列表不定长,因此将其初始化为m个元素的容量 } mrbm = new Image<Ycc, byte>(width, height); backgroundMask = new Image<Gray, byte>(width, height); frameCount = 0; } /// <summary> /// 检查参数是否正确,如果不正确,抛出异常 /// </summary> /// <param name="width">图像宽度</param> /// <param name="height">图像高度</param> /// <param name="param">非参数背景模型的参数</param> private void CheckParameters(int width, int height, NBGParameter param) { if (width <= 0) throw new ArgumentOutOfRangeException("width", width, "图像宽度必须大于零。"); if (height <= 0) throw new ArgumentOutOfRangeException("height", height, "图像高度必须大于零。"); if (param.n <= 0) throw new ArgumentOutOfRangeException("n", param.n, "样本数目必须大于零。"); if (param.m <= 0) throw new ArgumentOutOfRangeException("m", param.m, "典型点数目必须大于零。"); if (param.n % param.m != 0) throw new ArgumentException("样本数目必须是典型点数目的整数倍。", "n,m"); if (param.theta <= 0 || param.theta > 1) throw new ArgumentOutOfRangeException("theta", param.theta, "权重系数必须大于零,并且小于1。"); if (param.t <= 0) throw new ArgumentOutOfRangeException("t", param.t, "最小差值必须大于零。"); } /// <summary> /// 释放资源 /// </summary> public void Dispose() { if (historyImages != null && historyImages.Count > 0) { foreach (Image<Ycc, byte> historyImage in historyImages) { if (historyImage != null) historyImage.Dispose(); } } if (sampleImage != null) sampleImage.Dispose(); if (clusterCenters != null && clusterCenters.Length > 0) { foreach (List<ClusterCenter<Ycc>> clusterCentersElement in clusterCenters) clusterCentersElement.Clear(); } if (mrbm != null) mrbm.Dispose(); if (backgroundMask != null) backgroundMask.Dispose(); } /// <summary> /// 增加历史图像 /// </summary> /// <param name="historyImage">历史图像</param> /// <returns>是否增加成功</returns> public bool AddHistoryImage(Image<Ycc, byte> historyImage) { bool success = false; if (historyImage != null && historyImage.Width == width && historyImage.Height == height) { if (historyImages.Count >= param.n) { if (historyImages[0] != null) historyImages[0].Dispose(); historyImages.RemoveAt(0); } historyImages.Add(historyImage.Copy()); success = true; } return success; } /// <summary> /// 增加历史图像 /// </summary> /// <param name="historyImages">历史图像数组</param> /// <returns>返回成功增加的历史图像数目</returns> public int AddHistoryImage(Image<Ycc,byte>[] historyImages) { int added = 0; if (historyImages != null && historyImages.Length > 0) { foreach (Image<Ycc,byte> historyImage in historyImages) { if (AddHistoryImage(historyImage)) added++; } } return added; } /// <summary> /// 增加历史图像 /// </summary> /// <param name="historyImages">历史图像列表</param> /// <returns>返回成功增加的历史图像数目</returns> public int AddHistoryImage(List<Image<Ycc,byte>> historyImages) { return AddHistoryImage(historyImages.ToArray()); } /// <summary> /// 增加历史图像 /// </summary> /// <param name="historyImage">历史图像</param> /// <returns>是否增加成功</returns> public bool AddHistoryImage(Image<Bgr, byte> historyImage) { Image<Ycc, byte> image = historyImage.Convert<Ycc, byte>(); bool success = AddHistoryImage(image); image.Dispose(); return success; } /// <summary> /// 增加历史图像 /// </summary> /// <param name="historyImages">历史图像数组</param> /// <returns>返回成功增加的历史图像数目</returns> public int AddHistoryImage(Image<Bgr, byte>[] historyImages) { int added = 0; if (historyImages != null && historyImages.Length > 0) { foreach (Image<Bgr, byte> historyImage in historyImages) { if (AddHistoryImage(historyImage)) added++; } } return added; } /// <summary> /// 增加历史图像 /// </summary> /// <param name="historyImages">历史图像列表</param> /// <returns>返回成功增加的历史图像数目</returns> public int AddHistoryImage(List<Image<Bgr, byte>> historyImages) { return AddHistoryImage(historyImages.ToArray()); } /// <summary> /// 训练背景模型 /// </summary> /// <returns>返回训练是否成功</returns> unsafe public bool TrainBackgroundModel() { bool success = false; if (historyImages.Count >= param.n) { //0.初始化收敛图像,个数为param.m List<Image<Ycc, byte>> convergenceImages = new List<Image<Ycc, byte>>(param.m); for (int i = 0; i < param.m; i++) convergenceImages.Add(new Image<Ycc, byte>(width, height)); //1.将历史图像分为m组,以每组的位置为矩形窗的起点,对通道Y在历史图像中进行MeanShift计算,结果窗的中点为收敛中心,该中心的值为收敛值,将收敛值加入到convergenceImageData中。 int numberPerGroup = param.n / param.m; //每组图像的数目 MCvConnectedComp comp; //保存Mean Shift计算结果的连接部件 int offsetHistoryImage; //历史图像中某个像素相对图像数据起点的偏移量 int widthStepHistoryImage = historyImages[0].MIplImage.widthStep; //历史图像的每行字节数 byte*[] ptrHistoryImages = new byte*[param.n]; //历史图像的数据部分起点数组 byte* ptrSampleImage = (byte*)sampleImage.MIplImage.imageData.ToPointer(); //样本图像的数据部分起点 int offsetConvergenceImage; //收敛图像中某个像素相对图像数据起点的偏移量 int widthStepConvergenceImage = convergenceImages[0].MIplImage.widthStep; //收敛图像的每行字节数 byte*[] ptrConvergenceImages = new byte*[param.m]; //收敛图像的数据部分起点数组 for (int i = 0; i < param.n; i++) ptrHistoryImages[i] = (byte*)historyImages[i].MIplImage.imageData.ToPointer(); for (int i = 0; i < param.m; i++) ptrConvergenceImages[i] = (byte*)convergenceImages[i].MIplImage.imageData.ToPointer(); //遍历图像的每一行 for (int row = 0; row < height; row++) { //遍历图像的每一列 for (int col = 0; col < width; col++) { offsetHistoryImage = row * widthStepHistoryImage + col * 3; offsetConvergenceImage = row * widthStepConvergenceImage + col * 3; //用历史图像在该点的Y通道值组成一副用于Mean Shift计算的样本图像 for (int sampleIdx = 0; sampleIdx < param.n; sampleIdx++) *(ptrSampleImage + sampleIdx) = *(ptrHistoryImages[sampleIdx] + offsetHistoryImage); //以每组的位置为矩形窗的起点,用MeanShift过程找到局部极值,由局部极值组成的图像是收敛图像 for (int representativeIdx = 0; representativeIdx < param.m; representativeIdx++) { Rectangle window = new Rectangle(representativeIdx * numberPerGroup, 0, numberPerGroup, 1); CvInvoke.cvMeanShift(sampleImage.Ptr, window, param.criteria, out comp); int center = comp.rect.Left + comp.rect.Width / 2; //收敛中心 *(ptrConvergenceImages[representativeIdx] + offsetConvergenceImage) = *(ptrHistoryImages[center] + offsetHistoryImage); *(ptrConvergenceImages[representativeIdx] + offsetConvergenceImage + 1) = *(ptrHistoryImages[center] + offsetHistoryImage + 1); *(ptrConvergenceImages[representativeIdx] + offsetConvergenceImage + 2) = *(ptrHistoryImages[center] + offsetHistoryImage + 2); } } } //2.将近似的收敛点分类,以得到聚集中心 //(1)得到收敛中心的最小值Cmin;(2)将[0 , Cmin+t]区间中的收敛中心划为一类;(3)计算已分类点的平均值,作为聚集中心的值,并将聚集中心添加到clusterCenters;(4)删除已分类的收敛中心;(5)重复(0)~(4)直到收敛中心全部归类。 for (int row = 0; row < height; row++) { //遍历图像的每一列 for (int col = 0; col < width; col++) { offsetConvergenceImage = row * widthStepConvergenceImage + col * 3; //得到该像素的收敛中心列表 List<Ycc> convergenceCenters = new List<Ycc>(param.m); for (int convergenceIdx = 0; convergenceIdx < param.m; convergenceIdx++) convergenceCenters.Add(new Ycc(*(ptrConvergenceImages[convergenceIdx] + offsetConvergenceImage), *(ptrConvergenceImages[convergenceIdx] + offsetConvergenceImage + 1), *(ptrConvergenceImages[convergenceIdx] + offsetConvergenceImage + 2))); while (convergenceCenters.Count > 0) { Ycc Cmin = MinYcc(convergenceCenters); double regionHigh = Cmin.Y + param.t; Ycc sum = new Ycc(0d, 0d, 0d); double li = 0d; for (int i = convergenceCenters.Count - 1; i >= 0; i--) { Ycc ci = convergenceCenters[i]; if (ci.Y <= regionHigh) { sum.Y += ci.Y; sum.Cr += ci.Cr; sum.Cb += ci.Cb; li++; convergenceCenters.RemoveAt(i); } } Ycc avg = new Ycc(sum.Y / li, sum.Cr / li, sum.Cb / li); double wi = li / param.m; ClusterCenter<Ycc> clusterCenter = new ClusterCenter<Ycc>(avg, wi, li, 0d); clusterCenters[row, col].Add(clusterCenter); } } } //3.得到最可靠背景模型 GetMrbm(); //4.释放资源 for (int i = 0; i < param.m; i++) convergenceImages[i].Dispose(); convergenceImages.Clear(); success = true; } return success; } /// <summary> /// 训练背景模型(没有使用指针运算进行优化,用于演示流程) /// </summary> /// <returns>返回训练是否成功</returns> private bool TrainBackgroundModel2() { bool success = false; if (historyImages.Count >= param.n) { //0.初始化收敛图像,个数为param.m List<Image<Ycc, byte>> convergenceImages = new List<Image<Ycc, byte>>(param.m); for (int i = 0; i < param.m; i++) convergenceImages.Add(new Image<Ycc, byte>(width, height)); //1.将历史图像分为m组,以每组的位置为矩形窗的起点,对通道Y在历史图像中进行MeanShift计算,结果窗的中点为收敛中心,该中心的值为收敛值,将收敛值加入到convergenceImageData中。 int numberPerGroup = param.n / param.m; //每组图像的数目 MCvConnectedComp comp; //保存Mean Shift计算结果的连接部件 //遍历图像的每一行 for (int row = 0; row < height; row++) { //遍历图像的每一列 for (int col = 0; col < width; col++) { //用历史图像在该点的Y通道值组成一副用于Mean Shift计算的样本图像 for (int sampleIdx = 0; sampleIdx < param.n; sampleIdx++) sampleImage[0, sampleIdx] = new Gray(historyImages[sampleIdx][row, col].Y); //这里可以用指针优化访问像素的速度 //以每组的位置为矩形窗的起点,用MeanShift过程找到局部极值,由局部极值组成的图像是收敛图像 for (int representativeIdx = 0; representativeIdx < param.m; representativeIdx++) { Rectangle window = new Rectangle(representativeIdx * numberPerGroup, 0, numberPerGroup, 1); CvInvoke.cvMeanShift(sampleImage.Ptr, window, param.criteria, out comp); int center = comp.rect.Left + comp.rect.Width / 2; //收敛中心 Ycc ci = historyImages[center][row, col]; //收敛中心对应的像素值 convergenceImages[representativeIdx][row, col] = ci; //将收敛中心添加到收敛图像数据中去(这两句可以用指针优化访问像素的速度) } } } //2.将近似的收敛点分类,以得到聚集中心 //(1)得到收敛中心的最小值Cmin;(2)将[0 , Cmin+t]区间中的收敛中心划为一类;(3)计算已分类点的平均值,作为聚集中心的值,并将聚集中心添加到clusterCenters;(4)删除已分类的收敛中心;(5)重复(0)~(4)直到收敛中心全部归类。 for (int row = 0; row < height; row++) { //遍历图像的每一列 for (int col = 0; col < width; col++) { //得到该像素的收敛中心列表 List<Ycc> convergenceCenters = new List<Ycc>(param.m); for (int convergenceIdx = 0; convergenceIdx < param.m; convergenceIdx++) convergenceCenters.Add(convergenceImages[convergenceIdx][row, col]); while (convergenceCenters.Count > 0) { Ycc Cmin = MinYcc(convergenceCenters); double regionHigh = Cmin.Y + param.t; Ycc sum = new Ycc(0d, 0d, 0d); double li = 0d; for (int i = convergenceCenters.Count - 1; i >= 0; i--) { Ycc ci = convergenceCenters[i]; if (ci.Y <= regionHigh) { sum.Y += ci.Y; sum.Cr += ci.Cr; sum.Cb += ci.Cb; li++; convergenceCenters.RemoveAt(i); } } Ycc avg = new Ycc(sum.Y / li, sum.Cr / li, sum.Cb / li); double wi = li / param.m; ClusterCenter<Ycc> clusterCenter = new ClusterCenter<Ycc>(avg, wi, li, 0d); clusterCenters[row, col].Add(clusterCenter); } } } //3.得到最可靠背景模型 GetMrbm(); //4.释放资源 for (int i = 0; i < param.m; i++) convergenceImages[i].Dispose(); convergenceImages.Clear(); success = true; } return success; } /// <summary> /// 训练背景模型 /// </summary> /// <param name="historyImages">历史图像数组</param> /// <returns>返回训练是否成功</returns> public bool TrainBackgroundModel(Image<Ycc,byte>[] historyImages) { AddHistoryImage(historyImages); return TrainBackgroundModel(); } /// <summary> /// 训练背景模型 /// </summary> /// <param name="historyImages">历史图像数组</param> /// <returns>返回训练是否成功</returns> public bool TrainBackgroundModel(List<Image<Ycc,byte>> historyImages) { AddHistoryImage(historyImages); return TrainBackgroundModel(); } /// <summary> /// 训练背景模型 /// </summary> /// <param name="historyImages">历史图像数组</param> /// <returns>返回训练是否成功</returns> public bool TrainBackgroundModel(Image<Bgr, byte>[] historyImages) { AddHistoryImage(historyImages); return TrainBackgroundModel(); } /// <summary> /// 训练背景模型 /// </summary> /// <param name="historyImages">历史图像数组</param> /// <returns>返回训练是否成功</returns> public bool TrainBackgroundModel(List<Image<Bgr, byte>> historyImages) { AddHistoryImage(historyImages); return TrainBackgroundModel(); } /// <summary> /// 得到Ycc列表中的最小值(仅比较Y分量) /// </summary> /// <param name="yccList"></param> /// <returns></returns> private Ycc MinYcc(List<Ycc> yccList) { Ycc min = yccList[0]; foreach (Ycc ycc in yccList) { if (ycc.Y < min.Y) min = ycc; } return min; } /// <summary> /// 得到聚集中心列表中的最大值(比较wi) /// </summary> /// <param name="clusterCenters"></param> /// <returns></returns> private ClusterCenter<Ycc> MaxClusterCenter(List<ClusterCenter<Ycc>> clusterCenters) { ClusterCenter<Ycc> max = clusterCenters[0]; foreach (ClusterCenter<Ycc> center in clusterCenters) { if (center.wi > max.wi) max = center; } return max; } /// <summary> /// 得到最可靠背景模型MRBM:在聚集中心选择wi最大的值(没有使用指针运算进行优化,用于演示流程) /// </summary> private void GetMrbm2() { //遍历图像的每一行 for (int row = 0; row < height; row++) { //遍历图像的每一列 for (int col = 0; col < width; col++) mrbm[row, col] = MaxClusterCenter(clusterCenters[row, col]).ci; } } /// <summary> /// 得到最可靠背景模型MRBM:在聚集中心选择wi最大的值 /// </summary> unsafe private void GetMrbm() { int widthStepMrbm = mrbm.MIplImage.widthStep; byte* ptrMrbm = (byte*)mrbm.MIplImage.imageData.ToPointer(); byte* ptrPixel; Ycc ci; //遍历图像的每一行 for (int row = 0; row < height; row++) { //遍历图像的每一列 for (int col = 0; col < width; col++) { ci = MaxClusterCenter(clusterCenters[row, col]).ci; ptrPixel = ptrMrbm + row * widthStepMrbm + col * 3; *ptrPixel = (byte)ci.Y; *(ptrPixel + 1) = (byte)ci.Cr; *(ptrPixel + 2) = (byte)ci.Cb; } } } /// <summary> /// 更新背景模型,同时计算相应的前景和背景(没有使用指针运算进行优化,用于演示流程) /// </summary> /// <param name="currentFrame">当前帧图像</param> private void Update2(Image<Ycc, byte> currentFrame) { //1.将当前帧加入到历史图像的末尾 AddHistoryImage(currentFrame); frameCount++; //2.将背景掩码图像整个设置为白色,在检测时再将前景像素置零 backgroundMask.SetValue(255d); //3.遍历图像的每个像素,确定前景或背景;同时进行背景维持操作。 int numberPerGroup = param.n / param.m; int lastIdx = param.n - 1; //遍历图像的每一行 for (int row = 0; row < height; row++) { //遍历图像的每一列 for (int col = 0; col < width; col++) { int nearestIndex; //最近的聚集中心索引 double d = GetMinD(historyImages[lastIdx][row, col].Y, clusterCenters[row, col], out nearestIndex); //得到最小差值d if (d > param.t) { //该点为前景:以该点附近的矩形窗{n-numberPerGroup,0,numberPerGroup,1}开始进行MeanShift运算,并得到新的收敛中心Cnew(wi=1/m),将Cnew加入到聚集中心clusterCenters //用历史图像在该点的Y通道值组成一副用于Mean Shift计算的样本图像 for (int sampleIdx = 0; sampleIdx < param.n; sampleIdx++) sampleImage[0, sampleIdx] = new Gray(historyImages[sampleIdx][row, col].Y); //这里可以用指针运算来提高速度 Rectangle window = new Rectangle(param.n - numberPerGroup, 0, numberPerGroup, 1); MCvConnectedComp comp; CvInvoke.cvMeanShift(sampleImage.Ptr, window, param.criteria, out comp); int center = comp.rect.Left + comp.rect.Width / 2; //收敛中心 ClusterCenter<Ycc> cnew = new ClusterCenter<Ycc>(historyImages[center][row, col], 1d / (param.m + frameCount), 1d, frameCount); //将收敛中心作为新的聚集中心 clusterCenters[row, col].Add(cnew); //设置背景掩码图像的前景点 backgroundMask[row, col] = new Gray(0d); } else { //该点为背景:更新最近聚集中心的ci为(li+1)/m clusterCenters[row, col][nearestIndex].li++; clusterCenters[row, col][nearestIndex].wi = clusterCenters[row, col][nearestIndex].li / (param.m + frameCount); clusterCenters[row, col][nearestIndex].updateFrameNo = frameCount; } } } //4.生成最可靠背景模型 GetMrbm(); } /// <summary> /// 更新背景模型,同时计算相应的前景和背景 /// </summary> /// <param name="currentFrame">当前帧图像</param> private void Update2(Image<Bgr, byte> currentFrame) { Image<Ycc, byte> image = currentFrame.Convert<Ycc, byte>(); Update(image); image.Dispose(); } /// <summary> /// 更新背景模型,同时计算相应的前景和背景 /// </summary> /// <param name="currentFrame">当前帧图像</param> unsafe public void Update(Image<Ycc, byte> currentFrame) { //1.将当前帧加入到历史图像的末尾 AddHistoryImage(currentFrame); frameCount++; //2.将背景掩码图像整个设置为白色,在检测时再将前景像素置零 backgroundMask.SetValue(255d); //3.遍历图像的每个像素,确定前景或背景;同时进行背景维持操作。 int numberPerGroup = param.n / param.m; int lastIdx = param.n - 1; int offsetHistoryImage; //历史图像中某个像素相对图像数据起点的偏移量 int widthStepHistoryImage = historyImages[0].MIplImage.widthStep; //历史图像的每行字节数 byte*[] ptrHistoryImages = new byte*[param.n]; //历史图像的数据部分起点数组 byte* ptrSampleImage = (byte*)sampleImage.MIplImage.imageData.ToPointer(); //样本图像的数据部分起点 int widthStepBackgroundMask = backgroundMask.MIplImage.widthStep; //背景掩码图像的每行字节数 byte* ptrBackgroundMask = (byte*)backgroundMask.MIplImage.imageData.ToPointer(); //背景掩码图像的数据部分起点 byte f = 0; //前景对应的颜色值 for (int i = 0; i < param.n; i++) ptrHistoryImages[i] = (byte*)historyImages[i].MIplImage.imageData.ToPointer(); //遍历图像的每一行 for (int row = 0; row < height; row++) { //遍历图像的每一列 for (int col = 0; col < width; col++) { offsetHistoryImage = row * widthStepHistoryImage + col * 3; int nearestIndex; //最近的聚集中心索引 double d = GetMinD((double)(*(ptrHistoryImages[lastIdx] + offsetHistoryImage)), clusterCenters[row, col], out nearestIndex); //得到最小差值d if (d > param.t) { //该点为前景:以该点附近的矩形窗{n-numberPerGroup,0,numberPerGroup,1}开始进行MeanShift运算,并得到新的收敛中心Cnew(wi=1/m),将Cnew加入到聚集中心clusterCenters //用历史图像在该点的Y通道值组成一副用于Mean Shift计算的样本图像 for (int sampleIdx = 0; sampleIdx < param.n; sampleIdx++) *(ptrSampleImage + sampleIdx) = *(ptrHistoryImages[sampleIdx] + offsetHistoryImage); Rectangle window = new Rectangle(param.n - numberPerGroup, 0, numberPerGroup, 1); MCvConnectedComp comp; CvInvoke.cvMeanShift(sampleImage.Ptr, window, param.criteria, out comp); int center = comp.rect.Left + comp.rect.Width / 2; //收敛中心 ClusterCenter<Ycc> cnew = new ClusterCenter<Ycc>(historyImages[center][row, col], 1d / (param.m + frameCount), 1d, frameCount); //将收敛中心作为新的聚集中心 clusterCenters[row, col].Add(cnew); //设置背景掩码图像的前景点 *(ptrBackgroundMask + row * widthStepBackgroundMask + col) = f; } else { //该点为背景:更新最近聚集中心的ci为(li+1)/m clusterCenters[row, col][nearestIndex].li++; clusterCenters[row, col][nearestIndex].wi = clusterCenters[row, col][nearestIndex].li / (param.m + frameCount); clusterCenters[row, col][nearestIndex].updateFrameNo = frameCount; } } } //4.生成最可靠背景模型 GetMrbm(); } public void Update(Image<Bgr, byte> currentFrame) { Image<Ycc, byte> image = currentFrame.Convert<Ycc, byte>(); Update(image); image.Dispose(); } /// <summary> /// 用wi>=theta作为条件选择可能的背景模型Cb;对每个观测值x0,计算x0与Cb的最小差值d /// </summary> /// <param name="x0">观测值x0</param> /// <param name="centerList">某像素对应的聚集中心列表</param> /// <param name="nearestIndex">输出参数:最近的聚集中心索引</param> /// <returns>返回最小差值d</returns> private double GetMinD(double x0, List<ClusterCenter<Ycc>> centerList, out int nearestIndex) { double d = double.MaxValue; nearestIndex = 0; for (int idx = 0; idx < centerList.Count; idx++) { ClusterCenter<Ycc> center = centerList[idx]; if (center.wi >= param.theta) { double d0 = Math.Abs(center.ci.Y - x0); if (d0 < d) { d = d0; nearestIndex = idx; } } } return d; } /// <summary> /// 清除不活跃的聚集中心 /// </summary> /// <param name="staleThresh">不活跃阀值,不活跃帧数大于该值的聚集中心将被清除</param> public void ClearStale(int staleThresh) { //遍历每个像素的聚集中心 for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { for (int idx = clusterCenters[row, col].Count - 1; idx >= 0; idx--) { if (frameCount - clusterCenters[row, col][idx].updateFrameNo > staleThresh) clusterCenters[row, col].RemoveAt(idx); } } } } } /// <summary> /// 聚集中心 /// </summary> /// <typeparam name="TColor">聚集中心使用的色彩空间</typeparam> public class ClusterCenter<TColor> where TColor : struct, IColor { public TColor ci; //聚集中心的像素值 public double wi; //聚集中心的权重 public double li; //聚集中心包含的收敛点数目 public double updateFrameNo; //更新该聚集中心时的帧数:用于消除消极的聚集中心 public ClusterCenter(TColor ci, double wi, double li, double updateFrameNo) { this.ci = ci; this.li = li; this.wi = wi; this.updateFrameNo = updateFrameNo; } } /// <summary> /// 非参数背景模型的参数 /// </summary> public struct NBGParameter { public int n; //样本数目:需要被保留的历史图像数目 public int m; //典型点数目:历史图像需要被分为多少组 public double theta; //权重系数:权重大于该值的聚集中心为候选背景 public double t; //最小差值:观测值与候选背景的最小差值大于该值时,为前景;否则为背景 public MCvTermCriteria criteria; //Mean Shift计算的终止条件:包括最大迭代次数和终止计算的精度 public NBGParameter(int n, int m, double theta, double t, MCvTermCriteria criteria) { this.n = n; this.m = m; this.theta = theta; this.t = t; this.criteria.type = criteria.type; this.criteria.max_iter = criteria.max_iter; this.criteria.epsilon = criteria.epsilon; } public static NBGParameter GetDefaultNBGParameter() { return new NBGParameter(100, 10, 0.3d, 10d, new MCvTermCriteria(100, 1d)); } } }
3 NBGModel类介绍
3.1 属性
Width——获取图像的宽度
Height——获取图像的高度
Param——获取参数设置
Mrbm——获取最可靠背景模型图像
BackgroundMask——获取背景掩码图像
ForegroundMask——获取前景掩码图像
FrameCount——获取已被检测的帧数
3.2 构造函数
public NBGModel(int width, int height)——用默认的参数初始化NBGModel,等价于NBGModel(width, height, NBGParameter.GetDefaultNBGParameter())
public NBGModel(int width, int height, NBGParameter param)——用指定的参数初始化NBGModel
3.3 方法
AddHistoryImage——添加一幅或者一组历史图像
TrainBackgroundModel——训练背景模型;如果传入了历史图像,则先添加历史图像,然后再训练背景模型
Update——更新背景模型,同时检测前景
ClearStale——清除消极的聚集中心
Dispose——释放资源
4 体会
NBGModel的确非常有效,非常简洁,特别适用于伴随复杂运动对象的背景建模。我特意选取了PETS2009中的素材对其做了一些测试,结果也证明了NBGModel的优越性。不过需要指出的是,它需要占用大量的内存(主要因为需要保存n幅历史图像);它的计算量比较大。
在使用的过程中,它始终需要在内存中缓存n幅历史图像,1幅最可靠背景模型图像,1幅背景掩码图像,近似m幅图像(聚集中心);而在训练阶段,更需要临时存储m幅收敛图像。
例如:样本数目为100,典型点数目为10,图像尺寸为768x576时,所用的内存接近300M,训练背景需要大约需要33秒,而对每幅图像进行前景检测大约需要600ms。虽然可以使用并行编程来提高性能,但是并不能从根本上解决问题。
(注:测试电脑的CPU为AMD闪龙3200+,内存1.5G。)
看来,有必要研究一种新的方法,目标是检测效果更好,内存占用低,处理更快速。目前的想法是使用《Wallflower: Principles and Practice of Background Manitenance》中的3层架构(时间轴上的像素级处理,像素间的区域处理,帧间处理),但是对每层架构都选用目前流行的处理方式,并对处理方式进行优化。时间轴上的像素级处理打算使用CodeBook方法,但是增加本文的一些思想。像素间的区域处理打算参考《基于区域相关的核函数背景建模算法》中的方法。帧间处理预计会采用全局灰度统计值作为依据。
最后,按照惯例:感谢您耐心看完本文,希望对您有所帮助。
本文所述方法及代码仅用于学习研究,不得用于商业目的。
参考文献 Multi-Layer Background Subtraction Based on Color and Texture CVPR-VS 2007
与前一篇文章的大体思路一致,提取纹理特征和颜色特征,建立背景模型,并实时更新背景模型.
纹理特征:LBP.
颜色特征:借鉴码本模型,颜色空间的分布模型,参用当前像素点与背景像素点的夹角,以及最小和最大值作为颜色特征.
纹理特征相似度计算:
颜色特征相似度计算:
最终的相似度计算,是纹理特征和颜色特征相似度的加权和.
背景模描述:
背景更新,与前一篇文章类似,只不过其权值更新的学习因子,是根据该模型最大权值的大小来调整的.
对于当前权值较小, 但是曾经具有大权值的模型如果出现匹配情况时权值的增加非常快.
对于当前权值较小,但是曾经具有大权值的模型如果出现不匹配情况时权值的减少非常慢.
对于曾经具有过大权值的模型, 其实很可能属于背景模型,
当再次出现匹配这类模型的像素时, 快速恢复其权值是非常合理的,
不匹配这类模型时,缓慢减少其权值是合理的.
其存在匹配的模型时,对应的的第k个模型更新公式如下:
其他模型,只更新其权值,其他的保持不变,
前景检测的步骤:
1).计算与背景模型中K个模型的最小距离,记最小距离的模型为第i个模型.
2).判断第i个模型,是否为背景模型,是否曾经可判断为可信的背景模型.
3).若是,则保持最小距离不变,若不是,则更新最小距离的值.
4).利用the cross bilateral filter平滑最小距离值.
5).根据最小距离值与阈值的大小,确定对应像素点是前景,还是背景.
其具体计算公式如下:
本文创新点:
1).颜色特征的选择.
2).权值的更新方式,利用了权值对应的历史信息.
3).引入分层的概念,利用模型被匹配为可信背景模型的历史信息,来判断当前模型匹配的可信度.
4).在计算得最小距离值,进行平滑处理,而不是得到前景和背景后再做平滑处理.
不足之处:参数的选择和阈值的选择对检测结果的影响,如何选择最合适的一组参数值.
参考文献 基于颜色和纹理特征背景模型的多层差分运动目标检测算法 计算机应用 2009
初看这篇文章,感觉与上面的英语文章有许多类似之处,不过细看,仍有些不同之处.
1).该文是将纹理特征和颜色特征分开进行匹配计算,得到两种特征下结果,最后再将两种结果融合.
2).该文中纹理特征匹配和更新时,其与混合高斯模型的思路一致,利用方差来调整匹配对应的阈值.