• 背景建模与前景检测之三(Background Generation And Foreground Detection Phase 3)


    作者:王先荣

        在上一篇文章里,我尝试翻译了《Nonparametric Background Generation》,本文主要介绍以下内容:如何实现该论文的算法,如果利用该算法来进行背景建模及前景检测,最后谈谈我的一些体会。为了使描述更加简便,以下将该论文的算法及实现称为NBGModel。
    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)在必要的时候,清理消极的聚集中心。

        2.4 NBGModel的实现代码
    值得注意的是:在实现代码中,有好几个以2结尾的私有方法,它们主要用于演示算法流程,实际上并未使用。为了优化性能而增加了不少指针操作之后的代码可读性变得很差。

    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方法,但是增加本文的一些思想。像素间的区域处理打算参考《基于区域相关的核函数背景建模算法》中的方法。帧间处理预计会采用全局灰度统计值作为依据。

    最后,按照惯例:感谢您耐心看完本文,希望对您有所帮助。
    本文所述方法及代码仅用于学习研究,不得用于商业目的。

  • 相关阅读:
    Red packet
    Pie
    River Hopscotch
    5.spring使用注解开发
    11.Java邮件发送
    10.Java文件传输
    HTML中标签的嵌套原则
    9.SMBMS超市订单管理系统
    8.MVC和过滤器Filter
    7.JSP基础语法,指令和标签以及Java Bean
  • 原文地址:https://www.cnblogs.com/xrwang/p/BackgroundGenerationAndForegroundDetectionPhase3.html
Copyright © 2020-2023  润新知