4、SORT状态估计:卡尔曼滤波详解(预测与更新)、状态向量设计(7维:cx,cy,w,h,vx,vy,vr)
好,咱们进入SORT的核心环节——状态估计。说白了,就是卡尔曼滤波怎么在跟踪过程中“猜”目标的位置和速度。
我刚开始接触多目标跟踪时,觉得卡尔曼滤波特别玄乎。后来自己手推了一遍公式,又在项目里调了无数个bug,才真正理解它的精髓。今天咱们就把这块掰开揉碎了讲。
4.1 状态向量设计:为什么是7维?
先说说状态向量。SORT里用的是7维状态:[cx, cy, w, h, vx, vy, vr]。
你可能会问:为什么不是4维?或者8维?
嗯,这里有个设计思路。SORT的作者们做了个权衡:既要能描述目标运动,又不能太复杂导致计算量爆炸。
| 维度 | 含义 | 单位 |
|---|---|---|
| cx | 边界框中心x坐标 | 像素 |
| cy | 边界框中心y坐标 | 像素 |
| w | 边界框宽度 | 像素 |
| h | 边界框高度 | 像素 |
| vx | 中心x方向速度 | 像素/帧 |
| vy | 中心y方向速度 | 像素/帧 |
| vr | 宽高比变化率 | 1/帧 |
我个人习惯把状态向量想象成“目标在当前帧的完整画像”。前4维是位置信息,后3维是运动信息。有了速度,卡尔曼滤波就能预测下一帧目标大概在哪。
关键点:为什么用宽高比变化率vr,而不是直接预测w和h的变化?
因为实际场景中,目标宽高比变化相对稳定。直接预测w和h的变化容易发散。我在项目中遇到过,用8维状态(加dw、dh)时,框经常越变越离谱。换成7维后,稳定多了。
4.2 卡尔曼滤波:预测与更新
卡尔曼滤波就两个步骤:预测和更新。循环往复,直到目标消失。
4.2.1 预测阶段
预测,就是根据上一帧的状态,猜当前帧的状态。
公式长这样:
x' = F * x + u
P' = F * P * F^T + Q
解释一下:
- x:上一帧的状态向量(7维)
- F:状态转移矩阵。说白了,就是“匀速运动”这个假设的数学表达
- u:控制量。SORT里设为0,因为我们没有外部控制信息
- P:协方差矩阵。表示我们对当前状态估计的不确定性
- Q:过程噪声。表示模型本身的不确定性
你想想看,F矩阵长什么样?对于匀速运动模型:
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]
]
嗯,这里要注意:位置部分(前4维)加上了速度项,速度部分保持不变。这就是“匀速”假设的体现。
我的经验:Q矩阵的调参很关键。Q设太大,滤波结果会跟着检测噪声乱跳;Q设太小,又跟不上目标突然加速。我一般从单位矩阵的0.01倍开始调,然后看跟踪效果微调。
4.2.2 更新阶段
更新,就是用当前帧的检测结果,修正预测值。
公式:
y = z - H * x'
S = H * P' * H^T + R
K = P' * H^T * S^(-1)
x = x' + K * y
P = (I - K * H) * P'
别被公式吓到。核心就一个东西:卡尔曼增益K。
K的作用是:决定“相信预测”多一点,还是“相信检测”多一点。
- 如果检测很准(R小),K就大,更相信检测
- 如果模型很准(Q小),K就小,更相信预测
我曾经在调参时犯过一个错误:把R设得太小,结果检测偶尔漏检时,跟踪框直接飞走了。后来把R适当调大,跟踪就稳了。
避坑指南:更新阶段要注意z的维度。SORT里检测输出是4维:[cx, cy, w, h]。所以观测矩阵H是4x7的:
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]
]
这意味着我们只能观测到位置,速度是“隐藏状态”,靠卡尔曼滤波自己推断。
4.3 实际代码中的卡尔曼滤波
说了这么多理论,咱们看看代码怎么写。SORT官方实现里,卡尔曼滤波封装在KalmanBoxTracker类中。
class KalmanBoxTracker(object):
def __init__(self, bbox):
# 初始化状态向量
self.kf = KalmanFilter(dim_x=7, dim_z=4)
self.kf.F = np.array(...) # 状态转移矩阵
self.kf.H = np.array(...) # 观测矩阵
self.kf.R[2:, 2:] *= 10. # 检测噪声
self.kf.P[4:, 4:] *= 1000. # 速度不确定性初始值
# 用第一帧检测初始化
self.kf.x[:4] = bbox
def predict(self):
# 预测下一帧
self.kf.predict()
return self.kf.x
def update(self, bbox):
# 用检测结果更新
self.kf.update(bbox)
注意看初始化部分:
R[2:, 2:] *= 10.:对宽高检测噪声放大。因为实际中宽高检测误差比中心点大P[4:, 4:] *= 1000.:速度初始不确定性设很大。因为我们不知道目标初始速度
这些细节,都是前人踩坑踩出来的经验。我刚开始用SORT时,直接用了默认参数,结果前几帧跟踪总是不稳定。后来加上这些初始化技巧,效果立竿见影。
4.4 小结
卡尔曼滤波在SORT里,说白了就是:
- 用匀速模型预测目标位置
- 用检测结果修正预测
- 循环往复,直到目标消失
7维状态向量的设计,既保证了运动描述的完整性,又控制了计算复杂度。我个人觉得,这是SORT最巧妙的设计之一。
下一节,咱们聊聊数据关联——怎么把检测结果和跟踪轨迹匹配起来。这部分才是SORT真正出彩的地方。