对于一个固定的场景(背景),我们感兴趣的是在场景中运动的物体(前景)。前景提取是在智能监控应用中的一个基础步骤。
帧间差分:在运动目标检测中,简单来说,就是背景与当前帧之间的差异。数字图像可以表示成一个矩阵,矩阵中每一个元素叫一个像素点。帧间差分=绝对值(背景-当前帧图像)。
怎么确定前景?
我们取帧间差分足够大的像素点,将这些像素点作为前景像素。
基于帧间差分的运动目标的主要问题:
背景建模的好坏直接影响检测的效果,由于背景是随时间改变的。如果不对背景进行更新,可想而知,检测效果不会好。所以,第一个问题就是,背景如何建模?
另外,反射光(如运动物体在大理石地面的倒影)对运动目标检测也会造成一定影响。那么,怎样处理反射光?
除了这两个问题还有其他许多问题,想了解研究的童鞋可以看看这方面的论文。
准备
在opencv2中,下面的代码实现帧间差分。
cv::absdiff(backImage, //背景
gray, //当前帧
foreground//前景(未加工前)
);
帧间差分后,通过阈值,更加精确的标识出前景物体。
//阈值,前景(黑,0),背景(白,255)
cv::threshold(foreground, //前景(未加工前)
output,//前景(阈值后)
threshold, //阈值
255, cv::THRESH_BINARY_INV);
针对背景建模问题,我们采用动态建模。每一帧通过加权和更新背景,公式如下:
背景=(1-a)背景+a*第t帧图像(去除前景)
参数a时学习率,如a=0.001。当第t帧时,通过将当前的背景和第t帧图像(除掉前景像素点)加权和,获得新的背景,用来检测第t+1帧前景。
//对背景加(通过掩模去除前景像素)
cv::accumulateWeighted(gray, //第t帧图像
background, //背景
learningRate, //学习率
output //第t帧前景,所有像素为0(黑)的点不参加运算。
);
到此,核心的部分完成。
示例
程序使用了策略模式。策略模式说白了,就是将算法都封装在类中,面向对象的思想,方便扩展和重用。
本程序没有直接写在在一个源文件,而是在以期写好的一些类的基础上进行的。总之,建议用面向对象的方式写程序,掌握策略模式、控制器模式、单件、MVC模式。
VideoProcess类处理每一帧,FrameProcessor抽象类,用来继承,定义自己的帧处理函数。
源.cpp
#include"BGFGSegmentor.h"
int main()
{
//类对象
VideoProcess processor;
BGFGSegmentor sG;
//设置阈值
sG.setThreshold(30);
//处理的视频
processor.setInput("D:/MatlabWork/classroom.avi");
//设置帧处理函数
processor.setFrameProcessor(&sG);
processor.displayOut("前景");
processor.displayInput("输入");
//显示帧时的延迟
processor.setDelay(1000. / processor.getFrameRate());
//计算检测耗时
double time = cv::getTickCount();
processor.run();
time = cv::getTickCount() - time;
std::cout << time / cv::getTickFrequency() <<"秒"<< std::endl;
cv::waitKey(0);
}
VideoProcess.h
#include<opencv2\highgui\highgui.hpp>
#include<opencv2\imgproc\imgproc.hpp>
#include<opencv2\core\core.hpp>
#include<iostream>
class FrameProcessor
{
public:
// 帧处理函数(纯虚函数),用来继承
virtual void process(cv::Mat &input, cv::Mat &output) = 0;
};
class VideoProcess
{
private:
void(*process)(cv::Mat&, cv::Mat&);
cv::VideoCapture capture;
bool callIt;
std::string Input;
std::string Output;
int delay;
long fnumbers;
long frameToStop;
bool stop;
cv::VideoWriter w;
std::string outFlie;
FrameProcessor *frameProcessor;
public:
VideoProcess() :callIt(true), delay(0), fnumbers(0), stop(false), frameToStop(-1), frameProcessor(0){}
//设置回调函数
void setFrameProcessor(void(*P)(cv::Mat&, cv::Mat&))
{
frameProcessor = 0;
process = P;
}
void setFrameProcessor(FrameProcessor* frameProcessorPtr)
{
process = 0;
frameProcessor = frameProcessorPtr;
}
//得到视频编码类型
int getCodec(char codec[4]) {
union
{
int value;
char code[4];
} returned;
returned.value = static_cast<int>(capture.get(CV_CAP_PROP_FOURCC));
codec[0] = returned.code[0];
codec[1] = returned.code[1];
codec[2] = returned.code[2];
codec[3] = returned.code[3];
return returned.value;
}
//设置输出视频文件名
bool setOutput(const std::string &filename, int codec = 0, double framerate = 0.0, bool isColor = false)
{
outFlie = filename;
if (framerate == 0.0)
{
framerate = getFrameRate();
}
char c[4];
if (codec == 0)
{
codec = getCodec(c);
}
//摄像头
//return w.open(outFlie, -1, framerate, getFrameSize(), isColor);
//视频文件
return w.open(outFlie, codec, framerate, getFrameSize(), isColor);
}
//得到帧的尺寸
cv::Size getFrameSize() {
// get size of from the capture device
int w = static_cast<int>(capture.get(CV_CAP_PROP_FRAME_WIDTH));
int h = static_cast<int>(capture.get(CV_CAP_PROP_FRAME_HEIGHT));
return cv::Size(w, h);
}
//设置输入视频文件
bool setInput(std::string name)
{
fnumbers = 0;
capture.release();
return capture.open(name);
}
//设置摄像头ID
bool setInput(int i)
{
fnumbers = 0;
capture.release();
return capture.open(i);
}
//显示输入帧窗口
void displayInput(std::string wn)
{
Input = wn;
cv::namedWindow(wn);
}
//显示输出帧窗口
void displayOut(std::string wo)
{
Output = wo;
cv::namedWindow(wo);
}
//清空
void dontDisplay()
{
cv::destroyWindow(Input);
cv::destroyWindow(Output);
Input.clear();
Output.clear();
}
//判断打开帧状态
bool isOpened()
{
return capture.isOpened();
}
//设置停止标记位
bool isStopped()
{
return stop;
}
//读取下一帧
bool readNextFrame(cv::Mat& frame)
{
return capture.read(frame);
}
//调用回调函数
void callProcess()
{
callIt = true;
}
//不调用回调函数
void dontCallProcess()
{
callIt = false;
}
//停止
void stopIt() {
stop = true;
}
//得到帧数
long getFrameNumber() {
long f = static_cast<long>(capture.get(CV_CAP_PROP_POS_FRAMES));
return f;
}
//设置延迟
void setDelay(int d)
{
delay = d;
}
//帧率
double getFrameRate()
{
double r = capture.get(CV_CAP_PROP_FPS);
return r;
}
//写入视频帧
void wNextFrame(cv::Mat& frame)
{
w.write(frame);
}
//run函数
void run()
{
cv::Mat frame;
cv::Mat output;
//判断打开
if (!isOpened())
{
return;
}
stop = false;
//循环
while (!isStopped())
{
if (!readNextFrame(frame))
break;
if (Input.length() != 0)
{
cv::imshow(Input, frame);
}
if (callIt)
{
if (process)
{
process(frame, output);
}
else
{
frameProcessor->process(frame, output);
}
fnumbers++;
}
else
{
output = frame;
}
if (outFlie.length() != 0)
{
wNextFrame(output);
}
if (Output.length() != 0)
{
cv::imshow(Output, output);
if (delay >= 0 && cv::waitKey(delay) >= 0)
{
stopIt();
}
if (frameToStop >= 0 && getFrameNumber() == frameToStop)
stopIt();
}
}
}
};
BGFGSegmentor.h
#include"VideoProcess.h"
class BGFGSegmentor :public FrameProcessor
{
//当前灰度图
cv::Mat gray;
//累加的背景
cv::Mat background;
//背景图像
cv::Mat backImage;
//前景
cv::Mat foreground;
//学习率
double learningRate;
//阈值
int threshold;
public:
BGFGSegmentor() :threshold(10), learningRate(0.01){}
//设置阈值
void setThreshold(int T)
{
threshold = T;
}
//基于帧间差分和动态背景建模的运动目标检测
void process(cv::Mat& frame, cv::Mat& output)
{
cv::cvtColor(frame, gray, CV_BGR2GRAY);
//初始化背景
if (background.empty())
{
gray.convertTo(background, CV_32F);
}
//转换背景为8U格式
background.convertTo(backImage, CV_8U);
//计算差值
cv::absdiff(backImage, gray, foreground);
//阈值,前景(黑,0),背景(白,255)
cv::threshold(foreground, output, threshold, 255, cv::THRESH_BINARY_INV);
//对背景加(通过掩模去除前景像素)
cv::accumulateWeighted(gray, background, learningRate, output);
}
};
结果: