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里,说白了就是:

  1. 用匀速模型预测目标位置
  2. 用检测结果修正预测
  3. 循环往复,直到目标消失

7维状态向量的设计,既保证了运动描述的完整性,又控制了计算复杂度。我个人觉得,这是SORT最巧妙的设计之一。

下一节,咱们聊聊数据关联——怎么把检测结果和跟踪轨迹匹配起来。这部分才是SORT真正出彩的地方。