一、说明
实验介绍
本次实验将使用利用 OpenCV 来实现对视频中动态物体的追踪。
实验涉及的知识点
- C++ 语言基础
- g++ 的使用
- 图像基础
- OpenCV 在图像及视频中的应用
- Meanshift 和 Camshift 算法
本次实验要实现的效果是追踪太阳系中运动的行星(图中选择了浅黄颜色轨道上的木星,可以看到追踪的目标被红色的椭圆圈住):
二、环境搭建
进行本节的实验时,您需要先完成 C++实现太阳系行星运行系统 的相关实验,才能进行下面的相关学习。
创建视频文件
在实验楼环境中暂时还不支持连接用户计算机的摄像头进行实时取景,我们首先需要创建实验前的视频文件。
首先安装屏幕录制工具:
sudo apt-get update && sudo apt-get install gtk-recordmydesktop
安装完成后,我们能够在应用程序菜单中找到:
运行之前完成的太阳系行星系统程序 ./solarsystem
并使用 RecordMyDesktop
录制程序运行画面(10~30s 即可),并保存到 ~/Code/camshift
路径下,命名为 out
。时间足够后停止录制便能得到一个.ogv
结尾的视频文件out.ogv
:
图像基础
OpenCV 是一个开源的跨平台计算机视觉库,与 OpenGL 所提供的图像绘制不同,OpenCV 实现了图像处理和计算机视觉方面的很多通用的算法。在学习 OpenCV 之前,我们需要先了解一下图像、视频在计算机中的一些基本概念。
首先,我们要理解图片在计算机中的表示方式:图像在显示生活中以连续变化的形式而存在,但在计算机中,有两种常见的存储图片的方式:一种是矢量图,一种则是像素图。
矢量图,也称为面向对象的图像或绘图图像,在数学上定义为一系列由线连接的点。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。
而更常见的则是像素图,比如常说的一张图片的尺寸为1024*768,这就意味着这张图片水平方向上有1024个像素,垂直方向上有768个像素。
像素就是表示像素图的基本单位,通常情况下一个像素由三原色(红绿蓝)混合而成。由于计算机的本质是对数字的识别,一般情况下我们把一种原色按亮度的不同从 0~255 进行表示,换句话说,对于原色红来说,0表示最暗,呈现黑色,255表示最亮,呈现纯红色。
这样,一个像素就可以表示为一个三元组(B,G,R),比如白色可以表示为(255,255,255),黑色则为(0,0,0),这时我们也称这幅图像是 RGB 颜色空间中的一副图像,R、G、B 分别成为这幅图像的三个通道,除了 RGB 颜色空间外,还有很多其他的颜色空间,如 HSV、YCrCb 等等。
像素是表示像素图的基本单位,而图像则是表示视频的基本单位。一个视频由一系列图像组成,在视频中我们称其中的图像为帧。而通常我们所说的视频帧率,意味着这个视频每秒钟包含多少帧图像。比如帧率为 25,那么这个视频每秒钟就会播放25帧图像。
1秒钟共有1000毫秒,因此如果帧率为 rate 那么每一帧图像之间的时间间隔为 1000/rate。
图像颜色直方图
颜色直方图是描述图像的一种工具,它和普通的直方图类似,只是颜色直方图需是根据某一幅图片计算而来。
如果一副图片是 RGB 颜色空间,那么我们可以统计 R 通道中,颜色值为 0~255 这 256中颜色出现的次数,这边能获得一个长度为 256 的数组(颜色概率查找表),我们再将所有的值同时除以这幅图像中像素的总数,将这之后所得的数列转化为直方图,其结果就是颜色直方图。
直方图反向投影
人们已经证明,在 RGB 颜色空间中,对光照亮度变化较为敏感,为了减少此变化对跟踪效果的影响,就需要对直方图进行反向投影。这一共分为三个步骤:
首先将图像从RGB空间转换到HSV空间。 然后对其中的 H 颜色通道的直方图。 将图像中每个像素的值用颜色概率查找表中对应的概率进行替换,就得到了颜色概率分布图。 这个过程就叫反向投影,颜色概率分布图是一个灰度图像。
OpenCV 初步使用
首先安装 OpenCV:
sudo apt-get install libopencv-dev
我们已经熟悉了 C++的基本语法,几乎在写过的每一个程序中都有使用到 #include <iostream>
和 using namespace std;
或者 std::cout
。OpenCV
也有它自己的命名空间。
使用 OpenCV,只需要包含这一个头文件:
#include <opencv2/opencv.hpp>
我们可以使用
using namespace cv;
来启用 OpenCV 的名称空间,也可以在 OpenCV 提供的 API 前使用 cv::
来使用它提供的接口。
由于我们是初次接触 OpenCV,对 OpenCV 所提供的接口还不熟悉,所以我们推荐先使用 cv::
前缀来进行接下来的学习。
我们先写一个程序来读取之前录制的视频。
将这个文件和 video.ogv 同样至于 ~/Code/camshift/
下,使用 g++ 编译 main.cpp
:
g++ main.cpp `pkg-config opencv --libs --cflags opencv` -o main
运行,可以看到成功播放视频:
./main
你可能注意到运行后,命令行会提示:
libdc1394 error: Failed to initialize libdc1394
这是 OpenCV 的一个 Bug,虽然它并不影像我们程序的运行,但是如果你有强迫症,那么下面这条命令可以解决这个问题:
sudo ln /dev/null /dev/raw1394
三、Meanshift 和 Camshift 算法
Meanshift
Meanshift 和 Camshift 算法是进行目标追踪的两个经典算法,Camshift 是基于 Meanshift 的,他们的数学解释都很复杂,但想法却非常的简单。所以我们略去那些数学事实,先介绍 Meanshift 算法。
假设屏幕上有一堆红色的点集,这时候蓝色的圆圈(窗口)必须要移动到点最密集的地方(或者点数最多的地方):
如图所示,把蓝色的圆圈标记为 C1,蓝色的矩形为圆心标记为 C1_o。但这个圆的质心却为 C1_r,标记为蓝色的实心圆。
当C1_o和C1_r不重合时,将圆 C1移动到圆心位于 C1_r的位置,如此反复。最终会停留在密度最高的圆 C2 上。
而处理图像来说,我们通常使用图像的反向投影直方图。当要追踪的目标移动时,显然这个移动过程可以被反向投影直方图反应。所以 Meanshift 算法最终会将我们选定的窗口移动到运动目标的位置(收敛,算法结束)。
Camshift
经过之前的描述,我们可以看到 Meanshift 算法总是对一个固定的窗口大小进行追踪,这是不符合我们的需求的,因为在一个视频中,目标物体并不一定是很定大小的。
所以 Camshift 就是为了改进这个问题而产生的。这一点从 Camshift 的全称(Continuously Adaptive Meanshift)也能够看出来。
它的基本想法是:首先应用 Meanshift 算法,一旦 Meanshift 的结果收敛后,Camshift 会更新窗口的大小,并计算一个带方向的椭圆来匹配这个窗口,然后将这个椭圆作为新的窗口应用 Meanshift 算法,如此迭代,便实现了 Camshift。
OpenCV 提供了 Camshift 算法的通用接口:
RotatedRect CamShift(InputArray probImage, Rect& window, TermCriteria criteria) 其中第一个参数 probImage 为目标直方图的反向投影,第二个参数 window 为执行 Camshift 算法的搜索窗口,第三个参数为算法结束的条件。
分析
理解了 Camshift 算法的基本思想之后我们就可分析实现这个代码主要分为几个步骤了:
设置选择追踪目标的鼠标回调事件; 从视频流中读取图像; 实现 Camshift 过程; 下面我们继续修改 main.cpp 中的代码:
第一步:选择追踪目标区域的鼠标回调函数
第二步:从视频流中读取图像
我们已经在之前实现了读取视频流的基本结构,下面我们进一步细化:
提示: ROI(Region of Interest),在图像处理中,被处理的图像以方框、圆、椭圆、不规则多边形等方式勾勒出需要处理的区域,称为感兴趣区域,ROI。
第三步:实现 Camshift 过程
计算追踪目标的反向投影直方图为需要先使用 cvtColor 函数,这个函数可以将 RGB 颜色空间的原始图像转换到 HSV 颜色空间。计算直方图必须在选择初始目标之后,因此:
五、结果
重新编译 main.cpp:
g++ main.cpp `pkg-config opencv --libs --cflags opencv` -o main
运行:
./main
在视频中用鼠标拖拽选择一个要追踪的物体,即可看到追踪的效果:
图中选择了浅黄颜色轨道上的木星,可以看到追踪时的窗口呈现红色的椭圆状。