3、目标跟踪基础:卡尔曼滤波原理、匈牙利匹配算法、SORT跟踪器详解

各位同学,今天我们来啃一块硬骨头——目标跟踪。说实话,我刚入行那会儿,觉得跟踪不就是把检测框连起来嘛,有什么难的?直到我在一个雨夜场景里,看着目标框满天飞,才明白这里面水有多深。

目标跟踪的核心,说白了就是回答两个问题:“目标下一帧会在哪?” 和 “这些检测框到底谁是谁?”。前者靠卡尔曼滤波,后者靠匈牙利匹配。而SORT,就是把这两者拧在一起的螺丝刀。

3.1 卡尔曼滤波:预测与更新

卡尔曼滤波,我更喜欢叫它“最优状态估计器”。它不关心目标长什么样,只关心目标怎么动。你想想看,一个匀速运动的小球,上一秒在位置A,下一秒大概率在A附近。卡尔曼就是利用这种“运动惯性”来做预测。

3.1.1 核心思想

卡尔曼滤波有两个阶段:预测和更新。

  • 预测阶段:根据上一帧的状态,猜当前帧目标在哪。比如上一帧速度是5m/s,那这一帧位置大概往前挪了5米。
  • 更新阶段:用当前帧的观测(比如检测框)来修正这个猜测。如果检测框和预测框重合度高,我就更相信检测;如果检测被遮挡了,我就更相信预测。

我在项目中遇到过一个问题:当目标突然急刹车时,预测会严重偏离。后来我加了加速度项,把模型从匀速改成匀加速,效果好了不少。嗯,这里要注意,模型越复杂不一定越好,过拟合在跟踪里同样存在。

3.1.2 数学表达(简化版)

我们用一个7维状态向量来描述目标:[cx, cy, s, r, vx, vy, vs]。其中cx、cy是中心点坐标,s是面积,r是宽高比,vx、vy、vs是对应的速度。

预测公式长这样:

x' = F * x + u
P' = F * P * F^T + Q

更新公式:

K = P' * H^T * (H * P' * H^T + R)^(-1)
x = x' + K * (z - H * x')
P = (I - K * H) * P'

别被这些矩阵吓到。你只要记住:F是运动模型,Q是过程噪声,R是测量噪声。Q越大,说明我越不相信自己的预测;R越大,说明我越不相信传感器。

我的小技巧:调参时先固定R,只调Q。Q调大一点,跟踪会更“跟手”,但噪声也大;Q调小,轨迹更平滑,但响应慢。具体怎么选?看你场景。高速场景Q大一点,低速场景Q小一点。

3.2 匈牙利匹配算法

卡尔曼滤波帮我们预测了每个目标的位置,但新一帧来了几十个检测框,怎么知道哪个框对应哪个目标?这就是匈牙利算法干的事。

3.2.1 匹配的本质

匈牙利算法解决的是“指派问题”。说白了,就是有N个老目标和M个新检测,我要找一个最优的配对方案,让总代价最小。

代价怎么算?最常用的是IOU距离:

cost = 1 - IOU(pred_box, det_box)

IOU越大,代价越小,越可能匹配上。我见过有人用欧氏距离,但效果不如IOU。为什么?因为IOU天然考虑了框的大小和位置,更鲁棒。

3.2.2 算法流程

  1. 构建代价矩阵:N行(老目标)M列(新检测),每个元素是IOU距离。
  2. 执行匈牙利算法,找到最小代价的匹配组合。
  3. 设定阈值(比如IOU < 0.3),低于阈值的匹配直接丢弃,视为新目标出现或目标消失。
我曾经踩过的坑:匈牙利算法默认是一对一匹配。但实际场景中,一个检测框可能对应多个目标(比如遮挡),或者一个目标被多个检测框覆盖。这时候需要加一些后处理逻辑,比如用“级联匹配”或者“贪心匹配”来兜底。

3.3 SORT跟踪器详解

SORT(Simple Online and Realtime Tracking)是我个人非常推崇的一个基线方法。它简单、高效、够用。很多花里胡哨的跟踪器,底层逻辑和SORT一模一样。

3.3.1 SORT的四个步骤

步骤 操作 说明
1. 检测 用目标检测器(如YOLO)得到当前帧的检测框 检测质量直接决定跟踪上限
2. 预测 用卡尔曼滤波预测每个已有目标在当前帧的位置 匀速模型,状态向量7维
3. 匹配 用匈牙利算法,基于IOU代价进行关联 阈值通常设为0.3
4. 更新 匹配上的目标用检测框更新卡尔曼状态;未匹配的检测视为新目标;未匹配的目标若连续丢失超过阈值则删除 丢失阈值一般设3-5帧

3.3.2 代码骨架

class SORT:
    def __init__(self):
        self.tracks = []  # 所有活跃轨迹
        self.next_id = 0

    def update(self, detections):
        # 1. 预测所有已有轨迹
        for track in self.tracks:
            track.predict()

        # 2. 构建代价矩阵(IOU距离)
        cost_matrix = self.build_cost_matrix(self.tracks, detections)

        # 3. 匈牙利匹配
        matches, unmatched_dets, unmatched_trks = \
            hungarian_match(cost_matrix, threshold=0.3)

        # 4. 更新匹配上的轨迹
        for trk_idx, det_idx in matches:
            self.tracks[trk_idx].update(detections[det_idx])

        # 5. 创建新轨迹
        for det_idx in unmatched_dets:
            self.tracks.append(Track(detections[det_idx], self.next_id))
            self.next_id += 1

        # 6. 删除丢失过久的轨迹
        self.tracks = [t for t in self.tracks if t.time_since_update < 5]

        return [t.get_state() for t in self.tracks]

你看,核心代码就这么几行。SORT之所以快,就是因为它把复杂问题简单化了——只用位置和运动信息,不用外观特征。

3.3.3 SORT的局限性

  • 对遮挡敏感:一旦目标被遮挡超过几帧,IOU匹配就会失效,轨迹容易断。
  • ID切换频繁:两个目标交错时,SORT经常搞混谁是谁。
  • 依赖检测质量:检测漏了,跟踪必断。

怎么解决?DeepSORT引入了外观特征(ReID),用余弦距离代替纯IOU代价。但代价是速度变慢。我个人建议:如果你的场景遮挡不严重,SORT完全够用;如果遮挡严重,再考虑加ReID。

核心总结:卡尔曼滤波负责“猜”,匈牙利算法负责“配”,SORT把两者串起来形成一个闭环。理解了这个闭环,你就掌握了90%的跟踪算法精髓。
SORT跟踪器核心流程 目标检测 YOLO / Faster R-CNN 卡尔曼预测 状态估计 + 协方差 匈牙利匹配 IOU代价矩阵 更新 反馈到下一帧 未匹配检测 → 新轨迹 未匹配轨迹 → 丢失计数 丢失 > 阈值?删除

这张图把SORT的闭环画得很清楚。你仔细看,从检测到预测到匹配再到更新,最后反馈回下一帧,形成一个循环。这就是跟踪的本质——用历史信息指导未来,用当前观测修正历史。

好了,这一章的内容就到这里。记住,卡尔曼滤波是“骨架”,匈牙利匹配是“关节”,SORT是“躯体”。理解这三者的关系,你就能自己搭一个跟踪系统出来。

可可精品资料库