1. 检测器已经把行人框出来了为什么还要再做一层跟踪1.1 单帧检测的三大痛点抖动、漏检、跳变刚开始接触目标跟踪的同学最容易产生的一个念头是检测器不是已经把行人框出来了吗直接把这些框连成线就是轨迹啊为什么还要额外加一个卡尔曼滤波我在实际项目里也是从这条路走过来的。第一次把YOLO的检测结果直接按帧画成轨迹立刻被三个问题教育了。第一个是抖动。行人站在原地等红灯时检测框中心并不会乖乖停在同一个点而是每帧都在几个像素范围内乱跳。把这种坐标直接画出来轨迹就是一条毛茸茸的噪声带后面要做速度估计或行为判断时根本没法用。第二个是漏检。检测器再强也有失手的时候——行人被广告牌挡住、穿深色衣服融进背景、运动模糊严重都可能让某几帧没有检测框。一旦中间断了两三帧拿到的轨迹就断成几截后端的统计直接出错。第三个是跳变。相邻两帧偶尔会出现检测框错位前一秒框在左肩后一秒框到右胯。这种异常值不处理轨迹会出现一个尖锐的毛刺算速度时会瞬间得到一个离谱的数值。这三大痛点本质上是同一个原因单帧检测没有记忆。它每一帧都是从零开始找行人从不参考上一帧的结果。而卡尔曼滤波干的事情就是给检测结果加一层“时序记忆”——用运动模型把帧与帧之间的信息串起来在检测准的时候平滑噪声在检测丢失的时候用预测顶上去再把跳变当作异常观测压低权重。1.2 卡尔曼滤波在行人跟踪里的真实角色很多教程把卡尔曼滤波说成“预测行人位置”的算法这个说法容易让人误解。它更像是一个带权重的数据融合器一边相信我们自己的运动模型预测出来的位置一边相信检测器当前帧报上来的位置两边都有不确定性谁更可信就多信谁。最终输出的位置是一个比单帧检测更稳定、更有物理约束的估计值。举个例子。行人走上天桥的那个转弯位置检测器连续几帧都丢了框。没有卡尔曼滤波时轨迹在转弯处直接断掉有卡尔曼滤波时系统会按照上一帧的速度方向继续外推几步虽然外推久了会飘但丢失一两帧完全能撑住轨迹保持连续。卡尔曼滤波不负责识别行人也不负责验证检测框框得准不准。它就是一层“清洗预测”的结构放在检测器和轨迹管理之间。把这个定位想清楚了后面建模、调参、误差分析才不会跑偏。1.3 为什么用MATLAB做这件事做卡尔曼滤波研究我始终推荐先用MATLAB把流程跑通不要一上来就上Python加一堆库。原因很直接MATLAB的矩阵运算是天然契合卡尔曼滤波的数学表达式的你写出来的代码和教科书上的公式几乎逐行对应不容易在维度、转置、逆运算这些地方出错。调试时打断点看协方差矩阵P、看残差序列y都有现成的变量面板连画图看轨迹都是几行代码的事。做算法验证、毕设课题、项目原型最怕的不是算法复杂而是花了大量时间处理工程细节到头来没验证核心思路。MATLAB能把这些成本压得很低。下面所有代码我都会按“直接能跑、跑了能出图”的标准来给。2. 行人跟踪场景下的卡尔曼建模状态向量、F和H到底怎么定2.1 状态向量选4维还是8维卡尔曼滤波建模的第一步是想清楚系统的状态到底由哪些量组成。行人跟踪里有两种常见方案。第一种是4维模型[ x [p_x, p_y, v_x, v_y]^T ]只把检测框中心的横纵坐标和对应的速度放进状态。这个模型假设行人在短时间内近似匀速直线运动——实际监控场景里行人每秒走一米左右帧率在25到30fps时相邻两帧的位移只有几个像素匀速假设在短时间窗口内基本成立。第二种是8维模型[ x [p_x, p_y, w, h, v_x, v_y, v_w, v_h]^T ]把检测框的宽高也纳进状态里同时估计宽高的变化速度。这样做的好处是检测框自身的抖动也能被平滑缺点是模型更复杂调参工作量更大。我做入门项目和课程设计时通常先用4维模型跑通全流程感受卡尔曼滤波的平滑和预测能力再视情况升级到8维。下面所有推导先按4维讲宽高的问题放在第五部分单独处理。2.2 F矩阵匀速模型加帧间隔dt匀速模型描述的是“如果没有任何随机扰动下一帧的状态应该是多少”。假设相邻两帧时间间隔为dt那么[ p_x(tdt) p_x(t) v_x(t) \cdot dt ][ v_x(tdt) v_x(t) ]y方向同理。写成矩阵形式就是[ F \begin{bmatrix} 1 0 dt 0 \ 0 1 0 dt \ 0 0 1 0 \ 0 0 0 1 \end{bmatrix} ]预测步的第一行代码就是kf.x kf.F * kf.x;这一步的物理含义很朴素在已知当前帧位置和速度的情况下我们猜下一帧位置大概在哪。dt取多少视频流通常是30fps也就是dt 1/30。这里有个需要养成的好习惯永远不要把帧间隔当作固定的1一旦数据源换成15fps或可变帧率F矩阵里写死的dt会让预测直接错位。2.3 H矩阵检测器只给我们位置观测卡尔曼滤波里有个容易绕晕的点状态向量包含位置和速度但检测器能给我们的只有位置没有速度。观测向量只有两维[ z [c_x, c_y]^T ]所以观测矩阵H要把状态映射到观测空间[ H \begin{bmatrix} 1 0 0 0 \ 0 1 0 0 \end{bmatrix} ]H矩阵的行数为观测维度列数为状态维度。这个映射的意义是虽然系统内部在估计4个量但每一帧外部送进来的只有两个位置读数卡尔曼滤波会利用这两个读数去同时修正位置和速度两个估计。速度是不可观测的直接量但它能通过位置的前后帧差异被间接估计出来这正是卡尔曼滤波设计的精妙之处。2.4 Q与R两个噪声矩阵如何决定“信谁”Q是过程噪声协方差矩阵代表运动模型自身的不确定性。行人不可能永远走匀速直线他会突然停下、转身、加速这些没被建进模型里的随机运动都要靠Q来兜底。Q越小系统越信任“行人会继续匀速走下去”Q越大系统越允许行人做出意料之外的动作。R是观测噪声协方差矩阵代表检测器测量结果的不确定性。检测框中心标注的像素误差有多大R就有多大。一个比较合理的初始估计是检测框中心的标准差在2到3个像素那么R可以取对角线为9的对角矩阵。Q和R的比值直接决定了卡尔曼增益K的大小也就是滤波结果更偏向预测还是更偏向检测q/r 很小比如 1e-4系统非常信任运动模型输出轨迹极其平滑但一旦行人有机动会明显滞后。q/r 很大比如 100系统几乎完全跟随检测结果平滑效果很弱等于没滤波。实际调参时不需要分别调4维Q矩阵里的每个数。最常见的高效办法是把Q写成一个标量q乘以一个固定的结构矩阵把R写成一个标量r乘以单位阵然后只调节q和r两个数看输出轨迹的效果。我在代码里也是这样处理的dt 1/30; q 0.1; % 过程噪声强度 r 9; % 观测噪声方差 kf.Q q * [dt^3/3, dt^2/2, 0, 0; dt^2/2, dt, 0, 0; 0, 0, dt^3/3, dt^2/2; 0, 0, dt^2/2, dt]; kf.R r * eye(2);Q矩阵上半部分的 dt^3/3 和 dt^2/2 是把“随机加速度噪声”从连续时间模型离散化后得到的积分结果。用这个形式比直接在对角线上填数字要严谨因为位置项和速度项的噪声在数学上是有关联的——加速度先影响速度速度再积分影响位置所以Q矩阵的非对角线也不该是零。3. MATLAB手写核心循环初始化、预测、更新、关联一把梭3.1 初始化如何从一个检测框“冷启动”当一个行人第一次进入画面检测器给出第一个框这时候我们没有历史信息需要做冷启动。位置直接用检测框中心初始化。速度没有观测值只能设为0但要把协方差矩阵P中速度对应的方差设得很大表示“我们其实完全不知道这个人的速度”。P矩阵的初始值代表状态估计的不确定度设得越保守滤波收敛到真实轨迹的速度越快。function kf initKalman(cx, cy) kf.dt 1/30; kf.x [cx; cy; 0; 0]; kf.F [1, 0, kf.dt, 0; 0, 1, 0, kf.dt; 0, 0, 1, 0; 0, 0, 0, 1]; kf.H [1, 0, 0, 0; 0, 1, 0, 0]; kf.P diag([5, 5, 20, 20]); q 0.1; r 9; kf.Q q * [kf.dt^3/3, kf.dt^2/2, 0, 0; kf.dt^2/2, kf.dt, 0, 0; 0, 0, kf.dt^3/3, kf.dt^2/2; 0, 0, kf.dt^2/2, kf.dt]; kf.R r * eye(2); end注意P矩阵里速度初始方差设成了20单位是(像素/帧)的平方换算成标准差大约是每帧4.5个像素这个量级对行人速度来说是合理偏保守的。位置初始方差5对应2.2个像素的误差和检测精度基本匹配。3.2 预测与更新的代码骨架卡尔曼滤波的主循环只有两个函数预测和更新。预测是在检测结果到来之前用上一帧的状态外推出这一帧的先验估计更新是等检测结果到来之后用观测残差修正先验估计得到后验估计。function kf kfPredict(kf) kf.x kf.F * kf.x; kf.P kf.F * kf.P * kf.F kf.Q; end预测步的核心其实是P矩阵的更新它代表着系统对自己估计的不确定度在增大。因为模型本身有噪声走得越远我们越不确定自己猜的位置还准不准。更新步稍微复杂一点但逻辑很清晰先算残差y检测值减预测值然后算残差的协方差S卡尔曼增益K就是“预测不确定度与观测不确定度的比值“最后用K乘以残差得到修正量。function kf kfUpdate(kf, z) y z - kf.H * kf.x; % 残差 S kf.H * kf.P * kf.H kf.R; % 残差协方差 K kf.P * kf.H / S; % 卡尔曼增益 kf.x kf.x K * y; % 状态修正 kf.P (eye(4) - K * kf.H) * kf.P; % 协方差更新 end这一套写下来就完成了卡尔曼滤波的全部数学核心。实际跑视频时整个主循环只有三行kf kfPredict(kf); kf kfUpdate(kf, [detCenterX; detCenterY]);预测、更新、再预测、再更新周而复始。3.3 多行人时的数据关联最近邻加IoU闸门上面写的循环只解决单个目标的滤波问题。真实场景里画面中往往有好几个行人这就引出了一个躲不开的问题预测出来的3号目标到底对应检测器报出来的哪个框这个问题的学名叫做数据关联。最简单实用的方案是“最近邻匹配加IoU闸门”对每个已跟踪目标计算它当前帧预测框和所有检测框之间的IoU先排除掉IoU低于阈值的候选比如0.3然后在剩下的候选里挑IoU最大的作为配对。iouMat computeIoUMatrix(predBoxes, detBoxes); % 排除低分候选按分数降序贪心匹配贪心匹配的过程是第一轮先找全局最大的IoU组合配对成功后标记两个框都“已使用”第二轮再从剩余框里找最大IoU直到没有超过阈值的候选为止。这个方案在行人数不超过10个的场景里完全够用实现简单效果稳定。它最大的弱点是两个行人贴近时可能配对错但那是卡尔曼滤波范畴之外的问题在多目标跟踪系统里通常由更高层的轨迹管理逻辑去兜底。4. 误差分析没有真值的时候怎么量误差4.1 造一个带真值的仿真场景误差分析最大的难题是拿不到真值。真实视频里行人每一帧的真实像素位置没人知道检测框中心只能当作近似不能当作标准答案。所以我做误差分析时通常先在MATLAB里造一个完全可控的仿真场景让“真实位置”成为已知量再把检测误差加进去最后对比卡尔曼滤波的输出和已知真值。仿真场景我推荐这样构造让行人沿着一条带正弦波动的轨迹运动模拟直行和转弯交替出现的情况。这样既能检验滤波的平滑能力又能检验对机动目标的跟随能力。rng(1); T 300; dt 1/30; t (0:T-1) * dt; xTrue 100 50 * t 30 * sin(0.8 * t); yTrue 80 5 * t 20 * cos(0.6 * t); z [xTrue; yTrue] randn(2, T) * 3;这里检测噪声标准差设为3像素相当于真实检测器中等偏上的定位精度。拿这个z序列直接当轨迹你会看到每条曲线都布满毛刺拿卡尔曼滤波跑一遍输出才会逼近xTrue和yTrue。4.2 指标选择RMSE之外我还会看滞后和平滑度误差评估只看RMSE不够。我实际分析时一般同时看四个指标定位误差RMSE滤波输出与真值之间的均方根误差单位是像素。这个指标最直观是误差分析的主指标。平均滞后量当行人做机动运动时滤波输出会相对真值慢半拍。滞后量可以通过估计序列与真值序列的互相关峰值位置来估计单位是帧。滞后是卡尔曼调参里最需要盯住的指标。轨迹平滑度计算相邻两帧估计位置之差的方差。平滑度越小说明轨迹越干净。但注意平滑并不是无限小就好平滑过头往往是滞后变大的征兆。连续跟踪率整个测试序列中轨迹没有断开的帧数占总帧数的比例。当检测丢失时卡尔曼滤波的预测能力能撑住轨迹不断这个指标就是测试它到底能撑多久。我在分析时会同时盯住RMSE和平滑度两个矛盾的指标因为卡尔曼滤波本质上就是在“平滑”和“跟随”之间取平衡。只报RMSE好看可能意味着调参时牺牲了太多平滑性拿到真实场景里还是会抖。4.3 q/r 参数敏感性从1e-4到1的实测趋势固定R只调节q观测滤波效果随q变化的行为是理解卡尔曼滤波最直观的方法。下面是我用上述仿真场景跑出来的典型结果。q 固定 r9直线段RMSE像素转弯段RMSE像素平均滞后帧轨迹平滑度1e-40.73.61.30.31e-21.11.80.40.812.41.20.12.11002.81.002.9这个表透露的信息非常关键q越小直线段的RMSE越漂亮因为系统用模型压制了检测噪声但转弯段的RMSE急剧恶化因为模型还在按原来的速度方向外推跟不上实际转向。q增大后系统更信任检测转弯段误差降下来但直线段的平滑效果变差。我跑完这组实验后最大的体会是不存在一个万能的最优q。它取决于你的场景如果是商场里闲逛的行人轨迹转弯少q可以给小一点如果是路口抢行的行人频繁变向q就得给大一点。工程上稳妥的做法是取一组典型场景逐个扫一遍q找一个整体RMSE最小的折中值。我这边通常q取0.01到0.1之间。4.4 检测丢失15帧协方差怎么发散和恢复误差分析还有一个重要场景模拟检测器连续丢失若干帧观察卡尔曼滤波的输出误差如何变化。我把z序列中间一段直接清零模拟检测丢失15帧然后看滤波输出。预测步的P矩阵每预测一次就加一次Q所以丢失期间P的数值会持续增长代表系统对自己预测结果越来越不信任。有趣的是这时卡尔曼滤波输出的位置会沿着最后的速度方向继续往前走误差随丢失时长线性扩大。以我的仿真参数为例丢失15帧后位置误差大约膨胀到20像素左右这已经超出了检测框可以接受的范围。真正让系统恢复的是检测框重新出现的第一帧。卡尔曼滤波会在更新步里用一个很大的残差把状态从偏离的位置拉回来。拉回来的速度取决于当时的卡尔曼增益而增益又取决于P和R的比值。如果检测丢失期间P涨得很大增益接近1系统几乎完全采用新的检测位置一次修正就能回到正常轨道。所以这里有个矛盾丢失期间希望预测稳一点恢复时又希望修正猛一点而这两个要求恰好都被P矩阵的演化自动处理了。5. 真实视频里一定会踩的坑机动、遮挡、框宽高、ID切换5.1 匀速模型追不上拐弯的行人4维匀速模型的最大软肋是机动目标。当行人突然转弯检测位置开始偏离预测位置残差增大但卡尔曼滤波对残差的响应有一个滞后过程这就表现为轨迹“切弯”走出来的路径比真实路径更圆滑、更靠内侧。我的处理思路有两个。第一个是动态调整q当残差绝对值连续几帧超过阈值时认定目标正在机动临时把q调大一个数量级让滤波更快地信任检测。补充一下这个方案在MATLAB里非常好实现本质就是在主循环里加一个对残差的判断。第二个方向是在状态向量里增加加速度项用匀加速模型替代匀速模型代价是状态维数升到6维调参复杂度上升。如果只是想快速改善先用动态q效果已经很明显。5.2 检测框宽高变化把w和h放进状态目标走近和走远时检测框的宽高会连续变化。如果只在4维模型里跟踪中心点框的宽高直接用检测结果那前后两帧宽度突然抖一下的时候轨迹的中心虽然没有大跳但整个框的表现会很难看视觉上显得很不稳定。解决办法就是升级到8维状态把w、h和它们的速度一并纳入估计。F矩阵扩展为8乘8原有4维结构的公式在宽高方向完全复用[ F_{wh} \begin{bmatrix} 1 0 dt 0 \ 0 1 0 dt \ 0 0 1 0 \ 0 0 0 1 \end{bmatrix} ]观测矩阵H也要扩展观测向量变成 [cx, cy, w, h]H的行数变成4。这样检测框的宽高不再是直接拿过来用而是经过卡尔曼滤波平滑后的输出尺寸变化会更温柔。唯一要注意的是宽高的测量噪声方差通常和中心位置不一样我给R矩阵填值时会把宽高方向的噪声设得更大一些因为检测框宽高的标注通常比中心位置更不稳定。5.3 遮挡和ID切换不是卡尔曼的锅但会影响卡尔曼卡尔曼滤波本身没有记忆身份的能力两个行人短暂交叠然后分开如果数据关联阶段配对错了跟踪器会交换两人的轨迹这就是常说的ID switch。这个问题无法在卡尔曼滤波内部彻底解决因为它本质上是“航迹管理”层面的问题不是“滤波层面”的问题。但卡尔曼滤波能帮上一个小忙通过预测框的位置和大小信息数据关联阶段可以更准确地判断哪个检测框更可能是这个目标的延续从而减少错误配对。实际中我会在关联之后加一个后验校验用预测位置和检测位置的马氏距离做二次确认距离过大的配对即使IoU高也放弃让轨迹进入待匹配状态这样能显著减少ID switch。6. 从仿真走向真实视频流的五个提醒6.1 时间戳不规则时别用固定dt真实视频流常有丢帧、跳帧的情况固定dt1/30的模型会把所有帧间差异算成一帧预测会离谱。稳妥的做法是每次预测前用前后两帧的时间戳实际算出dt再动态生成F矩阵。MATLAB里处理VideoReader时可以通过帧的时间戳元数据拿到真实间隔。6.2 检测置信度低时动态调大R检测器的置信度分数能反映这一帧检测框的可靠程度。我在处理低置信度检测结果时会动态把R调大相当于告诉滤波器这次测量不太靠谱你少信一点。这个技巧在真实监控中能明显减少误跟踪和轨迹抖动。6.3 像素坐标直接滤波没问题但别把像素速度当真值很多场合我们最终需要行人的真实世界速度。直接对像素坐标做卡尔曼滤波得到的速度是像素/帧单位受相机距离影响很大不能直接拿去做物理分析。有条件的话先做透视变换把坐标投影到地面平面再做滤波。如果必须在像素域滤波至少把速度结果标注清楚单位避免下游误用。6.4 异常值是残差统计的天敌先剔除再调参做误差分析时如果检测结果里混了几个离群的错位框RMSE会被这几个异常值剧烈拉高掩盖真实水平。我一般会先统计残差分布把超过3倍标准差的观测标为异常值在误差分析时单独报告有无异常值两套结果。这样更能反映卡尔曼滤波的常态表现而不是被几个极端值牵着走。6.5 对照实验永远是调参的试金石最后一条经验是调参数不要凭感觉每改一个q或r值都固定在同一段测试序列上跑一遍并记录指标。我在MATLAB里会写一个批量扫描脚本嵌套循环q和r的组合自动生成指标表格和轨迹对比图。有了对照实验哪怕是第一次接触卡尔曼滤波的实习生也能在半小时内找到一组合理参数。我在实际项目中调试这套算法的感受是卡尔曼滤波的数学非常成熟真正的难点永远在建模和调参的权衡上。只要把状态向量、噪声矩阵和误差指标这三件事想清楚一套行人跟踪系统很快就能从仿真跑通到真实视频。若后续要做更复杂的多行人场景再往上加匈牙利匹配、轨迹管理、交互多模型也不迟但卡尔曼滤波这个内核仍然会稳稳当当地坐在中间。
阅读完成 · 觉得有帮助?