Vibe演算法原理與實踐(C++)
簡介
ViBe - a powerful technique for background detection and subtraction in video sequences
ViBe是一種畫素級視訊背景建模或前景檢測的演算法,效果優於所熟知的幾種演算法,對硬體記憶體佔用也少。ViBe是一種畫素級的背景建模、前景檢測演算法,該演算法主要不同之處是背景模型的更新策略,隨機選擇需要替換的畫素的樣本,隨機選擇鄰域畫素進行更新。在無法確定畫素變化的模型時,隨機的更新策略,在一定程度上可以模擬畫素變化的不確定性。
背景差方法實現運動物體檢測面臨的挑戰主要有:
- 可直接應用在產品中,軟硬體相容性好;
- 必須適應環境的變化(比如光照的變化造成影象色度的變化);
- 相機抖動引起畫面的抖動(比如手持相機拍照時候的移動);
- 影象中密集出現的物體(比如樹葉或樹幹等密集出現的物體,要正確的檢測出來);
- 須能夠正確的檢測出背景物體的改變(比如新停下的車必須及時的歸為背景物體,而有靜止開始移動的物體也需要及時的檢測出來)。
- 物體檢測中往往會出現Ghost區域,Ghost區域也就是指當一個原本靜止的物體開始運動,背景差檢測演算法可能會將原來該物體所覆蓋的區域錯誤的檢測為運動的,這塊區域就成為Ghost,當然原來運動的物體變為靜止的也會引入Ghost區域,Ghost區域在檢測中必須被儘快的消除。一個Ghost區域的例項如圖1,在圖中可以發現相比於原圖,檢測的結果中錯誤的多出現了兩個人的形狀,這就是Ghost。
ViBe檢測方法
ViBe是本篇論文中所提出的一個檢測方法,相比於其他方法它有很多的不同和優點。具體的思想就是為每個畫素點儲存了一個樣本集,樣本集中取樣值就是該畫素點過去的畫素值和其鄰居點的畫素值,然後將每一個新的畫素值和樣本集進行比較來判斷是否屬於背景點。
該模型主要包括三個方面:
模型的工作原理;
模型的初始化方法;
模型的更新策略。
模型的工作原理
背景物體就是指靜止的或是非常緩慢的移動的物體,而前景物體就對應移動的物體。所以我們可以把物體檢測看成一個分類問題,也就是來確定一個畫素點是否屬於背景點。在ViBe模型中,背景模型為每個背景點儲存了一個樣本集,然後將每一個新的畫素值和樣本集進行比較來判斷是否屬於背景點。可以知道如果一個新的觀察值屬於背景點那麼它應該和樣本集中的取樣值比較接近。
具體的講,我們記V(x):x點處的畫素值;M(x)={V1,V2,…VN}為x處的背景樣本集(樣本集大小為N);SR(v(x)):以x為中心R為半徑的區域,如果M(x) [{SR(v(x))∩ {v1,v2, . . . , vN}}]大於一個給定的閾值min,那麼就認為x點屬於背景點。
背景模型的初始化
初始化是建立背景模型的過程,一般的檢測演算法需要一定長度的視訊序列學習完成,影響了檢測的實時性,而且當視訊畫面突然變化時,重新學習背景模型需要較長時間。
ViBe演算法主要是利用單幀視訊序列初始化背景模型,對於一個畫素點,結合相鄰畫素點擁有相近畫素值的空間分佈特性,隨機的選擇它的鄰域點的畫素值作為它的模型樣本值。ViBe的初始化僅僅通過一幀影象即可完成。ViBe初始化就是填充畫素的樣本集的過程但是由於在一幀影象中不可能包含畫素點的時空分佈資訊,我們利用了相近畫素點擁有相近的時空分佈特性,具體來講就是:對於一個畫素點,隨機的選擇它的鄰居點的畫素值作為它的模型樣本值。M0(x)=v0(y|y∈NG(x)),t=0初始時刻,NG(x)即為鄰居點 。這種初始化方法優點是對於噪聲的反應比較靈敏,計算量小速度快,可以很快的進行運動物體的檢測,缺點是容易引入Ghost區域。
- 優點:不僅減少了背景模型建立的過程,還可以處理背景突然變化的情況,當檢測到背景突然變化明顯時,只需要捨棄原始的模型,重新利用變化後的首幀影象建立背景模型。
- 缺點:由於可能採用了運動物體的畫素初始化樣本集,容易引入拖影(Ghost)區域。
前景檢測過程
1.背景模型為每個背景點儲存一個樣本集,然後每個新的畫素值和樣本集比較判斷是否屬於背景。
2.計算新畫素值和樣本集中每個樣本值的距離,若距離小於閾值,則近似樣本點數目增加。
3.如果近似樣本點數目大於閾值,則認為新的畫素點為背景。
4.檢測過程主要由三個引數決定:樣本集數目N,閾值min和距離相近判定的閾值R,一般具體實現,引數設定為N=20,min=2,R=20。
背景模型的更新策略
背景模型的更新就是使得背景模型能夠適應背景的不斷變化,比如光照的變化,背景物體的變更等等。
保守的更新策略:前景點永遠不會被用來填充背景模型,會引起死鎖,比如初始化的時候如果一塊靜止的區域被錯誤的檢測為運動的,那麼在這種策略下它永遠會被當做運動的物體來對待;
Blind策略:對死鎖不敏感,前景背景都可以來更新背景模型,缺點是緩慢移動的物體會融入背景中無法被檢測出來。在本方法中採用的更新策略是保守的更新策略+前景點計數方法。
前景點計數:對畫素點進行統計,如果某個畫素點連續N次被檢測為前景,則將其更新為背景點。
隨機的子取樣:在每一個新的視訊幀中都去更新背景模型中的每一個畫素點的樣本值是沒有必要的,當一個畫素點被分類為背景點時,它有1/ φ的概率去更新背景模型。
具體的更新方法:每一個背景點有1/ φ的概率去更新自己的模型樣本值,同時也有1/ φ的概率去更新它的鄰居點的模型樣本值。更新鄰居的樣本值利用了畫素值的空間傳播特性,背景模型逐漸向外擴 散,這也有利於Ghost區域的更快的識別。同時當前景點計數達到臨界值時將其變為背景,並有1/ φ的概率去更新自己的模型樣本值。
在選擇要替換的樣本集中的樣本值時候,我們是隨機選取一個樣本值進行更新,這樣可以保證樣本值的平滑的生命週期由於是隨機的更新,這樣一個樣本值在時刻t不被更新的概率是 (N-1)/N,假設時間是連續的,那麼在dt的時間過去後,樣本值仍然保留的概率是
也可以寫作:
這就表明一個樣本值在模型中是否被替換與時間t無關 ,隨機策略是合適的。
1).無記憶更新策略
每次確定需要更新畫素點的背景模型時,以新的畫素值隨機取代該畫素點樣本集的一個樣本值。
2).時間取樣更新策略
並不是每處理一幀資料,都需要更新處理,而是按一定的更新率更新背景模型。當一個畫素點被判定為背景時,它有1/rate的概率更新背景模型。rate是時間取樣因子,一般取值為16。
3).空間鄰域更新策略
針對需要更新畫素點,隨機的選擇一個該畫素點鄰域的背景模型,以新的畫素點更新被選中的背景模型。
ViBe的改進
- 不同的距離函式和二值化標準
- 對更新掩膜和輸出掩膜的分割進行適當的濾波操作
- 抑制鄰域更新
- 檢測閃爍的畫素點
- 增加更新因子
距離計算方法
以自適應閾值代替原來固定的距離判定閾值,閾值大小與樣本集的方差成正比,樣本集方差越大,說明背景越複雜,判定閾值應該越大.
分離UpdatingMask和SegmentationMask
引入目標整體的概念,彌補基於畫素級前景檢測的不足。針對updating mask和segmentation mask採用不同尺寸的形態學處理方法,提高檢測準確率。
抑制鄰域更新
在updating mask裡,計算畫素點的梯度,根據梯度大小,確定是否需要更新鄰域。梯度值越大,說明畫素值變化越大,說明該畫素值可能為前景,不應該更新。
檢測閃爍畫素點
引入閃爍程度的概念,當一個畫素點的updating label與前一幀的updating label不一樣時,blinking level增加15,否則,減少1,然後根據blinking level的大小判斷該畫素點是否為閃爍點。閃爍畫素主要出現在背景複雜的場景,如樹葉、水紋等,這些場景會出現畫素背景和前景的頻繁變化,因而針對這些閃爍應該單獨處理,可以作為全部作為背景。
增加更新因子
ViBe演算法中,預設的更新因子是16,當背景變化很快時,背景模型無法快速的更新,將會導致前景檢測的較多的錯誤。因而,需要根據背景變化快慢程度,調整更新因子的大小,可將更新因子分多個等級,如rate = 16,rate = 5,rate = 1。
vibeRely.h
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
using namespace std;
using namespace cv;
#define defaultNbSamples 20 //每個畫素點的樣本個數
#define defaultReqMatches 2 //#min指數
#define defaultRadius 20 //Sqthere半徑
#define defaultSubsamplingFactor 16 //子取樣概率
#define background 0 //背景畫素
#define foreground 255 //前景畫素
void Initialize(CvMat* pFrameMat,RNG rng);//初始化
void update(CvMat* pFrameMat,CvMat* segMat,RNG rng,int nFrmNum);//更新
vibeRely.cpp
#include "vibeRely.h"
#include <opencv2/opencv.hpp>
#include "highgui.h"
#include <math.h>
#include <iostream>
using namespace std;
using namespace cv;
static int c_xoff[9] = {-1, 0, 1, -1, 1, -1, 0, 1, 0};//x的鄰居點
static int c_yoff[9] = {-1, 0, 1, -1, 1, -1, 0, 1, 0};//y的鄰居點
float samples[1024][1024][defaultNbSamples+1];//儲存每個畫素點的樣本值
//初始化
void Initialize(CvMat* pFrameMat,RNG rng){
//記錄隨機生成的 行(r) 和 列(c)
int rand,r,c;
//對每個畫素樣本進行初始化
for(int y=0;y<pFrameMat->rows;y++){//Height
for(int x=0;x<pFrameMat->cols;x++){//Width
for(int k=0;k<defaultNbSamples;k++){
//隨機獲取畫素樣本值
rand=rng.uniform( 0, 9 );
r=y+c_yoff[rand]; if(r<0) r=0; if(r>=pFrameMat->rows) r=pFrameMat->rows-1; //行
c=x+c_xoff[rand]; if(c<0) c=0; if(c>=pFrameMat->cols) c=pFrameMat->cols-1; //列
//儲存畫素樣本值
samples[y][x][k]=CV_MAT_ELEM(*pFrameMat,float,r,c);
}
samples[y][x][defaultNbSamples]=0;
}
}
}
//更新函式
void update(CvMat* pFrameMat,CvMat* segMat,RNG rng,int nFrmNum){
for(int y=0;y<pFrameMat->rows;y++){ //Height
for(int x=0;x<pFrameMat->cols;x++){ //Width
//用於判斷一個點是否是背景點,index記錄已比較的樣本個數,count表示匹配的樣本個數
int count=0,index=0;float dist=0;
//
while((count<defaultReqMatches) && (index<defaultNbSamples)){
dist=CV_MAT_ELEM(*pFrameMat,float,y,x)-samples[y][x][index];
if(dist<0) dist=-dist;
if(dist<defaultRadius) count++;
index++;
}
if(count>=defaultReqMatches){
//判斷為背景畫素,只有背景點才能被用來傳播和更新儲存樣本值
samples[y][x][defaultNbSamples]=0;
*((float *)CV_MAT_ELEM_PTR(*segMat,y,x))=background;
int rand=rng.uniform(0,defaultSubsamplingFactor);
if(rand==0){
rand=rng.uniform(0,defaultNbSamples);
samples[y][x][rand]=CV_MAT_ELEM(*pFrameMat,float,y,x);
}
rand=rng.uniform(0,defaultSubsamplingFactor);
if(rand==0){
int xN,yN;
rand=rng.uniform(0,9);yN=y+c_yoff[rand];if(yN<0) yN=0; if(yN>=pFrameMat->rows) yN=pFrameMat->rows-1;
rand=rng.uniform(0,9);xN=x+c_xoff[rand];if(xN<0) xN=0; if(xN>=pFrameMat->cols) xN=pFrameMat->cols-1;
rand=rng.uniform(0,defaultNbSamples);
samples[yN][xN][rand]=CV_MAT_ELEM(*pFrameMat,float,y,x);
}
}
else {
//判斷為前景畫素
*((float *)CV_MAT_ELEM_PTR(*segMat,y,x))=foreground;
samples[y][x][defaultNbSamples]++;
if(samples[y][x][defaultNbSamples]>50){ ////保守策略的閾值,大於該數則為前景 預設50
int rand=rng.uniform(0,defaultNbSamples);
if(rand==0){
rand=rng.uniform(0,defaultNbSamples);
samples[y][x][rand]=CV_MAT_ELEM(*pFrameMat,float,y,x);
}
}
}
}
}
}
main.cpp
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
#include<cxcore.h>
#include <cvaux.h>
#include <ctime>
#include<direct.h>
#include<time.h>
#include "vibeRely.h"
using namespace std;
using namespace cv;
int nFrmNum = 0;//記錄幀數
int Width;//記錄幀的寬度
int Height;//記錄幀的高度
int FrameRate;//記錄視訊幀率
int main(int argc, char* argv[])
{
clock_t startTime;
clock_t endTime;
double duration;
IplImage* pFrame=NULL;CvMat* pFrameMat = NULL;//pFrame物件
IplImage* pAfter=NULL;CvMat* pAfterMat=NULL;//儲存pFrame對應的灰度影象
IplImage* segMap=NULL;CvMat* segMat=NULL;//儲存處理後的資訊
IplImage* bFrame=NULL;
RNG rng(0xFFFFFFFF); //建立一個隨機數生成器
char pBuf[100];
_getcwd(pBuf,sizeof(pBuf)); //獲得當前工程所在路徑
strcat_s(pBuf,"//car.flv"); //pbuf為當前工程目錄下的需要讀取的視訊檔案
char outfile[7] ;
//char filename[100];//儲存的影象位置和名稱
outfile[0] = 'o';
outfile[1] = 'u';
outfile[2] = 't';
outfile[3] = '.';
outfile[4] = 'a';
outfile[5] = 'v';
outfile[6] = 'i';
//開啟視訊檔案
CvCapture* pCapture=cvCreateFileCapture(pBuf);
if(pCapture==NULL) {
cout<<"video file open error!"<<endl;
return -1;
}
int array_cross[] = { 0, 0xff, 0,
0xff,0xff, 0xff,
0 ,0xff, 0
};
IplConvKernel * rectCross = cvCreateStructuringElementEx(3, 3, 1, 1, CV_SHAPE_CROSS, array_cross);
//獲取視訊相關資訊,幀率和大小
double fps = cvGetCaptureProperty(pCapture, CV_CAP_PROP_FPS);
CvSize pSize=cvSize((int)cvGetCaptureProperty(pCapture,CV_CAP_PROP_FRAME_WIDTH),(int)cvGetCaptureProperty(pCapture,CV_CAP_PROP_FRAME_HEIGHT));
//建立輸出視訊檔案
CvVideoWriter* Save_result = NULL;
Save_result = cvCreateVideoWriter(outfile, CV_FOURCC('X', 'V', 'I', 'D'), fps, pSize, 1);
// Save_result = cvCreateVideoWriter(outfile, CV_FOURCC('M', 'J', 'P', 'G'), fps, pSize, 1);
IplImage* dstImg = cvCreateImage(pSize, IPL_DEPTH_8U, 1);//建立要儲存的影象
cvNamedWindow("src",CV_WINDOW_AUTOSIZE);
cvMoveWindow("src", 100,200);
cvNamedWindow("dst",CV_WINDOW_AUTOSIZE);
cvMoveWindow("dst",100,200);
//VideoWriter w_cap("re_video.avi", CV_FOURCC('M', 'J', 'P', 'G'), rate, cv::Size(width, height));
//逐幀讀取視訊並進行處理
while(pFrame = cvQueryFrame( pCapture )){
nFrmNum++;
startTime = clock();
//如果是第一幀,申請記憶體並進行初始化
if(nFrmNum==1){
segMap = cvCreateImage(cvSize(pFrame->width, pFrame->height),
IPL_DEPTH_8U,1);
segMat = cvCreateMat(pFrame->height, pFrame->width, CV_32FC1);
//原始影象的灰度圖
pAfter=cvCreateImage(cvSize(pFrame->width, pFrame->height),
IPL_DEPTH_8U,1);
// 原始影象灰度圖矩陣
pAfterMat=cvCreateMat(pFrame->height, pFrame->width, CV_32FC1);
//轉化成單通道影象再處理
cvCvtColor(pFrame, pAfter, CV_BGR2GRAY);
cvConvert(pAfter, pAfterMat);
Initialize(pAfterMat,rng);
}
else {
IplConvKernel * myModel;
myModel=cvCreateStructuringElementEx(3,3,1,1,CV_SHAPE_CROSS);
cvCvtColor(pFrame,pAfter,CV_BGR2GRAY);
cvConvert(pAfter,pAfterMat);
update(pAfterMat,segMat,rng,nFrmNum); //更新背景
cvConvert(segMat,segMap);
//cvErode( segMap,segMap,myModel,1);
// cvDilate(segMap,segMap, NULL,1);
cvReleaseStructuringElement(&myModel);
//載入原影象到目標影象
cvSetImageROI(dstImg, cvRect(0, 0, pFrame->width, pFrame->height));
cvCopy(pFrame, dstImg);
cvResetImageROI(dstImg);
//將segMap轉換成三通道影象存在pFrame中
cvCvtColor(segMap, pFrame, CV_GRAY2BGR);
//載入檢測後的影象到目標影象
cvSetImageROI(dstImg, cvRect(pFrame->width, 0, pFrame->width, pFrame->height));
cvCopy(pFrame, dstImg);
cvResetImageROI(dstImg);
cvSetImageROI(dstImg, cvRect(pFrame->width * 2, 0, pFrame->width, pFrame->height));
cvDilate(pFrame, dstImg);
cvResetImageROI(dstImg);
//顯示提示文字
//cvPutText(dstImg, "Origin Video", cvPoint(0, pFrame->height - 5), &font, CV_RGB(255, 255, 0));
//cvPutText(dstImg, "ViBe Video", cvPoint(pFrame->width, pFrame->height - 5), &font, CV_RGB(255, 255, 0));
}
//儲存視訊和輸出
//cvWriteFrame(Save_result,dstImg);
//cvWriteFrame(Save_result, segMap);
/*輸出圖片
if(nFrmNum<11)
sprintf(filename,"E:\\Result\\Vibe\\AVSS_PV_Easy_Divx_Xvid\\000%d_Vibe.jpg",nFrmNum-1);
else if(nFrmNum<101)
sprintf(filename,"E:\\Result\\Vibe\\AVSS_PV_Easy_Divx_Xvid\\00%d_Vibe.jpg",nFrmNum-1);
else if(nFrmNum<1001)
sprintf(filename,"E:\\Result\\Vibe\\AVSS_PV_Easy_Divx_Xvid\\0%d_Vibe.jpg",nFrmNum-1);
else if(nFrmNum<10001)
sprintf(filename,"E:\\Result\\Vibe\\AVSS_PV_Easy_Divx_Xvid\\%d_Vibe.jpg",nFrmNum-1);
cvSaveImage(filename,dstImg);
*/
endTime = clock();
cvShowImage("src",pFrame);
cvShowImage("dst",segMap);
cvWaitKey(1);
duration = (double)(endTime-startTime)/CLOCKS_PER_SEC*1000; //計算該次迴圈所以時間
cout<<duration<<"ms"<<endl;
//儲存視訊和輸出
//cvWriteFrame(Save_result,dstImg);
cvWriteFrame(Save_result, segMap);
}
cvReleaseImage(&pFrame);cvReleaseMat(&pFrameMat);
cvReleaseImage(&pAfter);cvReleaseMat(&pAfterMat);
cvReleaseImage(&segMap);cvReleaseMat(&segMat);
cvReleaseVideoWriter(&Save_result);
cvReleaseImage(&dstImg);
cvDestroyWindow("dst");
cvDestroyWindow("src");
return 0;
}
以上都是借鑑的別人的程式碼,我想把輸出的視訊個儲存下來,但是出現錯誤:OpenCV Error: Assertion failed (src.channels() == dst.channels()) in cvCopy, file C:\build\master_winpack-build-win64-vc14\opencv\modules\core\src\copy.cpp, line 1297
網上查是說dst和src圖片之間的深度和位數不一致,需要做什麼改動我也不清楚。希望有大神可以幫我看看。