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下链接库配置
相关文章推荐
- 基于OpenCV的人脸检测——C++和Python实现
- 【opencv】基于opencv实现运动目标检测之帧差法
- 基于VS C++平台的OpenCV设置,实现简单的行人检测
- 基于DCT系数的实时监控中运动目标检测
- 基于C++和OpenCv的SIFT_图像局部特征检测算法代码的实现
- 运动物体目标检测实现—基于OpenCV
- 基于DCT系数的实时监控中运动目标检测
- 我的OpenCV学习笔记(3):基于混合高斯模型GMM的运动目标检测
- LLC(Locality-constrained Linear Coding)基于OpenCV的C++源码实现
- 基于Silverlight + WCF设计实现汽车实时数据监控
- OpenCV_基于混合高斯模型GMM的运动目标检测
- 基于opencv人脸检测原理及实现
- OpenCV实现运动目标检测的函数
- 直方图均衡化的 C++ 实现(基于 openCV)
- OpenCV实现静止背景下运动目标的检测
- OpenCV_基于混合高斯模型GMM的运动目标检测
- opencv基于轮廓寻找的视频流运动检测
- 基于OpenCV的AVI视频文件读取及运动检测
- 基于Opencv的目标检测与跟踪阴影去除算法实现
- (学习笔记二)——基于opencv人脸检测原理及实现