使用OpenCV进行目标跟踪(C++/Python)

译自:https://www.learnopencv.com/object-tracking-using-opencv-cpp-python/,有删改。

本教程中,我们将学习OpenCV 3中新引入的一些目标跟踪API,包括BOOSTING, MIL, KCF, TLD, MEDIANFLOW和GOTURN,此外还将介绍现代跟踪算法中的一般理论。

什么是目标跟踪?

简单来说,在视频的连续帧中定位目标即为目标跟踪。

该定义听起来很直接,在计算机视觉和机器学习中,跟踪是一个非常宽泛的概念,涵盖各种概念上相似但技术实现上不同的理论。 例如,以下理论均会用于目标跟踪的研究:

  1. 密集光流:用于估计视频帧中每个像素的运动矢量。
  2. 稀疏光流:如Kanade-Lucas-Tomashi(KLT)特征跟踪器,用于跟踪图像中几个特征点的位置。
  3. 卡尔曼滤波:一种非常受欢迎的信号处理算法,用于根据先前的运动信息预测移动物体的位置,该算法的早期应用之一是导弹制导,指导阿波罗11号登月舱降落到月球的机载计算机也搭载了卡尔曼滤波器。
  4. Meanshift和Camshift:用于定位密度函数的最大值,通常也用于跟踪。
  5. 单目标跟踪器:在这类跟踪器中,使用矩形标记我们要跟踪的对象在帧中的位置,然后使用跟踪算法在随后的帧中跟踪对象。在大多数实际应用中,跟踪器与检测器是一起使用的。
  6. 多目标追踪算法:使用快速目标检测器检测每个帧中的多个物体,然后使用跟踪算法识别一个帧中的哪个矩形与下一个帧中的矩形相对应。

目标跟踪与目标检测

如果你玩过OpenCV人脸检测,就会发现它可以实时轻松检测到人脸。 我们来探讨一下,为什么我们需要跟踪而不是重复不断地进行目标检测。

1、跟踪比检测快:通常跟踪算法比检测算法快。原因很简单,在跟踪前一帧中检测到的对象时,会知晓很多关于对象外观的信息,并且还知道前一帧中的位置以及其运动的方向和速度。因此,在下一帧中,可以使用所有这些信息预测下一帧中对象的位置,并在对象的预期位置周围进行小范围搜索以精确定位对象。一个好的跟踪算法能够使用跟踪目标的所有历史信息,而检测算法对每一帧总是需要从头开始。因此,在设计高效检测系统时,通常在每n帧运行一次目标检测,而跟踪算法应用于其余n-1帧。为什么不简单地检测第一帧中的对象,随后进行跟踪呢?跟踪算法确实得益于其拥有的额外信息,但如果被跟踪物体长时间受到障碍物遮挡,或者物体移动太快以至跟踪算法无法跟上时,就可能会丢失跟踪对象的信息。跟踪算法的累积误差也会使被跟踪物体的边界框逐渐偏离正在跟踪的物体。为了解决跟踪算法中这些问题,就需要每隔一段时间运行一次检测算法。检测算法使用大量对象实例进行训练,因此,他们对该对象一般情况拥有更多的了解,而跟踪算法通常只了解跟踪对象的特定实例。

2、当检测失败时,跟踪可以提供帮助:如果运行人脸检测器时人脸受到障碍物遮挡,那么人脸检测器很可能会失效, 而一个好的跟踪算法,能够处理一定程度的物体遮挡。在下面的视频中(YouTube视频自行脑补),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发生变化,因此代码将检查OpenCV版本,然后使用相应的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。

在机器学习中,我们使用“在线”一词来指代在运行时即时训练的算法。离线分类器可能需要数千个示例来训练分类器,但在线分类器通常在运行时使用少数示例进行训练即可。

分类器通过正面(目标)和负面(背景)样本进行训练,例如如果你想建立一个检测猫的分类器,你可以使用成千上万个不包含猫和包含猫的图像进行训练。 这样分类器就会学会区分什么是猫,什么不是。 你可以在此了解更多图像分类的知识(https://www.learnopencv.com/image-recognition-and-object-detection-part1/)。构建在线分类器则无需拥有如此大量的正负样本。

让我们看看不同的跟踪算法是如何解决这个在线学习这个问题的。

BOOSTING

此跟踪器基于AdaBoost的在线版本 – 基于HAAR级联的人脸检测器内部使用的算法。这个分类器在运行时使用对象的正负样本进行训练。用户提供的初始边界框(或其他物体检测算法)作为该对象的正样本,边界框外的图像块被视为背景。给定一个新的帧,分类器运行在前一个位置附近的每个像素上,并记录分类器返回的分数,目标的新位置是得分最高的位置。然后我们就有了更多分类器的正样本。随着处理的帧数增加,分类器将使用这些额外数据进行更新。

优点:无。这个算法已有十几年历史,并且可以正常工作,但我无法找到一个很好的理由使用它,尤其是当其他基于类似原理的高级跟踪器(MIL,KCF)可用时。

缺点:跟踪性能不佳,不能可靠地知道跟踪失败的时间。

MIL

该跟踪器在思想上与BOOSTING跟踪器类似,最大的区别在于,它不仅仅将物体的当前位置作为正样本,而且在当前位置周围的一个小的邻域中生成几个潜在的正样本。你可能会认为这不是个好想法,因为大多数这些“正”样本中,对象并不是集中的。

而这正是在线多实例学习(MIL)优秀的地方。MIL算法里提出样本袋的概念,所有的正样本(当前跟踪位置的小邻域范围里选取)加入到一个样本袋里,正样本袋具有不确定性(并非都是正样本,仅有一个图像是正样本,含有虚检),每一个负样本单独作为一个样本袋,负样本袋没有不确定性。一个正样本袋包含以该对象的当前位置为中心的图像和其小领域内的图像块。即使被跟踪对象的当前位置不准确,那么样本袋中来自当前位置附近的样本也很可能包含至少一个图像,其中含有被跟踪目标。 MIL项目页面(http://vision.ucsd.edu/~bbabenko/new/project_miltrack.shtml)为希望更深入地了解MIL工作原理的人提供了更多信息,希望了解更多的可以自行查看。

优点:性能非常好,不像BOOSTING漂移严重,并且在部分遮挡情况下运行稳定。如果你使用OpenCV 3.0,这可能是你可以使用的最佳跟踪器,但是如果你使用的是更高版本,请考虑KCF。

缺点:跟踪失败不能可靠地报告,不能从完全遮挡中恢复。

KCF

KCF代表核化相关滤波(Kernelized Correlation Filters)。该跟踪器基于前两个跟踪器中提出的想法,利用MIL跟踪器中使用的多个正样本具有大量重叠区域的事实,从这些重叠的数据中可以导出一些很好的数学属性,KCF利用它来使跟踪更快,更精确。

优点:准确性和速度都比MIL好,报告跟踪失败也比BOOSTING和MIL更好。如果你使用的是OpenCV 3.1或更高版本,我建议在大多数应用中使用它。

缺点:不能从完全遮挡中恢复,没有在OpenCV 3.0中实现。

Bug:OpenCV 3.1(仅限Python)中存在一个bug,因为它返回了不正确的边界框。 查看错误报告(https://github.com/opencv/opencv_contrib/issues/640)。 感谢Andrei Cheremskoy指出了这一点。

TLD

TLD代表跟踪,学习和检测(Tracking, learning and detection)。顾名思义,该跟踪器将跟踪任务分解为三个部分 – 跟踪,学习和检测。作者在论文中写道,“跟踪器逐帧跟踪对象;检测器本地化了迄今为止观察到的所有外观,并在必要时纠正跟踪器;学习器估计检测器误差并更新它以避免将来出现这些错误。“此跟踪器的输出往往会发生跳跃,例如,如果你正在追踪行人,并且现场还有其他行人,则此追踪器有时可能会暂时追踪其他行人。从正面方面来看,这种算法对大规模的、运动的、遮挡的情况会有很好的适用。如果被跟踪物体发生遮挡,则TLD可能是不错的选择。

优点:发生遮挡效果最佳。

缺点:大量的报错使其几乎无法使用。

MEDIANFLOW

该跟踪器在时间上向前和向后跟踪对象,并测量这两个轨迹之间的差异。 最小化正向/反向错误率(ForwardBackward error)使其能够可靠地检测跟踪失败并在视频序列中选择可靠的轨迹。

在我的测试中,我发现这个跟踪器在运动可预测和规模较小时效果最好。与其他跟踪器不同,即使跟踪明显失败,该跟踪器也能准确报告跟踪失败的时间。

优点:优秀的跟踪失败报告,当运动是可预测的并且没有遮挡时效果很好。

缺点:运动幅度巨大时跟踪失败。

GOTURN

在上述所有跟踪算法中,这是唯一基于卷积神经网络(CNN)的算法。 它也是唯一一个使用离线训练的模型,所以它也比其他跟踪器速度更快。 从OpenCV文档中,我们知道它对视点变化,灯光变化和目标形变很健壮,但它不能很好地处理遮挡。