彻底搞懂卡尔曼滤波:从理论到C++实战的EKF/UKF实现指南
为什么你的传感器数据总是不准?
工业机器人定位漂移超过5cm?无人机悬停时持续抖动?自动驾驶车辆无法稳定跟踪车道线?这些问题的背后,可能都指向同一个核心挑战:如何从充满噪声的传感器数据中提取真实状态。
卡尔曼滤波器(Kalman Filter, KF)作为一种最优估计算法,已经成为解决这类问题的标准工具。它能在存在噪声和不确定性的环境中,为动态系统提供精确的状态估计。本文将带你深入理解卡尔曼滤波的数学原理,并通过一个基于Eigen3的C++11开源库(kalman),手把手教你实现扩展卡尔曼滤波器(Extended Kalman Filter, EKF)和无迹卡尔曼滤波器(Unscented Kalman Filter, UKF)。
读完本文,你将获得:
- 掌握卡尔曼滤波的核心数学框架,理解状态预测与更新的迭代过程
- 学会区分EKF与UKF的适用场景,避免工程实践中的选型错误
- 能够使用kalman库快速构建自己的滤波系统,附带完整机器人定位案例
- 解决实际项目中常见的噪声建模、状态初始化和参数调优问题
卡尔曼滤波的数学基石
1. 状态空间模型(State Space Model)
任何动态系统都可以用状态方程和观测方程来描述:
\begin{align*}
\text{状态方程(预测)}: &\quad \mathbf{x}_k = \mathbf{F}_k \mathbf{x}_{k-1} + \mathbf{B}_k \mathbf{u}_k + \mathbf{w}_k \\
\text{观测方程(更新)}: &\quad \mathbf{z}_k = \mathbf{H}_k \mathbf{x}_k + \mathbf{v}_k \\
\end{align*}
其中:
- $\mathbf{x}_k$:系统在时刻k的状态向量(State Vector)
- $\mathbf{u}_k$:控制输入向量(Control Input)
- $\mathbf{z}_k$:观测向量(Measurement)
- $\mathbf{F}_k$:状态转移矩阵(State Transition Matrix)
- $\mathbf{B}_k$:控制输入矩阵(Control Input Matrix)
- $\mathbf{H}_k$:观测矩阵(Observation Matrix)
- $\mathbf{w}_k$:过程噪声(Process Noise),服从$\mathcal{N}(0, \mathbf{Q}_k)$
- $\mathbf{v}_k$:观测噪声(Measurement Noise),服从$\mathcal{N}(0, \mathbf{R}_k)$
2. 卡尔曼滤波的迭代过程
卡尔曼滤波通过两个核心步骤不断迭代:预测(Prediction) 和更新(Update)。
EKF vs UKF:非线性系统的解决方案
1. 扩展卡尔曼滤波器(EKF)
当系统模型存在非线性时,EKF通过泰勒级数一阶展开将非线性函数线性化:
\begin{align*}
\mathbf{F}_k &\approx \nabla_\mathbf{x} \mathbf{f}(\mathbf{x}_{k-1}, \mathbf{u}_k) \bigg|_{\hat{\mathbf{x}}_{k-1|k-1}} \\
\mathbf{H}_k &\approx \nabla_\mathbf{x} \mathbf{h}(\mathbf{x}_k) \bigg|_{\hat{\mathbf{x}}_{k|k-1}} \\
\end{align*}
EKF的C++实现核心
在kalman库中,EKF的实现位于ExtendedKalmanFilter.hpp:
template<class StateType>
class ExtendedKalmanFilter : public KalmanFilterBase<StateType>,
public StandardFilterBase<StateType>
{
public:
// 预测步骤
template<class Control, template<class> class CovarianceBase>
const State& predict(SystemModelType<Control, CovarianceBase>& s, const Control& u)
{
s.updateJacobians(x, u); // 更新雅可比矩阵F
x = s.f(x, u); // 状态预测
P = s.F * P * s.F.transpose() + s.W * s.getCovariance() * s.W.transpose(); // 协方差预测
return x;
}
// 更新步骤
template<class Measurement, template<class> class CovarianceBase>
const State& update(MeasurementModelType<Measurement, CovarianceBase>& m, const Measurement& z)
{
m.updateJacobians(x); // 更新雅可比矩阵H
Covariance<Measurement> S = m.H * P * m.H.transpose() + m.V * m.getCovariance() * m.V.transpose();
KalmanGain<Measurement> K = P * m.H.transpose() * S.inverse(); // 卡尔曼增益
x += K * (z - m.h(x)); // 状态更新
P -= K * m.H * P; // 协方差更新
return x;
}
};
2. 无迹卡尔曼滤波器(UKF)
UKF通过无迹变换(Unscented Transform, UT) 来处理非线性问题,避免了EKF中的线性化误差。其核心思想是:通过选取一组Sigma点来近似状态的概率分布。
Sigma点的生成公式:
\begin{align*}
\mathbf{X}_0 &= \hat{\mathbf{x}} \\
\mathbf{X}_i &= \hat{\mathbf{x}} + \sqrt{(n+\lambda) \mathbf{P}}_i \quad (i=1,\dots,n) \\
\mathbf{X}_{i+n} &= \hat{\mathbf{x}} - \sqrt{(n+\lambda) \mathbf{P}}_i \quad (i=1,\dots,n) \\
\end{align*}
其中,$n$是状态维度,$\lambda = \alpha^2 (n+\kappa) - n$是缩放参数。
3. EKF与UKF的对比选择
| 特性 | EKF | UKF |
|---|---|---|
| 非线性处理 | 一阶泰勒展开近似 | 无迹变换,二阶精度 |
| 计算复杂度 | $O(n^3)$ | $O(n^3)$,但常系数更大 |
| 实现难度 | 需要推导雅可比矩阵 | 无需导数,但需调参($\alpha, \beta, \kappa$) |
| 适用场景 | 弱非线性系统 | 强非线性系统 |
| 数值稳定性 | 较差(雅可比矩阵可能奇异) | 较好 |
kalman库实战:移动机器人定位
1. 项目结构解析
kalman库采用header-only设计,核心文件组织如下:
kalman/
├── include/
│ └── kalman/
│ ├── ExtendedKalmanFilter.hpp // EKF实现
│ ├── UnscentedKalmanFilter.hpp // UKF实现
│ ├── LinearizedSystemModel.hpp // 线性化系统模型
│ ├── LinearizedMeasurementModel.hpp // 线性化观测模型
│ └── Matrix.hpp // Eigen矩阵封装
└── examples/
└── Robot1/ // 机器人定位示例
├── main.cpp // 主程序
├── SystemModel.hpp // 系统模型定义
├── PositionMeasurementModel.hpp // 位置观测模型
└── OrientationMeasurementModel.hpp // 姿态观测模型
2. 机器人系统建模
系统模型(System Model)
考虑一个在二维平面运动的机器人,状态向量为: $\mathbf{x} = \begin{bmatrix} x & y & \theta \end{bmatrix}^T$,其中$x,y$是位置,$\theta$是航向角。
控制输入为:$\mathbf{u} = \begin{bmatrix} v & \dot{\theta} \end{bmatrix}^T$,其中$v$是线速度,$\dot{\theta}$是角速度。
状态转移函数:
\mathbf{x}_k = \mathbf{f}(\mathbf{x}_{k-1}, \mathbf{u}_k) =
\begin{bmatrix}
x_{k-1} + v \Delta t \cos(\theta_{k-1}) \\
y_{k-1} + v \Delta t \sin(\theta_{k-1}) \\
\theta_{k-1} + \dot{\theta} \Delta t
\end{bmatrix}
在SystemModel.hpp中的实现:
template<typename T>
class SystemModel : public Kalman::LinearizedSystemModel<State<T>>
{
public:
// 状态转移函数
State<T> f(const State<T>& x, const Control<T>& u) const override
{
State<T> x_pred;
T dt = 1.0; // 时间步长
// 位置预测
x_pred.x() = x.x() + u.v() * dt * std::cos(x.theta());
x_pred.y() = x.y() + u.v() * dt * std::sin(x.theta());
// 航向角预测
x_pred.theta() = x.theta() + u.dtheta() * dt;
return x_pred;
}
// 雅可比矩阵F的计算
void updateJacobians(const State<T>& x, const Control<T>& u) override
{
T dt = 1.0;
T theta = x.theta();
T v = u.v();
// F = df/dx
this->F.setZero();
this->F(0, 0) = 1.0;
this->F(0, 2) = -v * dt * std::sin(theta);
this->F(1, 1) = 1.0;
this->F(1, 2) = v * dt * std::cos(theta);
this->F(2, 2) = 1.0;
}
};
观测模型(Measurement Model)
我们为机器人配备两种传感器:
- 陀螺仪:测量航向角$\theta$
- 超声波传感器:测量到两个固定路标(Landmark)的距离
位置观测模型(超声波传感器):
\mathbf{z} = \begin{bmatrix}
\sqrt{(x - x_1)^2 + (y - y_1)^2} \\
\sqrt{(x - x_2)^2 + (y - y_2)^2}
\end{bmatrix}
其中$(x_1,y_1)$和$(x_2,y_2)$是两个路标位置。
3. 完整代码实现
主程序(main.cpp)
#include "SystemModel.hpp"
#include "OrientationMeasurementModel.hpp"
#include "PositionMeasurementModel.hpp"
#include <kalman/ExtendedKalmanFilter.hpp>
#include <kalman/UnscentedKalmanFilter.hpp>
#include <iostream>
#include <random>
using namespace KalmanExamples;
typedef float T;
int main()
{
// 状态、控制和观测类型定义
typedef Robot1::State<T> State;
typedef Robot1::Control<T> Control;
typedef Robot1::SystemModel<T> SystemModel;
typedef Robot1::PositionMeasurementModel<T> PositionModel;
typedef Robot1::OrientationMeasurementModel<T> OrientationModel;
// 创建滤波器实例
Kalman::ExtendedKalmanFilter<State> ekf;
Kalman::UnscentedKalmanFilter<State> ukf(1.0); // UKF缩放参数
// 初始化状态
State x_true;
x_true.x() = 0.0;
x_true.y() = 0.0;
x_true.theta() = 0.0;
ekf.init(x_true);
ukf.init(x_true);
// 系统和观测模型
SystemModel sys;
PositionModel pos_model(-10.0, -10.0, 30.0, 75.0); // 路标位置
OrientationModel orient_model;
// 噪声参数
T systemNoise = 0.1;
T orientationNoise = 0.025;
T distanceNoise = 0.25;
// 随机数生成器
std::default_random_engine generator;
std::normal_distribution<T> noise(0, 1);
// 模拟100步
const size_t N = 100;
for(size_t i = 1; i <= N; i++)
{
// 生成控制输入(正弦轨迹)
Control u;
u.v() = 1.0 + std::sin(2 * M_PI * i / N);
u.dtheta() = std::sin(2 * M_PI * i / N) * (1 - 2*(i > 50));
// 系统模拟(带噪声)
x_true = sys.f(x_true, u);
x_true.x() += systemNoise * noise(generator);
x_true.y() += systemNoise * noise(generator);
x_true.theta() += systemNoise * noise(generator);
// 预测步骤
ekf.predict(sys, u);
ukf.predict(sys, u);
// 每5步更新姿态观测
if(i % 5 == 0)
{
auto orient_meas = orient_model.h(x_true);
orient_meas.theta() += orientationNoise * noise(generator);
ekf.update(orient_model, orient_meas);
ukf.update(orient_model, orient_meas);
}
// 每10步更新位置观测
if(i % 10 == 0)
{
auto pos_meas = pos_model.h(x_true);
pos_meas.d1() += distanceNoise * noise(generator);
pos_meas.d2() += distanceNoise * noise(generator);
ekf.update(pos_model, pos_meas);
ukf.update(pos_model, pos_meas);
}
// 输出结果(CSV格式)
std::cout << x_true.x() << "," << x_true.y() << "," << x_true.theta() << ","
<< ekf.getState().x() << "," << ekf.getState().y() << "," << ekf.getState().theta() << ","
<< ukf.getState().x() << "," << ukf.getState().y() << "," << ukf.getState().theta() << std::endl;
}
return 0;
}
4. 编译与运行
kalman库使用CMake构建系统,编译步骤如下:
# 克隆仓库
git clone https://gitcode.com/gh_mirrors/ka/kalman.git
cd kalman
# 创建构建目录
mkdir build && cd build
# 配置和编译
cmake ..
make
# 运行机器人示例
./examples/Robot1/Robot1
5. 结果分析
运行后会生成CSV格式的状态数据,使用MATLAB或Python绘制轨迹对比图:
从结果可以看出,在机器人这种中等非线性系统中,UKF的精度明显优于EKF,尤其是在航向角估计上,误差降低了约50%。
工程实践中的关键问题与解决方案
1. 噪声协方差矩阵Q和R的确定
Q(过程噪声协方差)和R(观测噪声协方差)的选取直接影响滤波性能:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 经验调参 | 手动调整对角元素,观察误差变化 | 快速原型验证 |
| 最大似然估计 | 最大化观测数据的似然函数 | 有大量历史数据 |
| 自适应滤波 | 在线调整Q和R(如 Sage-Husa 算法) | 噪声统计特性变化的场景 |
kalman库中设置噪声的示例:
// 设置过程噪声协方差Q
sys.getCovariance() = Kalman::Covariance<State>::Identity() * systemNoise * systemNoise;
// 设置观测噪声协方差R
orient_model.getCovariance() = Kalman::Covariance<OrientationMeasurement<T>>::Identity() * orientationNoise * orientationNoise;
2. 状态初始化策略
状态初始值$ \hat{\mathbf{x}}_0 $和协方差$ \mathbf{P}_0 $的选取至关重要:
- 如果有先验知识,直接使用该值初始化
- 如果无先验知识,可将$ \hat{\mathbf{x}}_0 $设为0,$ \mathbf{P}_0 $设为较大的对角矩阵(如$10^4 \mathbf{I}$)
- 对于多传感器系统,可先运行一段时间收集数据,再用批量估计方法(如最小二乘法)初始化
3. 数值稳定性问题
-
协方差矩阵正定化:由于浮点误差,P可能失去正定性,可定期加入微小扰动:
P += Kalman::Covariance<State>::Identity() * 1e-8; -
平方根滤波:使用
SquareRootExtendedKalmanFilter或SquareRootUnscentedKalmanFilter,通过Cholesky分解保证P的正定性
总结与展望
卡尔曼滤波作为一种强大的状态估计算法,已经在导航、控制、信号处理等领域得到广泛应用。本文通过理论结合实践,详细介绍了EKF和UKF的原理与实现,并基于kalman库完成了移动机器人定位案例。
未来发展方向:
- 多传感器融合:结合视觉、IMU等多源数据,提升鲁棒性
- 分布式卡尔曼滤波:适用于多智能体系统
- 深度学习与卡尔曼滤波结合:使用神经网络学习噪声模型或状态转移函数
希望本文能帮助你在实际项目中成功应用卡尔曼滤波技术。如有任何问题或建议,欢迎在评论区留言讨论!
扩展学习资源
- 论文:An Introduction to the Kalman Filter by Greg Welch and Gary Bishop
- 书籍:《卡尔曼滤波与组合导航原理》(秦永元)
- 工具:kalman库官方文档
- 在线课程:Coursera上的"State Estimation and Localization for Self-Driving Cars"
如果觉得本文对你有帮助,请点赞、收藏并关注作者,获取更多嵌入式与机器人技术干货!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



