多目标跟踪与视频记忆


文档摘要

多目标跟踪与视频记忆 本节摘要:跟踪 = 检测 + 关联。每帧检测,把这帧的检测按 ID 匹配到上帧的轨迹。本节区分跟踪即检测与基于查询的跟踪,命名算法家族(SORT、DeepSORT、ByteTrack、BoT-SORT、SAM 2 记忆跟踪器、SAM 3.1 Object Multiplex);从零实现 IoU 代价矩阵 + 匈牙利分配的经典跟踪即检测;讲清 SAM 2 的记忆库为何比基于 IoU 的关联更能扛遮挡,以及 SAM 3.1 Object Multiplex 如何用共享记忆高效跟踪同概念的许多实例;读懂 MOTA、IDF1、HOTA 三个指标并按用例挑。读完本节,你能为体育分析、监控、自动驾驶选对跟踪栈。 对应原课程:Phase 4 · Lesson 27 · (原英文 )。

多目标跟踪与视频记忆

本节摘要:跟踪 = 检测 + 关联。每帧检测,把这帧的检测按 ID 匹配到上帧的轨迹。本节区分跟踪即检测与基于查询的跟踪,命名算法家族(SORT、DeepSORT、ByteTrack、BoT-SORT、SAM 2 记忆跟踪器、SAM 3.1 Object Multiplex);从零实现 IoU 代价矩阵 + 匈牙利分配的经典跟踪即检测;讲清 SAM 2 的记忆库为何比基于 IoU 的关联更能扛遮挡,以及 SAM 3.1 Object Multiplex 如何用共享记忆高效跟踪同概念的许多实例;读懂 MOTA、IDF1、HOTA 三个指标并按用例挑。读完本节,你能为体育分析、监控、自动驾驶选对跟踪栈。

对应原课程:Phase 4 · Lesson 27 · multi-object-tracking(原英文 phases/04-computer-vision/27-multi-object-tracking/docs/en.md)。

学习目标

阅读完本节,你应当能够:

  1. 区分跟踪即检测基于查询的跟踪,命名算法家族(SORT、DeepSORT、ByteTrack、BoT-SORT、SAM 2 记忆跟踪器、SAM 3.1 Object Multiplex)。
  2. 从零实现经典跟踪即检测的 IoU + 匈牙利分配
  3. 解释 SAM 2 的记忆库,以及它为何比基于 IoU 的关联更能扛遮挡。
  4. 读懂三个跟踪指标(MOTA、IDF1、HOTA),按用例挑要紧的那个。

一、问题与直觉

检测器告诉你单帧里物体在哪;跟踪器告诉你帧 t 的哪个检测与帧 t−1 的某个检测是同一物体。没有它,你无法数过线物体、跟踪穿过遮挡的球、或知道「4 号车已在这条道 8 秒」。

跟踪对每个面向视频的产品都不可或缺:体育分析、监控、自动驾驶、医学视频分析、野生动物监测、计数。核心构件共享:逐帧检测器、运动模型(卡尔曼滤波或更丰富者)、关联步(在 IoU/余弦/学得特征上的匈牙利算法)、轨迹生命周期(诞生、更新、消亡)。

2026 年带来两个新模式:SAM 2 基于记忆的跟踪(特征记忆替代运动模型关联)和 SAM 3.1 Object Multiplex(同概念多实例的共享记忆)。本节先走经典栈,再走基于记忆的方法。

跟踪即检测

2026 年你会遇到的每个跟踪器都是这个循环的变体,差异在:

  • SORT(2016):卡尔曼 + IoU 匈牙利,简单快,无外观模型。
  • DeepSORT(2017):SORT + 每轨迹一个基于 CNN 的外观特征(ReID 嵌入),更抗交叉。
  • ByteTrack(2021):把低置信度检测作第二阶段关联;无需外观特征却是 MOT17 顶尖。
  • BoT-SORT(2022):Byte + 相机运动补偿 + ReID。
  • StrongSORT / OC-SORT——ByteTrack 后继,运动和外观更好。

卡尔曼滤波一段话讲清

卡尔曼滤波维护每轨迹状态 (x, y, w, h, dx, dy, dw, dh) 加协方差。每帧用恒速模型预测状态,再用匹配检测更新。预测不确定性高时更新更信检测。这给出平滑轨迹,且能穿过短遮挡(1~5 帧)延续轨迹。

每个经典跟踪器在运动预测步都用卡尔曼滤波。

匈牙利算法

给定 M×N 代价矩阵(轨迹×检测),找总代价最小的一对一分配。代价通常是 1 − IoU(轨迹框, 检测框) 或外观特征的负余弦相似度。运行时 O((M+N)³),M、N 到约 1000 时 Python 经 scipy.optimize.linear_sum_assignment 仍够快。

ByteTrack 的关键思想

标准跟踪器丢掉低置信度检测(<0.5)。ByteTrack 把它们留作第二阶段候选:把轨迹与高置信度检测匹配后,未匹配轨迹用稍宽松的 IoU 阈值再试低置信度检测,恢复短遮挡、人群附近的 ID 切换。

SAM 2 基于记忆的跟踪

SAM 2 通过维护每实例时空特征的记忆库处理视频。给定一帧上的提示(点、框、文本),它把实例编码进记忆。后续帧上,记忆与新帧特征做交叉注意力,解码器在新帧产出同一实例的掩码。

无卡尔曼、无匈牙利,关联隐含在记忆-注意力操作里。

优点:对大遮挡鲁棒(记忆跨多帧携带身份);配 SAM 3 文本提示时开放词表;无需独立运动模型。
缺点:对多目标跟踪比 ByteTrack 慢;记忆库增长,限制上下文窗口。

SAM 3.1 Object Multiplex

先前 SAM 2 / SAM 3 跟踪每实例一个独立记忆库,50 个物体就 50 个记忆库。Object Multiplex(2026 年 3 月)把它们折叠成一个带每实例查询 token 的共享记忆,代价随实例数次线性增长。

Multiplex 是 2026 人群跟踪的新默认:演唱会、仓库工人、交通路口。

三个要懂的指标

  • MOTA(多目标跟踪精度)——1 − (FN + FP + ID 切换) / GT,按错误类型加权,单一指标但混淆了检测和关联失败。
  • IDF1(ID F1)——ID 精确率与召回率的调和均值,专看每条真值轨迹能多久保持 ID,对 ID 切换敏感的任务比 MOTA 好。
  • HOTA(高阶跟踪精度)——分解为检测精度(DetA)和关联精度(AssA),2020 年起的社区标准,最全面。

监控(谁是谁)报 IDF1;体育分析(数传球)报 HOTA;学术通用比较报 HOTA。

二、从零实现

步骤 1:基于 IoU 的代价矩阵

import numpy as np def bbox_iou(a, b): """ a, b: (N, 4) 的 [x1, y1, x2, y2] 数组。 返回 (N_a, N_b) IoU 矩阵。 """ ax1, ay1, ax2, ay2 = a[:, 0], a[:, 1], a[:, 2], a[:, 3] bx1, by1, bx2, by2 = b[:, 0], b[:, 1], b[:, 2], b[:, 3] inter_x1 = np.maximum(ax1[:, None], bx1[None, :]) inter_y1 = np.maximum(ay1[:, None], by1[None, :]) inter_x2 = np.minimum(ax2[:, None], bx2[None, :]) inter_y2 = np.minimum(ay2[:, None], by2[None, :]) inter = np.clip(inter_x2 - inter_x1, 0, None) * np.clip(inter_y2 - inter_y1, 0, None) area_a = (ax2 - ax1) * (ay2 - ay1) area_b = (bx2 - bx1) * (by2 - by1) union = area_a[:, None] + area_b[None, :] - inter return inter / np.clip(union, 1e-8, None)

步骤 2:最小 SORT 风格跟踪器

固定恒速卡尔曼此处省略——这里用简单 IoU 关联;生产中卡尔曼预测必不可少。sort Python 包提供完整版。

from scipy.optimize import linear_sum_assignment class Track: def __init__(self, tid, bbox, frame): self.id = tid self.bbox = bbox self.last_frame = frame self.hits = 1 def update(self, bbox, frame): self.bbox = bbox self.last_frame = frame self.hits += 1 class SimpleTracker: def __init__(self, iou_threshold=0.3, max_age=5): self.tracks = [] self.next_id = 1 self.iou_threshold = iou_threshold self.max_age = max_age def step(self, detections, frame): if not self.tracks: for d in detections: self.tracks.append(Track(self.next_id, d, frame)) self.next_id += 1 return [(t.id, t.bbox) for t in self.tracks] track_boxes = np.array([t.bbox for t in self.tracks]) det_boxes = np.array(detections) if len(detections) else np.empty((0, 4)) iou = bbox_iou(track_boxes, det_boxes) if len(det_boxes) else np.zeros((len(track_boxes), 0)) cost = 1 - iou cost[iou < self.iou_threshold] = 1e6 matched_track = set() matched_det = set() if cost.size > 0: row, col = linear_sum_assignment(cost) for r, c in zip(row, col): if cost[r, c] < 1.0: self.tracks[r].update(det_boxes[c], frame) matched_track.add(r); matched_det.add(c) for i, d in enumerate(det_boxes): if i not in matched_det: self.tracks.append(Track(self.next_id, d, frame)) self.next_id += 1 self.tracks = [t for t in self.tracks if frame - t.last_frame <= self.max_age] return [(t.id, t.bbox) for t in self.tracks]

60 行。吃逐帧检测,吐逐帧轨迹 ID。真实系统加卡尔曼预测、ByteTrack 第二阶段再匹配、外观特征。

步骤 3:合成轨迹测试

def synthetic_frames(num_frames=20, num_objects=3, H=240, W=320, seed=0): rng = np.random.default_rng(seed) starts = rng.uniform(20, 200, size=(num_objects, 2)) velocities = rng.uniform(-5, 5, size=(num_objects, 2)) frames = [] for f in range(num_frames): dets = [] for i in range(num_objects): cx, cy = starts[i] + f * velocities[i] dets.append([cx - 10, cy - 10, cx + 10, cy + 10]) frames.append(dets) return frames tracker = SimpleTracker() for f, dets in enumerate(synthetic_frames()): tracks = tracker.step(dets, f)

三个直线运动的物体应跨全部 20 帧保持 ID。

步骤 4:ID 切换指标

def count_id_switches(tracks_per_frame, gt_per_frame): """ tracks_per_frame: (track_id, bbox) 的列表的列表 gt_per_frame: (gt_id, bbox) 的列表的列表 返回 ID 切换次数。 """ prev_assignment = {} switches = 0 for tracks, gts in zip(tracks_per_frame, gt_per_frame): if not tracks or not gts: continue t_boxes = np.array([b for _, b in tracks]) g_boxes = np.array([b for _, b in gts]) iou = bbox_iou(g_boxes, t_boxes) for g_idx, (gt_id, _) in enumerate(gts): j = iou[g_idx].argmax() if iou[g_idx, j] > 0.5: t_id = tracks[j][0] if gt_id in prev_assignment and prev_assignment[gt_id] != t_id: switches += 1 prev_assignment[gt_id] = t_id return switches

这是简化的 IDF1 邻近指标:数真值物体换了多少次分配到的预测轨迹 ID。真实 MOTA / IDF1 / HOTA 工具在 py-motmetricsTrackEval

三、框架对比

2026 生产跟踪器:

  • ultralytics——内置 YOLOv8 + ByteTrack / BoT-SORT,results = model.track(source, tracker="bytetrack.yaml"),默认之选。
  • supervision(Roboflow)——ByteTrack 包装加标注工具。
  • SAM 2 / SAM 3.1——经 processor.track() 做基于记忆的跟踪。
  • 自定义栈:检测器(YOLOv8 / RT-DETR)+ sort-tracker / OC-SORT / StrongSORT

挑选:

  • 30+ fps 的行人/车/框:ultralytics + ByteTrack
  • 人群中同类多实例:SAM 3.1 Object Multiplex
  • 重遮挡且外观可识别:DeepSORT / StrongSORT(ReID 特征)。
  • 体育/复杂交互:BoT-SORT 或学得的跟踪器(MOTRv3)。

四、可复用产物

本节产出两个可复用文件(位于原课程 outputs/):

  • prompt-tracker-picker.md:按场景类型、遮挡模式、延迟预算,在 SORT / ByteTrack / BoT-SORT / SAM 2 / SAM 3.1 间挑。
  • skill-mot-evaluator.md:写出对真值轨迹评估 MOTA / IDF1 / HOTA 的完整评估台。

五、练习

  1. (简单) 用 3、10、30 个物体跑上述合成跟踪器,报告每种 ID 切换数,找出纯 IoU 关联开始失败的地方。
  2. (中等) 在关联前加恒速卡尔曼预测步,证明短(2~3 帧)遮挡不再导致 ID 切换。
  3. (困难) 集成 SAM 2 的基于记忆的跟踪器(经 transformers)作替代后端,在 30 秒人群片段上跑 SimpleTracker 和 SAM 2,对比 ID 切换数,为 5 个显著人物手工标真值 ID。

本节要点回顾

  1. 跟踪 = 检测 + 关联——逐帧检测 + 匈牙利分配(IoU/外观);运动模型(卡尔曼)+ 关联 + 轨迹生命周期。
  2. 算法家族:SORT(卡尔曼+IoU)、DeepSORT(+ReID 外观)、ByteTrack(低置信度第二阶段)、BoT-SORT(+相机运动补偿)、StrongSORT/OC-SORT。
  3. 卡尔曼滤波——恒速预测+协方差,平滑轨迹、扛短遮挡(1~5 帧)。
  4. 匈牙利算法——最小代价二部匹配,scipy.optimize.linear_sum_assignment,O((M+N)³)。
  5. ByteTrack 关键思想:低置信度检测留作第二阶段候选,恢复短遮挡与人群 ID 切换。
  6. SAM 2 记忆库——每实例时空特征跨帧存,交叉注意力替代显式关联,扛大遮挡、开放词表,但慢于 ByteTrack。
  7. SAM 3.1 Object Multiplex——共享记忆+每实例查询 token,代价随实例数次线性,人群跟踪新默认。
  8. 三指标:MOTA(混淆检测+关联)、IDF1(专看 ID 保持,监控用)、HOTA(分解 DetA+AssA,社区标准,体育/学术用)。
  9. 生产挑选:ultralytics+ByteTrack(默认)、SAM 3.1(人群)、DeepSORT/StrongSORT(重遮挡+外观)、BoT-SORT/MOTRv3(体育)。

下一节(本章最后一节)进入世界模型与视频扩散——让模型理解物理世界的动态,生成符合物理的视频,迈向具身智能的下一步。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U