平均法是一種簡單的通過學習得到背景像素的均值和方差(或者平均差代替方差)作爲背景的模型的前景提取方法。考慮一個像素行,可以使用在整個視頻序列中各個像素的均值和平均差來描述該像素行的行爲(穩定程度)。可以想象,在整個序列中,像素的灰度值沒有變動或者變動在一個很小範圍內(人爲確定這個範圍)的話,就可以認爲是背景像素。
平均背景模板法的實現步驟:
- 累加訓練幀序列圖像,累加各個訓練幀之間的絕對差;
- 確定圖像的平均值和平均差;
- 確定背景判定閾值(基於均值和平均差),就是訓練得到背景模板;
- 對於一個新的幀,基於背景模板確定背景和前景(移動區域)。
代碼
// Float, 3-channel
cv::Mat frame; //當前幀
cv::Mat IavgF, IdiffF, IprevF, IhiF, IlowF; // 均值、平均差、前一幀、高閾值、低閾值
cv::Mat tmp, tmp2, mask;
// Float, 1-channel
vector<cv::Mat> Igray(3);
vector<cv::Mat> Ilow(3); //高閾值的三個通道
vector<cv::Mat> Ihi(3); //低閾值的三個通道
// Byte, 1-channel
cv::Mat Imaskt; //區分前景和背景的二值圖
// Thresholds
float high_thresh = 20.0; //變化因子(用於確定高閾值)
float low_thresh = 28.0; //變化因子(用於確定低閾值)
// Counts number of images learned for averaging later
float Icount; // 訓練幀數
void AllocateImages( const cv::Mat& I ) {
cv::Size sz = I.size();
IavgF = cv::Mat::zeros(sz, CV_32FC3 );
IdiffF = cv::Mat::zeros(sz, CV_32FC3 );
IprevF = cv::Mat::zeros(sz, CV_32FC3 );
IhiF = cv::Mat::zeros(sz, CV_32FC3 );
IlowF = cv::Mat::zeros(sz, CV_32FC3 );
Icount = 0.00001; // Protect against divide by zero
tmp = cv::Mat::zeros( sz, CV_32FC3 );
tmp2 = cv::Mat::zeros( sz, CV_32FC3 );
Imaskt = cv::Mat( sz, CV_32FC1 );
}
void accumulateBackground( cv::Mat& I )
{
static int first = 1; // nb. Not thread safe
I.convertTo( tmp, CV_32F ); // convert to float
if( !first ){
IavgF += tmp; //背景幀累加
Icount += 1.0; //幀數加一(計算均值時用到)
cv::absdiff( tmp, IprevF, tmp2 ); //計算幀間差分
IdiffF += tmp2; //累加幀間差分
}
first = 0;
IprevF = tmp;
}
void setHighThreshold( float scale ) {
IhiF = IavgF + (IdiffF * scale);
cv::split( IhiF, Ihi );
}
void setLowThreshold( float scale ) {
IlowF = IavgF - (IdiffF * scale);
cv::split( IlowF, Ilow );
}
void createModelsfromStats() {
IavgF *= (1.0/Icount);
IdiffF *= (1.0/Icount);
// 限制平均差圖像中的值至少爲1
IdiffF += cv::Scalar( 1.0, 1.0, 1.0 );
setHighThreshold( high_thresh);
setLowThreshold( low_thresh);
}
void backgroundDiff(
cv::Mat& I,
cv::Mat& Imask)
{
I.convertTo( tmp, CV_32F ); // To float
cv::split( tmp, Igray );
// Channel 1
cv::inRange( Igray[0], Ilow[0], Ihi[0], Imask );
// Channel 2
cv::inRange( Igray[1], Ilow[1], Ihi[1], Imaskt );
Imask = cv::min( Imask, Imaskt );
// Channel 3
cv::inRange( Igray[2], Ilow[2], Ihi[2], Imaskt );
Imask = cv::min( Imask, Imaskt );
// Finally, invert the results
Imask = 255 - Imask;
}
void showForgroundInRed( char** argv, const cv::Mat &img) {
cv::Mat rawImage;
cv::split( img, Igray );
Igray[2] = cv::max( mask, Igray[2] );
cv::merge( Igray, rawImage );
cv::imshow( argv[0], rawImage );
cv::imshow("Segmentation", mask);
}
void adjustThresholds(char** argv, cv::Mat &img) {
int key = 1;
while((key = cv::waitKey()) != 27 && key != 'Q' && key != 'q') // Esc or Q or q to exit
{
if(key == 'L') { low_thresh += 0.2;}
if(key == 'l') { low_thresh -= 0.2;}
if(key == 'H') { high_thresh += 0.2;}
if(key == 'h') { high_thresh -= 0.2;}
cout << "H or h, L or l, esq or q to quit; high_thresh = " << high_thresh << ", " << "low_thresh = " << low_thresh << endl;
setHighThreshold(high_thresh);
setLowThreshold(low_thresh);
backgroundDiff(img, mask);
showForgroundInRed(argv, img);
}
}
int main( int argc, char** argv)
{
cv::VideoCapture cap;
if( !cap.open("test.avi") )
{
cerr << "Couldn't run the program" << endl;
cap.open(0);
return -1;
}
int number_to_train_on = 20;
// FIRST PROCESSING LOOP (TRAINING):
//
int frame_count = 0;
int key;
bool first_frame = true;
cout << "Total frames to train on = " << number_to_train_on << endl; //db
while(1) {
cout << "frame#: " << frame_count << endl;
cap >> frame;
if( !frame.data ) exit(1); // Something went wrong, abort
if(frame_count == 0) { AllocateImages(frame);}
accumulateBackground( frame );
frame_count++;
if( (key = cv::waitKey(7)) == 27 || key == 'q' || key == 'Q' || frame_count >= number_to_train_on) break; //Allow early exit on space, esc, q
}
// We have accumulated our training, now create the models
//
cout << "Creating the background model" << endl;
createModelsfromStats();
cout << "Done! Hit any key to continue into single step. Hit 'a' or 'A' to adjust thresholds, esq, 'q' or 'Q' to quit\n" << endl;
// SECOND PROCESSING LOOP (TESTING):
cv::namedWindow("Segmentation", cv::WINDOW_AUTOSIZE ); //For the mask image
while((key = cv::waitKey()) != 27 || key == 'q' || key == 'Q' ) { // esc, 'q' or 'Q' to exit
cap >> frame;
if( !frame.data ) exit(0);
cout << frame_count++ << endl;
backgroundDiff( frame, mask );
cv::imshow("Segmentation", mask);
// A simple visualization is to write to the red channel
//
showForgroundInRed( argv, frame);
if(key == 'a') {
cout << "In adjust thresholds, 'H' or 'h' == high thresh up or down; 'L' or 'l' for low thresh up or down." << endl;
cout << " esq, 'q' or 'Q' to quit " << endl;
adjustThresholds(argv, frame);
cout << "Done with adjustThreshold, back to frame stepping, esq, q or Q to quit." << endl;
}
}
exit(0);
}
主要代碼段分析
首先創建一個用來爲所有必要中間臨時圖片申請內存的函數。的爲方便起見,我們傳入一個圖片參數(從視頻中取得) ,這個圖片參數只用於指定臨時圖片的大小。注意各個圖像的數據類型,設計到均值和平均計算的變量和圖像,選擇float類型。
void AllocateImages( const cv::Mat& I ) {
cv::Size sz = I.size();
IavgF = cv::Mat::zeros(sz, CV_32FC3 );
IdiffF = cv::Mat::zeros(sz, CV_32FC3 );
IprevF = cv::Mat::zeros(sz, CV_32FC3 );
IhiF = cv::Mat::zeros(sz, CV_32FC3 );
IlowF = cv::Mat::zeros(sz, CV_32FC3 );
Icount = 0.00001; // Protect against divide by zero
tmp = cv::Mat::zeros( sz, CV_32FC3 );
tmp2 = cv::Mat::zeros( sz, CV_32FC3 );
Imaskt = cv::Mat( sz, CV_32FC1 );
}
累計背景圖片和累計絕對值幀間差分,這個沒什麼說的。
void accumulateBackground( cv::Mat& I )
{
static int first = 1; // nb. Not thread safe
I.convertTo( tmp, CV_32F ); // convert to float
if( !first ){
IavgF += tmp; //訓練幀累加
Icount += 1.0; //幀數加一(計算均值時用到)
cv::absdiff( tmp, IprevF, tmp2 ); //計算幀間差分
IdiffF += tmp2; //累加幀間差分
}
first = 0;
IprevF = tmp;
}
累加了所有的訓練幀和幀間差分後,我們就可以將其轉化爲背景的一個統計模型,即計算每個像素的均值和偏移值(差分)。注意這個地方,手動的將差分加1,是爲了防止差分等於0(導致沒有偏移,高低兩個閾值相等)。
void createModelsfromStats() {
IavgF *= (1.0/Icount);
IdiffF *= (1.0/Icount);
IdiffF += cv::Scalar( 1.0, 1.0, 1.0 );
setHighThreshold( high_thresh);
setLowThreshold( low_thresh);
}
再下來是根據圖像的均值和平均差來設置閾值,將差分按因子進行縮放,然後從均值加上或者減去縮放後的平均差得到上下兩個閾值。接下來通過cv::split()把IhiF或者IlowF應用於圖像的每個通道。注意,平均差可以視爲判斷兩個數據是否差異明顯的距離度量,設置完高閾值後,高於均值加上scale倍IdiffF的像素被認爲是前景,低閾值類似。
void setHighThreshold( float scale ) {
IhiF = IavgF + (IdiffF * scale);
cv::split( IhiF, Ihi );
}// 表示在低於平均值scale倍IdiffF的像素被認爲是前景
void setLowThreshold( float scale ) {
IlowF = IavgF - (IdiffF * scale);
cv::split( IlowF, Ilow );
}
一旦得到背景模型,完成高低閾值的設定,我們就可以使用它對圖像進行前景(不是背景的部分)和背景(像素值位於背景模型的高低閾值之間的部分)的分割。先通過Mat::convertTo()將輸入圖片I(待分割圖片)轉化爲一個浮點型圖片。然後使用cv::split()將三通道圖片分爲三個單通道圖片。接下來,我們對每個通道分別進行處理,使用cv::inRange()以判斷每個像素是夠位於背景的高低闕值之間,將背景對應的8位灰度圖Imask中的位置置爲255,前景部分置爲0。
void backgroundDiff(
cv::Mat& I,
cv::Mat& Imask)
{
I.convertTo( tmp, CV_32F ); // To float
cv::split( tmp, Igray );
// Channel 1
cv::inRange( Igray[0], Ilow[0], Ihi[0], Imask );// Channel 2
cv::inRange( Igray[1], Ilow[1], Ihi[1], Imaskt );
Imask = cv::min( Imask, Imaskt );// Channel 3
cv::inRange( Igray[2], Ilow[2], Ihi[2], Imaskt );
Imask = cv::min( Imask, Imaskt );// Finally, invert the results
Imask = 255 - Imask;
}
前景提取效果
注意:這只是一種人爲控制背景閾值得到的效果(平均差的變換因子是人爲定的),可以通過輸入“H”(增大高閾值)和“h”(降低低閾值),“L”(增大低閾值)和“l”(降低低閾值)來微調分割的效果。
總結
平均背景模板是一種在幀間差的基礎上的一種前景提取方法,最大的不同點在於用到了學習場景,使用視頻中的一些幀作爲背景模板的訓練集,進而分割出前景。然後用到了確定閾值的方法,認爲像素越穩定(即灰度值被控制在一個高斯閾值範圍內),作爲背景的可能性越大。
但是它也有解決不了的缺點:只有在場景中沒有移動的背景物體的時候它才工作良好,在場景中存在飄動的窗簾或是樹枝等具有雙或多模態的特徵時,該方怯會失效,所以只能應用於室內等穩定的環境中。