图像处理背景差分原理

2019-02-11 17:16:10 qq_30815237 阅读数 5117

      背景提取是在视频图像序列中提取出背景,背景就是场景中静止不动的景物。因为摄像机不动,因此图像中的每个像素点都有一个对应的背景值,在一段时间内,这个背景值是比较固定的。背景提取的目标就是根据视频图像序列,找出图像中每一点的背景值。 背景提取有很多算法。针对静止摄像机的帧间差分法、高斯背景差分法,还有针对运动摄像机的光流法等。 

一. 帧间差分法

相邻帧间图像差分思想:检测出了相邻两帧图像中发生变化的区域。该方法是用图像序列中的连续两帧图像进行差分,然后二值化该灰度差分图像来提取运动信息。由帧间变化区域检测分割得到的图像,区分出背景区域和运动车辆区域,进而提取要检测的车辆目标。它是通过比较图像序列中前后两帧图像对应像素点灰度值的不同,通过两帧相减,如果灰度值很小,可以认为该点无运动物体经过;反之灰度变化很大,则认为有物体经过。
1. 算法原理

     帧间差分法是将视频流中相邻两帧或相隔几帧图像的两幅图像像素值相减,并对相减后的图像进行阈值化来提取图像中的运动区域。 
若相减两帧图像的帧数分别为第k帧, 第(k+1)帧,其帧图像分别为f_{k}(x,y)f_{k+1}(x,y)差分图像二值化阈值为T,差分图像用D(x, y)表示,则帧间差分法的公式如下: 

                                     

帧间差分法的优缺点如下:

  • 优点:算法简单,不易受环境光线影响
  • 缺点: 
    • 不能用于运动的摄像头中;
    • 无法识别静止或运动速度很慢的目标;
    • 运动目标表面有大面积灰度值相似区域的情况下,在做差分时图像会出现孔洞;

我们以年辆检测为例,车辆检测除了要检测出运动车辆.同时还要检测出暂时停止的车辆,在这个方面,此类方法无能为力。而且如果车辆的体积较大,那么车辆在前后帧中根容易产生重叠部分,尤其是大货车,这使得帧问差分的结果主要为车头和车尾。车辆中间部分的差分值相对报小.形成空洞,不利于检测。

实例:

#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;

int main()//帧间差分法
{
VideoCapture capture("tree.avi");//获取视频
if (!capture.isOpened())
return -1;
double rate = capture.get(CV_CAP_PROP_FPS);//获取视频帧率
int delay = 10000 / rate;
Mat framepro, frame, dframe;
bool flag = false;
namedWindow("image");
namedWindow("test");
while (capture.read(frame))
{
if (false == flag)
{
framepro = frame.clone();//将第一帧图像拷贝给framePro
flag = true;
}
else
{
absdiff(frame, framepro, dframe);//帧间差分计算两幅图像各个通道的相对应元素的差的绝对值。
framepro = frame.clone();//将当前帧拷贝给framepro
threshold(dframe, dframe, 80, 255, CV_THRESH_BINARY);//阈值分割
imshow("image", frame);
imshow("test", dframe);
waitKey(delay);
}
}
waitKey();
return 0;
}

结果:

  

 

二. 背景差分法

      为达到前景目标的识别,譬如,交通路口对车辆的识别、监控器对行人的识别,常用的且较为有效的方法就是背景差分法(还有其他的方法,比如光流场法,帧差法),背景差分法是一种对静止场景进行运动分割的通用方法,它将当前获取的图像帧与背景图像做差分运算,得到目标运动区域的灰度图,对灰度图进行阈值化提取运动区域,而且为避免环境光照变化影响,背景图像根据当前获取图像帧进行更新。 根据前景检测,背景维持和后处理方法,存在几种不同的背景差方法。如何获取一个“美好”的背景图,是背景差分法的关键和难点。此处介绍一种最为简单的获取背景的方法——平均背景法。

顾名思义,其基本思想就是,将所采集到的背景图片叠加求和,而后求取平均值作为要求的背景。

平均背景法使用四个OpenCV函数:

1.将图像视频的前几帧,利用opencv的cvAcc函数累加求和,并统计累加次数,求取平均值,记为A。(注意理解该平均值的意思是在(x,y)位置处,所有参与求和计算的图像在该点出的像素平均值);

2.前一帧图像和当前参与累加的图像求差的绝对值,利用cvAbsDiff函数,而后该绝对值图像也进行累加求平均值,记为D。(同理这是某点的差值平均值)它描述的是某点像素值得波动幅度。

3.那么有1和2便可以认为,图像某点的像素值P若满足A-D < P < A+D,则认为此点属于背景,然而由于前景目标的加入,可能对背景点的亮度有一定的影响,故对波动幅度D进行一定的放缩,若 A -KD < p < A + KD,便认为该点为背景点。自然在该范围外的点,便是我们需要的前景点。那么用cvThreshold函数完成二值化。我们的目的就达到了。
此为背景建模最简单的方法,在室内等干扰较少的环境下使用时,效果较好,一旦有些许干扰则效果严重下降.

from :https://blog.csdn.net/q6541oijhk/article/details/41850129

实例:

#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
Mat IavgF, IdiffF, IprevF, IhiF, IlowF;//平均,差分,前景,判断是否前景的高低阈值,在之间表示是背景(变化小)
Mat tmp, tmp2;//作为中间变量,传递变量
Mat Imaskt;
float Icount;
vector<Mat> Igray(3);
vector<Mat> Ilow(3);//将高阈值按通道分别存放
vector<Mat> Ihi(3);//将低阈值按通道分别存放

void AllocateImages(Mat& I);
void accumulateBackground(Mat& I);
void creatModelsfromStats();
void setHighThreshold(float scale);
void setLowThreshold(float scale);
void backgroundDiff(Mat& I, Mat& Imask);
int main() {
	Mat image;
	VideoCapture capture("tree.avi");
	double rate = capture.get(CV_CAP_PROP_FPS);//获取视频帧率
	int delay = 10000 / rate;
	capture >> image;
	if (image.empty())//如果某帧为空则退出循环
		cout << "read fail" << endl;
	AllocateImages(image);
	if (!capture.isOpened())
		return -1;
	int num = 1;
	while (num==50) {//这一个是学习背景模型,也可以设置为前50帧用来学习,不一定非要学习整个视频
		accumulateBackground(image);
		capture >> image;
		if (image.empty())
			break;
		imshow("intput", image);
		waitKey(delay);
		num++;
	}
	destroyAllWindows();

	creatModelsfromStats();//获得高低阈值
	Mat mask;
	while (1) {
		capture >> image;
		if (image.empty())
			break;
		backgroundDiff(image, mask);//mask输出二值化图像,前景为255
		imshow("ed", mask);
		split(image, Igray);
		Igray[2] = max(mask, Igray[2]);//用红色标记前景
	    merge(Igray, image);
		imshow("标记", image);
		waitKey(delay);
	}
	destroyAllWindows();//防止异常退出
}
void AllocateImages(Mat& I) {
	Size sz = I.size();
	IavgF = Mat::zeros(sz, CV_32FC3);
	IdiffF = Mat::zeros(sz, CV_32FC3);
	IprevF = Mat::zeros(sz, CV_32FC3);
	IhiF = Mat::zeros(sz, CV_32FC3);
	IlowF = Mat::zeros(sz, CV_32FC3);
	Icount = 0.00001f;//防止除以0
	tmp = Mat::zeros(sz, CV_32FC3);
	tmp2 = Mat::zeros(sz, CV_32FC3);
	Imaskt = Mat(sz, CV_32FC3);
}
void accumulateBackground(Mat& I) {
	static int flag = 1;
	I.convertTo(tmp, CV_32F);//类型转换
	if (!flag) {
		IavgF += tmp;
		absdiff(tmp, IprevF, tmp2);
		IdiffF += tmp2;
		Icount += 1;
	}
	flag = 0;
	IprevF = tmp;
}
void creatModelsfromStats() {//得到背景模型
	IavgF *= (1 / Icount);
	IdiffF *= (1 / Icount);
	IdiffF += Scalar(1.0, 1.0, 1.0);
	setHighThreshold(12.0);//参数指 倍数
	setLowThreshold(13.0);
}
void setHighThreshold(float scale) {//高低阈值的设定
	IhiF = IavgF + (IdiffF*scale);
	split(IhiF, Ihi);
}
void setLowThreshold(float scale) {
	IlowF = IavgF - (IdiffF*scale);
	split(IlowF, Ilow);
}
void backgroundDiff(Mat& I, Mat& Imask) {
	I.convertTo(tmp, CV_32F);//类型转换
	split(tmp, Igray);
	//对三个通道依次比较是否在阈值区间内
	inRange(Igray[0], Ilow[0], Ihi[0], Imask);//输出二值图像Imask
	inRange(Igray[1], Ilow[1], Ihi[1], Imaskt);
	Imask = min(Imask, Imaskt);//与运算
	inRange(Igray[2], Ilow[2], Ihi[2], Imaskt);
	Imask = min(Imask, Imaskt);
	Imask = 255 - Imask;//在阈值内的置255,表示背景,所以做翻转,使背景标为0,前景为255.
}

结果:

  

程序可以运行,但我使用opencv例程里的tree.avi做检测,但结果失败了,未能检测出手掌,求各位大佬能指出算法哪地方出了问题? 

下面是网上普遍流传的c版本的平均背景法,亲测有效,效果一般。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<opencv\cv.h>
#include<opencv\highgui.h>

//float 3-channel iamges
IplImage *IavgF, *IdiffF, *IprevF, *IhiF, *IlowF;

IplImage *Iscratch, *Iscratch2;

//float 1-channel iamges
IplImage *Igray1, *Igray2, *Igray3;
IplImage *Ilow1, *Ilow2, *Ilow3;
IplImage *Ihi1, *Ihi2, *Ihi3;

//byte  1-channel image
IplImage *Imaskt;

//counts number of images learned for averaging later
float Icount;

void  AllocateImages(IplImage* I);
void  accumulateBackground(IplImage* I);
void  createModelsfromStats();
void  setHighThreshold(float scale);
void  setLowThreshold(float scale);
void  backgroundDiff(IplImage *I, IplImage * Imask);
void  DeallocateImage();

int main(int argc, char** argv)
{
	cvNamedWindow("intput", CV_WINDOW_AUTOSIZE);                    //创建输入显示窗口
	cvNamedWindow("output", CV_WINDOW_AUTOSIZE);                    //创建输出显示窗口
	CvCapture* capture = cvCreateFileCapture(argv[1]);          //返回一个capture指针,指向视频

	IplImage* img = cvQueryFrame(capture);                          //从视频中取出的图片

	IplImage* Imask = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);//创建输出图片,这里不能去掉cvCreateImage(cvGetSize(Img),IPL_DEPTH_8U,1),虽然我看例程里省略了

	AllocateImages(img);//给图像分配内存

						/*累积图像,只取了前30帧图片*/
	while (Icount<30) {
		accumulateBackground(img);                                   //调用累积图像的函数,循环30次                               
		img = cvQueryFrame(capture);								 //从视频中取出的图片
		cvShowImage("intput", img);
		cvWaitKey(20);
	}


	createModelsfromStats();                                        //背景建模

	while (1)
	{
		img = cvQueryFrame(capture);
		if (!img) break;

		backgroundDiff(img, Imask);                                 //根据模型分割前景

		cvShowImage("output", Imask);                               //显示图像,视频是一张一张图片连续播放的结果
		cvShowImage("intput", img);
		char c = cvWaitKey(33);                                    //当前帧被显示后,等待33ms再读取下一张图片
		if (c == 27) break;                                           //等待期间按下esc键,ASCII码为27,则循环退出
	}
	cvReleaseCapture(&capture);
	cvDestroyWindow("output");
	cvDestroyWindow("intput");
	DeallocateImage();
}




//给需要的所有临时图像分配内存
void  AllocateImages(IplImage* I) {
	CvSize sz = cvGetSize(I);
	IavgF = cvCreateImage(sz, IPL_DEPTH_32F, 3); cvZero(IavgF);
	IdiffF = cvCreateImage(sz, IPL_DEPTH_32F, 3); cvZero(IdiffF);
	IprevF = cvCreateImage(sz, IPL_DEPTH_32F, 3); cvZero(IprevF);
	IhiF = cvCreateImage(sz, IPL_DEPTH_32F, 3); cvZero(IhiF);
	IlowF = cvCreateImage(sz, IPL_DEPTH_32F, 3); cvZero(IlowF);
	Ilow1 = cvCreateImage(sz, IPL_DEPTH_32F, 1);
	Ilow2 = cvCreateImage(sz, IPL_DEPTH_32F, 1);
	Ilow3 = cvCreateImage(sz, IPL_DEPTH_32F, 1);
	Ihi1 = cvCreateImage(sz, IPL_DEPTH_32F, 1);
	Ihi2 = cvCreateImage(sz, IPL_DEPTH_32F, 1);
	Ihi3 = cvCreateImage(sz, IPL_DEPTH_32F, 1);
	Icount = 0.00001;//protect against divide by zero

	Iscratch = cvCreateImage(sz, IPL_DEPTH_32F, 3);
	Iscratch2 = cvCreateImage(sz, IPL_DEPTH_32F, 3);
	Igray1 = cvCreateImage(sz, IPL_DEPTH_32F, 1);
	Igray2 = cvCreateImage(sz, IPL_DEPTH_32F, 1);
	Igray3 = cvCreateImage(sz, IPL_DEPTH_32F, 1);
	Imaskt = cvCreateImage(sz, IPL_DEPTH_8U, 1);
	cvZero(Iscratch);
	cvZero(Iscratch2);
}
/*积累背景图像和每一帧图像差值的绝对值
/函数调用需要通道为3,深度为8的彩色图像
*/
void  accumulateBackground(IplImage* I) {
	static int first = 1;
	cvCvtScale(I, Iscratch, 1, 0);//convert to float,3-channel
	if (!first) {
		cvAcc(Iscratch, IavgF);
		cvAbsDiff(Iscratch, IprevF, Iscratch2);
		cvAcc(Iscratch2, IdiffF);
		Icount += 1.0;
	}
	first = 0;
	cvCopy(Iscratch, IprevF);
}

//计算每一个像素的均值和方差观测(平均绝对差分)
void createModelsfromStats() {
	cvConvertScale(IavgF, IavgF, (double)(1.0 / Icount));
	cvConvertScale(IdiffF, IdiffF, (double)(1.0 / Icount));

	//make sure diff is always something
	cvAddS(IdiffF, cvScalar(1.0, 1.0, 1.0), IdiffF);
	setHighThreshold(7.0);
	setLowThreshold(6.0);
}

void setHighThreshold(float scale) {
	cvConvertScale(IdiffF, Iscratch, scale);
	cvAdd(Iscratch, IavgF, IhiF);
	cvSplit(IhiF, Ihi1, Ihi2, Ihi3, 0);
}

void setLowThreshold(float scale) {
	cvConvertScale(IdiffF, Iscratch, scale);
	cvAdd(IavgF, Iscratch, IlowF);
	cvSplit(IlowF, Ilow1, Ilow2, Ilow3, 0);
}

//图像分割
void backgroundDiff(IplImage *I, IplImage *Imask) {
	cvConvertScale(I, Iscratch, 1, 0);//to float
	cvSplit(Iscratch, Igray1, Igray2, Igray3, 0);
	//channel-1
	cvInRange(Igray1, Ilow1, Ihi1, Imask);
	//channel-2
	cvInRange(Igray2, Ilow2, Ihi2, Imaskt);
	cvOr(Imask, Imaskt, Imask);
	//channel-3
	cvInRange(Igray3, Ilow3, Ihi3, Imaskt);
	cvOr(Imask, Imaskt, Imask);
	cvSubRS(Imask, cvScalar(255), Imask);
}

//内存释放
void DeallocateImage() {
	cvReleaseImage(&IavgF);
	cvReleaseImage(&IdiffF);
	cvReleaseImage(&IprevF);
	cvReleaseImage(&IhiF);
	cvReleaseImage(&IlowF);
	cvReleaseImage(&Ilow1);
	cvReleaseImage(&Ilow2);
	cvReleaseImage(&Ilow3);
	cvReleaseImage(&Ihi1);
	cvReleaseImage(&Ihi2);
	cvReleaseImage(&Ihi3);
	cvReleaseImage(&Iscratch);
	cvReleaseImage(&Iscratch2);
	cvReleaseImage(&Igray1);
	cvReleaseImage(&Igray2);
	cvReleaseImage(&Igray3);
	cvReleaseImage(&Imaskt);
}

关于int argc, char** argv的参数传递和调试时如何使用请参见:

https://blog.csdn.net/qq_30815237/article/details/87006586 

 

2017-07-10 16:41:45 chongzi865458 阅读数 11041

背景差分法


背景差分法是一种很常用而且广泛传感的技术,主要用于背景不动的情况下提取前景。它主要的原理是在当前帧和背景做减法,然后使用threshold进行二值化得到前景掩码。下面是背景减法的示意图。
这里写图片描述
背景差分法主要包含以下两个步骤:
1.背景的建立
2.背景的更新
两个关键点,第一个就是如何选择背景,第二个就是什么时候更新背景,这样可以适应背景变化的情况,更新背景的快慢也很重要,如果更新慢了,快速运动的物体如果没有重叠就认为是两个物体了,如果更新快了,慢速运动的物体会被认为是背景,这样就造成漏检。
这里学习一下在opencv中如何使用背景差分法。

代码

使用了两个方法来产生前景掩码:
1. cv::bgsegm::BackgroundSubtractorMog
2. cv::BackgroundSubtractorMog2

//opencv
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/videoio.hpp"
#include <opencv2/highgui.hpp>
#include <opencv2/video.hpp>
//C
#include <stdio.h>
//C++
#include <iostream>
#include <sstream>
using namespace cv;
using namespace std;
// Global variables
Mat frame; //current frame
Mat fgMaskMOG2; //fg mask fg mask generated by MOG2 method
Ptr<BackgroundSubtractor> pMOG2; //MOG2 Background subtractor
char keyboard; //input from keyboard
void help();
void processVideo(char* videoFilename);
void processImages(char* firstFrameFilename);
void help()
{
    cout
    << "--------------------------------------------------------------------------" << endl
    << "This program shows how to use background subtraction methods provided by "  << endl
    << " OpenCV. You can process both videos (-vid) and images (-img)."             << endl
                                                                                    << endl
    << "Usage:"                                                                     << endl
    << "./bg_sub {-vid <video filename>|-img <image filename>}"                     << endl
    << "for example: ./bg_sub -vid video.avi"                                       << endl
    << "or: ./bg_sub -img /data/images/1.png"                                       << endl
    << "--------------------------------------------------------------------------" << endl
    << endl;
}
int main(int argc, char* argv[])
{
    //print help information
    help();
    //check for the input parameter correctness
    if(argc != 3) {
        cerr <<"Incorret input list" << endl;
        cerr <<"exiting..." << endl;
        return EXIT_FAILURE;
    }
    //create GUI windows
    namedWindow("Frame");
    namedWindow("FG Mask MOG 2");
    //create Background Subtractor objects
    pMOG2 = createBackgroundSubtractorMOG2(); //MOG2 approach
    if(strcmp(argv[1], "-vid") == 0) {
        //input data coming from a video
        processVideo(argv[2]);
    }
    else if(strcmp(argv[1], "-img") == 0) {
        //input data coming from a sequence of images
        processImages(argv[2]);
    }
    else {
        //error in reading input parameters
        cerr <<"Please, check the input parameters." << endl;
        cerr <<"Exiting..." << endl;
        return EXIT_FAILURE;
    }
    //destroy GUI windows
    destroyAllWindows();
    return EXIT_SUCCESS;
}
void processVideo(char* videoFilename) {
    //create the capture object
    VideoCapture capture(videoFilename);
    if(!capture.isOpened()){
        //error in opening the video input
        cerr << "Unable to open video file: " << videoFilename << endl;
        exit(EXIT_FAILURE);
    }
    //read input data. ESC or 'q' for quitting
    keyboard = 0;
    while( keyboard != 'q' && keyboard != 27 ){
        //read the current frame
        if(!capture.read(frame)) {
            cerr << "Unable to read next frame." << endl;
            cerr << "Exiting..." << endl;
            exit(EXIT_FAILURE);
        }
        //update the background model
        pMOG2->apply(frame, fgMaskMOG2);
        //get the frame number and write it on the current frame
        stringstream ss;
        rectangle(frame, cv::Point(10, 2), cv::Point(100,20),
                  cv::Scalar(255,255,255), -1);
        ss << capture.get(CAP_PROP_POS_FRAMES);
        string frameNumberString = ss.str();
        putText(frame, frameNumberString.c_str(), cv::Point(15, 15),
                FONT_HERSHEY_SIMPLEX, 0.5 , cv::Scalar(0,0,0));
        //show the current frame and the fg masks
        imshow("Frame", frame);
        imshow("FG Mask MOG 2", fgMaskMOG2);
        //get the input from the keyboard
        keyboard = (char)waitKey( 30 );
    }
    //delete capture object
    capture.release();
}
void processImages(char* fistFrameFilename) {
    //read the first file of the sequence
    frame = imread(fistFrameFilename);
    if(frame.empty()){
        //error in opening the first image
        cerr << "Unable to open first image frame: " << fistFrameFilename << endl;
        exit(EXIT_FAILURE);
    }
    //current image filename
    string fn(fistFrameFilename);
    //read input data. ESC or 'q' for quitting
    keyboard = 0;
    while( keyboard != 'q' && keyboard != 27 ){
        //update the background model
        pMOG2->apply(frame, fgMaskMOG2);
        //get the frame number and write it on the current frame
        size_t index = fn.find_last_of("/");
        if(index == string::npos) {
            index = fn.find_last_of("\\");
        }
        size_t index2 = fn.find_last_of(".");
        string prefix = fn.substr(0,index+1);
        string suffix = fn.substr(index2);
        string frameNumberString = fn.substr(index+1, index2-index-1);
        istringstream iss(frameNumberString);
        int frameNumber = 0;
        iss >> frameNumber;
        rectangle(frame, cv::Point(10, 2), cv::Point(100,20),
                  cv::Scalar(255,255,255), -1);
        putText(frame, frameNumberString.c_str(), cv::Point(15, 15),
                FONT_HERSHEY_SIMPLEX, 0.5 , cv::Scalar(0,0,0));
        //show the current frame and the fg masks
        imshow("Frame", frame);
        imshow("FG Mask MOG 2", fgMaskMOG2);
        //get the input from the keyboard
        keyboard = (char)waitKey( 30 );
        //search for the next image in the sequence
        ostringstream oss;
        oss << (frameNumber + 1);
        string nextFrameNumberString = oss.str();
        string nextFrameFilename = prefix + nextFrameNumberString + suffix;
        //read the next frame
        frame = imread(nextFrameFilename);
        if(frame.empty()){
            //error in opening the next image in the sequence
            cerr << "Unable to open image frame: " << nextFrameFilename << endl;
            exit(EXIT_FAILURE);
        }
        //update the path of the current frame
        fn.assign(nextFrameFilename);
    }
}

关键代码说明

  1. 创建三个全局变量,frame用于保存当前图像,fgMaskMog用于保存前景掩码,fgMaskMog2是第二种方法产生的前景掩码。
Mat frame; //current frame
Mat fgMaskMOG; //fg mask generated by MOG method
Mat fgMaskMOG2; //fg mask fg mask generated by MOG2 method
  1. 使用两个方法产生前景掩码,cv::BackgroundSubtractor。在本例中,使用默认的参数,你也可以尝试其它参数。
Ptr<BackgroundSubtractor> pMOG; //MOG Background subtractor
Ptr<BackgroundSubtractor> pMOG2; //MOG2 Background subtractor
...
//create Background Subtractor objects
pMOG = createBackgroundSubtractorMOG(); //MOG approach
  1. 传入视频还是单张图像,使用-vid参数传入视频 使用-img参数传入图像
if(strcmp(argv[1], "-vid") == 0) {
  //input data coming from a video
  processVideo(argv[2]);
}
else if(strcmp(argv[1], "-img") == 0) {
  //input data coming from a sequence of images
  processImages(argv[2]);
}
  1. 建议使用视频进行测试。如果要停止,输入“q”或者esc。
while( (char)keyboard != 'q' && (char)keyboard != 27 ){
  //read the current frame
  if(!capture.read(frame)) {
    cerr << "Unable to read next frame." << endl;
    cerr << "Exiting..." << endl;
    exit(EXIT_FAILURE);
  }
  1. 每一帧图像都用于计算前景掩码和更新背景,可以通过传入参数来修改更新速度。
//update the background model
pMOG->apply(frame, fgMaskMOG);
pMOG2->apply(frame, fgMaskMOG2);
  1. 在在上角显示当前帧号。
//get the frame number and write it on the current frame
stringstream ss;
rectangle(frame, cv::Point(10, 2), cv::Point(100,20),
          cv::Scalar(255,255,255), -1);
ss << capture.get(CAP_PROP_POS_FRAMES);
string frameNumberString = ss.str();
putText(frame, frameNumberString.c_str(), cv::Point(15, 15),
        FONT_HERSHEY_SIMPLEX, 0.5 , cv::Scalar(0,0,0));
  1. 然后显示输入图像和结果
//show the current frame and the fg masks
imshow("Frame", frame);
imshow("FG Mask MOG", fgMaskMOG);
imshow("FG Mask MOG 2", fgMaskMOG2);
  1. 如果选择图片进行处理,需要输入几张图像。使用cv::imread进行处理。
//read the first file of the sequence
frame = imread(fistFrameFilename);
if(!frame.data){
  //error in opening the first image
  cerr << "Unable to open first image frame: " << fistFrameFilename << endl;
  exit(EXIT_FAILURE);
}
...
//search for the next image in the sequence
ostringstream oss;
oss << (frameNumber + 1);
string nextFrameNumberString = oss.str();
string nextFrameFilename = prefix + nextFrameNumberString + suffix;
//read the next frame
frame = imread(nextFrameFilename);
if(!frame.data){
  //error in opening the next image in the sequence
  cerr << "Unable to open image frame: " << nextFrameFilename << endl;
  exit(EXIT_FAILURE);
}
//update the path of the current frame
fn.assign(nextFrameFilename);

结果显示:

结果显示如果人停下了,那么就检测不到了。
这里写图片描述

保存

保存使用cv::imread就可以了,代码实现很简单

string imageToSave = "output_MOG_" + frameNumberString + ".png";
bool saved = imwrite(imageToSave, fgMaskMOG);
if(!saved) {
  cerr << "Unable to save " << imageToSave << endl;
}
2017-05-22 17:40:25 ajianyingxiaoqinghan 阅读数 49751

背景提取算法——帧间差分法、背景差分法、ViBe算法、ViBe+算法

背景提取是在视频图像序列中提取出背景,背景就是场景中静止不动的景物。因为摄像机不动,因此图像中的每个像素点都有一个对应的背景值,在一段时间内,这个背景值是比较固定的。背景提取的目标就是根据视频图像序列,找出图像中每一点的背景值。
背景提取有很多算法。针对静止摄像机的帧间差分法、高斯背景差分法、ViBe背景提取算法以及它的改进算法ViBe+,还有针对运动摄像机的光流法等。
本文针对静止摄像机的背景提取算法进行讲解,主要讲解帧间差分法、背景差分法,ViBe算法,以及ViBe+算法。
笔者已经将本文所有源码上传到笔者的GitHub账号上,地址如下:
https://github.com/upcAutoLang/BackgroundSplit-OpenCV

一. 帧间差分法

1. 算法原理

帧间差分法是将视频流中相邻两帧或相隔几帧图像的两幅图像像素值相减,并对相减后的图像进行阈值化来提取图像中的运动区域。
若相减两帧图像的帧数分别为第k帧, 第(k+1)帧,其帧图像分别为fk(x,y),fk+1(x,y),差分图像二值化阈值为T,差分图像用D(x, y)表示,则帧间差分法的公式如下:

D(x,y)={1,|fk+1(x,y)fk(x,y)|>T0,others

帧间差分法的优缺点如下:

  • 优点:算法简单,不易受环境光线影响
  • 缺点:
    • 不能用于运动的摄像头中;
    • 无法识别静止或运动速度很慢的目标;
    • 运动目标表面有大面积灰度值相似区域的情况下,在做差分时图像会出现孔洞;

2. 算法源码

笔者已经将把源码上传到GitHub网站上,地址如下:
https://github.com/upcAutoLang/BackgroundSplit-OpenCV/tree/master/src/FramesDifference

二. 背景差分法

参考网址:
《背景差分法》
《帧间差分法和背景建模法》

1. 算法原理

背景差分法是一种对静止场景进行运动分割的通用方法,它将当前获取的图像帧与背景图像做差分运算,得到目标运动区域的灰度图,对灰度图进行阈值化提取运动区域,而且为避免环境光照变化影响,背景图像根据当前获取图像帧进行更新。
根据前景检测,背景维持和后处理方法,存在几种不同的背景差方法。若设It,Bt分别为当前帧与背景帧图像,T为前景灰度阈值,则其中一种方法流程如下:

  • 取前几帧图像的平均值,将其作为初始的背景图像Bt
  • 当前帧图像与背景图像作灰度减运算,并取绝对值;公式即为|It(x,y)Bt(x,y)|
  • 对当前帧的像素(x,y),若有|It(x,y)Bt(x,y)|>T,则该像素点为前景点;
  • (可选)对前景像素图进行形态学操作(腐蚀、膨胀、开闭操作等)
  • 用当前帧图像对背景图像进行更新;

背景差分法的优缺点如下:

  • 优点:
    • 算法比较简单;
    • 一定程度上克服了环境光线的影响;
  • 缺点:
    • 不能用于运动的摄像头;
    • 对背景图像实时更新困难;

2. 算法源码

笔者已经将把源码上传到GitHub网站上,地址如下:
https://github.com/upcAutoLang/BackgroundSplit-OpenCV/tree/master/src/GaussBGDifference

三. ViBe背景提取算法

ViBe - a powerful technique for background detection and subtraction in video sequences
——摘自ViBe算法官网

ViBe是一种像素级视频背景建模或前景检测的算法,效果优于所熟知的几种算法,对硬件内存占用也少。该算法主要不同之处是背景模型的更新策略,随机选择需要替换的像素的样本,随机选择邻域像素进行更新。在无法确定像素变化的模型时,随机的更新策略,在一定程度上可以模拟像素变化的不确定性。

参考地址:
《ViBe算法原理和代码解析 》
《背景建模–Vibe 算法优缺点分析》
《第一次总结报告——Vibe 》
《运动检测(前景检测)之(一)ViBe 》
《VIBE改进算法》

参考论文:
《O. Barnich and M. Van Droogenbroeck. ViBe: a powerful random technique to estimate the background in video sequences.》
《O. Barnich and M. Van Droogenbroeck. ViBe: A universal background subtraction algorithm for video sequences.》

算法官网:
http://www.telecom.ulg.ac.be/research/vibe/

1. 一般背景提取算法存在的问题

前文提到的帧间差分法、背景差分法中存在若干问题如下:

  • 对于环境变化的适应并不友好(如光照的变化造成色度的变化);
  • 相机抖动导致画面抖动
  • 物体检测中常出现的Ghost区域;

其中值得一提的是Ghost区域:Ghost区域常常出现于帧间差分法,当一个原本静止的物体开始运动时,帧间差分法检测时,可能会将原本该物体覆盖区域错误的检测为运动的,这块被错误检测到的区域被称为Ghost。同样的,原本正在运动的物体变成静止物体时,也会出现Ghost区域。
例如下图,原图像中只有三个正在运动的人,但由于帧间差分法取得的背景图中包含这三个运动的人的某一帧运动状态,后面的一系列帧序列与背景图相减,都会存在背景图中三个人所在的位置,这时候取得的前景会多出三个被检测区域,即Ghost区域。

Ghost区域在检测中,一定要尽快消除。

2. ViBe算法原理

ViBe比较特殊的地方它的思想:它为所有像素点存储了一个样本集,样本集里面保存的采样值是该像素点过去的像素值其邻居点的像素值。后面每一帧的新像素值和样本集里的样本历史值进行比较,判断是否属于背景点。
下面从几点讲解ViBe算法:

(1) 背景、前景模型

模型中,背景就是静止的,或者移动非常缓慢的物体;前景就是相对于背景的物体,即正在移动的物体。所以背景提取算法也可以看成是一个分类问题,遍历像素点的过程中,来确定一个像素点是属于前景点,还是属于背景点。
在ViBe模型中,背景模型为每个像素点存储了样本集,样本集大小一般为20个点。对于采入的新一帧图像,该帧的某个像素点与该像素点的样本集内采样值比较接近时,就可以判断其是一个背景点。
用公式表示,我们可以认为:

  • v(x,y):像素点(x, y)处的当前像素值;
  • M(x,y)={v1(x,y),v2(x,y),...vN(x,y)}:像素点(x, y)的背景样本集(样本集大小为N);
  • R:上下取值范围;

v(x,y)M(x,y)中所有样本值作差,所有差值中,在±R范围内的个数为Nb,若Nb大于一个给定的阈值min,就说明当前像素值与该点历史样本中的多个值相似,那么就认为(x,y)点属于背景点。

(2) 背景模型初始化

初始化是建立背景模型的过程,一般的检测算法需要一定长度的视频序列学习完成,影响了检测的实时性,而且当视频画面突然变化时,重新学习背景模型需要较长时间。
ViBe算法建立背景模型只需要一帧,即使用单帧视频序列初始化背景模型。将视频的第一帧作为背景模型的同时,算法也将该帧中每一个像素点周围随机取多个像素点,填充该像素点的样本集,这样样本集中就包含了像素点的时空分布信息。
用公式表示,我们可以认为:

  • M0(x,y):初始背景模型中的像素点(x, y);
  • NG:邻居点;
  • v0(x,y):初始原图像中像素点(x, y)的像素值;

于是有:
M0(x)={v0(y|yNG(x))},t=0

这种背景模型初始化的优缺点如下:

  • 优点:
    • 对于噪声的反应比较灵敏;
    • 计算量小速度快;
    • 不仅减少了背景模型建立的过程,还可以处理背景突然变化的情况,当检测到背景突然变化明显时,只需要舍弃原始的模型,重新利用变化后的首帧图像建立背景模型。
  • 缺点:
    • 用于作平均的几帧初始图像中可能采用了运动物体的像素,这种条件下初始化样本集,容易引入拖影(Ghost)区域;

初始背景模型建立完毕后,就可以进行前景的检测和背景模型的更新了。

(3) 前景检测

此时已经建立起了背景模型,便可以已经建立好的背景模型进行前景的检测。
遍历新一帧图像的所有像素点。用公式表示,则有:

  • v(x,y):新一帧的像素点(x, y);
  • M(x,y)={v1(x,y),v2(x,y),...vN(x,y)}:像素点(x, y)的背景样本集(样本集大小为N);
  • D(x,y)={d1(x,y),d2(x,y),...dN(x,y)}:像素点(x, y)当前值与样本集里所有样本值之差(样本集大小为N)
    • 其中,di=v(x,y)vi(x,y)
  • R:判断像素点与历史样本值是否相近的阈值;
  • T:判断前景点的阈值;
    • 统计当前像素点的值与历史样本值之差大于R的个数,若个数大于T,则判断该点为前景点;

检测前景的流程如下:

  • 将某像素点的当前像素值v(x,y),与该像素点的样本集M(x,y)作差值,即得到D(x,y)
  • 遍历D(x,y)中的元素di(x,y),比较它与阈值R的大小;并计满足di(x,y)>R的个数为Nf
  • 若有Nf>T,则该点为前景点;

检测过程的主要三个参数是:样本集数目N,阈值R,与阈值T。一般设置N = 20, R = 20, T = 2;

(4) 背景模型更新策略

即使已经建立起了背景模型,也应该对背景模型进行不断的更新,这样才能使得背景模型能够适应背景的不断变化(如光照变化,背景物体变更等)。

A. 普通更新策略

对于其他的背景提取算法,背景模型有两种不同的更新策略:

  • 保守更新策略:前景点永远不会用来填充模型
    • 这样会引起死锁,产生Ghost区域。比如初始化的时候如果一块静止的区域被错误的检测为运动的,那么在这种策略下它永远会被当做运动的物体来对待;
  • Blind策略:对死锁不敏感,前景和背景都可以用来更新背景模型;
    • 这样的缺点在于,缓慢移动的物体会融入到背景中,无法检测出来;

B. ViBe算法更新策略

ViBe算法中,使用的更新策略是:保守更新策略 + 前景点计数法 + 随机子采样。

  • 前景点计数法:对像素点进行统计,如果某个像素点连续N次被检测为前景,则将其更新为背景点;
  • 随机子采样:在每一个新的视频帧中都去更新背景模型中的每一个像素点的样本值是没有必要的,当一个像素点被分类为背景点时,它有1/φ的概率去更新背景模型。

这就决定了ViBe算法的更新策略的其他属性:

  • 无记忆更新策略:每次确定需要更新像素点的背景模型时,以新的像素值随机取代该像素点样本集的一个样本值;
  • 时间取样更新策略:并非每处理一帧数据,都需要更新处理,而是按一定的更新率更新背景模型;
    • 当一个像素点被判定为背景时,它有1/φ的概率更新背景模型;
    • φ是时间采样因子,一般取值为16;
  • 空间邻域更新策略:针对需要更新像素点,在该像素点的邻域中随机选择一个像素点,以新选择的像素点更新被选中的背景模型;

C. ViBe算法具体更新的方法:

  • 每个背景点都有1/φ的概率更新该像素点的模型样本值
  • 有1/φ的概率去更新该像素点邻居点的模型样本值
  • 前景点计数达到临界值时,将其变为背景,并有1/ φ的概率去更新自己的模型样本值。

更新邻居的样本值利用了像素值的空间传播特性,背景模型逐渐向外扩散,这也有利于Ghost区域的更快的识别。
在选择要替换的样本集中的样本值时,我们是随机选取一个样本值进行更新。这样就可以保证,样本值的平滑的生命周期的原因是由于是随机的更新,这种情况下一个样本值在时刻t不被更新的概率是(N - 1) / N。假设时间是连续的,那么在极小时间dt过去后,样本值仍然保留的概率是:
P(t,t+dt)=(N1N)(t+dt)t
也可以写作:
P(t,t+dt)=eln(NN1)dt
上面的公式表明,样本值在模型中是否被替换,与时间t无关,即更新策略是合适的。

3. ViBe算法优缺点

(1) 优点

Vibe背景建模为运动目标检测研究领域开拓了新思路,是一种新颖、快速及有效的运动目标检测算法。优点主要有两点:

  • 思想简单,易于实现:
    • 初始化背景图像时,Vibe算法通常随机选取邻域20个样本,作为每个像素点建立一个基于样本的背景模型,具有初始化速度快、内存消耗少和占用资源少等优点;
    • 随后,利用一个二次抽样因子φ,使有限的样本基数能近似表示无限的时间窗口,即在较少样本前提下,保证算法的准确性;
    • 最后,并采用一种邻域传播机制保证算法的空间一致性。
  • 样本衰减最优:
    • 有人通过增加样本基数(上至200个)来处理复杂场景,也有人结合两个子模型分别处理快速更新和缓慢更新的情况。其实,选取被替换样本更新背景模型,实质上是样本寿命问题。
    • 传统方式采用先进先出的替换策略,而Vibe背景模型中每个样本被选中为替换样本的概率是相等的,与样本存在时间的长短无关,这种策略保证背景模型中的样本寿命呈指数衰减,模型更新达到最佳状态。
  • 运算效率高:
    • Vibe背景模型是基于少量样本的背景模型;
    • Vibe算法优化了背景模型中的相似度匹配算法;

关于运算效率的比较,《背景建模–Vibe 算法优缺点分析》中做了实验:为了得到最佳样本数量N值,分别选取N为5、15、20、25进行了实验对比:结果如图所示:

实验结果表明,N取20、25时,检测结果理想;考虑计算负载,N取20最优。与混合高斯的3-5个高斯模型的计算匹配比较,基于20个样本的背景模型计算具有计算开销低、检测速度快等优点。
Vibe的背景模型相似度匹配函数只与判断像素点与历史样本值是否相近的阈值R,以及判断前景点的阈值T有关(具体见本文三.2.(3))。背景模型中的样本与待分类像素的欧式距离小于R的个数超过T时,更新背景模型;而找到T个匹配样本时,便立即判断该像素为背景像素点,并停止计算,这样提高了运算效率。

(2) 缺点

ViBe算法自身也存在着局限性。主要有静止目标、阴影前景和运动目标不完整等问题。

A. 静止目标

如下图所示:

图(a)红框中的人在等地铁,从图(a)到图(c)经过498帧,长时间驻留未运动,该人物运动目标逐渐被背景吸收。而在本视频中,将在450帧以上都没有明显位移的运动目标区域定义成为静止目标区域。
这样可以总结产生静止目标问题的原因有两个:

  • 运动目标从运动到静止;
  • 运动目标运动速度太过缓慢:当ViBe背景模型更新速度过快时,会将静止或缓慢运动目标吸收成为背景的一部分;

B. 阴影前景

如下图所示:

图(b)和图(d)分别是用Vibe算法对人体运动目标(a)和车体运动目标(c)的检测结果。由于光线被人体或车体运动目标所遮挡,投射阴影区的背景被误检为运动目标前景。阴影的存在导致检测出来的运动目标形状不准确,影响后续目标分类、跟踪、识别和分析等其他智能视频处理模块。
产生阴影前景问题的根源是:光线被运动目标前景遮挡,投射阴影区的颜色比背景的颜色暗,即阴影和背景颜色值的距离相差较大,背景差分后被误检为运动目标前景。

C. 运动目标不完整问题

如下图所示:

  • 图(a)中的人内部出现空洞;
  • 图(b)中的人中间出现断层;
  • 图(c)中的人上半身出现边缘残躯;
  • 图(d)车体的挡风玻璃出现空洞;

总结图中的结果,可以将运动目标不完整现象大致分为三类:

  • 运动目标内部有大量空洞(图a);
  • 运动目标边缘残缺,呈现C字形凹陷(图d);
  • 运动目标中间有断层(图b);

产生运动目标不完整问题的根源主要有两点:

  • ViBe算法自身存在的缺陷;
    • 基于统计学原理的Vibe样本模型受限于模型的样本个数,当样本趋于无穷大时才能准确描述场景,这在实际应用中是不可能实现的;
  • 场景或运动目标的复杂性和多变性;
    • 瞬时的光线突变,背景模型来不及更新;
    • 前景与背景颜色相近,将前景误判为背景;
    • 噪声干扰,出现孤立噪声点和连通噪声区域;

4. ViBe算法源码

笔者已经将把源码上传到GitHub网站上,地址如下:
https://github.com/upcAutoLang/BackgroundSplit-OpenCV/tree/master/src/ViBe

5. ViBe的改进算法ViBe+

笔者对ViBe+进行了学习研究,博客地址如下:
《论文翻译:ViBe+算法(ViBe算法的改进版本)》

笔者已经将把源码上传到GitHub网站上,地址如下:
https://github.com/upcAutoLang/BackgroundSplit-OpenCV/tree/master/src/ViBe%2B

相关参考地址:
《ViBe算法原理和代码解析 》
《VIBE改进算法》

参考论文:
《M. Van Droogenbroeck and O. Paquot. Background Subtraction: Experiments and Improvements for ViBe.》

2016-01-27 19:58:23 u010402786 阅读数 37741

前提

    运动目标的检测是计算机图像处理与图像理解领域里一个重要课题,在机器人导航、智能监控、医学图像分析、视频图像编码及传输等领域有着广泛的应用。
                                      这里写图片描述

目标检测方法分类

  第一,已知目标的先验知识。在这种情况下检测目标有两类方法,第一类方法是用目标的先验知识训练一堆弱分类器,然后这些弱分类器一起投票来检测目标,如boosting, random forest 都是这个思路,大家熟知的adaboost人脸检测也是如此。第二类方法是根据先验知识找到目标和非目标的最佳划分线,如SVM.这两类方法各成一家,各有所长,都有着不错的表现。

  第二,未知目标的先验知识。此时不知道要检测的目标是什么,于是什么是目标就有了不同的定义。一种方法是检测场景中的显著目标,如通过一些特征表达出场景中每个像素的显著性概率,然后找到显著目标。另一种方法就是检测场景当中的运动目标了。

经典目标检测方法

1、背景差分法
  在检测运动目标时,如果背景是静止的,利用当前图像与预存的背景图像作差分,再利用阈值来检测运动区域的一种动态目标识别技术。
  背景差分算法适用于背景已知的情况,但难点是如何自动获得长久的静态背景模型。
  matlab中单纯的背景差分直接是函数imabsdiff(X,Y)就可以。
2、帧差分法
  利用视频序列中连续的两帧或几帧图像的差来进行目标检测和提取。在运动的检测过程中,该方法利用时间信息,通过比较图像中若干连续帧获得对应像素点的灰度差值,如果均大于一定的阈值T2,则可以判断该位置存在运动的目标。
  较适合于动态变化场景。
3、光流场法
  利用相邻两帧中对应像素的灰度保持原理来评估二维图像的变化。能够较好的从背景中检测到相关前景目标,甚至是运动屋里中的部分运动目标,适用于摄像机运动过程中相对运动目标的检测。
  开口问题、光流场约束方程的解的不唯一性问题。不能正确的表示实际的运动场。
        例子如下:
       1.首先在一帧图像内随机均匀选取k个点,并滤除那些邻域纹理太光滑的点,因为这些点不利于计算光流。
这里写图片描述
       2.计算这些点与上一帧图像的光流矢量,如上右图,此时已经可以看出背景运动的大概方向了。
       这里写图片描述
       3.接下来的这一步方法因人而异了。
       2007年cvpr的一篇文章Detection and segmentation of moving objects in highly dynamic scenes的方法是把这些光流点的(x,y,dx,dy,Y,U,V)7个特征通过meanshift聚类来聚合到一起,最后形成运动目标轮廓。

新目标检测方法

       其实写到这里想了想到底能不能叫目标检测,博主认为图像的前背景分离也是目标检测的一种(博主才疏学浅,求赐教)

1、像素点操作
  对每个像素点进行操作,判别为前景或者背景两类。如下面的图片所示:
  这里写图片描述
2、低秩矩阵应用
  背景建模是从拍摄的视频中分离出背景和前景。下面的例子就是将背景与前景分离开。使用的方法是RPCA的方法。
  其网址以及效果如下:
  http://perception.csl.illinois.edu/matrix-rank/introduction.html
  这里写图片描述
3、深度学习
  FCN + denseCRF 精确分割+语义标签。图像中的前景目标检测分割做的很好,下面还能做出语义检测,判断出图中的东西属于什么。This demo is based on our ICCV 2015 paper :Conditional Random Fields as Recurrent Neural Networks,
  测试网址以及测试图像如下:
  http://www.robots.ox.ac.uk/~szheng/crfasrnndemo
  这里写图片描述


推荐另外一篇关于神经网络改进方法的上篇内容:
http://blog.csdn.net/u010402786/article/details/49272757
       另外附上一个深度学习未来发展趋势之一:
       “注意力模型” 在未来的发展,注意力模型的升温。一些系统,但不是全部,开始放到“注意力模型”的背景中,或者说让神经网络在完成任务的过程中试图学习在哪里放置其“注意力”。这些还不是一个正规神经网络流水线中的一部分,但是已经时不时的出现在模型中了。

2018-03-19 15:50:28 primetong 阅读数 4745

OpenCV图像处理入门学习教程系列,上一篇第四篇:基于LoG算子的图像边缘检测

运动目标检测

关于运动目标检测的方法总结,目前能够实现运动物体检测的方法主要有以下几种:
1)背景差分法:能完整快速地分割出运动图像。其不足之处是易受光线变化影响,背景的更新是关键。不适用于摄像头运动的情况;
2)光流法:能检测独立运动的图像,可用于摄像头运动的情况,但是计算复杂耗时,较难实现实时监测;
3)帧差法:受光线变化影响较小,简单快速,但不能分割出完整的运动对象,需进一步运用目标分割算法。还有一些改进的算法,主要致力于减少光照影响和检测慢速物体变化。

以上是大多数文献中对这三种方法的评价,由于是初次接触,而且实验中的需求是静止相机,因此采用最简单的方法:背景差分法。

背景差分法实现步骤

将背景差分法的实现步骤总结如下:
1)进行图像的预处理:主要包括对图像进行灰度化以及滤波。
灰度化的方法及其C语言实现可参考《Canny边缘检测算法原理及其VC实现详解(二)》一文;关于图像滤波,通常可采用的方法有中值滤波、均值滤波以及高斯滤波等。关于高斯滤波的实现详见《高斯图像滤波原理及其编程离散化实现方法》一文。
2)背景建模:这是背景差法较为重要的第一步。目前大多的思路都是根据前N帧图像的灰度值进行区间统计从而得到一个具有统计意义的初始背景。在第一次的实现过程中,采用第一幅图片作为背景图,这样比较简单。

3)前景提取:将当前最新的图像与背景做差,即可求得背景差图,然后根据一定的方法对改图进行二值化,最终获得运动前景区域,即实现图像分割。

运动检测和背景更新方法实现的步骤

所研究的运动检测和背景更新方法实现的步骤如下:
(1)开辟静态内存,对图像进行初始化准备采集;
(2)采集图像,定义参数k,作为图像序列计数。采集第1幅图像时,则根据第一帧的大小信息进行矩阵、图像的初始化,
并且将第一帧图像进行灰度化处理,并转化为矩阵,作为背景图像及矩阵;如果k不等于1则把当前帧进行灰度化处理,
并转化为矩阵,作为当前帧的图像及矩阵。用当前帧的图像矩阵和背景帧的图像矩阵做差算出前景图矩阵并对其进行
二值化以便计算它与背景帧差别较大的像素个数,也就是二值化后零的个数。
当第一帧的异物大于1W个像数点则需要将当前帧存储为第一帧,并且将系统的状态转为1——采集第二帧;
第一帧和第二帧的异物都大于1W个像数点时,将当前帧存储为第二帧,通过判断第一帧和第二帧的差值来确定两帧是否连续,
若连续则将系统状态转为2——采集第三帧,若不连续则报警,并把系统状态转为0——采集背景帧;
当第一帧和第二帧的异物都大于1W个像数点, 而第三帧没有时则报警;
若连续3帧的异物都大于1W个像数点时,将当前帧存储为第三帧,通过判断第二帧和第三帧的差值来确定两帧是否连续,
若连续则将更新背景,若不连续则报警。然后把系统状态转为0——采集背景帧。
注意其中有一个0-1-2-0....的状态机。


一、读取本地视频实现基于背景差分法的视频目标运动侦测

左上角是原视频,右上角是背景,下面是分割出来的前景,结果如下:


二、打开笔记本摄像头实现基于背景差分法的视频目标运动侦测

这部分就不做演示了,实现起来也很简单,只需在上述读取本地视频实现基于背景差分法的视频目标运动侦测的工程项目中,修改属性页-配置属性-调试-命令参数-中传入主函数的参数,后面的处理步骤是一样的。
1.为空时为打开默认摄像头:

2.非空时默认在工程目录下寻找对应同名的本地视频文件并开始读取处理:



三、代码解析

下面是一段基于背景差分法的视频目标运动侦测的代码,当传入主函数的参数为空时(argc的值为1

main(int argc, char *argv[])中的两个参数, argc表示参数个数,*argv则是具体的参数.
默认情况下,project本身是作为第一个参数的(比如,我的应用输出是test.exe,则argv[0]对应的值为test.exe的绝对路径 
- D:\program files\vs2012\vctest\debug\test.exe),即默认情况下argc的值为1(该值无需手动改变), 
如果需设定其他参数,可以通过如下配置:
<1>选择PROJECT—>Properties—>Configuration Properties—>Debugging—>Command Arguments
<2>在Command Arguments中添加参数,假设 : 要设定argv[1] = ”23”, argv[2] = ”Hello”, 
那么输入值23  Hello即可(两个值之间空格隔开,要传空格就包括在" "中)然后保存即可。运行之后可发现参数值已经改变
中文版:菜单[项目]->属性页->配置属性->调试,在[命令行参数]里填上即可。不同参数之前用空格隔开。)

打开当前运行环境计算机默认的摄像头,此时可在镜头前做小幅度动作即可看到分割出的前景与背景视频。

当传入主函数的参数不为空时(传入的是视频的全部文件名,包含.文件扩展名,例如traffic.flv),默认会进入工程目录下寻找对应的同名本地视频文件进行下一步的处理操作。

  • IDE:Visual Studio 2013

  • 语言:C++

  • 依赖OpenCV 2.4.9

程序是在VS2013和OpenCV2.4.9下运行的,部分参考代码如下,相应位置有详细注释,如果有fopen_s()报错的问题,请看:

解决VS2013中fopen替代为fopen_s的问题
最普通的解决方法,就是使用fopen_s替代,这是fopen_s()函数的用法
fopen_s(_Outptr_result_maybenull_ FILE ** _File, _In_z_ const char * _Filename, _In_z_ const char * _Mode);
这是fopen()函数:
fopen(_In_z_ const char * _Filename, _In_z_ const char * _Mode);
但fopen_s参数要比fopen多一个,并且返回的类型为:errno_t __cdecl,但fopen()返回的类型为:FILE * __cdecl
因此,fopen_s函数可能并不适合自己的程序,解决方法有一比较好的方法:
更改预处理定义:
项目->属性->配置属性->C / C++->预处理器->预处理器定义,增加_CRT_SECURE_NO_DEPRECATE
这样就可以解决vs2013报错的问题了。

cvCloneImage的原型是:
IplImage* cvCloneImage(const IplImage* image);
在使用函数之前,不用开辟内存。该函数会自己开一段内存,然后复制好image里面的数据,然后把这段内存中的数据返回给你。
clone是把所有的都复制过来,也就是说不论你是否设置Roi, Coi等影响copy的参数,clone都会原封不动的克隆过来。
copy就不一样,只会复制ROI区域等。

整个工程文件见下载页面

#include <stdio.h>
#include <time.h>
#include <cv.h>
#include <cxcore.h>
#include <highgui.h>

////调用以下函数可以对运动目标寻找轮廓并绘制矩形框,但是效果不佳暂且就不找问题所在了
//CvMemStorage *stor;
//CvSeq *cont;
//void DrawRec(IplImage* pImgFrame, IplImage* pImgProcessed, int MaxArea);

int main( int argc, char** argv )
{
//声明IplImage指针
  IplImage* pFrame = NULL;     //pFrame为视频截取的一帧
  IplImage* pFrame1 = NULL;      //第一帧
  IplImage* pFrame2 = NULL;//第二帧
  IplImage* pFrame3 = NULL;//第三帧
  IplImage* pFrImg = NULL;     //pFrImg为当前帧的灰度图
  IplImage* pBkImg = NULL;     //pBkImg为当前背景的灰度图
  IplImage* pBkImgTran = NULL;//pBkImgTran为当前背景处理过的图像
  IplImage* pFrImgTran = NULL;//pFrImgTran为当前前景处理过的图像
  CvMat* pFrameMat = NULL;     //pFrameMat为当前灰度矩阵
  CvMat* pFrMat = NULL;      //pFrMat为当前前景图矩阵,当前帧减去背景图
  CvMat* bg1 = NULL;
  CvMat* bg2 = NULL;
  CvMat* bg3 = NULL;
  CvMat* pFrMatB = NULL;     //pFrMatB为二值化(0,1)的前景图
  CvMat* pBkMat = NULL;
  CvMat* pZeroMat = NULL;               //用于计算bg1 - bg2 的值
  CvMat* pZeroMatB = NULL;//用于计算 pZeroMat阈值化后来判断有多少个零的临时矩阵
  CvCapture* pCapture = NULL;
  int warningNum = 0;      //检测到有异物入侵的次数
  int nFrmNum = 0;//帧计数
  int status = 0;        //状态标志位
//创建窗口
  cvNamedWindow("video", 1);
  cvNamedWindow("background",1);//背景
  cvNamedWindow("foreground",1);//前景
//使窗口有序排列
  cvMoveWindow("video", 30, 0);
  cvMoveWindow("background", 720, 0);
  cvMoveWindow("foreground", 365, 330);
  if ( argc > 2 )
    {
      fprintf(stderr, "Usage: bkgrd [video_file_name]\n");
      return -1;
    }
//打开摄像头,从摄像头取出码流可以使用海康、大唐等网络或者模拟摄像头,这里cvCaptureFromCAM(0)可直接打开笔记本摄像头
  if (argc ==1)
    if ( !(pCapture = cvCaptureFromCAM(0)))
      {
        fprintf(stderr, "Can not open camera.\n");
        return -2;
      }
//打开视频文件,是通过*argv传入的
  if (argc == 2)
    if ( !(pCapture = cvCaptureFromFile(argv[1])))
      {
        fprintf(stderr, "Can not open video file %s\n", argv[1]);
        return -2;
      }

////开始计时
//  time_t start,end;
//  time(&start);        //time() 返回从1970年1月1号00:00:00开始以来到现在的秒数(有10为数字)。
//  printf("%d\n",start);
//逐帧读取视频
  while (pFrame = cvQueryFrame( pCapture ))
    {
      nFrmNum++;
      //如果是第一帧,需要申请内存,并初始化
      if (nFrmNum == 1)
        {
          pBkImg = cvCreateImage(cvSize(pFrame->width, pFrame->height), IPL_DEPTH_8U,1);
          pFrImg = cvCreateImage(cvSize(pFrame->width, pFrame->height), IPL_DEPTH_8U,1);
          pBkImgTran = cvCreateImage(cvSize(pFrame->width,pFrame->height), IPL_DEPTH_8U,1);
          pFrImgTran = cvCreateImage(cvSize(pFrame->width,pFrame->height), IPL_DEPTH_8U,1);
          pBkMat = cvCreateMat(pFrame->height, pFrame->width, CV_32FC1);
          pZeroMat = cvCreateMat(pFrame->height, pFrame->width, CV_32FC1);
          pFrMat = cvCreateMat(pFrame->height, pFrame->width, CV_32FC1);
          pFrMatB = cvCreateMat(pFrame->height, pFrame->width, CV_8UC1);
          pZeroMatB = cvCreateMat(pFrame->height, pFrame->width, CV_8UC1);
          pFrameMat = cvCreateMat(pFrame->height, pFrame->width, CV_32FC1);
          cvZero(pZeroMat);
          //转化成单通道图像再处理
          cvCvtColor(pFrame, pBkImg, CV_BGR2GRAY);
          //转换为矩阵
          cvConvert(pFrImg, pBkMat);
        }
      else /* 不是第一帧的就这样处理 */
        {
          //pFrImg为当前帧的灰度图
          cvCvtColor(pFrame, pFrImg, CV_BGR2GRAY);
          //pFrameMat为当前灰度矩阵
          cvConvert(pFrImg, pFrameMat);
          //pFrMat为前景图矩阵,当前帧减去背景图
          cvAbsDiff(pFrameMat, pBkMat, pFrMat);
          //pFrMatB为二值化(0,1)的前景图
          cvThreshold(pFrMat,pFrMatB, 60, 1, CV_THRESH_BINARY);
          //将图像矩阵转化为图像格式,用以显示
          cvConvert(pBkMat, pBkImgTran);   
          cvConvert(pFrMat, pFrImgTran);  
          //显示图像
          cvShowImage("video", pFrame);
          cvShowImage("background", pBkImgTran); //显示背景
          cvShowImage("foreground", pFrImgTran); //显示前景

		  //DrawRec(pFrame, pFrImgTran, 16);		//对运动目标寻找轮廓并绘制矩形框,但是效果不佳暂且不找问题所在了

          //以上是每抓取一帧都要做的工作,下面进行危险检测
          if (cvCountNonZero(pFrMatB) > 10000 && status == 0) //表示是第一帧的异物大于1W个像数点
            {/* 则需要将当前帧存储为第一帧 */
              pFrame1 = cvCloneImage(pFrame);
              bg1 = cvCloneMat(pFrMat);
              status = 1;      //继续采集第2帧
            }
          else if (cvCountNonZero(pFrMatB) < 10000 && status == 1) // 表示第一帧的异物大于1W个像数点,而第二帧没有,则报警
            {
              printf("NO.%d warning!!!!\n\n",warningNum++);
              status = 0;
            }
          else if (cvCountNonZero(pFrMatB) > 10000 && status == 1)// 表示第一帧和第二帧的异物都大于1W个像数点
            {
              pFrame2 = cvCloneImage(pFrame);
              bg2 = cvCloneMat(pFrMat);
              cvAbsDiff(bg1, bg2, pZeroMat);
              cvThreshold(pZeroMat,pZeroMatB, 20, 1, CV_THRESH_BINARY);
              if (cvCountNonZero(pZeroMatB) > 3000 ) //表示他们不连续,这样的话要报警
                {
                  printf("NO.%d warning!!!!\n\n",warningNum++);
                  status = 0;
                }
              else
                {
                  status = 2;                   //继续采集第3帧
                }
            }
          else if (cvCountNonZero(pFrMatB) < 10000 && status == 2)//表示第一帧和第二帧的异物都大于1W个像数点,而第三帧没有
            {
              //报警
              printf("NO.%d warning!!!!\n\n",warningNum++);
              status = 0;
            }
          else if (cvCountNonZero(pFrMatB) > 10000 && status == 2)//表示连续3帧的异物都大于1W个像数点
            {
              pFrame3 = cvCloneImage(pFrame);
              bg3 = cvCloneMat(pFrMat);
              cvAbsDiff(bg2, bg3, pZeroMat);
              cvThreshold(pZeroMat,pZeroMatB, 20, 1, CV_THRESH_BINARY);
              if (cvCountNonZero(pZeroMatB) > 3000 ) //表示他们不连续,这样的话要报警
                {
                  printf("NO.%d warning!!!!\n\n",warningNum++);
                }
              else //表示bg2,bg3连续
                {
                  cvReleaseMat(&pBkMat);
                  pBkMat = cvCloneMat(pFrameMat); //更新背景
                }
                status = 0;                //进入下一次采集过程
            }
          //如果有按键事件,则跳出循环
          //此等待也为cvShowImage函数提供时间完成显示
          //等待时间可以根据CPU速度调整
          if ( cvWaitKey(2) >= 0 )
            break;
        }/* The End of the else */
    }/* The End of th while */
//销毁窗口
    cvDestroyWindow("video");
    cvDestroyWindow("background");
    cvDestroyWindow("foreground");
//释放图像和矩阵
    cvReleaseImage(&pFrImg);
    cvReleaseImage(&pBkImg);
    cvReleaseMat(&pFrameMat);
    cvReleaseMat(&pFrMat);
    cvReleaseMat(&pBkMat);
    cvReleaseCapture(&pCapture);
  return 0;
}

从实验结果来看,OpenCV中基于背景差分法的视频目标运动侦测可以很好地完成预期的视频目标运动检测的。

基于背景差分法的视频目标运动侦测,整个工程文件见下载页面



背景差分法

阅读数 15726