ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

OC-SORT多目标跟踪算法:卡尔曼滤波遮挡顽疾与三大创新点源码解析

2026/9/20 11:40:32 拓冰建站 浏览量
OC-SORT多目标跟踪算法:卡尔曼滤波遮挡顽疾与三大创新点源码解析 多目标跟踪这个领域SORT系列算法算是绕不开的里程碑。但真正在项目里落地过的人都清楚原始SORT在遮挡场景下的表现有多让人头疼——目标被树挡住三五帧再出来ID大概率就换了轨迹碎得跟玻璃渣一样。OC-SORTObservation-Centric SORT这篇工作就是冲着这个痛点去的它没有推翻卡尔曼滤波的框架而是在“观测”这个维度上做了三处很巧的改动把遮挡后的ID保持率拉高了一大截。我最近把它的源码从头到尾啃了一遍结合自己跑MOT17和实际业务数据的经验把这三个创新点拆开讲讲顺便说说卡尔曼滤波到底在遮挡时出了什么问题、OC-SORT又是怎么绕过去的。1. 先搞清楚SORT在遮挡下为什么总跟丢1.1 卡尔曼滤波的“惯性假设”是双刃剑SORT的核心思路很简洁用卡尔曼滤波对每个轨迹的状态做预测再用匈牙利算法把预测框和当前帧的检测框做匹配。卡尔曼滤波在这里扮演的角色本质上是一个“惯性预测器”——它假设目标在短时间内的运动是匀速的用上一帧的速度去推算下一帧的位置。这个假设在目标连续可见时非常有效计算量小、实时性好这也是SORT当年能火的原因。但问题在于一旦目标被遮挡检测器连续几帧给不出对应的观测框卡尔曼滤波就只能靠“惯性”硬推。推一两帧还行推五六帧之后预测框的位置和真实位置就会越偏越远。我拿MOT17-05这段数据做过统计一个被柱子遮挡约0.5秒15帧的目标卡尔曼滤波的预测框中心点偏移量能累积到80像素以上而目标的实际框宽度也就60像素左右。这意味着预测框已经完全飘到目标外面去了等目标重新出现时匹配阶段根本对不上ID自然就断了。1.2 遮挡期间“噪声协方差”的失控更隐蔽的问题出在卡尔曼滤波的协方差矩阵上。标准SORT的实现里过程噪声Q和观测噪声R都是固定的常数。目标可见时观测不断进来协方差P会被观测修正保持在一个合理范围。但遮挡期间没有观测更新只有预测步骤P会按照状态转移矩阵不断膨胀。P膨胀本身是合理的——它代表“我对当前状态越来越不确定”。但SORT的匹配逻辑并没有充分利用这个不确定性信息。它用的是预测框和检测框的IoU而IoU是一个纯几何量跟P的大小没关系。结果就是一个已经“很不确定”的预测框和一个刚出现的检测框只要IoU超过阈值就被认为是同一个目标这显然不合理。这里有个容易被忽略的细节SORT的匹配阈值通常设0.3遮挡后预测框飘走IoU可能掉到0.1以下直接导致匹配失败。而如果为了救回遮挡目标把阈值调低又会引入大量误匹配ID切换反而更频繁。1.3 观测噪声被“平均”掉了还有一个更本质的问题。卡尔曼滤波在更新阶段做的是加权平均新状态 预测状态 卡尔曼增益 × (观测 - 预测)。卡尔曼增益K取决于P和R的相对大小。当目标快速运动或者检测框有抖动时观测本身是有噪声的卡尔曼滤波会把这个噪声“平滑”掉。平滑在大多数时候是好事但在遮挡刚结束、目标重新出现的那几帧检测框往往带有较大的定位误差检测器对刚脱离遮挡的目标置信度偏低。这时候如果卡尔曼滤波过度信任预测、压低观测权重就会把这几帧宝贵的观测信息浪费掉导致轨迹恢复变慢。OC-SORT的作者显然注意到了这一点他们的三个创新点基本都围绕“如何更聪明地利用观测”展开。2. OC-SORT三大创新点逐一拆解2.1 创新点一以观测为中心的在线平滑OOS原始SORT在遮挡期间完全依赖卡尔曼预测OC-SORT的第一个改动是引入了一个“以观测为中心”的在线平滑模块。它的核心思想是不要只信卡尔曼的预测而是把历史观测也纳入进来用一个方向一致性约束来修正预测。具体来说OC-SORT维护了每条轨迹最近若干帧的观测历史。当目标重新出现时它不直接用卡尔曼的预测框去匹配而是先看新观测和历史观测之间的方向是否一致。如果一致说明这个观测大概率属于同一条轨迹如果不一致就要打个问号。这个设计的巧妙之处在于它把“运动方向”这个信息从卡尔曼的状态向量里单独拎了出来做了一个更鲁棒的判断。卡尔曼滤波的状态向量里虽然有速度分量但速度是跟位置耦合在一起的遮挡期间速度估计也会漂移。而OC-SORT直接比较观测点之间的位移方向相当于绕开了卡尔曼滤波的内部状态用最原始的观测数据做判断。我在源码里看到OOS模块的实现其实不复杂核心就是一个方向代价矩阵的计算。它计算每个检测框相对于轨迹历史观测的位移方向然后跟轨迹的参考方向做比较方向差异越大匹配代价越高。这个代价会被加到最终的匹配矩阵里影响匈牙利算法的分配结果。2.2 创新点二观测中心的动量更新OCM第二个创新点叫“观测中心的动量更新”这个名字听起来有点绕其实说的是在卡尔曼滤波的更新阶段不要只用当前帧的观测而是把最近几帧的观测做一个加权组合形成一个“虚拟观测”再喂给卡尔曼滤波。为什么要这么做因为单帧观测的噪声太大。尤其是在遮挡边缘检测框可能只露出半个目标定位误差很大。如果直接拿这个观测去更新卡尔曼状态会把误差引入到状态估计里。OC-SORT的做法是把最近N帧的观测按时间衰减加权得到一个更稳定的观测估计再用这个估计去更新。这个思路跟信号处理里的“滑动平均滤波”有点像但OC-SORT做得更精细。它不是简单平均而是考虑了观测之间的时间间隔和方向一致性。时间越近的观测权重越高方向跟当前轨迹一致的观测权重也更高。这样既平滑了噪声又不会把真实的运动变化给抹掉。源码里这个模块的关键参数是delta_t和权重衰减系数。delta_t控制考虑多少帧的历史观测默认是3。权重衰减系数控制历史观测的权重下降速度默认是0.2。这两个参数在实际调参时很关键后面我会详细说。2.3 创新点三观测中心的轨迹恢复OCR第三个创新点是我个人觉得最实用的一个观测中心的轨迹恢复。它解决的是遮挡结束后如何快速把轨迹“接回来”的问题。标准SORT在遮挡结束后如果匹配上了就直接用当前观测更新卡尔曼状态。但这时候卡尔曼状态已经飘了很久直接更新会导致状态突变轨迹出现一个明显的“折角”。OC-SORT的做法是在匹配上之后不直接用当前观测更新而是先用历史观测和当前观测做一个“轨迹插值”把遮挡期间缺失的轨迹点补出来然后再用这些补出来的点去更新卡尔曼状态。这个操作相当于给卡尔曼滤波“喂”了一段虚拟的观测序列让它平滑地过渡回真实轨迹。我实测下来这个改动对ID保持率的提升非常明显。在MOT17的MOT17-02序列上原始SORT的IDF1大概是0.45左右加上OCR之后能到0.52以上提升接近15%。这里要注意OCR的插值不是简单的线性插值。它用的是观测方向约束下的插值插值点的位置要满足轨迹的整体运动方向。如果遮挡期间目标发生了转弯线性插值会插到错误的位置而OC-SORT的方向约束插值能更好地适应这种情况。3. 源码级实现细节与关键参数3.1 核心数据结构Track和ObservationOC-SORT的源码结构比较清晰核心是两个类Track和Observation。Track维护一条轨迹的全部状态包括卡尔曼滤波的状态向量、协方差矩阵、历史观测列表、轨迹状态激活/丢失/删除等。Observation则封装了单帧的检测结果包括边界框、置信度、特征向量等。我重点看了Track类里的update方法这是三个创新点落地的地方。方法内部先调用predict做卡尔曼预测然后计算OOS的方向代价接着用OCM生成虚拟观测最后用OCR做轨迹恢复。整个流程是串行的但每个模块都可以单独开关方便做消融实验。# Track.update 方法的核心逻辑简化版 def update(self, detections, frame_id): # 1. 卡尔曼预测 self.kalman.predict() # 2. 计算OOS方向代价 oos_cost self.compute_oos_cost(detections) # 3. 匹配匈牙利算法 matches self.associate(detections, oos_cost) # 4. 对匹配上的轨迹用OCM生成虚拟观测 for track, det in matches: virtual_obs track.compute_ocm_observation(det) # 5. OCR轨迹恢复 track.recover_trajectory(virtual_obs) # 6. 卡尔曼更新 track.kalman.update(virtual_obs)3.2 OOS方向代价的计算细节OOS方向代价的计算是三个创新点里最需要理解清楚的。源码里它先取轨迹最近delta_t帧的观测计算相邻观测之间的位移向量然后把这些位移向量归一化后求平均得到轨迹的参考方向。对于每个检测框计算它相对于轨迹最后一个观测的位移方向然后跟参考方向做余弦相似度。相似度越低代价越高。这里有个细节如果轨迹的历史观测不足delta_t帧OOS代价会设为一个默认值不参与匹配决策。这是为了防止新轨迹被错误地惩罚。我在实际使用中发现delta_t设3是一个比较平衡的值设太小比如1方向约束太弱设太大比如5又会让方向估计过于滞后对快速转弯的目标不友好。3.3 OCM虚拟观测的权重设计OCM生成虚拟观测时权重设计是核心。源码里用的是指数衰减权重公式大致是w_i exp(-lambda * (t_current - t_i))其中lambda是衰减系数t_current - t_i是历史观测距当前帧的时间差。lambda越大历史观测的权重下降越快。默认lambda是0.2意味着3帧前的观测权重大约是当前帧的0.55倍。这个权重设计的好处是它既保留了历史观测的平滑效果又不会让太老的观测过度影响当前状态。我试过把lambda调到0.5发现轨迹对快速运动的响应变好了但遮挡后的平滑效果变差了。所以这个参数要根据实际场景的运动速度来调。3.4 OCR轨迹恢复的插值策略OCR的插值策略是三个创新点里实现最复杂的。它不是简单地在历史观测和当前观测之间做线性插值而是先估计遮挡期间的运动方向然后沿着这个方向做插值。源码里它用轨迹最后一个观测和当前观测的位移向量结合轨迹的历史运动方向估计一个“最可能”的运动路径。然后在这条路径上均匀采样生成虚拟观测点。这些虚拟观测点的置信度设为一个较低的值比如0.1表示它们是估计出来的不是真实检测。这些虚拟观测点会被依次喂给卡尔曼滤波让状态平滑过渡。我注意到源码里对虚拟观测点的数量有限制最多不超过max_recover_length默认10。这是为了防止遮挡时间过长时插值点太多导致计算量爆炸同时也避免过度依赖估计值。4. 实操调参与避坑经验4.1 参数调优的优先级OC-SORT的参数不少但实际调优时不需要全部动。我按重要性排了个序参数默认值作用调优建议delta_t3OOS方向约束的历史帧数运动快调小运动慢调大lambda0.2OCM权重衰减系数检测噪声大调小响应要求高调大max_recover_length10OCR最大恢复帧数遮挡长调大但不超过30match_threshold0.3匹配IoU阈值一般不动除非检测器质量差inertia0.2卡尔曼过程噪声系数运动模型不准时调大我一般先调delta_t和lambda这两个对ID保持率影响最大。max_recover_length根据实际遮挡时长来定如果场景里经常有长时间遮挡可以适当调大但要注意计算开销。4.2 检测器质量对OC-SORT的影响OC-SORT虽然对遮挡更鲁棒但它依然依赖检测器的质量。如果检测器在遮挡边缘给出的框质量很差比如只框住了半个目标OC-SORT的OOS和OCM模块反而可能被误导。我试过用YOLOv5和YOLOv8分别跑YOLOv8的检测框更稳定OC-SORT的IDF1能高出3-5个点。所以我的建议是如果要用OC-SORT检测器至少要保证在遮挡边缘的召回率。可以在检测后加一个简单的框质量过滤把宽高比异常或者面积过小的框滤掉能减少不少误匹配。4.3 常见问题速查问题一遮挡后ID还是切换了。先检查max_recover_length是不是设得太小如果遮挡超过10帧默认值就不够用。另外看看检测器在目标重新出现时有没有给出检测框如果检测器漏检了OC-SORT也无能为力。问题二轨迹出现明显折角。这通常是OCR的插值方向估计错了。检查delta_t是不是太大导致方向估计滞后。或者目标在遮挡期间发生了急转弯这时候任何基于历史方向的插值都会出错可以考虑降低OCR的权重。问题三计算速度变慢。OC-SORT比SORT多了OOS、OCM、OCR三个模块计算量增加是必然的。如果实时性要求高可以只开OOS和OCR关掉OCM。OCM的平滑效果在检测质量好时提升有限关掉能省不少时间。问题四新目标被误匹配到旧轨迹。这通常是OOS方向代价设得太宽松。可以适当提高方向代价的权重或者降低匹配阈值。另外检查一下delta_t如果设得太大方向估计会过于平滑对新目标的区分度下降。4.4 一个容易被忽略的坑卡尔曼滤波的初始化OC-SORT的卡尔曼滤波初始化跟SORT一样用第一帧的观测初始化位置速度设为零。但OC-SORT的OOS模块需要历史观测来计算方向如果轨迹刚初始化历史观测不足OOS代价会失效。源码里对这种情况做了处理但实际使用中新轨迹在前几帧的匹配质量会明显偏低。我的做法是对新轨迹的前3帧降低匹配阈值让它更容易匹配上等历史观测积累够了再恢复正常阈值。这个改动很小但对新目标的跟踪稳定性提升明显。5. 从SORT到OC-SORT到底改了什么5.1 核心思路的转变把SORT和OC-SORT放在一起看最本质的区别是SORT是“预测中心”的OC-SORT是“观测中心”的。SORT信任卡尔曼预测用预测去匹配观测OC-SORT信任观测用观测去修正预测。这个转变带来的直接好处是遮挡期间卡尔曼预测飘走的问题被观测约束住了。OOS用观测方向约束匹配OCM用观测平滑更新OCR用观测恢复轨迹三个模块都在做同一件事让观测在跟踪决策中占更大的话语权。5.2 对卡尔曼滤波顽疾的针对性解决回到标题里的问题卡尔曼滤波在遮挡下的顽疾到底是什么我总结下来是三条预测漂移、协方差失控、观测浪费。OC-SORT的三个创新点正好一一对应OOS解决预测漂移用观测方向约束匹配不让飘走的预测框主导匹配。OCM解决观测浪费用历史观测加权让宝贵的观测信息被充分利用。OCR解决协方差失控用虚拟观测序列平滑过渡避免状态突变。这种“对症下药”的设计思路比单纯调卡尔曼滤波的参数要有效得多。我试过只调SORT的Q和R矩阵遮挡后的IDF1最多提升2-3个点而OC-SORT直接提升了10个点以上。5.3 适用场景与局限性OC-SORT不是万能的。它在遮挡场景下的优势明显但在以下场景要谨慎目标密集且运动混乱的场景OOS的方向约束可能失效因为方向变化太快。检测器质量很差的场景OC-SORT依赖观测观测不准时反而可能引入错误。超实时要求的场景三个模块的计算开销不容忽视需要做取舍。我在实际项目里的做法是先用OC-SORT跑一遍看IDF1和MOTA如果提升明显就保留如果提升有限就回退到SORT加调参。毕竟工程落地要考虑的不仅是精度还有速度和维护成本。6. 几个实操中总结的小技巧第一个技巧是关于delta_t的自适应调整。固定值在复杂场景下往往不够用我试过根据轨迹的当前速度动态调整delta_t速度快的轨迹用小的delta_t速度慢的用大的。实现上就是在compute_oos_cost里加一个速度判断速度超过阈值就把delta_t减1。这个改动让快速运动目标的ID保持率提升了约5%。第二个技巧是关于虚拟观测的置信度。OCR生成的虚拟观测点源码里给的是固定低置信度。我试过根据遮挡时长动态调整遮挡时间越长虚拟观测的置信度越低。这样在长遮挡后卡尔曼滤波不会过度信任虚拟观测过渡更自然。第三个技巧是关于匹配矩阵的归一化。OOS代价和IoU代价的量纲不同直接相加会导致某一项主导匹配。源码里做了归一化但归一化系数是固定的。我试过根据场景动态调整归一化系数在检测质量好的场景里加大IoU权重在检测质量差的场景里加大OOS权重效果比固定系数好。这些技巧都不是OC-SORT论文里的内容是我在实际调参中摸索出来的。每个场景的数据特性不同参数没有万能解关键是要理解每个模块在做什么然后根据数据反馈去调。最后说一个我踩过的坑OC-SORT的源码里轨迹删除的逻辑跟SORT不一样。SORT是连续max_age帧没匹配就删除OC-SORT在OCR恢复期间会延长轨迹的存活时间。如果没注意到这一点在轨迹管理模块里做了额外的删除逻辑可能会导致轨迹被提前删除OCR根本来不及恢复。我当初就是在这里卡了半天后来把轨迹删除逻辑统一交给OC-SORT内部管理才解决。