视频里一群人陆续经过屏幕上一堆检测框在跳。如果只看单帧结果你完全说不清第 3 帧的那个框和第 7 帧的那个框是不是同一个人。只做目标检测每一帧都是“失忆”的独立案件同一个行人换个位置就成了新嫌疑人。多目标跟踪MOT这个领域核心就是给每个检测框发一张长期有效的“身份证”——从目标出现到消失ID 不能乱person 还是那个 person。这篇文章不堆论文公式就讲讲 Trackers 在工程上到底怎么落地、检测框输出之后系统内部发生了什么、以及我实际调跟踪器时踩过的那些坑。适合刚做完目标检测、想把单帧检测升级成视频级分析的开发者也适合已经被 ID 跳变、轨迹断裂折磨到怀疑人生的老哥们。1. 给检测框当“户籍警”MOT 要解决的到底是什么1.1 单帧检测的“失忆”问题先说清楚一个基本事实主流检测算法YOLO、DETR、FCOS 这些本质上是静态图像的单帧分类加回归任务。它们没有时序概念对每一帧独立输出一组[cls, conf, xyxy]。你把几百帧视频逐帧送进检测器得到的是一堆彼此毫不相干的框列表。这时候问题就来了。我想统计一个路口 10 分钟内过来了多少辆车如果只靠检测结果同一辆车在第 5 帧、第 50 帧、第 500 帧出现的三个框会被当成三辆车重复计数直接爆炸。我想分析某个行人是不是在禁入区域徘徊也需要知道他此前 30 秒的轨迹。这些下游需求全部依赖一个能力跨帧确认“同一个目标”。多目标跟踪做的就是这件事。它维护一个轨迹集合每来一帧把新的检测框和已有轨迹做匹配建立“哪一帧的哪个框对应哪一个目标”的时间对应关系。在领域里这个环节叫数据关联Data Association。可以这么理解检测器负责抓人跟踪器负责登记档案、维护身份、判断这个人什么时候走了。1.2 没有跟踪很多应用根本没法做举几个真实场景你就明白了。交通路口车辆计数同一辆车在视频里存在 2 秒按 30fps 就是 60 帧没有跟踪器这 60 帧里的所有框都会被当成 60 辆不同的车。商场客流统计需要知道一个人从入口走到出口的完整链路中途穿过人群、短暂被遮住都不能断。体育赛事球员轨迹分析每个球员必须保持唯一 ID篮球传给谁、谁跑了多少距离全靠 ID 一致性。自动驾驶/机器人避障不仅要看到障碍物还要预测它的运动方向这要求对目标做速度估计单帧检测拿不到速度信息。安防区域逗留检测人站在画面里没走如果检测框每帧乱跳 ID系统会认为“10 个人连续进入禁区”而不是“1 个人停留在禁区”。所以跟踪不是检测的锦上添花而是从“看见”升级到“理解”的必经步骤。只要你的业务逻辑里带着“同一个”、“连续”、“累计”、“轨迹”这些词你就绕不开 MOT。1.3 先分清MOT 和 OpenCV 里的单目标跟踪不是一回事这里必须插一嘴因为太多新手在第一步就搞混了。OpenCV 自带的 KCF、CSRT、MedianFlow 这类 trackers是单目标跟踪SOT给一个初始框跟着这个目标走目标一旦丢失基本就废了。而多目标跟踪MOT面对的是画面中任意数量、随时出现和消失的目标要做的是全局身份管理。也有一种工程策略叫“检测跟踪插值”每隔 N 帧跑一次检测器中间帧用 KCF 之类的手工特征跟踪去补框以减少检测开销。这是性能优化手段不是本文要讲的 MOT 范畴。你要做视频级的多目标分析正确姿势是走 Tracking-by-Detection 路线也就是下面要展开的核心方案。2. 核心方案拆解卡尔曼滤波、级联匹配与主流 Trackers 选型2.1 TBD 范式检测是眼睛跟踪是大脑现在工业界最稳的多目标跟踪思路叫 Tracking-by-DetectionTBD翻译过来就是“先检测再关联”。每个跟踪器本质上维护一组轨迹对象每来一帧新图像流程分三步先让检测器跑出所有框再把新框和已有轨迹匹配最后更新轨迹状态。为什么大家几乎都选这个范式而不是搞端到端联合模型因为解耦。检测技术迭代快今天 YOLOv8明天 DETR后天可能又有新结构但跟踪模块相对稳定卡尔曼加匹配这套逻辑已经跑了很多年。检测和跟踪解耦以后换个检测器只需要改输入格式跟踪逻辑一行不用动。端到端方案如 FairMOT、JDE 等检测和 ReID 特征共享一个网络在 MOTChallenge 榜单上确实亮眼但训练复杂、调参痛苦、部署灵活度低工程上除非有专门的算法团队持续投入否则 TBD 依然是首选。2.2 SORT、DeepSORT、ByteTrack到底怎么选这三者是 TBD 路线里最有代表性的方案也是面试和实战中出现频率最高的名字。先给一张对比表再逐个说清楚。方案核心匹配依据是否需要重识别特征速度抗 ID Switch 能力适合场景SORT卡尔曼预测 IoU不需要极快弱简单场景、实时性要求极高的边缘设备DeepSORT马氏距离门控 级联匹配 外观余弦距离需要 ReID 网络较慢强拥挤人群、遮挡多、需要稳定身份的场合ByteTrack两轮 IoU 匹配高分组低分组不需要快中上遮挡场景下性价比最高近年工业落地首选SORT 是最朴素的实现用卡尔曼滤波器预测每个轨迹的下一帧位置然后用匈牙利算法做基于 IoU 的匹配。它快且简单但完全不在乎目标长什么样所以一旦两个目标靠近交叠ID 很容易互换。它的名字全称是 Simple Online and Realtime Tracking直白点说我就是要简单、在线、实时。DeepSORT 在 SORT 基础上补了一块外观特征Appearance Feature。它用一个额外的 ReID 小网络通常基于行人重识别任务训练把检测框内的图像编码成特征向量计算两两之间的余弦距离再和运动模型给出的马氏距离一起作为匹配依据。这个改动让它在拥挤场景下的 ID 保持能力大幅提升代价是多一次神经网络的推理。ByteTrack 是 2021 年出现的后起之秀核心洞察非常朴素检测器为了降低误检通常把置信度低于 0.5 的框全部丢掉。但在遮挡场景中很多本来是真目标的框被压到 0.1~0.5 之间这些低分框一旦被丢轨迹就断给“身份证注销”了。ByteTrack 不丢低分框而是把检测框按分数切两档第一轮用高分框和所有已有轨迹匹配第二轮把没匹配上的轨迹拿去和低分框匹配。这个“捡漏”操作让它在没有 ReID 的情况下把关联效果拉到接近甚至超过 DeepSORT 的水平而且速度快得多。我在项目中选型的经验是如果场景简单、目标稀疏、帧率又高SORT 完全够用如果目标密集、经常互相遮挡且你有算力跑 ReIDDeepSORT 仍然是稳定可靠的选择如果你既想要速度又不想牺牲太多精度无脑先上 ByteTrack 当基线实测效果通常能打。现在 Ultralytics 的 YOLO 库里内置的也是 ByteTrack 和 StrongSORT可见这个选择已经被主流工程社区验证过。2.3 三个核心组件分别解决了什么不管用哪个方案都会碰到三个基础组件卡尔曼滤波、匈牙利匹配、ReID 特征。这里用大白话把原理讲透。卡尔曼滤波在跟踪里承担“运动预测与状态平滑”。它不神秘本质是对匀速或匀加速运动的假设做一个带不确定性估计的预测。典型的状态向量是[中心点x, 中心点y, 宽高比, 高度, 以及对应的速度分量]。它最大的工程价值是目标被暂时遮挡时没有检测框反馈给它它可以靠历史状态继续外推位置撑着轨迹不断。有了这个“预测”能力跟踪才能在短时遮挡时不断链。匈牙利算法解决的是“怎么分配检测框和轨迹”的组合优化问题。它接收一个代价矩阵比如 3 条轨迹和 5 个检测框算出每个轨迹匹配每个检测框的代价然后找出总代价最小的一对一分配方案。注意这跟贪心策略完全不同贪心是每次挑最小的匹配可能导致全局浪费匈牙利算法保证全局最优。在实现上你不需要会手写它调用scipy.optimize.linear_sum_assignment一行命令就行。ReID 特征就比较直白了。检测框内的行人或车辆图像被一个小网络压缩到 128 维或 512 维的特征向量两个向量越近表示外观越像。不过要小心ReID 模型在不同相机视角、不同光照下的鲁棒性有限它不是万能的。DeepSORT 里它和运动模型是互补关系——运动模型负责“近处锁定”外观负责“远处和遮挡时认人”。3. 从零落地数据流、状态管理与参数怎么定3.1 打通数据流从检测输出到跟踪结果在实际项目里跟踪器不是独立存在的它接在检测器后面。整个数据流大概是视频解码成帧序列。检测器输出每一帧的[x1, y1, x2, y2, class, conf]列表。给跟踪器传入当前帧的检测框列表和帧号。跟踪器内部做单帧数据关联返回[x1, y1, x2, y2, class, conf, track_id]。下游业务比如绘制框、计数、轨迹分析直接消费track_id。这里有一个非常容易出错但经常被忽略的细节坐标统一。YOLO 有些输出的是归一化坐标有些输出像素坐标还有的检测框架输出cxcywh。你接入跟踪器之前务必统一成同一个坐标系。我的习惯是全部转成像素坐标系下的xyxy因为可视化调试、交并比计算都最直接而且cxcywh到xyxy的转换容易在宽高比计算中埋雷。另外一个规范化问题检测框的过滤阈值应该放在检测侧而不是跟踪侧。我在早期实验里习惯把置信度阈值调到 0.3然后指望跟踪器自己去“消化”误检。后来发现这是错误的。跟踪器最怕的是不稳定的输入——一会儿误检一个框一会儿漏检一个真目标。这会让轨迹一会被误检骗出新 ID一会因为漏检断链。正确的做法是先用较高的置信度阈值比如 0.5在检测侧过滤掉明显噪声把跟踪器喂给一个相对干净的数据流再用跟踪器自己的状态机去处理边缘情况。3.2 轨迹生命周期出生、转正、失踪与注销跟踪器内部维护着一堆轨迹对象每个对象都有明确的生命周期状态。以 SORT/DeepSORT 这个体系为例状态流转是Tentative未确认一个新检测框刚出现被创建成候选轨迹。此时它没有正式 ID还在观察期。Confirmed已确认候选轨迹连续命中min_hits帧典型值 2~3后转正成正式轨迹开始对外输出 ID。Lost丢失已确认的轨迹在某帧没有匹配到任何检测框进入丢失状态。此时轨迹保留但不再输出结果内部继续用卡尔曼预测位置等待找回。Deleted删除丢失轨迹在max_age帧内一直找不回匹配轨迹销毁身份证号作废。为什么需要“观察期”这个设计因为检测器偶尔会出幽灵误检。如果第一次出现就发正式 ID背景里的一团噪点会立刻变成一个假目标。让候选轨迹“考察”三帧连续命中才转正能过滤掉大量随机误检实现成本几乎为零。我强烈建议先别追求算法上的花活把状态管理做扎实很多 ID 跳动问题已经能解决一半。3.3 匹配流程与参数取值一张表理清关键超参数用 DeepSORT 的级联匹配流程作为模板完整跑一遍数据关联的逻辑用卡尔曼滤波预测所有已确认轨迹在当前帧的边界框位置。计算每条轨迹和每个检测框之间的代价运动代价用马氏距离考虑协方差外观代价用特征余弦距离。设置门控Gating阈值马氏距离超过卡方分布阈值自由度 4 的 95% 分位约 9.4877 的平方根的匹配对直接排除。进入级联匹配按上次匹配成功距今的帧数从小到大排序优先处理最近匹配过的轨迹因为它们最有把握逐层匹配。剩余未匹配的轨迹和检测框再做一次 IoU 匹配兜底处理外观特征失效的情况。未匹配检测创建 Tentative 轨迹未匹配且是 Confirmed 态的轨迹进入 Lost 或按max_age删除。核心超参数怎么选我实测的经验值如下参数作用常用值调参思路max_age丢失轨迹最大保留帧数30~60按目标最长遮挡时间设比如被柱子遮挡 0.5 秒 30fps至少设 15 帧起步min_hits候选轨迹转正所需连续命中帧数2~5误检多就调大目标小且不清晰时调小confidence检测框置信度阈值0.5 左右结合检测器质量调整这是全局最重要的一个参数max_cosine_distance外观特征匹配阈值0.2~0.4越小越严格越大允许更多外观不同的对象匹配gating_threshold马氏距离门限自由度49.4877一般不动控制匹配的空间范围IoU_match_thresholdIoU 匹配阈值0.3~0.5太低会乱配太高会导致该配的配不上以一个 30fps 的行人视频为例。画面里的人从摄像头上方走到下方通常需要 2~3 秒期间可能被路过的另一个人挡 0.3 秒。此时max_age我通常给 15~20因为遮挡时间不到 10 帧给 30 帧的余量已经绰绰有余。而场景里有大量人互相穿插时ID Switch 很可能不是因为max_age不够而是外观特征区分度不足这时候你把max_cosine_distance从 0.4 调低到 0.2反而能减少误配。ByteTrack 的参数又不太一样它是两轮 IoU 匹配。第一轮高分框的阈值为 0.5第二轮低分框的区间为 0.2~0.5IoU 阈值第一轮用 0.5第二轮降为 0.2。为什么第二轮 IoU 阈值要降因为第二轮匹配的是丢失轨迹和低分框本来就是“捡漏”条件放宽松一点能捞回更多被遮挡的目标。重要提醒max_age不是越大越好。设得太大那些早已离开画面的轨迹会一直占用匹配资源还可能把新出现的检测框误配到一个老轨迹上导致 ID 迟迟不更新、位置却是全新目标造成诡异的长距离跳变。实际调参时我习惯统计一下业务场景里目标最长被遮挡的时间然后乘 1.5 到 2 倍的余量。3.4 评估指标MOTA、IDF1、HOTA 怎么读调参不能凭感觉得有指标。MOTChallenge 官方排行榜上常见三个指标含义各不相同。MOTAMultiple Object Tracking Accuracy多目标跟踪准确率计算方式是1 - (FP FN IDSW) / GT总数。它综合了误检、漏检和 ID 切换三种错误。看似是百分比其实没有上限误检严重时可能为负数。IDF1身份保持得分把跟踪结果按 ID 划分衡量预测 ID 与真实 ID 的匹配程度。它更关注“身份证”发得对不对。MOTP定位精度衡量匹配成功的轨迹对检测框与真值框之间的重叠程度。只关心框准不准不关心身份。一个常见的现象是MOTA 高但 IDF1 低。这往往意味着系统把目标轨迹切成了好几段每段都跟踪得挺准但 ID 反复变化——对计数来说还能接受对轨迹分析来说就是灾难。反过来IDF1 高但 MOTA 低说明 ID 保持得不错但漏检和误检一堆整体检测质量拖了后腿。工程上我的建议是先保证检测质量再用跟踪指标来判断调参方向。如果你连检测器的 AP 都没跑稳跟踪指标优化得再辛苦也是白搭。跑实验前先把验证集的帧率、分辨率、场景复杂度固化成配置否则对比毫无意义。我自己常用 MOTChallenge 的 MOT17 或者自己录一小段带标注的视频作为基准确保每次改动都能用数字看到增益。4. 实战排雷症状、对策与部署性能4.1 ID 乱跳、轨迹断裂、位置漂移怎么排查跟踪器的大部分故障症状是表象根源常常不止一处。我直接把最常碰到的几类问题整理成排查表症状可能性原因对策ID Switch 频繁检测置信度阈值过低大量低分误检干扰匹配或 ReID 特征区分度不足先调高检测阈值再评估外观特征的度量确认场景光照变化是否导致外观特征退化轨迹断裂 后新建不同 ID目标遮挡超过max_age检测漏检导致轨迹丢失后未能找回调大max_age如果漏检集中在特定位置考虑在该区域降低置信度过滤阈值跟踪框周围抖动/漂移卡尔曼滤波的过程噪声参数不合适检测框在目标来回移动时不够稳定调低状态速度方差配合帧率修正时间步长检查是否开启了相机运动补偿目标静止却发 Ghost 位置卡尔曼速度状态在目标停留时仍在累积导致预测位置持续外推设置速度冻结连续几帧位移小于阈值时把速度分量强制置零两个目标贴近后 ID 互换外观特征未参与匹配或余弦阈值过宽松启用级联匹配中的特征距离门限降低max_cosine_distance必要时换更强的 ReID 模型这里单独展开讲一个我印象很深的案例。之前做一个人脸密集的超市入口客流统计检测框每帧密度极高人贴着人走DeepSORT 的 ID Switch 惨不忍睹。我一开始疯狂调max_cosine_distance从 0.4 一直降到 0.1结果更糟由于目标在画面里占像素少ReID 特征本身可分性差太严格的门限导致大量本应匹配的轨迹配不上轨迹断裂更严重。后来回头查根因发现检测器的 NMS 阈值设得太低同一个人的原图、镜像、局部遮挡框全部输出来了跟踪器在好几条关于同一个人的假轨迹之间摇摆。把 NMS 阈值从 0.4 提到 0.6ID Switch 直接降低了近一半。这就是我反复强调检测稳定性的原因。4.2 遮挡、拥挤、静止目标这种场景专门说三句遮挡是最常见的跟踪失败诱因。行人被路灯杆挡住、车被前车挡住、球被球员身体挡住都属于短时遮挡。应对的核心思路是让轨迹“撑住”max_age给足余量配合卡尔曼预测让轨迹在无检测帧继续外推。但外推做不了太久超过 1 秒基本不可信因为目标可能已经转向。所以在实践中我采用两级策略短时遮挡靠卡尔曼硬撑长时遮挡靠 ReID 外观特征在目标重新出现时“认回来”。识别遮挡帧也有个小技巧如果一个轨迹的检测框连续 3 帧的 IoU 全为 0基本可以判定目标被遮了此时把它的优先匹配级提高用它最后出现的位置附近优先搜索。拥挤场景中低分框策略ByteTrack 思路非常管用。标准做法导致低分框被丢弃的瞬间恰好就是目标发生遮挡、置信度下降的时候。ByteTrack 把原本会被丢弃的 0.2~0.5 分框捡回来专门去匹配丢失轨迹等于给遮挡中的目标留了一条“救援通道”。我实测在密集行人和球类运动数据上这一招对 ID 保持的增益比任何 ReID 调参都来得更快。静止目标对应的是另一类问题。拿商场柜台前的人举例目标停住不动卡尔曼滤波器会怎么处理它有一个速度状态目标不运动时速度更新会不断趋近于零但受检测框抖动影响速度值会在零附近震颤导致预测框小幅漂移。积累几十帧后就可能出现“人在原地框在缓慢平移”的现象。我用的有效手段是在更新阶段计算检测框与预测框的中心点距离如果连续多帧位移小于 2 像素就把速度分量直接置零不再累积。4.3 部署性能与工程妥协在实际项目中跟踪器的推理开销要单独算账。SORT 那套卡尔曼加匈牙利处理几百个目标也只要几毫秒可以忽略不计。真正的性能大头有三个检测器前向推理、ReID 特征提取、以及大量目标存在时的全连接代价矩阵计算。DeepSORT 相比 ByteTrack 多出的 ReID 网络代价相当于又跑了一个小型检测模型末端部署时容易成为吞吐瓶颈。工程上常用三个优化手段。第一降低特征提取频率。外观特征不需要每一帧都更新连续 5 帧内目标运动范围有限运动模型足够维持匹配每隔 3~5 帧才提一次 ReID 特征整体效果下降很小。第二控制代价矩阵规模。当画面中目标数量超过 50 个时全连接代价矩阵是 50×50 的规模匈牙利算法复杂度约为 O(n^3)虽然绝对值不大但每帧都要算一遍。可以先用 IoU 粗筛出候选匹配对再对候选对计算精细代价把大部分不相关的轨迹和检测框挡在门外。第三选择轻量级 ReID 或干脆不用 ReID。用 MobileNet 这类轻量骨干提取 128 维特征在大多数行人场景下效果和重模型接近速度却快几倍。要是业务场景目标不算特别密集直接上 ByteTrack 彻底省掉 ReID 环节是性价比最高的选择。我做过一个边缘盒子上的实验用 YOLOv8s 检测 ByteTrack 跟踪在 Jetson Orin Nano 上跑 1080p 视频可以达到 25fps 以上CPU 占用稳定。而同样的硬件换成 YOLOv8s DeepSORTReID 每次推理要吃 8~12ms整体帧率掉到 18fps 左右。所以除非业务对 ID 保持有硬性要求否则我一般默认先上 ByteTrack。再提醒一句把跟踪器部署到多线程管道时检测和跟踪不要串行等待。用双缓冲让检测线程和跟踪线程并行跟踪消费上一帧检测缓冲区里的结果吞吐能再涨 10%~20%。最后分享一个我长期坚持的实测习惯拿到一个 MOT 任务第一件事不是跑模型、调参数而是先录一段运营环境的视频确定三类数据——目标最小尺寸、最长遮挡时间、同一画面最大目标数。这三个数字分别决定了检测器的输入分辨率、max_age的取值和匹配阈值量级。多数跟踪项目翻车翻在算法没有适配现场而不是算法本身不够新。踩过这些坑之后我最大的体会就是Trackers 本身并不神秘真正拉开差距的永远是你在它前面把检测质量磨得有多稳、后面把参数配对现场数据有多仔细。先跑通 SORT 或 ByteTrack 基线再按需升级 DeepSORT比一上来就扎进复杂算法的泥潭里要省太多时间。
