视频理解与多目标跟踪

本文系统讲解视频理解与多目标跟踪,回答 SORT 与 ByteTrack 怎么选、遮挡下 ID Switch 怎么降、视频动作识别怎么建模等实战问题。覆盖检测跟踪范式、卡尔曼滤波与数据关联、匈牙利算法、ByteTrack 低分框利用、3D CNN 与时序 Transformer 动作识别、采样策略、MOTA 与 IDF1 指标,并给出卡尔曼加匈牙利匹配与跟踪评估两段可运行代码。

引言

视频比图像多了一个维度:时间。这让视频理解同时面对两类问题——「每一帧里有什么」与「跨帧它是谁、在做什么」。前者是检测,后者是跟踪与动作识别。工业上最典型的需求是客流统计、行为分析、交通流量与视频监控,它们的共同点是要在时间轴上维持稳定的身份。

多目标跟踪(Multi-Object Tracking,MOT)的难点不在单帧检测,而在数据关联:把这一帧的检测框与上一帧的轨迹正确配对。遮挡、目标交叉、相似外观、相机运动都会让关联出错,表现为 ID Switch(同一目标被赋予不同 ID)或轨迹碎片化。而动作识别是另一条线:把一段时空体映射成一个类别,难点在时序建模与采样策略。

本文按「范式 → 关联算法 → 代码 → 遮挡处理 → 动作识别 → 指标 → 性能优化」的顺序讲,重点把卡尔曼滤波、匈牙利匹配、ByteTrack 的低分框策略讲透。检测基础见 目标检测与 YOLO 系列工程落地 ,外观特征与 特征提取与图像匹配 中的描述子思路相通。

目录

  1. 视频理解的两条主线
  2. 检测跟踪范式与联合检测
  3. SORT 与卡尔曼滤波
  4. DeepSORT 与外观特征
  5. ByteTrack 与低分框利用
  6. 数据关联与匈牙利算法
  7. 代码:卡尔曼预测与匈牙利匹配
  8. 遮挡、ID Switch 与轨迹管理
  9. 动作识别:3D CNN 与时序 Transformer
  10. 采样策略与视频输入
  11. 跟踪评估指标
  12. 视频推理的算力与延迟优化

1. 视频理解的两条主线

主线输入输出代表任务
逐帧感知单帧或短片段框、掩码、关键点检测、跟踪、姿态
时序理解一段视频类别、描述、时间定位动作识别、时序动作定位

两条主线的工程约束不同:逐帧感知追求帧率与延迟,通常每帧独立处理再加关联;时序理解追求片段级准确率,可以离线批处理但输入尺寸大。

一个常见的架构是「跟踪提供身份,时序模型提供行为」:先用检测跟踪得到每个 ID 的轨迹,再把轨迹裁剪成片段送动作识别,输出「3 号目标在打架」。这种解耦让两个模块可以独立迭代。

2. 检测跟踪范式与联合检测

MOT 有两代范式:

  • TBD(Tracking-by-Detection):先逐帧检测,再做跨帧关联。模块化、易替换、工程成熟,代表是 SORT、DeepSORT、ByteTrack。绝大多数落地系统属于这一类。
  • JDE(Joint Detection and Embedding):一个网络同时输出检测框与外观嵌入,省掉独立的外观模型。代表有 FairMOT、JDE、CenterTrack。速度快、端到端,但训练更复杂、对检测器改动大。

TBD 的优势在于可解释与可调试:关联失败时可以单独看是检测漏了还是匹配错了。JDE 的优势在于速度与部署简单。中小规模场景优先 TBD,超大并发或端侧优先 JDE。

3. SORT 与卡尔曼滤波

SORT(Simple Online and Realtime Tracking)的核心只有两件事:卡尔曼滤波预测 + 匈牙利算法匹配。

卡尔曼滤波用状态向量描述每个目标的运动。常用状态是 [cx, cy, s, r, vx, vy, vs]:中心坐标、面积、宽高比,以及它们的变化率。预测步骤用匀速模型外推,更新步骤用当前帧的检测框修正。它假设运动平滑,对匀速或缓变目标很准,对急转弯会滞后。

匹配的代价矩阵用预测框与检测框的 IoU,然后跑匈牙利算法求最小代价的分配。IoU 低于阈值的配对视为无效,未匹配的检测新建轨迹,未匹配的轨迹保留若干帧等待重新出现。

SORT 在 MOT17 上能跑到很高的 MOTA,但因为没有外观特征,遮挡后重新出现时容易换 ID。

4. DeepSORT 与外观特征

DeepSORT 在 SORT 上加了外观嵌入:用一个 ReID 网络(如 OSNet、ResNet 变体)为每个检测框提取 128 维或 512 维特征,用余弦距离构建第二重代价。

最终代价是运动代价与外观代价的加权和:

cost = λ * IoU_cost + (1 - λ) * cosine_cost

匹配时用级联匹配(matching cascade):优先匹配最近几帧出现过的轨迹,把「消失很久的轨迹」放到最后,降低错误重认的概率。外观特征用最近若干次匹配的加权平均维护,而不是只存最后一帧。

代价是每个检测框都要跑一次 ReID 网络,延迟与显存显著上升。工程上常把 ReID 换成轻量模型,或只在匹配歧义时才启用外观分支。

5. ByteTrack 与低分框利用

ByteTrack 的关键洞察是:低置信度检测框不该被直接丢弃。被遮挡的目标常常只有 0.1 到 0.3 的置信度,但它们的位置信息仍然有用。

ByteTrack 的做法是两阶段关联:

  1. 用高分框(如 score > 0.5)与所有轨迹做第一次匹配,关联方式同 SORT(IoU 加匈牙利)。
  2. 对剩余未匹配的轨迹,用低分框(0.1 到 0.5)再做一次匹配,只用 IoU 代价,不引入新轨迹。

这样既保住了被遮挡目标的轨迹,又避免了低分框制造大量假轨迹。ByteTrack 在 MOT17 上约 80 MOTA、77 IDF1,且不需要外观模型,速度极快,是目前工程首选。

def bytetrack_associate(tracks, dets_high, dets_low, iou_thr=0.3):
    # 第一阶段:高分框匹配
    matches, unmatched_tracks = hungarian_match(tracks, dets_high, iou_thr)
    # 第二阶段:用低分框抢救剩余轨迹(只更新位置,不新建轨迹)
    rescues, still_unmatched = hungarian_match(
        [tracks[i] for i in unmatched_tracks], dets_low, iou_thr)
    return matches, rescues, still_unmatched

6. 数据关联与匈牙利算法

关联问题的数学形式是「二分图最小代价匹配」:左边是轨迹,右边是检测,边权是代价,要求每个点最多连一条边且总代价最小。

  • 匈牙利算法(Kuhn-Munkres):精确解,复杂度 O(n³)。轨迹与检测数量在几十到几百时完全够用。
  • 贪心匹配:按代价排序逐个配对,O(n log n),快但非最优,密集场景容易次优。
  • 代价设计:IoU 距离 1 - IoU、中心点距离、外观余弦距离,或三者加权。

工程上要注意几点:代价矩阵要做门限裁剪(超过阈值置为无穷),否则会强行配对明显不相关的框;匹配失败的目标要区分「轨迹未匹配」(可能是遮挡)与「检测未匹配」(可能是新目标),处理方式完全不同。

7. 代码:卡尔曼预测与匈牙利匹配

下面手写一个最小可用的跟踪核心,便于理解框架内部行为。

import numpy as np
from scipy.optimize import linear_sum_assignment

class KalmanBox:
    def __init__(self, box):
        # 状态 [cx, cy, s, r, vx, vy, vs],观测 [cx, cy, s, r]
        cx, cy = (box[0] + box[2]) / 2, (box[1] + box[3]) / 2
        s = (box[2] - box[0]) * (box[3] - box[1])
        r = (box[2] - box[0]) / max(box[3] - box[1], 1e-6)
        self.x = np.array([cx, cy, s, r, 0, 0, 0], dtype=float)
        self.F = np.eye(7)
        for i in range(3):
            self.F[i, i + 4] = 1.0          # 匀速模型
        self.P = np.eye(7) * 10.0

    def predict(self):
        self.x = self.F @ self.x
        self.P = self.F @ self.P @ self.F.T + np.eye(7) * 0.1
        return self.x[:4]

def iou_matrix(tracks, dets):
    M = np.zeros((len(tracks), len(dets)))
    for i, t in enumerate(tracks):
        for j, d in enumerate(dets):
            xx1, yy1 = max(t[0], d[0]), max(t[1], d[1])
            xx2, yy2 = min(t[2], d[2]), min(t[3], d[3])
            inter = max(0, xx2 - xx1) * max(0, yy2 - yy1)
            area_t = (t[2] - t[0]) * (t[3] - t[1])
            area_d = (d[2] - d[0]) * (d[3] - d[1])
            M[i, j] = inter / (area_t + area_d - inter + 1e-6)
    return M

def hungarian_match(tracks, dets, iou_thr=0.3):
    if len(tracks) == 0 or len(dets) == 0:
        return [], list(range(len(tracks)))
    cost = 1.0 - iou_matrix(tracks, dets)
    cost[cost > 1 - iou_thr] = 1e6        # 门限裁剪,禁止强行配对
    rows, cols = linear_sum_assignment(cost)
    matches = [(r, c) for r, c in zip(rows, cols) if cost[r, c] < 1e6]
    matched_rows = {r for r, _ in matches}
    return matches, [i for i in range(len(tracks)) if i not in matched_rows]

注意 linear_sum_assignment 求的是最小代价,所以要用 1 - IoU 而不是 IoU 本身。门限裁剪是关键:没有它,匈牙利算法会把明显不相关的轨迹与检测强行配对。

8. 遮挡、ID Switch 与轨迹管理

遮挡是 ID Switch 的主要来源。工程手段有三类:

  • 轨迹保留:轨迹未匹配时不立即删除,保留 max_age 帧(常用 30 帧)等待重现,超时才回收。
  • 状态机:新轨迹先标记为「暂定」,连续命中若干帧才转「确认」,避免检测噪声制造假轨迹。
  • 运动预测补偿:遮挡期间用卡尔曼外推位置,重现后靠预测位置与检测框匹配。
  • 外观重识别:重现时用 ReID 特征确认身份,代价是额外算力。
  • 相机运动补偿:相机本身在动时,先用光流或特征点估计全局变换,再在补偿后的坐标系做关联。

评估上,ID Switch 数量是最直观的指标。若 ID Switch 集中在某些片段,通常是密集交叉或长时间遮挡,需要针对性地调 max_age 与匹配门限。

9. 动作识别:3D CNN 与时序 Transformer

动作识别把一段时空体映射成类别,主流三条路线:

方法代表特点
双流网络Two-Stream、TSNRGB 帧 + 光流,光流算得慢但有效
3D 卷积C3D、I3D、SlowFast直接建模时空,效果好,算力大
时序 TransformerTimeSformer、VideoMAE、ViViT注意力建模长程依赖,需大数据预训练

SlowFast 用两条通路:慢通路低帧率高分辨率抓外观,快通路高帧率低分辨率抓运动,两者融合。VideoMAE 把 MAE 的掩码重建搬到视频,自监督预训练后微调效果很好。

工程上的现实选择是:算力有限用 2D CNN 加时序聚合(TSN 思路),算力充足用 SlowFast 或 VideoMAE。Kinetics-400 上 SlowFast R50 约 77% Top-1,VideoMAE ViT-B 可达 81% 以上。

10. 采样策略与视频输入

视频输入的采样直接决定模型看到什么。

  • 均匀采样:从整段均匀取 T 帧(常用 8、16、32),覆盖全片段,适合长动作。
  • 密集采样:连续取帧,保留细节运动,适合短促动作。
  • 多片段采样:把视频切成若干段,每段取一帧,兼顾覆盖与效率,TSN 的经典做法。
  • 帧率与时长权衡:30fps 取 16 帧只覆盖 0.5 秒,长动作需要降采样率或增大 T。
import numpy as np

def uniform_sample(num_frames, T=16, seg=2):
    # 多片段均匀采样:切成 seg 段,每段均匀取 T//seg 帧
    per = T // seg
    idx = []
    seg_len = num_frames / seg
    for s in range(seg):
        start = s * seg_len
        idx += [int(start + (i + 0.5) * seg_len / per) for i in range(per)]
    return [min(i, num_frames - 1) for i in idx]

print(uniform_sample(300, T=16, seg=2))

采样之外,视频解码本身常是瓶颈:H.264 解码在 CPU 上串行,大规模处理时要考虑硬件解码或预抽帧。

11. 跟踪评估指标

指标含义关注点
MOTA1 − (FN + FP + IDSW) / GT综合,但 FP/FN 权重高于 IDSW
IDF1ID 维度的 F1,衡量 ID 保持能力身份一致性
HOTA检测与关联的几何平均更平衡的现代指标
ID Switch身份切换次数越低越好
MT / ML大部分跟踪 / 大部分丢失的轨迹占比轨迹完整性
FPS处理帧率实时性

MOTA 的缺陷是它对 ID Switch 的惩罚很轻:ID 换了但框仍覆盖目标时 MOTA 几乎不受影响。因此评估身份保持能力应看 IDF1 与 HOTA。现代跟踪论文普遍同时报 MOTA、IDF1、HOTA 三个数。

import numpy as np

def idf1(gt_ids, pred_ids):
    # 简化版:按 ID 对统计 TP/FP/FN,再算 ID 级 F1
    gt_ids, pred_ids = np.array(gt_ids), np.array(pred_ids)
    tp = int(((gt_ids > 0) & (pred_ids > 0) & (gt_ids == pred_ids)).sum())
    fp = int(((pred_ids > 0) & (gt_ids != pred_ids)).sum())
    fn = int(((gt_ids > 0) & (gt_ids != pred_ids)).sum())
    return 2 * tp / max(2 * tp + fp + fn, 1)

print("IDF1:", round(idf1([1, 1, 2, 2], [1, 1, 3, 2]), 3))

12. 视频推理的算力与延迟优化

视频推理的算力需求是单帧的 N 倍,优化空间很大。

  • 跳帧检测:检测每 2 到 3 帧跑一次,中间帧用跟踪外推,帧率提升明显、精度损失可控。
  • 批处理:把多帧或多路视频拼成一个批送 GPU,吞吐显著提升,方法见 视觉服务化与推理加速 。
  • 硬件解码:用 NVDEC 或 VAAPI 做硬件解码,把 CPU 从解码中解放。
  • 模型选择:检测用 YOLO-n 系列,跟踪用 ByteTrack,ReID 只在必要时启用。
  • TensorRT:导出引擎后 FP16 通常有 1.5 到 2 倍加速,多路并发收益更大。
  • 区域裁剪:只在感兴趣区域(ROI)做高分辨率推理,其余区域降采样。
  • 多路复用:一路 GPU 同时处理多路视频流,提高利用率。

实时场景要区分「端到端延迟」与「帧率」:跳帧检测能提升帧率,但会让检测结果滞后若干帧,交互类应用要谨慎。

权衡取舍

维度倾向代价
TBD 与 JDE中小规模用 TBD,端侧用 JDEJDE 训练复杂、难调试
是否用外观特征遮挡严重时启用 ReID延迟与显存上升
跳帧与逐帧实时优先跳帧检测滞后,快速运动漏检
采样帧数与算力帧数多精度高算力与显存线性上升
MOTA 与 IDF1身份重要时看 IDF1MOTA 掩盖 ID Switch
轨迹保留时长长保留抗遮挡误重认风险上升

几条实用原则:

  • 先确定业务最在意的是「不漏检」还是「ID 不乱」,这决定调参方向。
  • 遮挡密集场景优先上外观特征,稀疏场景 ByteTrack 足够。
  • 跳帧是性价比最高的提速手段,但要评估对 ID 稳定性的影响。
  • 评估必须同时报 MOTA 与 IDF1,只看一个会误导。

常见坑清单

  • 全类别一起做关联:不同类别目标互相抢匹配,需按类别分别跟踪。
  • 轨迹立即删除:未匹配就删,遮挡后重现必然换 ID,应设 max_age。
  • 代价矩阵不做门限:匈牙利算法强行配对无关目标,产生大量错误关联。
  • 检测阈值过高:低置信度的遮挡目标被过滤,轨迹断裂。
  • 忽略相机运动:相机平移时所有 IoU 都低,关联全崩,需做运动补偿。
  • 用 MOTA 汇报身份能力:ID Switch 被掩盖,业务验收不通过。
  • 视频抽帧与训练采样不一致:训练均匀采样,线上连续取帧,分布漂移。
  • 光流预处理成瓶颈:双流网络的光流在 CPU 上算得极慢,需 GPU 光流。
  • 跳帧后忘记时间对齐:跟踪结果时间戳与视频不同步,事件定位偏移。
  • 评估集与线上帧率不同:训练 30fps 测试 10fps,指标不可迁移。
  • 忽略轨迹初始化确认:噪声检测直接建轨迹,假目标泛滥。
  • 批处理维度弄错:视频批的维度顺序与图像不同,静默算出错误结果。

小结

视频理解工程的主线是:逐帧检测 → 卡尔曼预测 → IoU 与外观代价 → 匈牙利匹配 → 轨迹生命周期管理,动作识别则走「采样 → 时空网络 → 片段聚合」这条线。记住三个判断点:关联的质量决定 ID 稳定性、低分框不该直接丢、评估要看 IDF1 而非只看 MOTA。

选型上,中小规模用 ByteTrack 加轻量 ReID,端侧或超高并发考虑 JDE 方案。性能优化优先跳帧与批处理,其次才是换更小的模型。当业务从「谁在哪」升级到「谁在做什么」时,把跟踪结果作为时序模型的输入,是最省工程的组合方式。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机视觉」更多文章

  1. 检测与分割的评估指标
  2. 多模态视觉语言模型
  3. 3D 视觉:点云与深度估计