课程使用的操作系统为 Ubuntu 14.04
,OpenCV 版本为OpenCV 2.4.13.1
,你可以在这里查看该版本 OpenCV 的文档。官方文档中有两个例子可以帮助你理解此课程,分别是
你可以在我的 Github仓库 上找到 Windows 系统对应的 Visual Studio 工程。全部代码文件也可以在我的仓库中找到。
这里提供了完整的代码 http://labfile.oss.aliyuncs.com/courses/671/monitor-recorder.zip 。
本课程项目完成过程中将学习:
本实验需要先在实验平台安装 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
工程文件由一个头文件 monitor.hpp
和一个入口文件 main.cpp
构成。首先在头文件中定义将使用的库和相关变量。
代码中使用到的 OpenCV 头文件和 C++ 头文件在头文件 monitor.hpp
中声明如下,其中 unistd.h
包含了 Linux 下的 sleep
函数,参数为睡眠的秒数。
#ifndef __MONITOR_H_
#define __MONITOR_H_
//opencv
#include
#include
#include
#include
#include
//C++
#include
#include
#include
#include
#include
#include
using namespace cv;
using namespace std;
// ...
#endif __MONITOR_H_
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的基本功能后,梳理程序执行的步骤:
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
在processCamera
函数中使用到了一些函数中没有声明过的变量,这些变量有的是配置使用的常量如ranges
等,不需要理解,下面在头文件中声明。pMOG
和pMOG2
对应了对frame做变换的两个方式,他们将在main
函数中被定义。keyboard
用于接收外部键盘输入。其余的均为常量,用于配置 OpenCV 提供的函数。
// ...
extern Ptr pMOG; //MOG Background subtractor
extern Ptr 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];
// ...
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 pMOG; //MOG Background subtractor
Ptr 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;
}
因为实验楼的环境不提供摄像头,因此我们将程序捕获摄像头的部分修改为程序从一个本地的监控视频中读取,模拟读取摄像头的情况。这需要修改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 沟通。