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越大,说明我越不相信传感器。
3.2 匈牙利匹配算法
卡尔曼滤波帮我们预测了每个目标的位置,但新一帧来了几十个检测框,怎么知道哪个框对应哪个目标?这就是匈牙利算法干的事。
3.2.1 匹配的本质
匈牙利算法解决的是“指派问题”。说白了,就是有N个老目标和M个新检测,我要找一个最优的配对方案,让总代价最小。
代价怎么算?最常用的是IOU距离:
cost = 1 - IOU(pred_box, det_box)
IOU越大,代价越小,越可能匹配上。我见过有人用欧氏距离,但效果不如IOU。为什么?因为IOU天然考虑了框的大小和位置,更鲁棒。
3.2.2 算法流程
- 构建代价矩阵:N行(老目标)M列(新检测),每个元素是IOU距离。
- 执行匈牙利算法,找到最小代价的匹配组合。
- 设定阈值(比如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的闭环画得很清楚。你仔细看,从检测到预测到匹配再到更新,最后反馈回下一帧,形成一个循环。这就是跟踪的本质——用历史信息指导未来,用当前观测修正历史。
好了,这一章的内容就到这里。记住,卡尔曼滤波是“骨架”,匈牙利匹配是“关节”,SORT是“躯体”。理解这三者的关系,你就能自己搭一个跟踪系统出来。