卡尔曼滤波预测过程(7 维与 8 维)
Deep-OC-SORT 的每条轨迹由一个卡尔曼滤波器驱动:预测(predict)外推目标运动,修正(update)用检测框拉回。滤波器有两种规格:
| 规格 | 开关 | 状态向量 | 特点 |
|---|---|---|---|
| 8 维(默认) | 不传--new_kf_off | [x,y,w,h,vx,vy,vw,vh] | 宽高直接建模,噪声随目标尺寸自适应 |
| 7 维 | 传--new_kf_off | [cx,cy,s,r,vcx,vcy,vs] | 面积+宽高比间接表示尺寸,固定噪声 |
两种规格共用同一个滤波器本体KalmanFilterNew(predict / update / freeze / unfreeze / apply_affine_correction),差异在状态定义、噪声参数与保护逻辑。本文先讲 7 维——结构简单,且带建档、预测、修正、冻结、重放的逐步数字实算,是理解卡尔曼部分的最短路径;再讲 8 维(默认规格);最后给两者速查对照。
代码位置:deep_ocsort/kalmanfilter.py(滤波器本体)、deep_ocsort/ocsort.py(KalmanBoxTracker 封装:矩阵定义、噪声函数、保护逻辑、坐标转换)。
一、7 维 KF(--new_kf_off时的规格)
1.1 状态与观测
状态 x = [cx, cy, s, r, vcx, vcy, vs]ᵀ (7x1 列向量) └─中心─┘ └尺寸┘ └── 速度 ──┘ 观测 z = [cx, cy, s, r]ᵀ (4x1)| 分量 | 含义 |
|---|---|
| cx, cy | 检测框中心坐标 |
| s | 面积(scale)= w × h——尺寸不直接记 w、h,记它们的乘积 |
| r | 宽高比 = w / h——面积 + 宽高比可唯一还原宽高 |
| vcx, vcy | 中心速度 |
| vs | 面积变化速度——r 没有速度项,宽高比被认为帧间不变 |
坐标转换(convert_bbox_to_z/convert_x_to_bbox):
转观测:w = x2−x1, h = y2−y1, cx = x1+w/2, cy = y1+h/2, s = w·h, r = w/(h+1e-6) 转回框:w = √(s·r), h = s/w, x1 = cx−w/2, y1 = cy−h/2, x2 = cx+w/2, y2 = cy+h/2转移矩阵 F 与观测映射 H
# 7x7:cx += vcx、cy += vcy、s += vs 各前进一步;r 无速度保持不变F=[[1,0,0,0,1,0,0],[0,1,0,0,0,1,0],[0,0,1,0,0,0,1],[0,0,0,1,0,0,0],[0,0,0,0,1,0,0],[0,0,0,0,0,1,0],[0,0,0,0,0,0,1]]# 4x7:只观测到中心与尺寸(s、r),速度隐含H=[[1,0,0,0,0,0,0],[0,1,0,0,0,0,0],[0,0,1,0,0,0,0],[0,0,0,1,0,0,0]]1.2 初始化与建档实算
构造(KalmanBoxTracker.__init__的 7 维分支):F、H 如上,P、Q、R 从单位阵出发三步缩放:
self.kf.R[2:,2:]*=10.0# s、r 的观测噪声 ×10:面积/宽高比的检测抖动远比中心坐标大self.kf.P[4:,4:]*=1000.0# 初速度方差 ×1000self.kf.P*=10.0# 全体方差再 ×10self.kf.Q[-1,-1]*=0.01# vs 的过程噪声 ×0.01self.kf.Q[4:,4:]*=0.01# 三个速度的过程噪声 ×0.01(vs 被缩两次 → 最小)self.kf.x[:4]=convert_bbox_to_z(bbox)# 初速度全 0得到的三个矩阵(全部是对角阵,全局常数,与目标大小无关):
| 矩阵 | 值 | 含义 |
|---|---|---|
| P₀(状态协方差) | diag(10, 10, 10, 10, 10000, 10000, 10000) | 初速度高度不可信(×1000×10 = 万倍) |
| R(观测噪声) | diag(1, 1, 10, 10) | s、r 的观测噪声 ×10 |
| Q(过程噪声) | diag(1, 1, 1, 1, 0.01, 0.01, 0.0001) | 速度几乎不变,vs 最稳 |
建档实算——检测框 [x1,y1,x2,y2] = [100, 200, 180, 250]:
① 转观测:w = 180−100 = 80, h = 250−200 = 50 cx = 100+40 = 140, cy = 200+25 = 225 s = 80×50 = 4000, r = 80/50 = 1.6 z = [140, 225, 4000, 1.6]ᵀ ② 装状态:x = [140, 225, 4000, 1.6, 0, 0, 0]ᵀ ③ 装矩阵:P = P₀、R、Q、F、H(上表)反算验证:w = √(s·r) = √6400 = 80,h = 4000/80 = 50 ✓——观测和状态能无损还原出宽高。
1.3 predict:盲推一步(实算)
x = F·x 位置外推:cx+=vcx、cy+=vcy、s+=vs(r 不动) P = F·P·Fᵀ + Q 协方差外推:不确定性随时间膨胀实算——设跟踪若干帧后 x = [100, 200, 4000, 1.6, 20, 5, −10]ᵀ(cx 每帧 +20、cy +5、面积每帧 −10),P = diag(9, 9, 25, 1, 16, 16, 4):
x' = F·x = [100+20, 200+5, 4000−10, 1.6, 20, 5, −10]ᵀ = [120, 205, 3990, 1.6, 20, 5, −10]ᵀ P' 的非零项(P 是对角阵,P' 只在「量与同轴速度」之间长出交叉项): P'cx,cx = Pcx + 2P(cx,vcx) + Pvcx + Qcx = 9 + 0 + 16 + 1 = 26 ← 位置不确定性被速度不确定性撑大 P'cx,vcx = P(cx,vcx) + Pvcx = 0 + 16 = 16 ← 交叉项:见位置误差可反推速度误差 P'cy,cy / P'cy,vcy = 26 / 16 (与 cx 同构) P's,s = 25 + 0 + 4 + 1 = 30 P's,vs = 0 + 4 = 4 P'r,r = 1 + Qr = 2 ← r 没有速度,只加过程噪声 P'vcx,vcx = 16 + 0.01 = 16.01 (vcy 同构) P'vs,vs = 4 + 0.0001 = 4.0001交叉项 P(cx,vcx) = 16 是下一步 update 能"看到位置误差就修速度"的关键。
KalmanBoxTracker.predict 的保护与状态维护(7 维分支):
- 面积保护:
if s + vs ≤ 0: vs = 0——外推面积不能是负的; - Q = None:不传新 Q,KF 用建好的固定 Q;
- 状态维护:
age += 1(帧年龄,观测字典的键);上帧漏检(time_since_update > 0)则hit_streak = 0;time_since_update += 1。
1.4 update:观测修正(实算)
y = z − H·x 残差:观测与预测的差 S = H·P·Hᵀ + R 系统不确定度 K = P·Hᵀ·S⁻¹ 卡尔曼增益:在 0~1 之间调配"信预测还是信观测" x = x + K·y 状态修正:按增益分配残差 P = (I−K·H)·P·(I−K·H)ᵀ + K·R·Kᵀ 协方差收缩(Joseph 形式,数值稳定)实算——设预测后 x = [150, 250, 5000, 0.5, 25, 0, 0]ᵀ,P(含上一轮长出的位置-速度交叉项):
cx cy s r vcx vcy vs P = [ 12 0 0 0 11 0 0 ] [ 0 12 0 0 0 11 0 ] [ 0 0 21 0 0 0 11 ] [ 0 0 0 6 0 0 0 ] [ 11 0 0 0 11 0 0 ] [ 0 11 0 0 0 11 0 ] [ 0 0 11 0 0 0 11 ]检测观测 z = [152, 250, 5100, 0.48]ᵀ,R = diag(1, 1, 10, 10)。五步:
① y = z − H·x = [152−150, 250−250, 5100−5000, 0.48−0.5]ᵀ = [2, 0, 100, −0.02]ᵀ ② S = H·P·Hᵀ + R = diag(12+1, 12+1, 21+10, 6+10) = diag(13, 13, 31, 16) ③ K = P·Hᵀ·S⁻¹(7×4,交叉项让速度行也非零): cx 行 [0.923, 0, 0, 0 ] ← 12/13 cy 行 [0, 0.923, 0, 0 ] s 行 [0, 0, 0.677, 0] ← 21/31(s 的 R=10,增益低一截) r 行 [0, 0, 0, 0.375] ← 6/16 vcx 行 [0.846, 0, 0, 0 ] ← 11/13:位置残差直接驱动速度修正 vcy 行 [0, 0.846, 0, 0 ] vs 行 [0, 0, 0.355, 0] ← 11/31 ④ x' = x + K·y = [150+1.85, 250+0, 5000+67.7, 0.5−0.008, 25+1.69, 0, 0+35.48]ᵀ = [151.85, 250, 5067.7, 0.492, 26.69, 0, 35.48]ᵀ ⑤ P' 收缩(Joseph 形式): P'cx,cx = 0.923 ← 12 → 0.923:一次观测把位置方差压掉 92% P'cx,vcx = 0.846 P'vcx,vcx = 1.692 ← 11 → 1.69:速度方差大幅收缩 P's,vs = 3.548 P'vs,vs = 7.097 P'r,r = 3.75注意 ④ 的 vs:单次面积残差(+100)经由交叉项直接推出"面积正以 ~35/帧 增长";而 r 无速度建模,宽高比残差(−0.02)只能修 r 自己——这正是 7 维对"目标侧转/宽高渐变"响应慢的根源(8 维的对策见第二章)。
KalmanBoxTracker.update 的观测登记与状态维护(两种规格一致):
1. frozen = False(解除漏检态) 2. 从 observations 找 Δt(3) 帧前的观测,与本次观测算 speed_direction → self.velocity(VDC 惯性方向) 3. last_observation = 本次检测框(OCR 找回轮的锚点) 4. observations[age] = 本次观测;history_observations 追加 5. time_since_update = 0;hits += 1;hit_streak += 1若本帧是从漏检找回(此前收到过 None),KF.update 内部会先 unfreeze 重放再做本次修正——见 1.5。
1.5 ORU:漏检冻结与找回重放
问题:漏检期间每帧 predict,P 持续膨胀、速度被外推放大——找回时滤波器参数已被"盲推"污染,状态拉回容易,协方差与速度的信任关系已经乱了。ORU(Observation-centric Re-Update)的做法:漏检前冻结全部参数,找回后用两帧真实观测线性插值出"虚拟轨迹"重放 predict-update,把滤波器洗干净。
冻结(update 收到 None,即本帧这条轨迹没配上任何检测):
KF.update(None): history_obs.append(None) # 漏检帧占位 last_measurement = history_obs[-2] # 冻结前最后一次真实观测(z 空间) attr_saved = deepcopy(整个 __dict__) # 快照全部参数 observed = False # 进入漏检态 KalmanBoxTracker 侧:frozen = True (predict 时 7 维照常外推;8 维额外清零 vw/vh,见 2.4)找回重放(unfreeze:漏检后第一次 update 收到真实观测时触发):
1. __dict__ = attr_saved 整体回滚到冻结时刻(参数、协方差全回去) history_obs 去掉末尾 None;用完整历史定位最后两个真实观测的下标 2. box1 = last_measurement(冻结前最后观测,z 空间 [cx,cy,s,r]) box2 = 本次找回的观测 7 维先换算:w = √(s·r),h = s/r——插值要在直尺寸空间做 (源码 r = w/float(h) 已适配为 w/h 数组除法,数值等价——本文件唯一适配改动处) 3. 帧距 time_gap = 两观测的下标差 4. 匀速假设线性插值虚拟轨迹: dx = (x2−x1)/gap,dy、dw、dh 同式;第 i 步 = box1 + (i+1)×步长,逐点转回 [cx,cy,s,r] 5. 逐虚拟框 update(new_box),步间 predict —— 等效于"漏检期间目标匀速走且每帧都有观测" 6. 重放完的 P、K、速度信任关系是干净的,再对本次真实观测做正常 update重放实算——漏检 2 帧后找回,完整观测历史 = [z1, None, None, z2]:
z1 = [300, 400, 5120, 0.8](冻结前最后观测) z2 = [324, 406, 5120, 0.8](找回观测) 换算:w1 = √(5120×0.8) = 64, h1 = √(5120/0.8) = 80;z2 同宽高 下标:两个真实观测在下标 0 和 3 → time_gap = 3 步长:dx = (324−300)/3 = 8, dy = (406−400)/3 = 2, dw = dh = 0 虚拟框(每帧 update,步间 predict): i=0: [308, 402, 5120, 0.8] i=1: [316, 404, 5120, 0.8] i=2: [324, 406, 5120, 0.8] ← 最后一个只 update,不再 predict一句话:漏检期间不积累错误,找回时当无事发生、匀速补课。
1.6 CMC:相机运动的仿射搬移(7 维分支)
相机自身在动(抖动/平移/缩放)时,目标"看起来"整体位移,运动模型全部失准——CMC 每帧先估计相机运动的仿射变换(线性部分 m 2×2 + 平移 t 2×1),把轨迹搬到新相机坐标系再谈预测。
KalmanBoxTracker.apply_affine_correction先搬观测记录(两种规格一致):last_observation 与 observations 字典里 Δt 窗口内的每个观测框,4 个角点坐标 m·p + t。
KF 状态部分走KalmanFilterNew.apply_affine_correction的 7 维分支:
self.x[:2]=m @ self.x[:2]+t# 中心:旋转缩放 + 平移self.x[4:6]=m @ self.x[4:6]# 中心速度:跟着旋转缩放(方向语义保持)self.P[:2,:2]=m @ self.P[:2,:2]@ m.T# 中心块共轭变换(保持正定)self.P[4:6,4:6]=m @ self.P[4:6,4:6]@ m.T# 速度块共轭变换# s、r(x[2]、x[6])原地不动——非线性量无法直接仿射(源码的缩放补偿尝试被注释掉)冻结快照同步:若轨迹正处于漏检态(observed=False 且有快照),attr_saved 里的 x[:2]、x[4:6]、P 两个子块与 last_measurement[:2] 同样搬移——找回重放的起点也在新坐标系里。
代价:s、r 吃不到相机缩放补偿——相机变焦时 7 维的尺寸估计会失真,这是 8 维(见 2.6)与 CMC 配合更优的一处。
二、8 维 KF(默认,new_kf=True)
2.1 状态与观测
状态 x = [x, y, w, h, vx, vy, vw, vh]ᵀ (8x1 列向量) └中心┘ └宽高┘ └─ 速度 ─┘ 观测 z = [x, y, w, h]ᵀ (检测框转换而来,4x1)- 中心 +宽高直接建模:w、h 是线性量,各自有速度项 vw、vh——侧转、驶近镜头等尺寸变化有直接的动态模型;
- 坐标转换
convert_bbox_to_z_new/convert_x_to_bbox_new:中心换算与 7 维同式,尺寸直接取 w、h(无 s/r 换算)。
# 8x8:位置 += 速度 × 1 帧F=[[1,0,0,0,1,0,0,0],[0,1,0,0,0,1,0,0],[0,0,1,0,0,0,1,0],[0,0,0,1,0,0,0,1],[0,0,0,0,1,0,0,0],[0,0,0,0,0,1,0,0],[0,0,0,0,0,0,1,0],[0,0,0,0,0,0,0,1]]# 4x8:只观测到位置与宽高,速度隐含H=[[1,0,0,0,0,0,0,0],[0,1,0,0,0,0,0,0],[0,0,1,0,0,0,0,0],[0,0,0,1,0,0,0,0]]匀速模型:下一帧位置 = 当前位置 + 速度 × 1 帧,速度保持不变。
2.2 初始化
x = [检测框中心x, y, w, h, 0, 0, 0, 0]ᵀ 初速度为 0 Q = new_kf_process_noise(w, h) 尺寸自适应(见 2.3) P = Q 的结构放大:位置块(前 4 维)×4、速度块(后 4 维)×100P 的放大含义:协方差 = “我对这个估计有多不确定”。速度块 ×100(方差放大万倍)表示"初速度瞎猜的",前几帧 update 会以极快的速度把速度修正到观测差分出来的真实值;位置块 ×4 表示"检测框大体准但留余地"。(7 维是全局常数 ×10/×1000,见 1.2——8 维的 P 直接复用噪声结构,尺度随目标大小。)
2.3 噪声设计:随尺寸自适应(8 维核心改进)
过程噪声 Q —— 每帧 predict 前重算
defnew_kf_process_noise(w,h,p=1/20,v=1/160):Q=diag((p·w)²,(p·h)²,(p·w)²,(p·h)²,# x, y, w, h(v·w)²,(v·h)²,(v·w)²,(v·h)²)# vx, vy, vw, vh- 位置噪声 ∝ 目标尺寸的 1/20:w=10px 的远处小目标位置噪声 ~0.5px,w=200px 的近处大目标 ~10px——同一相对不确定度,不同绝对尺度;
- 速度噪声 ∝ 1/160:速度比位置稳得多(同上两目标分别0.06px、1.25px);
- 用当前估计的 w、h 每帧重算(predict 里
Q = new_kf_process_noise(self.kf.x[2,0], self.kf.x[3,0]))——目标变大变小,噪声尺度跟着变。
观测噪声 R —— 每次 update 前重算
defnew_kf_measurement_noise(w,h,m=1/20):R=diag((m·w)²,(m·h)²,(m·w)²,(m·h)²)观测(检测框)的可信度也随目标大小缩放:小目标 1px 的检测抖动和大目标 20px 的抖动不是一回事。
为什么这样设计
卡尔曼增益K = P·Hᵀ·S⁻¹(S = H·P·Hᵀ + R)由 P 与 R 的相对大小决定:R 大 → 信预测不信观测,R 小 → 信观测。全局常数 R 意味着"小目标的 2px 抖动和大目标的 2px 抖动被同等对待"——对小目标,2px 已经是 20% 的宽,滤波器却以为观测很准,轨迹跟着抖;尺寸自适应后两类目标获得一致的平滑度。
2.4 predict:盲推一步
x=F · x# 位置外推:x+=vx, y+=vy, w+=vw, h+=vhP=F · P · Fᵀ+Q# 协方差外推:不确定性随时间膨胀KalmanBoxTracker.predict 在调 KF.predict 之前做两件保护:
- 负尺寸保护:
if w + vw ≤ 0: vw = 0(h 同理)——外推出的框宽高不能是负的,速度项越界就钳回 0; - 冻结保护:
if frozen: vw = vh = 0——漏检中的轨迹不外推尺寸变化(目标被挡住时尺寸不变是更合理的先验),位置速度照常外推等找回。
随后按当前宽高算 Q 传入(Q = new_kf_process_noise(w, h))。状态维护与 7 维一致:age += 1;上帧漏检则hit_streak = 0;time_since_update += 1。
2.5 update:观测修正
五步公式与 1.4 完全一致(含 Joseph 形式与 1.4 的全部状态维护),差异只在 R 的来源:每次 update 前R = new_kf_measurement_noise(当前 w, h)传入 KF.update——观测噪声跟目标尺寸走(7 维是建好的固定 R)。直觉同 1.4:P 大(刚建档、长时间漏检后)K 趋大,观测把状态大步拉回;P 小(长期稳定跟踪)K 趋小,检测抖动被滤掉。
2.6 ORU 与 CMC:8 维的不同表现
ORU:冻结/重放机制与 1.5 完全一致,唯一差异在第 2 步——z 空间本来就是 [x,y,w,h],免去 s/r 换算,直接线性插值(unfreeze的 new_kf=True 分支)。
CMC:8 维走 kron 全量搬移:
big_m=np.kron(np.eye(4),m)# m 是 2x2 仿射线性部分 → 8x8 块对角x=big_m · x;x[:2]+=t# 每个 (量, 其速度) 对按 m 变换,平移只加中心P=big_m · P · big_mᵀ# 协方差共轭变换(保持正定)- [x,vx]、[y,vy]、[w,vw]、[h,vh] 四对各自被 m 旋转缩放——宽高的速度向量也跟着相机变换,方向语义保持;平移 t 只加在中心 x、y 上(相机平移不改变尺寸与速度方向);
- 冻结快照全量同步:attr_saved 的 x、P 整体变换,last_measurement 前 2 位做 m·p + t、后 2 位做 m·p——找回重放的起点也在新坐标系里;
- w、h、vw、vh 都能吃到相机的缩放/旋转补偿——没有 7 维 s/r 原地不动的失真问题。
三、7 维 / 8 维速查对照
| 环节 | 7 维(OC-SORT 规格,--new_kf_off) | 8 维(默认) |
|---|---|---|
| 状态 | [cx,cy,s,r,vcx,vcy,vs],r 无速度 | [x,y,w,h,vx,vy,vw,vh] |
| 尺寸语义 | s=面积、r=宽高比(非线性,需换算) | w、h 直接线性 |
| 建档 P₀ | diag(10,10,10,10,10⁴,10⁴,10⁴)(全局常数) | Q 结构:位置 ×4、速度 ×100(随尺寸) |
| Q | 固定 diag(1,1,1,1,0.01,0.01,0.0001),构造一次 | ∝ 当前宽高(p=1/20, v=1/160),每帧重算 |
| R | 固定 diag(1,1,10,10) | ∝ 当前宽高(m=1/20),每次 update 重算 |
| predict 保护 | s+vs≤0 钳 vs=0 | w/h 越界钳速度 + 冻结时清 vw/vh |
| update | 五步公式(1.4 实算),固定 R | 同左,仅 R 按尺寸重算 |
| ORU 重放 | s/r 先换算 w/h 插值(1.5 实算) | 直接 w/h 插值 |
| CMC 搬移 | 只搬中心与速度两块,s/r 不动 | kron(I4,m) 全状态搬移 |
| 转换函数 | convert_bbox_to_z / convert_x_to_bbox | convert_bbox_to_z_new / convert_x_to_bbox_new |
四、一帧里的完整时序
轨迹视角一帧(8 维默认): [CMC] apply_affine_correction(仿射) ← 相机动了先搬坐标(观测记录 + KF 状态 + 冻结快照) [预测] 负尺寸保护 → 冻结时清 vw/vh → 按当前宽高算 Q → x=Fx, P=FPFᵀ+Q → age+=1 [关联] 预测框进第一轮(IoU+VDC+外观)→ 配上或进 OCR 找回轮 [配上] update(bbox):状态维护 + 按宽高算 R + KF 修正(若从漏检找回,先 unfreeze 重放) update_emb(emb, alpha):特征 EMA [漏检] update(None):freeze 快照 → 下帧 predict 继续外推等找回 [输出] 本帧配上且 hit_streak≥min_hits → 输出 [x1,y1,x2,y2,id] [判死] time_since_update > max_age → 删除 7 维差异:CMC 只搬中心与速度两块;predict 无尺寸保护与冻结清零、Q 用固定矩阵; update 用固定 R;ORU 重放前先 s/r→w/h 换算。| 描述 | 链接 |
|---|---|
| 项目 | 基于 Ultralytics +Deep-OC-SORT 多目标跟踪与改进 |
| 往期回顾 | 链接 |
| 数据集 | 数据集汇总 |
| 软件开发 | 深度学习软件开发汇总 |