您的位置:首页 > 编程语言 > C语言/C++

C++基于OpenCV实现实时监控和运动检测记录

2017-02-11 13:28 926 查看


基于OpenCV实现实时监控并通过运动检测记录视频


一、课程介绍


1. 课程来源

课程使用的操作系统为 
Ubuntu 14.04
,OpenCV 版本为
OpenCV
2.4.13.1
,你可以在这里查看该版本 OpenCV 的文档。官方文档中有两个例子可以帮助你理解此课程,分别是
OpenCV 3.1.0 版本中背景减除的例子
OpenCV
2.4.13 版本中通过直方图比较相似度

你可以在我的 Github仓库 上找到 Windows 系统对应的 Visual Studio 工程。全部代码文件也可以在我的仓库中找到。

这里提供了完整的代码 http://labfile.oss.aliyuncs.com/courses/671/monitor-recorder.zip 。


2. 内容简介

课程实验使用PC机自带的摄像头作为监视器进行实时监控。
对原始图像做一定处理,使监控人员或监控软件更易发现监控中存在的问题。
当摄像头捕捉到运动产生时自动记录视频。


3. 课程知识点

本课程项目完成过程中将学习:
对摄像头数据的捕获
对捕获到的监控帧作背景处理
对监控视频做运动检测并记录视频


二、实验环境

本实验需要先在实验平台安装 OpenCV ,需下载依赖的库、源代码并编译安装。安装过程建议按照教程给出的步骤,或者你可以参考官方文档中 Linux 环境下的安装步骤,但 有些选项需要变更。安装过程所需时间会比较长,这期间你可以先阅读接下来的教程,在大致了解代码原理后再亲自编写尝试。

我提供了一个编译好的
2.4.13-binary.tar.gz
包,你可以通过下面的命令下载并安装,节省了编译的时间,通过这个包安装大概需要20~30分钟,视实验楼当前环境运转速度而定。
$ sudo apt-get update
$ sudo apt-get install build-essential libgtk2.0-dev libjpeg-dev libtiff5-dev libjasper-dev libopenexr-dev cmake python-dev python-numpy python-tk libtbb-dev libeigen2-dev yasm libfaac-dev libopencore-amrnb-dev libopencore-amrwb-dev libtheora-dev libvorbis-dev libxvidcore-dev libx264-dev libqt4-dev libqt4-opengl-dev sphinx-common texlive-latex-extra libv4l-dev libdc1394-22-dev libavcodec-dev libavformat-dev libswscale-dev
$ cd ~
$ mkdir OpenCV && cd OpenCV
$ wget http://labfile.oss.aliyuncs.com/courses/671/2.4.13-binary.tar.gz $ tar -zxvf 2.4.13-binary.tar.gz
$ cd opencv-2.4.13
$ cd build
$ sudo make install


如果你想体验编译的整个过程,我也提供了一个一键安装的脚本文件,你可以通过下面的命令尝试。这个过程会非常漫长,约2小时,期间可能还需要你做一定的交互确认工作。
$ cd ~
$ sudo apt-get update
$ wget http://labfile.oss.aliyuncs.com/courses/671/opencv.sh $ sudo chmod 777 opencv.sh
$ ./opencv.sh


如果你觉得有必要亲自尝试一下安装的每一步,可以按照下面的命令逐条输入执行,在实验楼的环境中大概需要两个小时。
$ sudo apt-get update
$ sudo apt-get install build-essential libgtk2.0-dev libjpeg-dev libtiff5-dev libjasper-dev libopenexr-dev cmake python-dev python-numpy python-tk libtbb-dev libeigen2-dev yasm libfaac-dev libopencore-amrnb-dev libopencore-amrwb-dev libtheora-dev libvorbis-dev libxvidcore-dev libx264-dev libqt4-dev libqt4-opengl-dev sphinx-common texlive-latex-extra libv4l-dev libdc1394-22-dev libavcodec-dev libavformat-dev libswscale-dev
$ wget https://github.com/Itseez/opencv/archive/2.4.13.zip $ unzip 2.4.13.zip
$ cd 2.4.13
$ mkdir release && cd release
$ cmake -D WITH_TBB=ON -D BUILD_NEW_PYTHON_SUPPORT=ON -D WITH_V4L=ON -D INSTALL_C_EXAMPLES=ON -D INSTALL_PYTHON_EXAMPLES=ON -D BUILD_EXAMPLES=ON -D WITH_QT=ON -D WITH_GTK=ON -D WITH_OPENGL=ON ..
$ sudo make
$ sudo make install
$ sudo gedit /etc/ld.so.conf.d/opencv.conf
$ 输入 /usr/local/lib,按 Ctrl + X 退出,退出时询问是否保存,按 Y 确认。
$ sudo ldconfig -v
$ sudo gedit /etc/bash.bashrc
$ 在文件末尾加入
$ PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig
export PKG_CONFIG_PATH
按 Ctrl + X 退出,按 Y 确认保存。


检验配置是否成功。将 OpenCV 自带的例子(在目录
PATH_TO_OPENCV/samples/C
下)运行检测。如果成功,将显示
lena 的脸部照片,同时圈出其面部。
$ cd samples/C
$ ./build_all.sh
$ ./facedetect --cascade="/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_alt.xml" --scale=1.5 lena.jpg



三、实验原理

实验通过 OpenCV 提供的 API 完成大部分任务,首先捕获摄像头数据,之后对捕获到的每一帧作背景减除处理,得出易于识别的图像,最后利用直方图做实时图像和背景图像的对比,实现运动检测并写入视频文件。


四、实验步骤

通过以下命令可下载项目源码,作为参照对比完成下面详细步骤的学习。
wget http://labfile.oss.aliyuncs.com/courses/671/monitor-recorder.zip unzip monitor-recorder.zip


1.定义头文件

工程文件由一个头文件 
monitor.hpp
 和一个入口文件 
main.cpp
 构成。首先在头文件中定义将使用的库和相关变量。

代码中使用到的 OpenCV 头文件和 C++ 头文件在头文件 
monitor.hpp
 中声明如下,其中 
unistd.h
 包含了
Linux 下的 
sleep
 函数,参数为睡眠的秒数。
#ifndef __MONITOR_H_
#define __MONITOR_H_

//opencv
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/video/background_segm.hpp>
#include <opencv2/objdetect/objdetect.hpp>
#include <opencv2/imgproc/imgproc.hpp>
//C++
#include <ctime>
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <sstream>

using namespace cv;
using namespace std;

// ...

#endif __MONITOR_H_


2.设计 
processCamera
 函数

processCamera
 负责完成主要功能,包括监控数据的获取和处理。首先要了解 OpenCV 中提供的几个 API。
CvCapture* cvCaptureFromCAM(int device)
:
此函数捕获指定设备的数据并返回一个 
cvCapture
 类型指针,捕获失败时返回 
NULL

cvCreateFileCapture(char * filepath)
:
从本地视频读入。
CvVideoWriter * cvCreateVideoWriter(char * filepath,
, fps, size, is_color)
: 新建一个视频写入对象,返回其指针,
filepath
指定写入视频的路径,
fps
指定写入视频的帧速,
size
指定写入视频的像素大小,
is_color
仅在windows下有效,指定写入是否为彩色。
double cvGetCaptureProperty(CvCapture* capture,
int property_id)
: 获取一个视频流的某个特性,
property_id
指定要获取的特性名称。
IplImage* cvQueryFrame(CvCapture* capture)
:
从视频流获取帧。
void cvCvtColor(const CvArr* src, CvArr* dst,
int code)
: 按
code
指定的模式将
src
指向的帧转换后写入
dst
指向的地址。
void calcHist(const Mat* images, int nimages,
const int* channels, InputArray mask, SparseMat& hist, int dims, const int* histSize, const float** ranges, bool uniform, bool accumulate)
: 为
image
指向的帧计算直方图,保存在 
hist
 中。
void normalize(const SparseMat& src, SparseMat&
dst, double alpha, int normType)
: 按指定模式将
src
正常化并写入
dst
中。
double compareHist(const SparseMat& H1, const
SparseMat& H2, int method)
:按
method
指定的方式比较两个直方图的相似程度。
int cvWriteFrame(CvVideoWriter* writer, const
IplImage* image)
: 向视频写入流写入一帧。成功返回 1,否则返回 0。

了解这些API的基本功能后,梳理程序执行的步骤:
程序开始执行,启动摄像头并获得数据流
进入循环:
捕获一帧
是否为第一帧?是则记录该帧作为监控区域的背景
将该帧做适当的变换,输出到监视器中
分析该帧和背景帧的相似程度
相似程度是否低于阈值且当前没有在记录视频?低于阈值开始记录。
相似程度是否低于阈值且当前已经开始记录视频?低于阈值继续记录,否则停止记录。

循环中程序的停止,通过接受外部中断相应。


3.代码实现 
processCamera

启动摄像头并获得数据流,调用上面提到的
cvCaptureFromCAM
函数,默认摄像头的 
device
 为
0。
void processCamera() {
CvCapture *capture = cvCaptureFromCAM(0);
if (!capture){
cerr << "Unable to open camera " << endl;
exit(EXIT_FAILURE);
}
// TODO...
}    // end processCamera


进入循环,循环条件中使用到一个 
keyboard
变量用于接收外部中断,如果 
Esc
 或者 
q
键被按下则退出循环。
keyboard
通过
OpenCV 提供的
waitKey()
函数获得外部按键情况。在下面的循环中,每次先检查视频输入流
capture
是否为空,防止访问违例内存。在
capture
不为
NULL
的情况下,从
capture
读取一帧。在退出
while循环后,要通过
cvReleaseCapture
释放此前申请的
capture

void processCamera() {
// ...
Mat frame;                    // current frame
while ((char)keyboard != 'q' && (char)keyboard != 27){
if (!capture) {
cerr << "Unable to read camera" << endl;
cerr << "Exiting..." << endl;
exit(EXIT_FAILURE);
}

frame = cvQueryFrame(capture);

// TODO...

keyboard = waitKey(30);
}    // end While
cvReleaseCapture(&capture);
}    // end processCamera


判断当前帧是否为第一帧,通过一个bool型变量
backGroundFlag
来标识。若
backGroundFlag
true
表示当前帧为第一帧,则记录该帧并将
backGroundFlag
置为
False
。此时代码如下。在当前帧为第一帧的情况下,我们不需要记录该帧的真实数据,只需要记录该帧对应的直方图,这里首先将RGB类型的图像转为HSV格式,之后计算该帧的直方图,保存在
base
中。
void processCamera() {
// ...
bool backGroundFlag = true;
Mat frame;                    // current frame
Mat HSV;                    // HSV format
MatND base;                    // histogram
while ((char)keyboard != 'q' && (char)keyboard != 27){
if (!capture) {
cerr << "Unable to read camera" << endl;
cerr << "Exiting..." << endl;
exit(EXIT_FAILURE);
}

frame = cvQueryFrame(capture);

// set background
if (backGroundFlag){
cvtColor(frame, HSV, CV_BGR2HSV);
calcHist(&HSV, 1, channels, Mat(), base, 2, histSize, ranges, true, false);
normalize(base, base, 0, 1, NORM_MINMAX, -1, Mat());
backGroundFlag = false;
}

// TODO...

keyboard = waitKey(30);
}    // end While
cvReleaseCapture(&capture);
}    // end processCamera


对当前帧做适当变换并输出到监视器。此时代码如下。我们要实现的程序可以对原始图像做两种背景减除处理,因此需要用户指定使用哪种方式,这里通过参数传给
processCamera
,method
为 0 代表使用 MOG2 方式减除, method为 1代表使用 MOG1 方式减除, method为0 代表不作任何变换。两种方式均可以突出背景外的变化情况,实际效果将在最终程序执行时展示。在对原始 
frame
 做处理并写入 
fgMask
 后,通过
imshow
函数输出到监视器中。
imshow
函数的第一个参数为输出的窗口名,这里先假设已经有一个名为
Monitor
的窗口等待接收输出,这个窗口将在最终的
main
函数中创建。用户应当可以指定这个监控程序是否将处理后的图像输出,因此我们传入一个
showWindow
 参数表明是否显示实时监控窗口。对每一帧的处理方式和上面对背景帧的处理方式相同。
void processCamera(bool showWindow,
unsigned int method) {
// ...
bool backGroundFlag = true;
Mat frame;                    // current frame
Mat HSV;                    // HSV format
MatND base;                    // histogram
while ((char)keyboard != 'q' && (char)keyboard != 27){
if (!capture) {
cerr << "Unable to read camera" << endl;
cerr << "Exiting..." << endl;
exit(EXIT_FAILURE);
}

frame = cvQueryFrame(capture);

if (method == 0)
pMOG2->operator()(frame, fgMask);
else if (method == 1)
pMOG->operator()(frame, fgMask);
else if (method == 2)
fgMask = frame;

// set background
if (backGroundFlag){
cvtColor(frame, HSV, CV_BGR2HSV);
calcHist(&HSV, 1, channels, Mat(), base, 2, histSize, ranges, true, false);
normalize(base, base, 0, 1, NORM_MINMAX, -1, Mat());
backGroundFlag = false;
}

cvtColor(frame, HSV, CV_BGR2HSV);
calcHist(&HSV, 1, channels, Mat(), cur, 2, histSize, ranges, true, false);
normalize(cur, cur, 0, 1, NORM_MINMAX, -1, Mat());

// TODO ...

if (showWindow && !fgMask.empty()){
imshow("Monitor", fgMask);
}
keyboard = waitKey(30);
} // end While
cvReleaseCapture(&capture);
}    // end processCamera


比较当前帧和背景帧的相似度,当出现异常时开始记录视频。这里直接调用
compareHist
 函数,输出一个
0 - 1 范围内的指标,越接近1 表示两个直方图代表的图像越相似。这里我设置的阈值为 0.65,这个阈值应当根据实际监控区域的光线、色彩等因素修正。我们创建了一个
recorder
指针用于写入视频,需要指定写入视频的帧速和大小,这里大小通过
cvGetCaptureProperty
 自动获取,帧速
fps
由用户传入参数指定。在这里为了避免监控过于敏感的情况出现,设置了一个
UnnormalFrames
参数,该参数记录当前已经持续出现了多少帧与背景不同的画面,也就是运动状态出现了多久。当 
UnnormalFrames
 达到用户指定的阈值 
unnormal
时,我们认为监控中确实出现了异常,因此开始记录。为了更完整的提供监控信息,一旦确认监控中有运动状态发生,在运动结束后,也就是检测到当前帧和背景重新一致后,程序将继续记录视频信息,继续记录的时长和之前运动状态持续的时长相同。代码中通过
recordFlag
 标识当前是否应该记录视频,在 
UnnormalFrames
> unnormal
时,
recordFlag
被置位,同时
UnnormalFrames
随着运动帧被检测持续增加,当运动结束后,
UnnormalFrames
将递减,至
0 时停止记录视频。这里
unnormal
通过参数传入
processCamera
。至此,
processCamera
函数编写完成。
void processCamera(bool showWindow,
unsigned int method,
unsigned int unnormal = 10,
unsigned int fps = 24) {
CvCapture *capture = cvCaptureFromCAM(0);
if (!capture){
cerr << "Unable to open camera " << endl;
exit(EXIT_FAILURE);
}

bool backGroundFlag = true, recordFlag = false;
Mat frame, fgMask;                    // current frame, fg mask
Mat HSV;                            // HSV format
MatND base, cur;                    // histogram
unsigned int UnnormalFrames = 0;
int channels[] = { 0, 1 };

CvSize size = cvSize(
(int)cvGetCaptureProperty(capture, CV_CAP_PROP_FRAME_WIDTH),
(int)cvGetCaptureProperty(capture, CV_CAP_PROP_FRAME_HEIGHT)
);

CvVideoWriter * recorder = cvCreateVideoWriter(recordName, CV_FOURCC('D', 'I', 'V', 'X'), 32, size, 1);

// ESC or 'q' for quitting
while ((char)keyboard != 'q' && (char)keyboard != 27){
if (!capture) {
cerr << "Unable to read camera" << endl;
cerr << "Exiting..." << endl;
exit(EXIT_FAILURE);
}

frame = cvQueryFrame(capture);
if (method == 0)
pMOG2->operator()(frame, fgMask);
else if (method == 1)
pMOG->operator()(frame, fgMask);
else if (method == 2)
fgMask = frame;

// set background
if (backGroundFlag){
cvtColor(frame, HSV, CV_BGR2HSV);
calcHist(&HSV, 1, channels, Mat(), base, 2, histSize, ranges, true, false);
normalize(base, base, 0, 1, NORM_MINMAX, -1, Mat());
backGroundFlag = false;
}

cvtColor(frame, HSV, CV_BGR2HSV);
calcHist(&HSV, 1, channels, Mat(), cur, 2, histSize, ranges, true, false);
normalize(cur, cur, 0, 1, NORM_MINMAX, -1, Mat());

double comp = compareHist(base, cur, 0);
if (comp < 0.65)
UnnormalFrames += 1;
else if (UnnormalFrames > 0)
UnnormalFrames--;
if (UnnormalFrames > unnormal)
recordFlag = true;
else if (UnnormalFrames <= 0){
UnnormalFrames = 0;
recordFlag = false;
}
// DO SOMETHING WARNING
// Here We Starting Recoding
if (recordFlag){
cvWriteFrame(recorder, &(IplImage(frame)));
}

if (showWindow && !fgMask.empty()){
imshow("Monitor", fgMask);
}
keyboard = waitKey(30);
}    // end While
cvReleaseVideoWriter(&recorder);
cvReleaseCapture(&capture);
}    // end processCamera



4.定义外部或全局变量

processCamera
函数中使用到了一些函数中没有声明过的变量,这些变量有的是配置使用的常量如
ranges
等,不需要理解,下面在头文件中声明。
pMOG
pMOG2
对应了对frame做变换的两个方式,他们将在
main
函数中被定义。
keyboard
 用于接收外部键盘输入。其余的均为常量,用于配置
OpenCV 提供的函数。
// ...
extern Ptr<BackgroundSubtractor> pMOG; //MOG Background subtractor
extern Ptr<BackgroundSubtractor> pMOG2; //MOG2 Background subtractor
extern int keyboard;

const float h_ranges[] = { 0, 256 };
const float s_ranges[] = { 0, 180 };
const float* ranges[] = { h_ranges, s_ranges };

const int h_bins = 50, s_bins = 60;
const int histSize[] = { h_bins, s_bins };

extern char recordName[128];

// ...


5.编写 
main.cpp

下面编写程序入口。
首先需要告知用户程序的使用方式,编写
help
函数输出帮助信息。
-vis
选项用于指定程序显示实时监控,
[MODE]
参数指定使用何种方式显示监控,
[FPS]
指定帧速,
[THRESHOLD]
指定经过多少异常帧后开始记录,
[OUTPUTFILE]
指定输出视频记录位置。
void help(){
cout
<< "----------------------------------------------------------------------------\n"
<< "Usage:                                                                      \n"
<< " ./MonitorRecorder.exe [VIS] [MODE] [FPS] [THRESHOLD] [OUTPUTFILE]          \n"
<< "   [VIS]  : use -vis to show the monitor window, or it will run background. \n"
<< "   [MODE] : -src   shows the original frame;                                \n"
<< "            -mog1       shows the MOG frame;                                \n"
<< "            -mog2      shows the MOG2 frame.                                \n"
<< "   [FPS]  : set the fps of record file, default is 24.                      \n"
<< "   [THRESHOLD]                                                              \n"
<< "          : set the number x that the monitor will start recording after    \n"
<< "            x unnormal frames passed.                                       \n"
<< "   [OUTPUTFILE]                                                             \n"
<< "          : assign the output recording file. It must be .avi format.       \n"
<< "                                                   designed by Forec        \n";
<< "----------------------------------------------------------------------------\n";
}

编写
main
函数。在
main
函数中我们需要用到外部声明的 
pMOG
pMOG2
以及
recordName
。这里需要在函数外声明。主函数中主要部分为解析用户的命令行参数,其中
stoi
函数需要使用
C++ 11 标准编译。我们使用
sleep(2)
processCamera
延时
2 秒执行,这个时间你可以离开电脑,让程序捕获你的背景,之后可以回到电脑前,观察监控程序的显示和记录情况。如果用户指定了 
-vis
 参数,则产生一个
Monitor
窗口显示实时监控,这个窗口就是此前
processCamera
函数中输出图像的窗口。在
main
函数最后,用
destroyAllWindows
销毁
namedWindow
产生的窗口。
#include "monitor.hpp"

Ptr<BackgroundSubtractor> pMOG; //MOG Background subtractor
Ptr<BackgroundSubtractor> pMOG2; //MOG2 Background subtractor
int keyboard;

char recordName[128];

void help();

int main(int argc, char* argv[]){
bool showWindow = false;
unsigned int method = 0, unnormal = 10, fps = 24;
if (argc > 6){
cerr << "Invalid Parameters, Exiting..." << endl;
exit(EXIT_FAILURE);
}
if (argc >= 2){
if (strcmp(argv[1], "-vis") == 0)
showWindow = true;
if (strcmp(argv[1], "-h") == 0 ||
strcmp(argv[1], "--help") == 0){
help();
exit(EXIT_SUCCESS);
}
}
if (argc >= 3){
if (strcmp(argv[2], "-mog2") == 0)
method = 0;
if (strcmp(argv[2], "-mog1") == 0)
method = 1;
if (strcmp(argv[2], "-src") == 0)
method = 2;
}
if (argc >= 4){
int param = stoi(argv[3], nullptr, 10);
if (param <= 10)
fps = 24;
else
fps = param;
}
if (argc >= 5){
int param = stoi(argv[4], nullptr, 10);
if (param <= 0)
unnormal = 10;
else
unnormal = param;
}
if (argc >= 6){
strcpy(recordName, argv[5]);
}
else{
// set record video file name
time_t t = time(NULL);
sprintf(recordName, "%d.avi", int(t));
}

cout << "Starts After 2s..." << endl;
sleep(2);
if (showWindow)
namedWindow("Monitor");

pMOG = new BackgroundSubtractorMOG(); //MOG approach
pMOG2 = new BackgroundSubtractorMOG2(); //MOG2 approach

processCamera(showWindow, method, unnormal, fps);

destroyAllWindows();
return EXIT_SUCCESS;
}


6.编译运行

因为实验楼的环境不提供摄像头,因此我们将程序捕获摄像头的部分修改为程序从一个本地的监控视频中读取,模拟读取摄像头的情况。这需要修改
monitor.hpp
中的第
38 行,将
CvCapture *capture = cvCaptureFromCAM(0);
 改为 
CvCapture
*capture = cvCreateFileCapture("test.mp4");
,假设将要读入的本地视频文件名为
test.mp4
。在修改完你的代码后,你可以通过以下命令下载
test.mp4
(该视频文件是周杰伦《浪漫手机》的MV),并检验代码。
$ wget http://labfile.oss.aliyuncs.com/courses/671/test.mp4


请确认你已经修改了代码,将读取摄像头改为读取
test.mp4
,并且将
test.mp4
已经拷贝到代码目录下。在代码目录下输入如下命令编译。请检查自己是否已经按照教程开始的环境配置方案配置成功。编译可能产生两个warning。
g++ -ggdb `pkg-config --cflags opencv` -std=c++11 -fpermissive -o `basename main` main.cpp `pkg-config --libs opencv`


编译成功后目录下将产生一个名为 
main
的可执行文件,在终端键入如下命令,将使程序输出MOG1处理后的实时监控画面,且画面连续异常10帧后开始记录视频,视频记录到当前文件夹下的
out.avi
中,帧速
24。
sudo vim /etc/ld.so.conf.d/opencv.conf
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
sudo ldconfig -v
./main -vis -mog1 24 10 out.avi


程序按上述命令的运行截图如下。



以下几张分别是程序在不同显示模式下的显示情况,你可以通过切换
-mog1
-mog2
-src
来自己观察对应的效果。












五、代码获取

你可以在我的 Github仓库 中获取到完整的代码。里面提供了Windows 版本和 Linux版本的配置、运行方案。如果你有建议或想法,欢迎提 PR 沟通。


六、参考资料

OpenCV在
Ubuntu下链接库配置
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息