运动目标检测就是先判断在视频序列的帧图像中是否由前景目标的运动,然后再对目标进行初始定位的过程。
传统的目标检测算法主要有相邻帧差法、光流法、背景差分法(又称背景减法)等等。2000年以来,随着神经网络的再次兴起,许多利用神经网络进行目标检测的算法也应运而生。
本文介绍的是背景差分法,它的主要原理是利用当前图像额背景图像的差来检测目标区域。首先对固定摄像机拍摄的视频序列进行计算,得到一个场景的静态背景初始化模型,之后将当前帧于 静态背景图像相比,从当前帧中将背景剔除,从而得到前景运动目标。不过由于背景可能发生变化,所以算法再实现的过程中,将不断地自动实现背景更新,之后再次利用新的背景模型来确定前景目标,以期得到更精确的结果。
用opencv2+vs2010编写代码如下:
#include<iostream>
#include<highgui.h>
#include<opencv2/imgproc/imgproc.hpp>
#include <iomanip>
using namespace std;
using namespace cv;
//帧处理基类
class FrameProcessor{
public:
virtual void process(Mat &input, Mat &ouput) = 0;
};
class VideoProcessor{
private:
//Opencv视频捕捉对象
VideoCapture capture;
//每帧调用的回调函数
void(*process)(Mat&, Mat&);
//确定是否调用回调函数的bool变量
bool callIt;
//输入窗口的名称
string windowNameInput;
string windowNameOutput;
//延迟
int delay;
//已处理的帧数
long fnumber;
//在该帧数停止
long frameToStop;
//是否停止处理
bool stop;
//opencv的写视频对象
VideoWriter writer;
//输出文件名称
string outputFile;
//输出图像的当前索引
int currentIndex;
//输出图像名称中的数字位数
int digits;
//输出图像的扩展名
string extension;
FrameProcessor *frameprocessor;
//图像序列作为输入视频流
vector<string> images;
public:
VideoProcessor() :callIt(true), delay(0), fnumber(0), stop(false), frameToStop(-1){}
//设置回调函数
void setFrameProcessor(
void(*frameProcessingCallback)(Mat&, Mat&)){
process = frameProcessingCallback;
}
//设置视频文件的名称
bool setInput(string filename){
fnumber = 0;
//释放之前打开过的资源
capture.release();
//打开视频文件
return capture.open(filename);
}
//创建输入窗口
void displayInput(string wn){
windowNameInput = wn;
namedWindow(windowNameInput);
}
//创建输出窗口
void displayOutput(string wn){
windowNameOutput = wn;
namedWindow(windowNameOutput);
}
//不再显示处理后的帧
void dontDisplay(){
destroyWindow(windowNameInput);
destroyWindow(windowNameOutput);
windowNameInput.clear();
windowNameOutput.clear();
}
//获取并处理序列帧
void run(){
//当前帧
Mat frame;
//输出帧
Mat output;
//打开失败时
if (!isOpened())return;
stop = false;
while (!isStopped())
{
//读取下一帧
if (!readNextFrame(frame))break;
//显示输出帧
if (windowNameInput.length() != 0)
imshow(windowNameInput, frame);
//调用处理函数
if (callIt){
//处理当前帧
if (process)
process(frame, output);
else if (frameprocessor)
frameprocessor->process(frame, output);
//增加帧数
fnumber++;
}
else
{
output = frame;
}
//输出图像序列
if (outputFile.length() != 0)
writeNextFrame(output);
//显示输出帧
if (windowNameOutput.length() != 0)
imshow(windowNameOutput, output);
//引入延迟
if (delay >= 0 && waitKey(delay) >= 0)
stopIt();
//检查是否需要停止运行
if (frameToStop >= 0 && getFrameNumber() == frameToStop)
stopIt();
}
}
//停止运行
void stopIt(){
stop = true;
}
//是否已停止
bool isStopped(){
return stop;
}
//是否开始了捕获设备
bool isOpened(){
return capture.isOpened();
}
//设置帧间的延迟 0意味着在每帧都等待用户按键 负数意味着没有延迟
void setDelay(int d){
delay = d;
}
//得到下一帧 可能是视频文件或摄像头
bool readNextFrame(Mat &frame){
return capture.read(frame);
}
//需要调用回调函数
void callProcess(){
callIt = true;
}
//不需要调用回调函数
void dontCallProcess(){
callIt = false;
}
void stopAtFrameNo(long frame){
frameToStop = frame;
}
//返回下一帧的帧数
long getFrameNumber(){
//得到捕获设备的信息
long fnumber = static_cast<long>(capture.get(CV_CAP_PROP_POS_FRAMES));
return fnumber;
}
int getFrameRate(){
return capture.get(CV_CAP_PROP_FPS);
}
//获得编码类型
int getCodec(char codec[4]) {
if (images.size() != 0)
return -1;
union { // 数据结构4-char
int value;
char code[4];
} returned;
//获得编码值
returned.value = static_cast<int>(
capture.get(CV_CAP_PROP_FOURCC));
// get the 4 characters
codec[0] = returned.code[0];
codec[1] = returned.code[1];
codec[2] = returned.code[2];
codec[3] = returned.code[3];
return returned.value;
}
//获得帧大小
Size getFrameSize() {
if (images.size() == 0) {
// 从视频流获得帧大小
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 Size(w, h);
}
else {
//从图像获得帧大小
cv::Mat tmp = cv::imread(images[0]);
return (tmp.data) ? (tmp.size()) : (Size(0, 0));
}
}
//设置输出视频文件
//默认使用于输入视频相同的参数
bool setOutput(const string &filename, int codec = 0, double framerate = 0.0, bool isColor = true){
outputFile = filename;
extension.clear();
if (framerate == 0.0)
framerate = getFrameRate();//与输入相同
char c[4];
//使用相同的编码格式
if (codec == 0)
{
codec = getCodec(c);
}
//打开输出视频
return writer.open(outputFile, //文件名
codec, //将使用的编码格式
framerate, //帧率
getFrameSize(), //尺寸
isColor); //是否是彩色视频?
}
//输出帧 可能是视频文件或图像文件
void writeNextFrame(Mat &frame){
if (extension.length())//我们输出到图像文件
{
stringstream ss;
//组成输出文件名称
ss << outputFile << setfill('0')
<< setw(digits)
<< currentIndex++ << extension;
imwrite(ss.str(), frame);
}
else
{//我们输出到视频文件
writer.write(frame);
}
}
//设置输出为独立的图像文件 扩展名必须是".jpg" ".bmp"
bool setOutput(const string &filename,//前缀
const string&ext,//后缀
int numberofDigits = 3,//数字位数
int startIndex = 0){ //开始索引
//数字位数必须是正的
if (numberofDigits < 0)return false;
//文件名及其后缀
outputFile = filename;
extension = ext;
//文件名中的数字位数
digits = numberofDigits;
//开始索引
currentIndex = startIndex;
return true;
}
void setFrameProcessor(FrameProcessor *frameprocessor){
process = 0;
this->frameprocessor = frameprocessor;
callProcess();
}
};
class BGFGSegmentor :public FrameProcessor{
Mat gray;
Mat background;
Mat backImage;
Mat foreground;
//背景累加中的学习率
double learningRate;
int ithreshold;
public:
BGFGSegmentor() :ithreshold(10), learningRate(0.01){}
//处理方法
void process(Mat &frame, Mat &output){
//转换为灰度图
cvtColor(frame, gray, CV_BGR2GRAY);
//初始化背景为第一帧
if (background.empty()) gray.convertTo(background, CV_32F);
//转换背景图像为8U格式
background.convertTo(backImage, CV_8U);
//计算差值
absdiff(backImage, gray, foreground);
//应用阈值化到前景图像
threshold(foreground, output, ithreshold, 255, THRESH_BINARY_INV);
//对图像累加
accumulateWeighted(gray, background, learningRate, output);
}
void setThreshold(int ithreshold){
this->ithreshold = ithreshold;
}
};
int main(){
//创建视频处理器实例
VideoProcessor processor;
//创建背景 / 前景分段器
BGFGSegmentor segmentor;
segmentor.setThreshold(25);
//打开视频文件
processor.setInput("../people.avi");
//设置帧处理器对象
processor.setFrameProcessor(&segmentor);
//声明显示窗口
processor.displayOutput("Extracted Foreground");
//以原始帧率播放视频
processor.setDelay(1000. / processor.getFrameRate());
//开始处理过程
processor.run();
return 0;
}
用一个监控视角作为示例得到的结果如下: