ViBe - a powerful technique for background detection and subtraction in video sequences
ViBe 是一种像素级视频背景建模或前景检测的算法,效果优于所熟知的几种算法,对硬件内存占用也少。ViBe 是一种像素级的背景建模、前景检测算法,该算法主要不同之处是背景模型的更新策略,随机选择需要替换的像素的样本,随机选择邻域像素进行更新。在无法确定像素变化的模型时,随机的更新策略,在一定程度上可以模拟像素变化的不确定性。
背景差方法实现运动物体检测面临的挑战主要有:

ViBe 是本篇论文中所提出的一个检测方法,相比于其他方法它有很多的不同和优点。具体的思想就是为每个像素点存储了一个样本集,样本集中采样值就是该像素点过去的像素值和其邻居点的像素值,然后将每一个新的像素值和样本集进行比较来判断是否属于背景点。
该模型主要包括三个方面:
背景物体就是指静止的或是非常缓慢的移动的物体,而前景物体就对应移动的物体。所以我们可以把物体检测看成一个分类问题,也就是来确定一个像素点是否属于背景点。在 ViBe 模型中,背景模型为每个背景点存储了一个样本集,然后将每一个新的像素值和样本集进行比较来判断是否属于背景点。可以知道如果一个新的观察值属于背景点那么它应该和样本集中的采样值比较接近。
具体的讲,我们记 V(x):x 点处的像素值; M(x)={$$V_1$$,$$V_2$$,…$$V_N$$} 为 x 处的背景样本集(样本集大小为 N);SR(v(x)):以 x 为中心 R 为半径的区域,如果 M(x) [{SR(v(x))∩ {$$v_1$$,$$v_2$$, . . . , $$v_N$$}}] 大于一个给定的阈值 min,那么就认为 x 点属于背景点。
初始化是建立背景模型的过程,一般的检测算法需要一定长度的视频序列学习完成,影响了检测的实时性,而且当视频画面突然变化时,重新学习背景模型需要较长时间。
ViBe 算法主要是利用单帧视频序列初始化背景模型,对于一个像素点,结合相邻像素点拥有相近像素值的空间分布特性,随机的选择它的邻域点的像素值作为它的模型样本值。ViBe 的初始化仅仅通过一帧图像即可完成。ViBe 初始化就是填充像素的样本集的过程但是由于在一帧图像中不可能包含像素点的时空分布信息,我们利用了相近像素点拥有相近的时空分布特性,具体来讲就是:对于一个像素点,随机的选择它的邻居点的像素值作为它的模型样本值。
M0(x)=v0(y|y∈NG(x)),t=0
初始时刻,NG(x) 即为邻居点 。这种初始化方法优点是对于噪声的反应比较灵敏,计算量小速度快,可以很快的进行运动物体的检测,缺点是容易引入 Ghost 区域。
1.背景模型为每个背景点存储一个样本集,然后每个新的像素值和样本集比较判断是否属于背景。
2.计算新像素值和样本集中每个样本值的距离,若距离小于阈值,则近似样本点数目增加。
3.如果近似样本点数目大于阈值,则认为新的像素点为背景。
4.检测过程主要由三个参数决定:样本集数目 N,阈值 min和距离相近判定的阈值 R,一般具体实现,参数设置为 N=20,min=2,R=20。
1).无记忆更新策略
每次确定需要更新像素点的背景模型时,以新的像素值随机取代该像素点样本集的一个样本值。
2).时间取样更新策略
并不是每处理一帧数据,都需要更新处理,而是按一定的更新率更新背景模型。当一个像素点被判定为背景时,它有 1/rate 的概率更新背景模型。rate 是时间采样因子,一般取值为 16。
3).空间邻域更新策略
针对需要更新像素点,随机的选择一个该像素点邻域的背景模型,以新的像素点更新被选中的背景模型。
以圆椎模型代替原来的几何距离计算方法

以自适应阈值代替原来固定的距离判定阈值,阈值大小与样本集的方差成正比,样本集方差越大,说明背景越复杂,判定阈值应该越大.
0.5×σm∈[20,40]
引入目标整体的概念,弥补基于像素级前景检测的不足。针对 updating mask 和 segmentation mask 采用不同尺寸的形态学处理方法,提高检测准确率。
在 updating mask 里,计算像素点的梯度,根据梯度大小,确定是否需要更新邻域。梯度值越大,说明像素值变化越大,说明该像素值可能为前景,不应该更新。
引入闪烁程度的概念,当一个像素点的 updating label 与前一帧的 updating label 不一样时,blinking level 增加 15,否则,减少 1,然后根据 blinking level 的大小判断该像素点是否为闪烁点。闪烁像素主要出现在背景复杂的场景,如树叶、水纹等,这些场景会出现像素背景和前景的频繁变化,因而针对这些闪烁应该单独处理,可以作为全部作为背景。
ViBe 算法中,默认的更新因子是 16,当背景变化很快时,背景模型无法快速的更新,将会导致前景检测的较多的错误。因而,需要根据背景变化快慢程度,调整更新因子的大小,可将更新因子分多个等级,如 rate = 16,rate = 5,rate = 1。
在实验中,我们和其他的一些检测算法在检测准确率和算法的计算量方面都进行了比较,实验表明我们的方法检测效果明显要好很多,对于光照的变化和相机抖动等的效果都十分稳定,而且计算量非常小,内存占用较少,这就使得该方法能够用于嵌入手持照相机中。一些具体的实验效果和数据如下:





在这片文章中,我们提出了一个新的背静差算法-ViBe,和以前相比它具有三个不同点。首先,我们提出了一个新的分类模型。其次,我们介绍了 ViBe 如何初始化,它只需要一帧图像即可完成初始化,而其他的算法通常需要等待数秒去完成初始化,这对于嵌入照相机中的要求实时性比较高的和一些比较短的视频序列很有帮助。 最后,我们提出了自己的更新策略,相比于其他算法将样本值在模型中保存一个固定的时间,我们采用随机的替换更新样本值,经过证明这样可以保证样本值的一个指数衰减的平滑生命周期,并且可以使得背景模型很好的适应视频场景的变化,从而达到更好的检测效果。
通过一系列实验表明 ViBe 方法相比于其他的一些检测算法具有计算量小、内存占用少、处理速度快、检测效果好、有更快的 Ghost 区域消融速度和应对噪声稳定可靠的特点,并且非常适合嵌入照相机等要求计算量小和内存占用少的情境中。
效果图:

算法执行效率测试程序,windows 和 Linux 操作系统下的程序和 c/c++文件都可以在作者官网下载,如下:
当然这里也借鉴 zouxy09 大神给出的 Mat 格式的代码(在 VS2010+OpenCV2.4.2 中测试通过):
#pragma once
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace cv;
using namespace std;
#define NUM_SAMPLES 20 //每个像素点的样本个数
#define MIN_MATCHES 2 //#min 指数
#define RADIUS 20 //Sqthere 半径
#define SUBSAMPLE_FACTOR 16 //子采样概率
class ViBe_BGS
{
public:
ViBe_BGS(void);
~ViBe_BGS(void);
void init(const Mat _image); //初始化
void processFirstFrame(const Mat _image);
void testAndUpdate(const Mat _image); //更新
Mat getMask(void){return m_mask;};
private:
Mat m_samples[NUM_SAMPLES];
Mat m_foregroundMatchCount;
Mat m_mask;
};
#include <opencv2/opencv.hpp>
#include <iostream>
#include "ViBe.h"
using namespace std;
using namespace cv;
int c_xoff[9] = {-1, 0, 1, -1, 1, -1, 0, 1, 0}; //x 的邻居点
int c_yoff[9] = {-1, 0, 1, -1, 1, -1, 0, 1, 0}; //y 的邻居点
ViBe_BGS::ViBe_BGS(void)
{
}
ViBe_BGS::~ViBe_BGS(void)
{
}
/**************** Assign space and init ***************************/
void ViBe_BGS::init(const Mat _image)
{
for(int i = 0; i < NUM_SAMPLES; i++)
{
m_samples[i] = Mat::zeros(_image.size(), CV_8UC1);
}
m_mask = Mat::zeros(_image.size(),CV_8UC1);
m_foregroundMatchCount = Mat::zeros(_image.size(),CV_8UC1);
}
/**************** Init model from first frame ********************/
void ViBe_BGS::processFirstFrame(const Mat _image)
{
RNG rng;
int row, col;
for(int i = 0; i < _image.rows; i++)
{
for(int j = 0; j < _image.cols; j++)
{
for(int k = 0 ; k < NUM_SAMPLES; k++)
{
// Random pick up NUM_SAMPLES pixel in neighbourhood to construct the model
int random = rng.uniform(0, 9);
row = i + c_yoff[random];
if (row < 0)
row = 0;
if (row >= _image.rows)
row = _image.rows - 1;
col = j + c_xoff[random];
if (col < 0)
col = 0;
if (col >= _image.cols)
col = _image.cols - 1;
m_samples[k].at<uchar>(i, j) = _image.at<uchar>(row, col);
}
}
}
}
/**************** Test a new frame and update model ********************/
void ViBe_BGS::testAndUpdate(const Mat _image)
{
RNG rng;
for(int i = 0; i < _image.rows; i++)
{
for(int j = 0; j < _image.cols; j++)
{
int matches(0), count(0);
float dist;
while(matches < MIN_MATCHES && count < NUM_SAMPLES)
{
dist = abs(m_samples[count].at<uchar>(i, j) - _image.at<uchar>(i, j));
if (dist < RADIUS)
matches++;
count++;
}
if (matches >= MIN_MATCHES)
{
// It is a background pixel
m_foregroundMatchCount.at<uchar>(i, j) = 0;
// Set background pixel to 0
m_mask.at<uchar>(i, j) = 0;
// 如果一个像素是背景点,那么它有 1 / defaultSubsamplingFactor 的概率去更新自己的模型样本值
int random = rng.uniform(0, SUBSAMPLE_FACTOR);
if (random == 0)
{
random = rng.uniform(0, NUM_SAMPLES);
m_samples[random].at<uchar>(i, j) = _image.at<uchar>(i, j);
}
// 同时也有 1 / defaultSubsamplingFactor 的概率去更新它的邻居点的模型样本值
random = rng.uniform(0, SUBSAMPLE_FACTOR);
if (random == 0)
{
int row, col;
random = rng.uniform(0, 9);
row = i + c_yoff[random];
if (row < 0)
row = 0;
if (row >= _image.rows)
row = _image.rows - 1;
random = rng.uniform(0, 9);
col = j + c_xoff[random];
if (col < 0)
col = 0;
if (col >= _image.cols)
col = _image.cols - 1;
random = rng.uniform(0, NUM_SAMPLES);
m_samples[random].at<uchar>(row, col) = _image.at<uchar>(i, j);
}
}
else
{
// It is a foreground pixel
m_foregroundMatchCount.at<uchar>(i, j)++;
// Set background pixel to 255
m_mask.at<uchar>(i, j) = 255;
//如果某个像素点连续 N 次被检测为前景,则认为一块静止区域被误判为运动,将其更新为背景点
if (m_foregroundMatchCount.at<uchar>(i, j) > 50)
{
int random = rng.uniform(0, SUBSAMPLE_FACTOR);
if (random == 0)
{
random = rng.uniform(0, NUM_SAMPLES);
m_samples[random].at<uchar>(i, j) = _image.at<uchar>(i, j);
}
}
}
}
}
}
#include "opencv2/opencv.hpp"
#include "ViBe.h"
#include <iostream>
#include <cstdio>
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
Mat frame, gray, mask;
VideoCapture capture;
capture.open("video.avi");
if (!capture.isOpened())
{
cout<<"No camera or video input!\n"<<endl;
return -1;
}
ViBe_BGS Vibe_Bgs;
int count = 0;
while (1)
{
count++;
capture >> frame;
if (frame.empty())
break;
cvtColor(frame, gray, CV_RGB2GRAY);
if (count == 1)
{
Vibe_Bgs.init(gray);
Vibe_Bgs.processFirstFrame(gray);
cout<<" Training GMM complete!"<<endl;
}
else
{
Vibe_Bgs.testAndUpdate(gray);
mask = Vibe_Bgs.getMask();
morphologyEx(mask, mask, MORPH_OPEN, Mat());
imshow("mask", mask);
}
imshow("input", frame);
if ( cvWaitKey(10) == 'q' )
break;
}
return 0;
}

