参考:
https://github.com/spmallick/learnopencv
使用OpenCV进行对象跟踪(C ++ / Python)
在本教程中,我们将学习OpenCV 3.0中引入的OpenCV跟踪API。 我们将学习如何以及何时使用OpenCV 3.2中的6种不同的跟踪器 - BOOSTING,MIL,KCF,TLD,MEDIANFLOW和GOTURN。 我们也将学习现代跟踪算法背后的一般理论。
这个问题已经被我的朋友Boris Babenko完美的解决了,就像下面这个无瑕的实时脸部跟踪器一样。 除了笑话之外,动画演示了我们从理想物体追踪器所需要的东西 - 速度,准确性和对遮挡的稳健性。
OpenCV对象跟踪器 Demo
如果您没有时间阅读整篇文章,请观看本视频并了解本部分的用法。 但如果你真的想了解对象跟踪,请继续阅读。
什么是对象跟踪?
简单地说,在视频的连续帧中定位对象称为跟踪。
这个定义听起来很直接,但是在计算机视觉和机器学习中,跟踪是一个非常广泛的术语,涵盖概念上相似但技术上不同的想法。例如,以下所有不同但相关的想法通常在对象跟踪下进行研究
1、Dense Optical flow: 这些算法帮助估计视频帧中每个像素的运动矢量。
2、Sparse optical flow: 这些算法,如Kanade-Lucas-Tomashi(KLT)特征跟踪器,跟踪图像中几个特征点的位置。
3、卡尔曼滤波(Kalman Filtering):一种非常受欢迎的信号处理算法,用于根据先前的运动信息来预测移动物体的位置。这种算法的早期应用之一是导弹制导!同样如这里提到的,“引导阿波罗11号登月舱下降到月球的机载计算机具有卡尔曼滤波器”。
4、Meanshift和Camshift:这是用于定位密度函数的最大值的算法。他们也用于跟踪。
5、单个对象跟踪器:在这类跟踪器中,第一个框架使用矩形标记,以指示我们要跟踪的对象的位置。然后使用跟踪算法在随后的帧中跟踪对象。在大多数实际应用中,这些跟踪器与物体检测器一起使用。
6、多目标跟踪查找算法:当我们有一个快速的目标检测器的情况下,检测每个帧中的多个对象,然后运行一个跟踪算法,确定一个帧中的哪个矩形对应于下一个帧中的矩形是有意义的。
跟踪与检测
如果你曾经玩过OpenCV人脸检测,你知道它是实时的,你可以很容易地检测到每一帧的脸。那么,为什么你需要首先跟踪?让我们来探索一下您可能想要跟踪视频中的对象的不同原因,而不是重复检测。
1、跟踪比检测快:通常跟踪算法比检测算法快。原因很简单。当您跟踪在前一帧中检测到的对象时,您会对该对象的外观有所了解。您还知道前一帧的位置以及其运动的方向和速度。因此,在下一帧中,可以使用所有这些信息来预测下一帧中对象的位置,并在对象的预期位置周围进行小型搜索,以精确定位对象。一个好的跟踪算法将使用所有关于该对象的信息,而检测算法总是从头开始。因此,在设计高效的系统时,通常在每n帧运行一次目标检测,而在其间的n-1帧中采用跟踪算法。为什么我们不简单地检测第一帧中的对象,然后跟踪呢?确实,追踪它所拥有的额外信息带来的好处,但是当它们长时间滞后于障碍物时,或者它们移动太快以致跟踪算法无法跟上时,也可能会丢失对象。跟踪算法累积误差也是常见的,跟踪对象的边界框慢慢地偏离正在跟踪的对象。为了用跟踪算法解决这些问题,每隔一段时间就要运行一次检测算法。检测算法是通过对象的大量例子进行训练的。因此,他们对这个客体的一般阶层有更多的了解。另一方面,跟踪算法更多地了解他们所跟踪的类的具体实例。
2、当检测失败时,跟踪可以提供帮助:如果您正在视频上运行人脸检测器,并且人脸被对象遮挡,则人脸检测器很可能会失败。一个好的跟踪算法,另一方面,将处理一定程度的遮挡。在下面的视频中,您可以看到MIL跟踪器的作者Boris Babenko博士演示MIL跟踪器如何在遮挡下工作。
3、跟踪保留标识:对象检测的输出是包含对象的矩形数组。 但是,没有标识附加到对象。 例如,在下面的视频中,检测红点的检测器将输出对应于在帧中检测到的所有点的矩形。 在下一帧中,它将输出另一个矩形数组。 在第一帧中,一个特定的点可能由数组中的位置10处的矩形表示,并且在第二帧中可能位于17处。在帧上使用检测时,我们不知道哪个矩形对应于哪个对象。 另一方面,跟踪提供了一种方法来从字面上连接点!
OpenCV 3跟踪API
OpenCV 3带有一个新的跟踪API,其中包含许多单一对象跟踪算法的实现。 OpenCV 3.2有6种不同的跟踪器,分别是BOOSTING,MIL,KCF,TLD,MEDIANFLOW和GOTURN。
注意:OpenCV 3.1已经实现了这5个跟踪器 - BOOSTING,MIL,KCF,TLD,MEDIANFLOW。 OpenCV 3.0具有以下4个跟踪器的实现 - BOOSTING,MIL,TLD,MEDIANFLOW。
更新:在OpenCV 3.3中,跟踪API已经改变。代码检查版本,然后使用相应的API。
在我们提供算法的简要描述之前,让我们看看设置和用法。在下面的评论代码中,我们首先通过选择跟踪器类型 - BOOSTING,MIL,KCF,TLD,MEDIANFLOW或GOTURN来设置跟踪器。然后,我们打开一个视频,并抓住一个框架。我们定义一个包含第一帧的对象的边界框,并用第一帧和边界框初始化跟踪器。最后,我们从视频中读取帧,然后只更新循环中的跟踪器,以获得当前帧的新边界框。结果随后显示。
C++
#include <opencv2/opencv.hpp>
#include <opencv2/tracking.hpp>
#include <opencv2/core/ocl.hpp>
using namespace cv;
using namespace std;
// Convert to string
#define SSTR( x ) static_cast< std::ostringstream & >( \
( std::ostringstream() << std::dec << x ) ).str()
int main(int argc, char **argv)
{
// List of tracker types in OpenCV 3.2
// NOTE : GOTURN implementation is buggy and does not work.
string trackerTypes[6] = {"BOOSTING", "MIL", "KCF", "TLD","MEDIANFLOW", "GOTURN"};
// vector <string> trackerTypes(types, std::end(types));
// Create a tracker
string trackerType = trackerTypes[2];
Ptr<Tracker> tracker;
#if (CV_MINOR_VERSION < 3)
{
tracker = Tracker::create(trackerType);
}
#else
{
if (trackerType == "BOOSTING")
tracker = TrackerBoosting::create();
if (trackerType == "MIL")
tracker = TrackerMIL::create();
if (trackerType == "KCF")
tracker = TrackerKCF::create();
if (trackerType == "TLD")
tracker = TrackerTLD::create();
if (trackerType == "MEDIANFLOW")
tracker = TrackerMedianFlow::create();
if (trackerType == "GOTURN")
tracker = TrackerGOTURN::create();
}
#endif
// Read video
VideoCapture video("videos/chaplin.mp4");
// Exit if video is not opened
if(!video.isOpened())
{
cout << "Could not read video file" << endl;
return 1;
}
// Read first frame
Mat frame;
bool ok = video.read(frame);
// Define initial boundibg box
Rect2d bbox(287, 23, 86, 320);
// Uncomment the line below to select a different bounding box
bbox = selectROI(frame, false);
// Display bounding box.
rectangle(frame, bbox, Scalar( 255, 0, 0 ), 2, 1 );
imshow("Tracking", frame);
tracker->init(frame, bbox);
while(video.read(frame))
{
// Start timer
double timer = (double)getTickCount();
// Update the tracking result
bool ok = tracker->update(frame, bbox);
// Calculate Frames per second (FPS)
float fps = getTickFrequency() / ((double)getTickCount() - timer);
if (ok)
{
// Tracking success : Draw the tracked object
rectangle(frame, bbox, Scalar( 255, 0, 0 ), 2, 1 );
}
else
{
// Tracking failure detected.
putText(frame, "Tracking failure detected", Point(100,80), FONT_HERSHEY_SIMPLEX, 0.75, Scalar(0,0,255),2);
}
// Display tracker type on frame
putText(frame, trackerType + " Tracker", Point(100,20), FONT_HERSHEY_SIMPLEX, 0.75, Scalar(50,170,50),2);
// Display FPS on frame
putText(frame, "FPS : " + SSTR(int(fps)), Point(100,50), FONT_HERSHEY_SIMPLEX, 0.75, Scalar(50,170,50), 2);
// Display frame.
imshow("Tracking", frame);
// Exit if ESC pressed.
int k = waitKey(1);
if(k == 27)
{
break;
}
}
}
Python
import cv2
import sys
(major_ver, minor_ver, subminor_ver) = (cv2.__version__).split('.')
if __name__ == '__main__' :
# Set up tracker.
# Instead of MIL, you can also use
tracker_types = ['BOOSTING', 'MIL','KCF', 'TLD', 'MEDIANFLOW', 'GOTURN']
tracker_type = tracker_types[2]
if int(minor_ver) < 3:
tracker = cv2.Tracker_create(tracker_type)
else:
if tracker_type == 'BOOSTING':
tracker = cv2.TrackerBoosting_create()
if tracker_type == 'MIL':
tracker = cv2.TrackerMIL_create()
if tracker_type == 'KCF':
tracker = cv2.TrackerKCF_create()
if tracker_type == 'TLD':
tracker = cv2.TrackerTLD_create()
if tracker_type == 'MEDIANFLOW':
tracker = cv2.TrackerMedianFlow_create()
if tracker_type == 'GOTURN':
tracker = cv2.TrackerGOTURN_create()
# Read video
video = cv2.VideoCapture("videos/chaplin.mp4")
# Exit if video not opened.
if not video.isOpened():
print "Could not open video"
sys.exit()
# Read first frame.
ok, frame = video.read()
if not ok:
print 'Cannot read video file'
sys.exit()
# Define an initial bounding box
bbox = (287, 23, 86, 320)
# Uncomment the line below to select a different bounding box
bbox = cv2.selectROI(frame, False)
# Initialize tracker with first frame and bounding box
ok = tracker.init(frame, bbox)
while True:
# Read a new frame
ok, frame = video.read()
if not ok:
break
# Start timer
timer = cv2.getTickCount()
# Update tracker
ok, bbox = tracker.update(frame)
# Calculate Frames per second (FPS)
fps = cv2.getTickFrequency() / (cv2.getTickCount() - timer);
# Draw bounding box
if ok:
# Tracking success
p1 = (int(bbox[0]), int(bbox[1]))
p2 = (int(bbox[0] + bbox[2]), int(bbox[1] + bbox[3]))
cv2.rectangle(frame, p1, p2, (255,0,0), 2, 1)
else :
# Tracking failure
cv2.putText(frame, "Tracking failure detected", (100,80), cv2.FONT_HERSHEY_SIMPLEX, 0.75,(0,0,255),2)
# Display tracker type on frame
cv2.putText(frame, tracker_type + " Tracker", (100,20), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (50,170,50),2);
# Display FPS on frame
cv2.putText(frame, "FPS : " + str(int(fps)), (100,50), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (50,170,50), 2);
# Display result
cv2.imshow("Tracking", frame)
# Exit if ESC pressed
k = cv2.waitKey(1) & 0xff
if k == 27 : break
目标跟踪算法
在本节中,我们将深入一些不同的跟踪算法。我们的目标不是要对每一个追踪者有一个深刻的理论认识,而是从实际的角度来理解他们。
首先让我先解释跟踪的一些基本原则。在跟踪中,我们的目标是在当前帧中找到一个对象,因为我们已经在所有(或几乎所有)前一帧中成功地跟踪了对象。
由于我们已经把目标追踪到当前帧,所以我们知道它是如何移动的。换句话说,我们知道运动模型的参数。运动模型只是一种奇特的说法,即您知道前一帧中对象的位置和速度(速度+运动方向)。如果对对象没有任何其他的了解,则可以根据当前的运动模型预测新的位置,并且将会非常接近对象的新位置。
但是,我们有更多的信息,只是对象的运动。我们知道这个对象在每个之前的帧中是如何看起来的。换句话说,我们可以建立一个外观模型来编码对象的外观。这个外观模型可以用来搜索运动模型预测的位置的小邻域,以更准确地预测对象的位置。
运动模型预测对象的大概位置。 外观模型很好地调整了这个估计,以提供基于外观的更准确的估计。
如果对象非常简单并且没有多少变化,我们可以使用一个简单的模板作为外观模型,然后查找该模板。但是,现实生活并不那么简单。对象的外观可能会发生巨大的变化。为了解决这个问题,在许多现代跟踪器中,这个外观模型是一个以在线方式训练的分类器。不要惊慌!让我简单地解释一下。
分类器的工作是将图像的矩形区域分类为对象或背景。分类器将图像补丁作为输入并返回0到1之间的分数,以指示图像补丁包含对象的概率。当绝对确定图像补丁是背景时,得分是0,当确定补丁是对象时,得分是1。
在机器学习中,我们使用“在线”这个词来指代在运行时即时训练的算法。离线分类器可能需要数千个示例来训练分类器,但是在线分类器通常在运行时使用极少数示例进行训练。
分类器通过喂食正面(对象)和负面(背景)的例子来训练。如果你想建立一个检测猫的分类器,你需要训练数以千计的包含猫和不包含猫的图像的图像。这样分类器就学会区分什么是猫,什么不是。你可以在这里了解更多关于图像分类。在建立一个在线分类器的同时,我们也没有成千上万的正面和负面的例子。
让我们来看看不同的跟踪算法如何处理这个在线培训的问题。
BOOSTING跟踪器
该跟踪器基于AdaBoost的在线版本 - 基于HAAR级联的人脸检测器在内部使用的算法。这个分类器需要在运行时训练对象的正面和负面的例子。以用户提供的初始边界框(或其他物体检测算法)作为对象的正面例子,边界框外的许多图像块作为背景。给定一个新的帧,分类器运行在前一个位置附近的每个像素上,记录分类器的分数。对象的新位置是得分最高的位置。所以现在我们有更多的分类器的正面例子。随着更多帧的进入,分类器将更新为附加数据。
优点:无。这个算法已经有十年的历史,并且工作正常,但是我没有找到一个很好的理由来使用它,尤其是当其他基于类似原理的高级跟踪器(MIL,KCF)可用时。
缺点:跟踪性能不佳。跟踪失败时,它不能可靠地知道。
MIL Tracker
这个跟踪器在思想上和上面提到的BOOSTING跟踪器类似。最大的区别在于,它不仅仅考虑对象的当前位置作为一个正面的例子,而是在当前位置周围的一个小的邻域中生成几个潜在的正面例子。你可能会认为这是一个坏主意,因为在这些“积极”的例子中,这个对象并不是以集中为中心的。
这是多重实例学习(MIL)来救援的地方。在MIL中,您没有指定正面和负面的例子,而是正面和负面的“袋子”。积极的袋子里的图像收集并不都是正面的例子。相反,只有正面袋子中的一张图片才是正面的例子!在我们的例子中,一个正面的包包含了以对象的当前位置为中心的补丁,并且在它周围的一个小的邻域中也有补丁。即使被追踪对象的当前位置不准确,当来自当前位置附近的样本被放入正面袋子时,这个袋子也很可能包含至少一个图像,其中该对象被很好地居中。 MIL项目页面有更多的信息给那些喜欢深入研究MIL跟踪器的内部工作的人。
优点:性能相当不错。它不像BOOSTING跟踪器漂移那么多,并且在部分遮挡下做了合理的工作。如果你使用的是OpenCV 3.0,这可能是最适合你的跟踪器。但是,如果您使用的是更高版本,请考虑KCF。
缺点:跟踪失败不能可靠地报告。不完全闭塞恢复。
KCF Tracker
KFC代表核化相关滤波器。 这个跟踪器建立在前两个跟踪器中提出的想法上。 该跟踪器利用MIL跟踪器中使用的多个正样本具有大的重叠区域的事实。 这种重叠的数据导致一些很好的数学属性,这个跟踪器利用它来使跟踪更快和更准确的同时。
优点:精确度和速度都比MIL好,报告追踪失败比BOOSTING和MIL更好。 如果您使用的是OpenCV 3.1或更高版本,我建议在大多数应用程序中使用此功能。
缺点:不能从完全闭塞中恢复。 没有在OpenCV 3.0中实现。
Bug警告:OpenCV 3.1(仅限Python)中存在一个错误,因为它返回了不正确的边界框。 查看错误报告。 感谢Andrei Cheremskoy指出这一点。
TLD Tracker
TLD代表跟踪,学习和检测。顾名思义,该跟踪器将长期跟踪任务分解为三个部分 - (短期)跟踪,学习和检测。从作者的论文中,“追踪者逐帧跟踪对象。探测器定位到目前为止所观察到的所有外观,并在必要时纠正跟踪器。学习估计检测器的错误,并更新它以避免将来出现这些错误。“这个跟踪器的输出往往会跳一点点。例如,如果您正在追踪行人,并且现场还有其他行人,则此追踪器有时可能会暂时追踪与您要追踪的行人不同的行人。从积极的方面来看,这个轨迹似乎是在一个更大的尺度,运动和遮挡下跟踪一个物体。如果您有一个视频序列,其中的对象被隐藏在另一个对象的后面,这个跟踪器可能是一个不错的选择。
优点:在多帧遮挡下最佳。此外,轨道最好的规模变化。
缺点:大量的误报使其几乎无法使用。
MEDIANFLOW Tracker
在内部,该跟踪器在时间上向前和向后跟踪对象,并测量这两个轨迹之间的差异。 最小化ForwardBackward错误使他们能够可靠地检测到跟踪失败并在视频序列中选择可靠的轨迹。
在我的测试中,我发现当运动是可预测和小的时候,这个跟踪器效果最好。 不像其他跟踪器即使在跟踪显然失败时也能继续运行,跟踪器知道跟踪失败的时间。
优点:出色的跟踪失败报告。 当运动是可预测的并且没有遮挡时,工作得很好。
缺点:大动作失败
GOTURN tracker
在跟踪器类中的所有跟踪算法中,这是唯一基于卷积神经网络(CNN)的算法。 它也是唯一一个使用离线训练的模型,因为其他跟踪器的速度更快。 从OpenCV文档中,我们知道它对于视点变化,灯光变化和变形是强健的。 但它不能很好地处理遮挡。
Bug警告:不幸的是,在写这篇文章的时候,OpenCV 3.2中有一个错误,当GOTURN被使用时,程序崩溃了。