畅联云平台丨06-Kalman隔帧轨迹预测

2026年9月1日
畅联云平台丨06-Kalman隔帧轨迹预测

Kalman 滤波与隔帧检测:轨迹如何在「无测量帧」上继续走

本文只讲一件事:当检测隔几帧才跑一次时,中间帧没有新的人体框测量,跟踪仍能给出连续轨迹,靠的是 Kalman 预测;有测量时再做 更新。

读完后应能独立想清:隔 3 帧检测、25 帧/秒时,中间帧的框从哪来;为何检测间隔变大时匹配阈值要放宽;过程噪声与测量噪声拧反了,跟拍会变成什么样。


一、问题:检测贵,跟拍不能停

人体检测(YOLO)是扫描阶段最重的算子之一。若 25fps 全帧都跑,算力与延迟都会顶满。折中是 隔帧检测:

  • detect_stride = 1:每帧都检测;
  • detect_stride = 3(默认常见配置):每 3 帧检测一次,中间两帧 不产生新测量。

跟拍与轨迹却必须 每帧都有位置。否则圆镜/裁切窗会在检测帧跳一下、中间帧卡住,观感像坏云台。

图:检测帧把框当测量做 Update;隔帧只靠运动模型 Predict。


二、本项目如何决定「本帧要不要检测」

配置里有检测步长检测步长。每一帧进入跟踪时,先按帧号门控,再把(可能为空的)检测列表交给 ByteTrack:

const bool do_detect = (ctx.cfg.detect_stride <= 1) || (idx % ctx.cfg.detect_stride == 0);
std::vector<PfByteTrackFaceBox> boxes;
if (do_detect) {
 // YOLO → boxes
}
const std::vector<PfByteTrackFaceState> tracks = bt.update(boxes); // 空 boxes 仍 Predict

也就是说:检测列表为空时不是跳过跟踪,而是做「无测量的一步」。

默认与兜底帧率:探测不到合法 源帧率(或配置帧率非法)时,一律按 25.0 帧/秒 处理;ByteTrack 构造也用该帧率。后文数值例子一律按 25fps。


三、时间轴:stride=3 时每一帧在干什么

设帧号 idx = 0,1,2,3,…,检测步长 = 3:

图:实心点为检测帧,空心为仅预测帧;轨迹折线在中间帧仍前进。

数值例子(25fps)

  • 帧间隔 Δt = 1/25 = 0.04 s。
  • 两次检测之间的墙钟时间:3 × 0.04 = 0.12 s。
  • 若人横向速度约 v_x = 80 px/s(慢走横穿 720p),两次检测间位移期望:

Δx ≈ vx × 0.12 = 9.6 像素

中间两帧若完全不预测,圆镜会落后约 3~6 px 再瞬移追上;有匀速预测则每帧挪约 80×0.04=3.2 px,观感连续。


四、ByteTrack 里的 Kalman:状态与模型

ByteTrack 使用经典 8 维匀速模型(中心 + 宽高比/高 + 速度),直觉上可把状态写成(符号与论文/实现细节略有差异,便于心算):

x = [cx, cy, a, h, vx, vy, va, vh]ᵀ

匀速离散预测(Δt=1 帧):

cx ← cx + vx

cy ← cy + vy

a ← a + va

h ← h + vh

协方差同步传播:

P ← F P Fᵀ + Q

有测量 z(检测框)时:

卡尔曼增益 K 由预测不确定度与测量噪声共同决定;

状态按「预测值 + 增益 × 残差」更新;

更新后不确定度通常变小。

  • Q:过程噪声(相信运动模型的程度;大 → 更信测量);
  • R:测量噪声(相信检测框的程度;大 → 更信预测)。

图:R 过大跟手慢;Q 过大框抖;隔帧场景要在两者间找平衡。

位置噪声大致按框高比例取值,速度噪声更小——符合「人不会瞬间瞬移」的先验。


五、封装层:如何把 YOLO 框喂进跟踪器

封装层把检测框转成跟踪器输入;

detections 为空时,objects 为空向量——底层对每条已有轨迹仍执行 predict,丢失缓冲(轨迹缓冲)继续倒数。本项目默认 track_buffer = 30(约 30 帧 ≈ 1.2 s @25fps)。

构造时帧率取整:

source_fps=25 → ByteTrack 内部 frame_rate=25,与输出时间轴一致。


六、为何 stride 大时要放宽 match_thresh

检测变稀后,相邻两次测量之间人已走过更远。IoU 匹配若仍很严,会把「同一个人的下一检测」当成新目标,ID 切换 ,主角锁丢。

跟踪分数下界 / 高分阈值 仍对齐 YOLO 分数阈值(high 至少 score+0.05 且 ≥0.30),与 stride 解耦——放宽的是关联距离门,不是检测分数门。

数值例子:IoU 与位移

设上一检测框 640×360 画面中人框为 x=400,y=200,w=120,h=280。0.12 s 后中心右移 30 px(快跑),新框 x=430,y=200,w=120,h=280。

轴对齐 IoU 近似:

IoU = 交集面积 ÷ 并集面积

交集宽 90、高 280,交集面积 25200;并集 2×33600−25200=42000;

IoU ≈ 25200/42000 = 0.60

match_thresh=0.70 下可能匹配失败;在 0.55 下可通过。这正是 stride≥3 放宽的动机。

扫描启动日志会打印实际阈值:

step= … 匹配阈值=0.55 检测步长=3


七、与 hold / 离线平滑的分工

Kalman 解决的是 扫描期逐帧状态(ByteTrack 输出的人体框序列)。成片跟拍还有后处理:

短时丢检时轨迹会保持一段时间(与检测步长相关,通常可达数十帧);

  • 离线高斯平滑(lens/crop)在锁定轨迹上再滤一次。

不要把三者混为一谈:

Kalman 错了,后面平滑只能「抹平错误」,不能恢复正确身份。


八、完整一步:从帧号到轨迹点

完整一步可以概括为:

  1. 判断本帧要不要跑检测;
  2. 要检测则得到人体框,否则检测列表为空;
  3. 无论是否检测都调用 bt.update(boxes)(空列表表示只预测);
  4. 对每条轨迹:做人脸识别与相似度,记录历史;
  5. 主角锁定后写入定位结果中的跟拍轨迹。

因此调试「中间帧框从哪来」时:看 ByteTrack 输出即可,不必指望中间帧有 YOLO 日志。


九、 Q/R 与隔帧的失效模式

9.1 测量过信(R 过小 / 等效)

检测框抖 → 更新后状态猛跟抖 → 中间帧预测带着错误速度冲出去 → 下一检测再拉回来 → 锯齿。

9.2 模型过信(Q 过小 / R 过大)

人突然加速或转向,预测仍沿旧速度走;下一检测 IoU 掉到 match 阈值以下 → 切 ID。stride 越大越危险,所以本项目用放宽 匹配阈值 补偿。

9.3 长时间无测量

若 轨迹缓冲 耗尽仍无检测,轨迹删除。检测步长=3、buffer=30 时,约可跨过 30 帧无更新(含中间预测帧),墙钟约 1.2 s。极端遮挡仍会丢轨——这是设计边界,不是 bug 。

9.4 数值演练:错误速度的积累

假设更新后误估 v_x = 15 px/帧(实际应为 3)。stride=3 下两次检测间预测位移:

15 × 3 = 45 像素

真实位移 9 px,误差 36 px。下一框 IoU 可能从 0.6 掉到 0.3 以下,触发 ID 切换。离线平滑无法修复切 ID,只能让错误轨迹「更圆滑地错」。


十、source_fps=25 为何重要

Kalman / ByteTrack 的 帧率 影响内部缓冲与部分时间相关逻辑。本项目:

  • 探测不到合法 fps → 25.0;
  • 成片滤镜图大量写死 fps=25;
  • 质量稳定性把运动除以检测步长做等效。

心算时固定:

时间(秒)≈ 帧号 / 25

隔帧检测的「物理秒」间隔是 detect_stride/25,不是「跳过了 stride 个神秘单位」。


十一、调参清单(只动跟踪相关)

建议:先定 stride,再接受自动 match_thresh;不要在 stride=3 时强行把 match 拧回 0.70。


十二、 小结公式卡

隔帧门控:

do_detect = (stride≤1) || (idx % stride == 0)

空测量仍更新跟踪器:

每一帧都调用 bt.update(boxes):有检测就关联并校正,无检测就只预测。

匹配阈值(本项目):

检测步长=1 时匹配阈值约 0.70;步长=2 时约 0.62;步长≥3 时约 0.55。

预测位移量级:

两次检测间位移 ≈ 速度 ×(检测步长 / 帧率)

把这四条背熟,就可以在日志里区分「检测没跑」与「跟踪没跑」,并解释为何中间帧仍有框。


美畅美畅物联畅联畅联云平台视频监控云平台云视频监控视频云平台视频开放平台视频感知云视频接入网关AIoT综合接入网关视频中台物联网中台视频上云网关EhomeI连锁行业智慧交通AI算法