畅联云平台丨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 错了,后面平滑只能「抹平错误」,不能恢复正确身份。
八、完整一步:从帧号到轨迹点
完整一步可以概括为:
- 判断本帧要不要跑检测;
- 要检测则得到人体框,否则检测列表为空;
- 无论是否检测都调用 bt.update(boxes)(空列表表示只预测);
- 对每条轨迹:做人脸识别与相似度,记录历史;
- 主角锁定后写入定位结果中的跟拍轨迹。
因此调试「中间帧框从哪来」时:看 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。
预测位移量级:
两次检测间位移 ≈ 速度 ×(检测步长 / 帧率)
把这四条背熟,就可以在日志里区分「检测没跑」与「跟踪没跑」,并解释为何中间帧仍有框。
