OC-SORT: Observation-Centric SORT

论文: Observation-Centric SORT: Rethinking SORT for Robust Multi-Object Tracking (CVPR 2023)
作者: Jinkun Cao, Xinshuo Weng, Rawal Khirodkar, Kris Kitani, Jiangmiao Pang (CMU / 商汤)
代码: noahcao/OC_SORT

一句话总结:OC-SORT 把 SORT 从”以估计为中心”改为”以观测为中心”–当轨迹从遮挡中恢复时,用观测历史修正卡尔曼参数(ORU);在关联代价中加入观测方向一致性(OCM);用最后一次观测做二次恢复匹配(OCR)。 无需外观特征,即超越 DeepSORT。


一、背景:SORT 的三个局限

OC-SORT 的出发点是分析 SORT 的三个根本局限,逐一解决。

1.1 局限一:对状态噪声敏感

SORT 假设匀速运动,用卡尔曼滤波估计速度。但在高帧率视频中,连续帧间位移很小,噪声与实际位移量级相当,导致速度估计方差大。

1
2
帧率 30fps: 每帧位移 ~10px, 噪声 ~2px -> 信噪比 5:1, 还行
帧率 60fps: 每帧位移 ~5px, 噪声 ~2px -> 信噪比 2.5:1, 速度估计噪声大

1.2 局限二:遮挡期间误差累积(O(t²))

当物体被遮挡(检测器漏检),轨迹没有新观测,KF 只做预测不做更新。预测只用上一次的速度估计,如果速度估计有偏差,误差会随时间平方增长

$$
\text{误差} \sim O(\Delta t^2)
$$

1
2
3
4
5
6
帧 10: 物体正常跟踪,速度估计 v=5px/帧
帧 11-20: 物体被遮挡,KF 持续预测
- 帧 11: 预测位置 = 上帧 + v (偏差小)
- 帧 15: 预测位置 += v × 5 (偏差累积)
- 帧 20: 预测位置 += v × 10 (偏差 ~O(10²) = 100倍基准)
帧 21: 物体重新出现,但 KF 预测位置已偏离真实位置很远 -> 匹配失败

这就是 SORT “丢失后不能恢复”的根因:不是故意不恢复,而是 KF 预测偏离太远,IoU=0,无法匹配

1.3 局限三:以估计为中心(Estimation-Centric)

SORT 严重依赖 KF 的状态估计(预测位置),把检测器的观测只当作”辅助修正”。但现代检测器(YOLOv8/RT-DETR)已经非常可靠,应该更信任观测

1
2
SORT 的信任链:  KF 预测 (主导) > 检测观测 (辅助)
OC-SORT 的信任链: 检测观测 (主导) > KF 预测 (辅助)

二、核心思想:从”以估计为中心”到”以观测为中心”

OC-SORT 的核心哲学转变:

SORT(以估计为中心) OC-SORT(以观测为中心)
信任谁 KF 状态估计(预测位置) 检测器观测(实际位置)
恢复丢失轨迹 用 KF 预测位置匹配(已偏离) 最后一次观测位置匹配
速度方向 用 KF 估计的速度 观测历史计算的方向
遮挡后 KF 参数 信任累积了误差的参数 用虚拟轨迹重新更新参数

关键洞察:现代检测器足够可靠,观测比 KF 预测更可信。OC-SORT 让观测”反客为主”。


三、三大创新

3.1 ORU:观测为中心的再更新(Observation-centric Re-Update)⭐

解决”遮挡后 KF 参数偏离”问题。

问题:轨迹丢失期间 KF 只预测不更新,误差累积。即使后来重新匹配成功,KF 参数(状态 + 协方差)已经偏离正确值,可能很快再次丢失。

解法:当丢失轨迹重新匹配成功时,回溯丢失期间,用虚拟轨迹重新更新 KF 参数

1
2
3
4
5
6
7
8
9
10
时间线:
t1: 最后一次真实观测 z_t1
t1~t2: 丢失期间(无观测,KF 只预测,误差累积)
t2: 重新匹配到观测 z_t2

ORU 过程:
1. 构建虚拟轨迹: z_hat = Traj_virtual(z_t1, z_t2, t), t1 < t < t2
(在 z_t1 和 z_t2 之间线性插值)
2. 从 t1 的 KF 状态出发,沿虚拟轨迹重新跑 predict-update 循环
3. 得到修正后的 KF 参数(状态 + 协方差)

公式(沿虚拟轨迹的再更新):

$$
\hat{\mathbf{x}}{t|t} = \hat{\mathbf{x}}{t|t-1} + \mathbf{K}_t (\tilde{\mathbf{z}}_t - \mathbf{H}t \hat{\mathbf{x}}{t|t-1})
$$

  • $\tilde{\mathbf{z}}_t$:虚拟轨迹上的”伪观测”
  • $\mathbf{K}_t$:卡尔曼增益
  • 效果:KF 参数被”拉回”到与真实运动一致的状态

直觉:KF 在丢失期间”走偏了”,ORU 相当于”假装这段时间有观测,重新校正一遍”。

源码:ORU 在 KalmanFilterNew 类(kalmanfilter.py)中实现。当 OCR 恢复成功后调用 tracker.update()KalmanFilterNew 内部会检查是否是从丢失状态恢复,如果是则沿虚拟轨迹($z_{t_1}$ 到 $z_{t_2}$ 的线性插值)重新执行 predict-update 循环,修正累积误差。

3.2 OCM:观测为中心的动量(Observation-Centric Momentum)⭐

解决”只用 IoU 匹配不够鲁棒”问题。

问题:SORT 的关联代价只用 IoU,当物体运动较快或预测有偏差时,IoU 可能不够高导致漏匹配。

解法:在代价矩阵中加入运动方向一致性项:

$$
C(\hat{X}, Z) = \underbrace{C_{\text{IoU}}(\hat{X}, Z)}{\text{SORT 原有}} + \lambda \underbrace{C_v(\hat{X}, Z, V)}{\text{OC-SORT 新增}}
$$

  • $C_{\text{IoU}}$:负的 IoU(IoU 越大代价越小)
  • $C_v$:方向一致性代价
  • $\lambda$:权重因子

方向计算(用观测而非估计):

1
2
3
4
5
6
7
θ_track = track 的历史运动方向
= arctan((v1-v2)/(u1-u2)) 用两次历史观测 (u1,v1) 和 (u2,v2) 计算

θ_interaction = track 最后观测 -> 新检测的方向
= arctan(...) 用最后观测和新检测计算

C_v = |θ_track - θ_interaction| 方向偏差越大, 代价越大

为什么用观测而非 KF 估计算方向? KF 估计的速度有误差累积,而观测是检测器的直接输出,噪声更可控。论文证明:增大两次观测的时间差 Δt 可以降低方向噪声(噪声与 Δt 负相关),但 Δt 太大线性假设会失效,需权衡。

直觉:如果一个轨迹一直在向右走(历史方向),新检测也在右边,那方向一致、代价低、应该匹配;如果新检测在左边(方向相反),代价高、可能不是同一个物体。

源码association.py: associate()):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def associate(detections, trackers, iou_threshold, velocities, previous_obs, vdc_weight):
# 1. 计算检测框与轨迹历史观测之间的方向
Y, X = speed_direction_batch(detections, previous_obs) # 方向向量
inertia_Y, inertia_X = velocities[:, 0], velocities[:, 1] # 轨迹历史速度方向

# 2. 计算方向一致性(余弦相似度 -> 角度偏差)
diff_angle_cos = inertia_X * X + inertia_Y * Y # cos(θ_track - θ_interaction)
diff_angle_cos = np.clip(diff_angle_cos, -1, 1)
diff_angle = np.arccos(diff_angle_cos)
diff_angle = (np.pi / 2.0 - np.abs(diff_angle)) / np.pi # 归一化到 [0, 0.5]

# 3. 方向代价 = 有效掩码 × 方向一致性 × 权重 × 检测分数
angle_diff_cost = (valid_mask * diff_angle) * vdc_weight # vdc_weight = inertia (默认 0.2)
angle_diff_cost = angle_diff_cost.T * scores

# 4. ★ 最终代价 = IoU + 方向一致性
iou_matrix = iou_batch(detections, trackers)
matched_indices = linear_assignment(-(iou_matrix + angle_diff_cost)) # 最大化 IoU+方向

关键linear_assignment(-(iou_matrix + angle_diff_cost)) – 代价 = IoU + 方向一致性,方向越一致代价越小。vdc_weight(默认 0.2)控制方向项的权重。

速度方向的计算(用观测,不用 KF 估计):

1
2
3
4
5
def speed_direction(bbox1, bbox2):
cx1, cy1 = (bbox1[0]+bbox1[2])/2, (bbox1[1]+bbox1[3])/2 # 旧观测中心
cx2, cy2 = (bbox2[0]+bbox2[2])/2, (bbox2[1]+bbox2[3])/2 # 新观测中心
speed = np.array([cy2-cy1, cx2-cx1]) # 方向向量
return speed / (np.linalg.norm(speed) + 1e-6) # 归一化

KalmanBoxTracker.update() 中,每次匹配成功时用 delta_t 步前的观测与当前观测计算速度方向:self.velocity = speed_direction(previous_box, bbox)delta_t 默认 3(用 3 帧前的观测算方向,降低噪声)。

3.3 OCR:观测为中心的恢复(Observation-Centric Recovery)⭐

解决”丢失轨迹的 KF 预测偏离太远导致无法恢复”问题。

问题:轨迹丢失后,KF 预测位置已偏离真实位置很远,与重新出现的检测框 IoU=0,正常关联阶段无法匹配。

解法:在正常关联之后,用轨迹的最后一次观测(而非 KF 预测)与未匹配检测做二次关联

1
2
3
4
5
6
7
8
正常关联阶段(同 SORT):
检测框 vs KF预测框 -> 匹配 / 未匹配检测 / 未匹配轨迹

OCR 二次恢复阶段(OC-SORT 新增):
未匹配检测 vs 未匹配轨迹的【最后一次观测】(不是KF预测) -> 二次匹配

理由: 物体被遮挡后重新出现, 位置大概率在它最后一次被看到的位置附近
(高斯分布: 均值=最后观测位置, 方差随丢失时间增长)
1
2
3
4
5
6
7
8
flowchart LR
A["正常关联<br/>(IoU + OCM)"] --> B{"有未匹配轨迹<br/>和未匹配检测?"}
B -->|"是"| C["OCR 二次恢复<br/>用最后观测 vs 未匹配检测"]
B -->|"否"| D["结束"]
C --> E["IoU 匹配<br/>(基于最后观测位置)"]
E --> F["匹配成功?"]
F -->|"是"| G["恢复轨迹 + 触发 ORU"]
F -->|"否"| H["保持丢失状态"]

直觉:KF 预测说”物体应该在 100px 外”,但物体其实没动(停在原地被遮挡)。用最后一次观测位置(”物体最后在这”)去匹配,比用偏离的 KF 预测靠谱得多。

源码ocsort.py: OCSort.update() 中的 OCR 二次恢复):

1
2
3
4
5
6
7
8
9
10
11
12
13
# 正常关联后,如果仍有未匹配检测和未匹配轨迹
if unmatched_dets.shape[0] > 0 and unmatched_trks.shape[0] > 0:
left_dets = dets[unmatched_dets]
left_trks = last_boxes[unmatched_trks] # ★ 用最后一次观测,不是 KF 预测!

iou_left = self.asso_func(left_dets, left_trks) # 计算 IoU
if iou_left.max() > self.iou_threshold:
rematched_indices = linear_assignment(-iou_left) # 匈牙利二次匹配
for m in rematched_indices:
if iou_left[m[0], m[1]] < self.iou_threshold:
continue
# ★ 恢复成功:用新检测更新轨迹(触发 KF update + ORU)
self.trackers[trk_ind].update(dets[det_ind, :])

关键left_trks = last_boxes[unmatched_trks] – 这里用的是 last_observation(最后一次真实检测),而非 KF 预测框。这是 OCR 与 SORT 的核心区别。

last_observation 的维护KalmanBoxTracker):

1
2
3
4
5
6
7
8
9
class KalmanBoxTracker:
def __init__(self, bbox):
self.last_observation = np.array([-1,-1,-1,-1,-1]) # 初始占位
self.observations = dict() # 观测历史 {age: bbox}

def update(self, bbox):
self.last_observation = bbox # 记录最后一次真实观测
self.observations[self.age] = bbox # 存入观测历史
self.kf.update(convert_bbox_to_z(bbox)) # 正常 KF 更新

每个轨迹独立维护 last_observation(最后一次匹配到的检测框)和 observations 字典(所有历史观测)。OCR 用 last_observation 做二次匹配,OCM 用 observations 中的 delta_t 步前观测算速度方向。


四、整体流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
flowchart TD
A["当前帧检测 dets"]
B["所有轨迹: KF 预测"]
C["关联代价 = IoU + λ×方向一致性(OCM)"]
D["匈牙利匹配"]
E{"匹配结果"}
F["匹配成功: KF 更新"]
G["未匹配检测: 新建轨迹"]
H["未匹配轨迹"]
I{"OCR: 用最后观测<br/>vs 未匹配检测二次匹配"}
I -->|"匹配成功"| J["恢复轨迹 + ORU 重新更新KF"]
I -->|"匹配失败"| K["标记丢失<br/>time_since_update++"]
K --> L{"> max_age?"}
L -->|"是"| M["删除"]
L -->|"否"| N["保留"]
F --> O["输出"]
G --> O
J --> O
N --> O
O --> P["下一帧"]

A --> C
B --> C
C --> D
D --> E
E -->|"匹配成功"| F
E -->|"未匹配检测"| G
E -->|"未匹配轨迹"| H
H --> I

与 SORT 相比,OC-SORT 多了两个阶段:OCM(在关联代价中加方向项)和 OCR + ORU(二次恢复 + 恢复后修正 KF)。


五、与 SORT 的对比

维度 SORT OC-SORT
关联代价 仅 IoU IoU + 方向一致性(OCM)
丢失恢复 用 KF 预测匹配(易偏离) 最后一次观测二次匹配(OCR)
恢复后 KF 直接用(参数已偏离) ORU 重新更新(沿虚拟轨迹修正)
速度方向来源 KF 估计(有误差累积) 观测历史(噪声更可控)
哲学 以估计为中心 以观测为中心
外观特征 无(仍纯运动模型)
IDSW 大幅降低
遮挡恢复

六、性能

模型 数据集 MOTA HOTA IDF1 IDSW↓
SORT MOT17 57.5 53.3 59.7 4872
DeepSORT MOT17 58.2 56.9 62.6 1296
ByteTrack MOT17 56.4 58.3 63.3 689
OC-SORT MOT17 59.1 63.2 67.3 659

OC-SORT 在 MOT17 上 HOTA 63.2,无需外观特征即超越 DeepSORT 和 ByteTrack。在 DanceTrack(非线性运动)上优势更明显。


七、改进路线总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SORT (2016)
└─ 卡尔曼 + IoU 匹配
问题: 遮挡后不可恢复, IDSW高

DeepSORT (2017)
└─ + 外观特征(ReID) + 马氏距离
问题: 需要额外ReID网络, 计算量大

ByteTrack (2022)
└─ + 低分检测二次匹配
问题: 仍以估计为中心

OC-SORT (2023) ← 本篇
└─ + OCM(方向一致性) + OCR(观测恢复) + ORU(KF重更新)
核心: 以观测为中心, 无需外观特征

八、总结

OC-SORT 的核心贡献是把 SORT 从”以估计为中心”改为”以观测为中心”,三个创新环环相扣:

  1. OCM:关联时不只看 IoU,还看运动方向是否一致(用观测算方向,不用 KF 估计);
  2. OCR:丢失轨迹恢复时,用最后一次观测位置匹配(不用偏离的 KF 预测);
  3. ORU:恢复成功后,沿虚拟轨迹重新更新 KF 参数(修正遮挡期间的累积误差)。

关键洞察:现代检测器足够可靠,观测比 KF 预测更可信。 OC-SORT 不加任何外观特征,仅靠”更聪明地使用观测”就超越了 DeepSORT 和 ByteTrack。


相关链接

  • 📝 [[20.notes/感知算法/目标跟踪/SORT/SORT 详解|SORT 详解]] - OC-SORT 的基础,理解 SORT 的局限才能理解 OC-SORT 的改进
  • 📋 论文原文: arxiv.org/abs/2203.14360
  • 📋 官方代码: noahcao/OC_SORT
  • 📋 [[10.clippings/感知算法/目标跟踪/OC-SORT/22.03 OC-SORT]]
  • 📋 [[10.clippings/感知算法/目标跟踪/OC-SORT/OC- SORT]]
  • 📋 [[10.clippings/感知算法/目标跟踪/OC-SORT/OC-SORT文章梳理(略读)]]
  • 📋 [[10.clippings/感知算法/目标跟踪/OC-SORT/以观察为中心的SORT:为鲁棒多目标跟踪重新思考SORT]]
  • 📋 [[10.clippings/感知算法/目标跟踪/OC-SORT/如何使用OC-SORT:从安装到实战的完整多目标跟踪指南]]
  • 📦 源码参考: deeplearning/tracking/OC_SORT/trackers/ocsort_tracker/(ocsort.py / association.py / kalmanfilter.py)