OpenCV实战(29)——视频对象追踪
- 0. 前言
- 1. 追踪视频中的对象
- 2. 中值流追踪器算法原理
- 3. 完整代码
- 小结
- 系列链接
0. 前言
我们已经学习了如何跟踪图像序列中点和像素的运动。但在多数应用中,通常要求追踪视频中的特定移动对象。首先确定感兴趣的对象,然后必须在视频序列中对其进行追踪。由于随着它在场景中的演变,视点和光照变化、非刚性运动、遮挡等,对象在视觉上会发生诸多变化,这为追踪视频中的对象带来了挑战。
本节中,我们将介绍一些在 OpenCV
库中实现的对象跟踪算法。我们将实现一个通用框架,以方便的对算法进行替换。我们可以将此实现与基于积分图像计算的直方图进行对象追踪的方法进行对比。
1. 追踪视频中的对象
对象追踪问题通常假设没有关于要追踪的对象的先验知识可用。因此,追踪是通过识别帧中的对象来启动的,并且追踪必须从对象识别开始。对象的初始识别是通过指定一个边界框来实现的,追踪器模块的目标是在后续帧中重新识别该对象。
因此,定义对象追踪框架的 OpenCV
的 cv::Tracker
类有两个主要方法。第一个是用于定义初始目标边界框的 init
方法;第二个是 update
方法,它在给定新帧的情况下输出新的边界框。这两种方法都接受帧(即 cv::Mat
实例)和边界框(即 cv::Rect2D
实例)作为参数;在第一种方法中,边界框是输入参数,而对于第二种方法,边界框则是输出参数。
(1) 为了测试所提出的对象追踪算法,我们使用视频序列处理一节中介绍的视频处理框架。我们定义了一个帧处理子类,当接收到图像序列的每一帧时,VideoProcessor
类将调用该子类,该子类具有以下属性:
class VisualTracker : public FrameProcessor {
cv::Ptr<cv::Tracker> tracker;
cv::Rect2d box;
bool reset;
public:
// 指定要使用的追踪器的构造函数
VisualTracker(cv::Ptr<cv::Tracker> tracker) : reset(true), tracker(tracker) {}
(2) 每当通过指定新目标的边界框重新启动追踪器时,reset
属性就会设置为 true
;它是用于存储新对象位置的 setBoundingBox
方法:
// 初始化追踪器,设定边界框
void setBoundingBox(const cv::Rect2d &bb) {
box = bb;
reset = true;
}
(3) 用于处理每一帧的回调方法简单地调用追踪器的相应方法,然后在要显示的帧上绘制新的计算边界框:
// 回调处理方法
void process(cv::Mat &frame, cv::Mat &output) {
if (reset) {
reset = false;
tracker->init(frame, box);
} else {
tracker->update(frame, box);
}
// 绘制边界框
frame.copyTo(output);
cv::rectangle(output, box, cv::Scalar(255, 255, 255), 2);
}
(4) 为了演示如何使用 VideoProcessor
和 FrameProcessor
实例追踪对象,我们使用在 OpenCV
中定义的中值流追踪器:
int main() {
// 创建视频处理实例
VideoProcessor processor;
// 生成文件名
std::vector<std::string> imgs;
std::string prefix = "test1/test_";
std::string ext = ".png";
// 添加用于追踪的图像名称
for (long i=3040; i<3076; i++) {
std::string name(prefix);
std::ostringstream ss; ss << std::setfill('0') << std::setw(3) << i; name += ss.str();
name += ext;
std::cout << name << std::endl;
imgs.push_back(name);
}
// 创建特征追踪器实例
cv::Ptr<cv::TrackerMedianFlow> ptr = cv::TrackerMedianFlow::create();
VisualTracker tracker(ptr);
// VisualTracker tracker(cv::TrackerKCF::createTracker());
// 打开文件
processor.setInput(imgs);
// 设置帧处理器
processor.setFrameProcessor(&tracker);
processor.displayOutput("Tracked object");
// 定义帧率
processor.setDelay(50);
// 指定原始对象位置
cv::Rect bb(1150, 150, 330, 330);
tracker.setBoundingBox(bb);
// 开始追踪
processor.run();
}
(5) 第一个边界框在测试图像序列中识别出一只狼先生,然后在后续帧中自动追踪:
(6) 但随着视频的播放,跟踪器将不可避免地出错。这些小误差的累积会导致追踪器从真实目标位置缓慢偏移:
(7) 最终,追踪器将失去对对象的追踪。追踪器长时间追踪对象的能力是表征对象追踪器性能的最重要标准。
2. 中值流追踪器算法原理
上一小节中,我们学习了如何使用通用 cv::Tracker
类来跟踪图像序列中的对象。我们选择使用中值流追踪器算法来执行追踪,这是一种简单但有效的追踪纹理对象的方法,只要对象的运动不是太快,也没有被太严重被遮挡。
中值流追踪器基于特征点追踪。首先在要追踪的对象上定义一个点网格,我们也可以选择使用检测兴趣点中介绍的 FAST
运算符来检测对象上的兴趣点。但是,使用预定义位置的点具有许多优点:
- 通过避免计算兴趣点来节省时间
- 保证有足够数量的点可用于跟踪
- 确保这些点很好地分布在整个对象上
中值流实现默认使用具有 10x10
点的网格:
使用追踪视频中的特征点一节中介绍的 Lukas-Kanade
特征追踪算法,在下一帧中追踪网格的每个点:
中值流算法估计追踪这些点时产生的误差。例如,可以通过计算在点的初始位置和追踪位置周围的窗口中的绝对像素差之和来估计误差,使用 cv::calcOpticalFlowPyrLK
函数可以方便地计算和返回误差。中值流算法可以使用的另一种误差度量是前向-后向误差,在当前帧和下一帧之间追踪这些点后,这些点在新位置被反向追踪以检查它们是否会返回到它们在初始图像中的原始位置,新获得的前后位置与初始位置之间的差异是追踪误差。
一旦计算了每个点的追踪误差,只需考虑 50%
的具有最小误差的点,然后将这组点用于计算下一个图像中边界框的新位置。这些点会投票选出一个位移值,并保留这些位移量的中值。对于尺度的变化,成对考虑这些点,估计初始帧中的两点与下一帧的距离的比值。同样,最终应用的是这些比值的中位数。
中值流追踪器是基于特征点追踪的视觉对象追踪器,另一类解决方案是基于模板匹配的方法,可以参考匹配局部模板方法一节,核相关滤波器 (Kernelized Correlation Filter
, KCF
) 算法是经典的基于模板匹配的算法,在 OpenCV
中可以使用 cv::TrackerKCF
类实现:
VisualTracker tracker(cv::TrackerKCF::createTracker());
本质上,该算法使用目标对象的边界框作为模板在另一图像中搜索新的对象位置。通常可以通过使用简单的相关性进行计算,但是 KCF
使用了一种基于傅立叶变换的方法,在图像滤波一节中,我们了解到,根据信号处理理论,在图像上关联模板等价于频域中的简单图像乘法。这可以极大的提高在另一图像中匹配窗口的识别速度,因此 KCF
是性能最好的追踪器之一。
3. 完整代码
头文件 (videoprocessor.h
) 完整代码参考视频序列处理一节,头文件 (visualTracker.h
) 完整代码如下所示:
#if !defined FTRACKER
#define FTRACKER
#include <string>
#include <vector>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/tracking/tracker.hpp>
#include "videoprocessor.h"
class VisualTracker : public FrameProcessor {
cv::Ptr<cv::Tracker> tracker;
cv::Rect2d box;
bool reset;
public:
// 指定要使用的追踪器的构造函数
VisualTracker(cv::Ptr<cv::Tracker> tracker) : reset(true), tracker(tracker) {}
// 初始化追踪器,设定边界框
void setBoundingBox(const cv::Rect2d &bb) {
box = bb;
reset = true;
}
// 回调处理方法
void process(cv::Mat &frame, cv::Mat &output) {
if (reset) {
reset = false;
tracker->init(frame, box);
} else {
tracker->update(frame, box);
}
// 绘制边界框
frame.copyTo(output);
cv::rectangle(output, box, cv::Scalar(255, 255, 255), 2);
}
};
#endif
主函数文件 (oTracker.cpp
) 完整代码如下所示:
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/video/tracking.hpp>
#include "visualTracker.h"
int main() {
// 创建视频处理实例
VideoProcessor processor;
// 生成文件名
std::vector<std::string> imgs;
std::string prefix = "test1/test_";
std::string ext = ".png";
// 添加用于追踪的图像名称
for (long i=3040; i<3076; i++) {
std::string name(prefix);
std::ostringstream ss; ss << std::setfill('0') << std::setw(3) << i; name += ss.str();
name += ext;
std::cout << name << std::endl;
imgs.push_back(name);
}
// 创建特征追踪器实例
cv::Ptr<cv::TrackerMedianFlow> ptr = cv::TrackerMedianFlow::create();
VisualTracker tracker(ptr);
// VisualTracker tracker(cv::TrackerKCF::createTracker());
// 打开文件
processor.setInput(imgs);
// 设置帧处理器
processor.setFrameProcessor(&tracker);
processor.displayOutput("Tracked object");
// 定义帧率
processor.setDelay(50);
// 指定原始对象位置
cv::Rect bb(1150, 150, 330, 330);
tracker.setBoundingBox(bb);
// 开始追踪
processor.run();
cv::waitKey();
// 中值追踪器
cv::Mat image1 = cv::imread("test1/test_3050.png", cv::ImreadModes::IMREAD_GRAYSCALE);
// 定义网格点
std::vector<cv::Point2f> grid;
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
cv::Point2f p(bb.x+i*bb.width/10.,bb.y+j*bb.height/10);
grid.push_back(p);
}
}
// 追踪下一帧
cv::Mat image2 = cv::imread("test1/test_3051.png", cv::ImreadModes::IMREAD_GRAYSCALE);
std::vector<cv::Point2f> newPoints;
std::vector<uchar> status; // 追踪特征点的状态
std::vector<float> err; // 追踪误差
// 追踪点
cv::calcOpticalFlowPyrLK(image1, image2, // 2 张连续图像
grid, // 第一幅图像中的输入点位置
newPoints, // 第二图像中的输出点位置
status, // 追踪状态
err); // 追踪误差
// 绘制点
for (cv::Point2f p : grid) {
cv::circle(image1, p, 1, cv::Scalar(255, 255, 255), -1);
}
cv::imshow("Initial points", image1);
for (cv::Point2f p : newPoints) {
cv::circle(image2, p, 1, cv::Scalar(255, 255, 255), -1);
}
cv::imshow("Tracked points", image2);
cv::waitKey();
return 0;
}
小结
视频对象追踪是在视频中随着时间的推移定位移动对象的过程,在智能安防等领域有着重要用途,本节介绍一些在 OpenCV
库中实现的对象跟踪算法,并实现了一个通用的对象追踪框架。
系列链接
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测
OpenCV实战(18)——特征匹配
OpenCV实战(19)——特征描述符
OpenCV实战(20)——图像投影关系
OpenCV实战(21)——基于随机样本一致匹配图像
OpenCV实战(22)——单应性及其应用
OpenCV实战(23)——相机标定
OpenCV实战(24)——相机姿态估计
OpenCV实战(25)——3D场景重建
OpenCV实战(26)——视频序列处理
OpenCV实战(27)——追踪视频中的特征点
OpenCV实战(28)——光流估计