平均法是一种简单的通过学习得到背景像素的均值和方差(或者平均差代替方差)作为背景的模型的前景提取方法。考虑一个像素行,可以使用在整个视频序列中各个像素的均值和平均差来描述该像素行的行为(稳定程度)。可以想象,在整个序列中,像素的灰度值没有变动或者变动在一个很小范围内(人为确定这个范围)的话,就可以认为是背景像素。
平均背景模板法的实现步骤:
- 累加训练帧序列图像,累加各个训练帧之间的绝对差;
- 确定图像的平均值和平均差;
- 确定背景判定阈值(基于均值和平均差),就是训练得到背景模板;
- 对于一个新的帧,基于背景模板确定背景和前景(移动区域)。
代码
// 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”(降低低阈值)来微调分割的效果。
总结
平均背景模板是一种在帧间差的基础上的一种前景提取方法,最大的不同点在于用到了学习场景,使用视频中的一些帧作为背景模板的训练集,进而分割出前景。然后用到了确定阈值的方法,认为像素越稳定(即灰度值被控制在一个高斯阈值范围内),作为背景的可能性越大。
但是它也有解决不了的缺点:只有在场景中没有移动的背景物体的时候它才工作良好,在场景中存在飘动的窗帘或是树枝等具有双或多模态的特征时,该方怯会失效,所以只能应用于室内等稳定的环境中。