卡尔曼滤波(二)姿态融合
这一篇继续以训练好的 TienKung 人形机器人为例,在 Isaac Lab 中加入一个 IMU 传感器,然后手动给理想测量叠加陀螺仪固定零偏、陀螺仪白噪声和加速度计白噪声,最后对比融合互补滤波、普通卡尔曼滤波和误差状态卡尔曼滤波三种姿态估计方法。
因为本来想验证一下卡尔曼滤波,但是实现过程中发现欧拉角三个方向的旋转有耦合,所以调研到了ESKF的方法,这里由于时间原因没有细纠,这部分还有待仔细推导,先埋个点[TODO]
当前实验使用三轴角速度和三轴加速度,没有磁力计或其他绝对航向观测。因此Roll和Pitch可以利用重力方向进行校正,Yaw主要依靠陀螺仪传播,长时间运行仍然可能出现漂移。
需要先说明,这里新增的三种滤波器只用于学习、绘图和对比,不会把滤波结果传给强化学习策略。策略使用的仍然是环境原来定义的 observation,因此这一部分不会改变训练好的控制策略。
Isaac Lab IMU 输出
首先在 play_kaleman.py 中临时继承原来的场景配置,然后给机器人 pelvis 部分加入一个 IMU。这样不会修改原来的训练场景配置。
1 | class PlayKalmanSceneCfg(base_scene_cfg): |
当前 IMU 相对于 pelvis 的位置偏移为0,旋转偏移为单位四元数,因此 IMU 坐标系与 pelvis 坐标系重合。
程序实际读取:
1 | imu_data = env.scene["pelvis_imu"].data |
quat_w 是 IMU 在世界坐标系中的姿态四元数,顺序为 WXYZ。它属于仿真提供的完整状态信息,真实六轴 IMU 不会直接提供这样的无误差姿态。
ang_vel_b 是当前 IMU/机体系下的三轴角速度,lin_acc_b 是当前 IMU/机体系下的三轴线加速度。Isaac Lab 2.1.0 中,世界系线加速度由速度数值差分并加入 gravity_bias 得到:
当前:
随后再把结果旋转到 IMU 坐标系。因此机器人静止时,加速度计输出接近机体系中的重力反方向;机器人运动时,输出还会包含 pelvis 的真实平动加速度,并不等于纯粹的重力投影。
传感器误差
当前 Isaac IMU 没有配置随机误差,所以读取理想输出后,再手动构造带误差的传感器测量:
其中 $b_g$ 是程序启动时随机生成的三轴固定陀螺仪零偏,$n_g$ 和 $n_a$ 分别是陀螺仪与加速度计白噪声。
1 | class SensorErrorModel: |
本次实验中,三轴固定零偏分别独立采样于:
陀螺仪白噪声标准差为:
加速度计白噪声标准差为:
当前没有给加速度计加入固定零偏,也没有让真实注入的陀螺仪零偏随时间变化。
初始姿态
三个滤波器在开始更新前都需要一个初始姿态。当前主实验使用仿真 quat_w 在初始化时赋值一次:
1 | q_initial = latest_q_truth.clone() |
这样做是为了先排除重力初始对准误差,只比较后续传播和校正效果。初始化完成后,quat_w 不再参与三个滤波器的预测和观测更新,只用于绘制 Truth 曲线和计算误差。
当前代码仍然保留了只依赖六轴 IMU 的重力对准选项。重力对准会利用一段时间内的陀螺仪相对旋转,把各帧加速度搬到同一个参考坐标系后求平均,再得到Roll和Pitch初值;Yaw初值只能人为规定。真实机器人上不能使用仿真姿态真值,应当采用这一类重力对准或其他外部姿态来源。
机器人控制频率为50 Hz,物理仿真与 IMU 更新频率为200 Hz。为了让三个滤波器按 IMU 频率更新,在环境的每个 physics 子步中加入一个默认关闭的回调:
1 | self.physics_step_callback: Callable[[float], None] | None = None |
回调放在 scene.update() 之后:
1 | for _ in range(self.cfg.sim.decimation): |
因此三个滤波器实际使用:
而不是50 Hz控制周期对应的 $0.02s$。
进入滤波器前,加速度还会经过一阶低通:
当前时间常数为 $\tau=0.4s$。只有原始加速度和低通加速度的模长都接近 $9.81m/s^2$ 时,才允许把当前加速度方向用于姿态校正。
当前 physics 回调中的主要计算为:
1 | gyro_measured, accel_measured = sensor_error.measure( |
普通卡尔曼滤波
普通卡尔曼滤波使用六维状态:
其中 $\phi$、$\theta$、$\psi$ 分别是Roll、Pitch、Yaw,$b_x$、$b_y$、$b_z$ 是三轴陀螺仪零偏估计。
输入是三轴陀螺仪:
加速度计只能给出Roll和Pitch观测:
状态空间方程
离散状态空间方程为:
其中:
展开后为:
这里的零偏预测模型确实是“当前零偏等于上一时刻零偏”。但是状态矩阵右上角的 $-\Delta t$ 会让零偏误差经过积分变成姿态误差,这一点后面会用于根据姿态残差修正零偏。
代码中对应:
1 | transition_matrix = torch.eye(6, dtype=self.dtype) |
观测方程
观测方程为:
其中:
后三列为0,表示加速度计没有直接测量陀螺仪零偏。但状态预测会在姿态误差和零偏误差之间建立交叉协方差,因此卡尔曼增益中对应 $b_x$、$b_y$ 的行仍然可以不为0。
可以把它理解为:如果 $b_y$ 估计不准确,那么扣除零偏后的Y轴角速度就不准确,经过积分后逐渐表现为Pitch误差;加速度计发现持续存在的Pitch残差后,再通过状态之间的相关性反向修正 $b_y$。
当前普通卡尔曼滤波中,Yaw与Roll/Pitch完全解耦,加速度计又不观测Yaw,所以 $b_z$ 没有校正通道,会保持初值0。
Q和R
过程噪声协方差为:
前三项表示陀螺仪白噪声经过一个采样周期积分后形成的角度误差,后三项表示零偏随机游走。
观测噪声协方差为:
当前普通卡尔曼滤波设置:
这里的 $R$ 不能只按照加速度计白噪声 $0.05m/s^2$ 直接换算,因为机器人运动时还存在没有建模的平动加速度和步态冲击。
滤波更新仍然沿用上一篇文章中的标准卡尔曼滤波公式,代码使用 Joseph Form 更新协方差:
1 | joseph_left = self.identity - kalman_gain @ self.observation_matrix |
代码中的变量 innovation 对应中文文献里常说的“新息”:
而:
称为新息协方差矩阵。当前代码利用 $y_k^TS_k^{-1}y_k$ 做NIS门控,拒绝明显不符合当前预测的加速度观测。
普通卡尔曼滤波内部始终使用欧拉角状态。四元数只在初始化时转换成欧拉角,并在滤波结束后把欧拉角重新转换成四元数,用于统一绘图接口。
ESKF
普通卡尔曼滤波直接把欧拉角作为状态,并且把机体系陀螺仪三轴近似当成Roll、Pitch、Yaw的变化率。ESKF则用四元数保存完整姿态,再对当前四元数附近的小误差进行线性卡尔曼滤波。
ESKF是Error-State Kalman Filter,即误差状态卡尔曼滤波。当前实现的名义状态为:
误差状态为:
姿态误差采用右乘定义:
这里要求较小的是估计误差 $\delta\theta$,不是机器人的实际姿态角。机器人即使已经发生较大转动,只要当前名义四元数与真实姿态接近,误差状态仍然可以使用小角度线性化。
名义状态传播
首先从陀螺仪测量中扣除当前零偏估计:
然后传播四元数:
代码为:
1 | corrected_gyro = gyro - self.gyro_bias |
误差状态方程
连续时间误差模型为:
一阶离散化后:
其中:
右上角的 $-I_3\Delta t$ 与普通卡尔曼滤波中的 $-\Delta t$ 含义相同:零偏误差会经过陀螺仪积分变成姿态误差,从而建立姿态与零偏之间的交叉协方差。
代码为:
1 | transition = torch.eye(6, dtype=torch.float64) |
观测方程
ESKF不先把加速度换成Roll和Pitch,而是直接使用归一化加速度方向:
当前四元数预测的机体系重力方向为:
非线性观测模型为:
在当前名义四元数附近线性化后:
代码中对应:
1 | measurement = accel / accel_norm |
观测矩阵的零偏部分依然为0。零偏能够更新,仍然依靠预测阶段形成的姿态—零偏交叉协方差,而不是加速度计直接测出了零偏。
当前加速度方向噪声标准差设置为 $1^\circ$。ESKF同样计算新息协方差矩阵与NIS,当加速度方向明显不符合当前姿态预测时,跳过这一帧校正。
误差注入
得到误差状态估计后:
姿态误差通过右乘注入名义四元数:
零偏误差直接加到名义零偏:
1 | delta_state = kalman_gain @ residual |
误差已经注入名义状态后,误差状态重新定义为0,并使用reset Jacobian把协方差转换到新的误差坐标系:
这就是当前实现与普通全状态EKF最明显的区别:ESKF维护名义四元数,但卡尔曼滤波处理的是局部姿态误差与零偏误差。
单个重力方向只能提供两个独立约束,绕重力方向旋转不会改变观测。因此仅靠当前六轴 IMU 无法获得绝对Yaw,Yaw以及对应的零偏估计仍然缺少充分约束。
融合互补滤波
融合互补滤波同样使用四元数传播姿态,但不维护协方差,也不估计陀螺仪零偏。
加速度给出的机体系上方向为:
当前四元数预测的机体系上方向为:
两个方向的叉积近似为姿态误差:
然后将比例校正加入角速度:
最后传播四元数:
当前 $k_p=2.0s^{-1}$。这个方法结构简单,短时间姿态跟踪效果也可以很好,但固定陀螺仪零偏仍然会造成长期漂移。
结果对比


在保存的一次12秒运动记录中,机器人前向速度命令为 $0.4m/s$,Yaw角速度命令为 $0.5rad/s$。ESKF的Roll/Pitch RMSE约为 $0.338^\circ/0.390^\circ$,互补滤波约为 $0.779^\circ/0.727^\circ$,普通卡尔曼滤波约为 $1.097^\circ/0.491^\circ$。
补充
加速度计
实际六轴 IMU 的加速度计测量的是比力。按照本文坐标约定:
机器人静止时 $a_W=0$,所以加速度计主要反映重力方向;机器人运动时 $a_W\neq0$,测量中同时存在平动加速度。
通常可以检查:
是否足够小,作为加速度观测是否相对可信的一个条件。但模长接近 $g$ 并不能证明方向一定等于重力方向,这也是动态机器人姿态估计比静止IMU更困难的地方。
四元数
四元数写成:
符号 $\otimes$ 表示四元数乘法,也称为 Hamilton product。它不是普通数字乘法、矩阵对应元素相乘或单独的向量点乘、叉乘,而是专门用于组合两个四元数的运算。
设:
那么:
如果把四元数写成标量部分和向量部分:
同一个运算也可以写成:
可以看到,Hamilton积内部同时包含标量乘法、向量点积和向量叉积,但整体并不等于其中任何一种运算。
四元数乘法的物理意义是组合旋转。按照本文使用的主动旋转约定:
因此旋转顺序不能随意交换。一般情况下:
当前代码中的四元数表示机体系到世界系的旋转,陀螺仪角速度又在机体系表达,所以采用右乘更新:
其中 $\delta q_k$ 是当前采样周期内的机体系增量旋转。
接下来具体看 $\delta q_k$ 如何由陀螺仪得到。陀螺仪给出机体系角速度:
在一个采样周期 $\Delta t$ 内,对应的旋转向量为:
旋转角度是旋转向量的模:
展开为:
这里得到的 $\Delta\theta$ 是一个标量,单位为弧度。
当 $\Delta\theta\neq0$ 时,旋转轴是旋转向量的单位方向:
展开为:
并且:
已知旋转角 $\Delta\theta$ 和单位旋转轴 $\mathbf u$ 后,增量四元数为:
也就是:
一个具体例子
为了把旋转向量、增量四元数和Hamilton积连起来,先假设机器人从单位姿态开始:
第一个旋转是在机体系Z轴旋转 $90^\circ$。对应旋转向量为:
所以旋转角和旋转轴分别为:
得到增量四元数:
更新姿态:
接着再绕当前机体系X轴旋转 $90^\circ$:
按照当前代码的右乘约定:
代入Hamilton积:
如果交换顺序:
两个结果不同,这就说明四元数乘法不能交换顺序。
最后再看向量旋转。假设当前机体系中指向机器人前方的向量为:
四元数 $q$ 表示机体系到世界系的旋转,那么该向量在世界系中的方向为:
这就是互补滤波和ESKF中陀螺仪传播四元数的核心计算。
欧拉角速度耦合
以常见的ZYX欧拉角为例:
其中 $\psi$、$\theta$、$\phi$ 分别为Yaw、Pitch、Roll。
注意,这三个旋转轴不是同一套坐标系里的三个固定轴。按照ZYX欧拉角的旋转顺序:
- Yaw $\psi$:绕世界坐标系的 $z_W$ 轴旋转;
- Pitch $\theta$:绕完成Yaw旋转后的中间坐标系 $y$ 轴旋转;
- Roll $\phi$:绕完成Yaw和Pitch旋转后的最终机体系 $x_B$ 轴旋转。
也就是说,Yaw、Pitch和Roll分别对应三个不同阶段的坐标轴,这些轴会随着前面的旋转发生变化。陀螺仪输出的 $p,q,r$ 则是同一个瞬时角速度向量在当前机体 $x_B,y_B,z_B$ 三轴上的分量,而 $\dot\phi,\dot\theta,\dot\psi$ 是三个欧拉角参数的变化率。两组三维量的物理含义并不相同,这就是欧拉角速度产生耦合的根本原因。
两者满足:
展开为:
反过来:
因此普通卡尔曼滤波中:
只是Roll和Pitch较小时的近似。当前普通卡尔曼滤波仍然能够工作,是因为实验使用了准确初始姿态、200 Hz更新以及持续的加速度校正,并不表示这个欧拉角传播模型在任意三维姿态下都严格成立。





