OpenCV进阶之路:一个简化的视频摘要程序
原文出处: Ronny 的博客(@RonnyYoung)
一、前言
视频摘要又称视频浓缩,是对视频内容的一个简单概括,先通过运动目标分析,提取运动目标,然后对各个目标的运动轨迹进行分析,将不同的目标拼接到一个共同的背景场景中,并将它们以某种方式进行组合。视频摘要在视频分析和基于内容的视频检索中扮演着重要角色。
视频摘要主要运用在对长时间的监控视频的压缩上,它可以将不同时刻场景内目标的运动显示在同一时刻,这样大量减少了整个场景事件的时间跨度。一般的视频摘要的步骤可以总结为:
视频读取→背景建模 → 前景提取→ 目标轨迹跟踪→ 目标的时序与空间规划 → 生成浓缩视频
但是本文并不讨论上面的这些主题,这里我只想通过一个简单的去除视频里非运动帧来实现一个简单的视频压缩的功能。视频摘要不是本文的主题,文章想通过做一个简单的视频摘要程序对OpenCV下面几个功能进行介绍:
1)OpenCV与XML数据通信
2)视频的读取与写入
3)如何在没有OpenCV的环境中运行编译好的程序
二、与XML的交互
很多程序都需要有一个配置文件,可以手动的去调整一些运行中的参数,xml文件格式就是我们常用到的一种配置文件格式。opencv中提供了一个处理xml的类用来与xml文件进行简单的数据存储与读取通信。但这个类的功能有限,如果需要更多的功能可以利用第三方的库,比如libxml等。
我们所设计的视频摘要程序,跟常规的视频摘要不同,这里只是通过删除一些无运动目标的帧来达到视频压缩的目的,所以我们的算法可以设计如下:
1,定义一个目标运动的兴趣区域,作为检测区域。 2,遍历指定目录下的所有视频文件,并逐一的进行视频处理。 3,针对视频的每一帧,在检测区域内运用帧差法检测前景移动。 4,如果检测区域内前景的面积超过区域面积的10%,则说明有运动物体,则此帧进行保留,写入压缩视频。否则,该帧直接舍弃。 5,所有视频处理结束,则程序终止。
么,我们需要一个配置文件,这个文件里需要保存下面几个内容:
1,检测区域的参数
2,视频文件的目录
3,视频文件的后缀格式
4,生存视频的保存目录
<?xml version="1.0"?> <opencv_storage> <roi> 3 460 1250 480</roi> <videoReadPath>D:\ExtractKeyImages\video\</videoReadPath> <videoSuffix>*.mp4</videoSuffix> <videoSavePath>../result.avi</videoSavePath> </opencv_storage>
注意所有的节点都保存在opencv_storage节点下。
在OpenCV中定义了一个叫FileStorage的类,提供了一些简单的打开与读取xml文件内容的操作。
我们先来看xml文件数据的读取:
1,用FileStorage的构造函数可以打开一个xml或yml文件,也可以用FileStorage::open()来打开一个数据文件。
FileStorage::FileStorage(); // 默认构造函数 FileStorage::FileStorage(const string& source, int flags, const string& encoding = string());
上面第二个构造函数中有三个参数。
第一个参数source指定读取文件的路径。
第二个参数flag指定操作的模式,可以设置为READ说明以只读的方式打开一个文件,或者设置为WRITE,这种情况下,如果文件不存在,则创建一个文件,如果文件已经存在,则会清空当前文件里的内容。还可以设置为APPEND用来打开一个存在的文件,并且可以在原来基础上写入。
第三个参数用来指定文件的编码格式,一般都为UTF-8。
而open成员函数的接口与第二个构造函数接口一致。
bool FileStorage::open(const string& filename, int flags, const string& encoding=string())
2,读取文件内的数据,FileStorage重载的操作符[],用来获得指定的节点内容。
FileNode FileStorage::operator[](const string& nodename) const FileNode FileNode::operator[](const string& nodename) const
上面两个操作符都返回FileNode类型,它是一个子节点类型。
比如:我们想读取<book>结点下的<name>结点,则可以:
FileStorage fs("../config.xml", FileStorage::READ); string book_name; fs["book"] ["name"]>> book_name;
如果要取出A节点下的B结点下的C结点则为fs["A"]["B"]["C"]>>content;要记住所有节点都是在根结点opencv_storage下的,但是访问时忽略它。
而如果需要将数据写入,则简单的写入可以直接用<<运算符,比如增加一个节点为book,内容为theOpenCV:
string book_name=”theOpenCV”; fs<<”book”<<book_name;
最后给出我们程序中读取配置参数的代码,我们需要4项配置项,上面已经介绍过了:
FileStorage fs("../config.xml", FileStorage::WRITE); string videoPath; string videoSuffix; Rect roiRect; string imgSavePath; fs["videoReadPath"] >> videoPath; fs["videoSuffix"] >> videoSuffix; fs["imgSavePath"] >> imgSavePath; fs["roi"] >> roiRect;
二、检测区域的运动检测
这里我们要进行简单的视频压缩就是想把完全静止不动的视频帧从原视频里删除,我们的兴趣目标一般是在移动的视频里。所以我们可以用帧差法来检测移动物体,它的原理是利用视频中物体的移动将引起相邻视频帧内容的不同,从而显示出移动的前景。
两帧之间的帧差图像可以这样定义:
其中imgCur代表当前帧的图像,imgPre代表前一帧图像
在得到帧差图像后,我们并不能得到很明显的判断条件,所以我们需要对帧差图像进行二值化,我们设置一个阈值T
然后我们只需遍历图像求出图像中所有白点的个数,即是运动前景的面积,计算一下面积比例即可以确定当前帧是否有物体移动。
当然我们得到的前景目标并不移动的物体的轮廓,而是与前一帧相比目标移动的部分。
下面为这一部分的OpenCV实现,相关的视频读取和写入的操作可以参考OpenCV成长之路中的相关文章。
// 查找文件目录下的所有视频文件 vector<string> videoPathStr = FindAllFile((videoPath + videoSuffix).c_str(), true); // 先读取一个视频文件,用于获取相关的参数 VideoCapture capture(videoPathStr[0]); // 视频大小 Size videoSize(capture.get(CV_CAP_PROP_FRAME_WIDTH), capture.get(CV_CAP_PROP_FRAME_HEIGHT)); // 创建一个视频写入对象 VideoWriter writer("../result.avi", CV_FOURCC('M', 'J', 'P', 'G'), 25.0, videoSize); for (auto videoName : videoPathStr) { capture.open(videoName); // 读入路径下的视频 Mat preFrame; bool stop(false); double totleFrameNum = capture.get(CV_CAP_PROP_FRAME_COUNT); // 获取视频总帧数 for (int frameNum = 0; frameNum < totleFrameNum; frameNum++) { Mat imgSrc; capture >> imgSrc; // 读一视频的一帧 if (!imgSrc.data) break; Mat frame; cvtColor(imgSrc, frame, CV_BGR2GRAY); ++frameNum; if (frameNum == 1) { preFrame = frame; } Mat frameDif; absdiff(frame, preFrame, frameDif); // 帧差法 preFrame = frame; threshold(frameDif, frameDif, 30, 255, THRESH_BINARY); // 二值化 Mat imgRoi = frameDif(roiRect); double matArea = computeMatArea(imgRoi); // 计算区域面积 if (matArea / (imgRoi.rows*imgRoi.cols) > 0.1) // 面积比例大于10% { writer << frameDif;// 写入视频 } } } capture.release(); writer.release();
三、在没有OpenCV的环境下运行程序
这里是指基于windows系统下VS平台的程序,很多时候我们编译好的程序需要在别人的电脑上运行,而别人电脑上是没有OpenCV的基本库的,而我们的编译的opencv程序一般是动态链接一些dll的。
有两种方法:一种是拷贝用到的dll到release目录下,另一种是把相关的源文件加入工程中一起编译。
下面主要介绍第一种方法,因为看起来简单,很多人还是运行不了。
我们从openCV的环境配置开始说起:
首先,我们先找到我们下载并解压后的OpenCV目录下的这几个目录:
头文件目录:F:\EvProjects\OpenCV\OpenCV248\build\include
运行库目录:F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\lib
上面的vc12指定你的vs的版本,这里是vs2013
然后我们在我们新建的工程中找到属性管理器:
然后分别在DeBug和Release下配置属性表:
我们可以新建一个名字为opencv248_debug.props的属性表,以后新建的工程,直接拷贝添加即可。
然后右键配置opencv248_debug.props的属性,在VC目录下配置两项:
一项是包含目录,加入:F:\EvProjects\OpenCV\OpenCV248\build\include
第二项是在库目录下加入:F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\lib
最后我们需要在链接器->输入->附加依赖项中加入一些常用到的库文件
opencv_core248d.lib opencv_imgproc248d.lib opencv_highgui248d.lib opencv_ml248d.lib opencv_video248d.lib opencv_features2d248d.lib opencv_calib3d248d.lib opencv_objdetect248d.lib opencv_contrib248d.lib opencv_legacy248d.lib opencv_flann248d.lib
注意上面的248说明了我的opencv版本,你的可能是246或247。
也可以把F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\lib目录里的lib文件都加入,注意只加入带d的表示debug库。
这样的话debug下就配置完了,我们按相同方法,在release下配置一个属性表opencv248_release.props,与debug不同的是,在链接器的配置里加入的库名,都是不包含d的。
OK,属性表都配置好后,我们把当前的编译环境改为Release:
在解决方案里,右键项目名->属性->配置管理器
然后把活动解决方案配置改为release即可。
所有的环境配置好后,只需要编译好程序,然后在release下找到exe文件,这个就是我们的可执行文件,但是它不能单独运行,我们需要把它需要依赖的一些dll拷贝过来,dll在opencv的F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\bin目录下,如果你不确定你的程序里需要哪些库,你就把全部都拷贝过来。或者可以用一个依赖库查看软件查看你的程序所依赖的库,把对应的dll拷贝过来即可。
另外值得注意,如果是VS的较高版本,如VS2012,VS2013你还安装对应的运行库。