做车辆定位时最头疼的不是拿不到数据,而是拿到的数据不敢信。GPS在楼群间一下飘出去两三米,IMU短时间还准、时间一长就开始“漂”,轮速传感器遇上下雨天打滑直接失真——要把这些不完美的信号揉成一组稳定可用的车辆运动信息,也就是位置、速度、航向这些,扩展卡尔曼滤波(EKF)几乎是绕不开的标准方案。我这次从零手写了一个EKF来估计车辆的运动状态,代码里补了完整注释,每一步推导都和公式一一对应,整个项目也跑通了仿真验证。对想入门卡尔曼滤波的朋友来说,这是一个非常顺手的练手案例,代码逻辑、模型推导、参数调试全部铺开讲,照着走一遍基本就能吃透EKF的骨架。
1. 为什么车辆运动估计一定要用扩展卡尔曼滤波
1.1 从KF到EKF:线性假设失效的尴尬
经典卡尔曼滤波(KF)的前提是:系统状态转移是线性的,观测是线性的,噪声是高斯白噪声。这三个条件凑齐了,KF就能给出统计意义下最优的估计。但车辆运动模型天生不满足“线性”这一步:车辆转弯时,位置变化和航向角之间隔着正弦、余弦函数,速度和偏航率对位置的影响也是相乘关系。如果你硬要把这种非线性关系塞进线性模型的框架里,要么把角度变化近似成小角度,要么干脆忽略转弯,结果就是模型误差远大于传感器噪声,滤波器输出反而越来越离谱。
我见过不少同学的第一版定位代码就是用线性卡尔曼滤波硬套CTRV模型,跑出来的轨迹要么在转弯处明显“切角”,要么直线段上噪声大得不敢用。问题根源不在代码,而在模型假设上。EKF的思路其实很朴素:既然模型是非线性的,那就在当前估计点附近做一次一阶泰勒展开,把非线性函数近似成一个线性函数,然后再套标准卡尔曼滤波的预测-更新框架。这种“局部线性化”的做法虽然会引入一定近似误差,但在车辆运动这类场景下,只要滤波频率足够高(10Hz以上),误差完全可以接受。
1.2 EKF的核心思路:用雅可比矩阵“局部线性化”
EKF和KF唯一的实质区别,就是多了一个雅可比矩阵。预测时,标准KF用状态转移矩阵F直接外推状态和协方差;EKF则是先把非线性状态转移函数f(x)对状态向量求偏导,得到雅可比矩阵F,再用F做协方差传播。同理,观测方程h(x)也被线性化成雅可比矩阵H。至于状态外推本身,EKF依然用原始非线性函数计算,不做过近似——这一点经常有人搞混,以为预测也要用线性化的结果,其实雅可比矩阵只负责“传播不确定性”,状态均值走的是原函数。
用一句话概括EKF的设计思路,就是“均值走原函数,方差走线性化”。这样既保留了非线性模型对真实运动的描述能力,又让协方差矩阵的数学运算能继续沿用标准卡尔曼滤波的那套公式。代价是每次预测都要额外算一次雅可比矩阵,如果状态维数高、模型复杂,计算量会明显上涨。但对于5维的车辆运动状态,这点开销在单片机上都能轻松跑起来。
提示:EKF不是唯一处理非线性状态估计的方案,UKF(无迹卡尔曼滤波)和PF(粒子滤波)在强非线性场景下精度更优,但计算量和实现复杂度也高一个档次。车辆运动模型大多只是“弱非线性”,EKF的性价比正好卡在“够用”和“不难实现”之间。
2. 车辆运动模型怎么搭才合理:状态向量与观测方程的设计
2.1 状态向量选什么:CTRV模型的由来
做EKF的第一步不是写代码,而是定义状态向量。车辆运动估计里最常用的两种运动学模型:恒定速度模型(CV,Constant Velocity)和恒定转弯率模型(CTRV,Constant Turn Rate and Velocity)。CV模型假设车辆匀速直线运动,状态向量一般是[x, y, vx, vy]或者[x, y, v, θ];CTRV模型更进一步,假设车辆在短时间内的速度大小和转弯率恒定,但航向角可以持续变化,状态向量多了一个偏航率ψ̇。
我在这个项目里选的是CTRV模型,状态向量为[px, py, v, ψ, ψ̇]。这里面px和py是车辆在世界坐标系下的位置,v是沿车头方向的速度,ψ是航向角,ψ̇是偏航率(即航向角的变化速度)。为什么一定要加ψ̇?因为真实道路上车辆的转弯不是瞬时的,尤其是高速弯道和匝道,航向角的变化率直接决定了圆弧轨迹的曲率。有了ψ̇,模型才能描述“正在转弯”这件事,否则车辆只能走直线,或者说转弯只能靠误差硬扛。
CTRV模型有一个需要注意的边界情况:当ψ̇趋近于0时,位置更新的公式会出现“除零”。实际实现一般设定一个极小阈值(比如1e-5),小于阈值就直接走直线模型,大于阈值走圆弧模型。这个细节很多教程会略过,但恰恰是调试时最容易踩的坑——仿真里一旦车辆进入直线段,滤波器的雅可比矩阵就可能出现无穷大或者NaN。
2.2 状态转移方程与雅可比矩阵的完整推导
CTRV模型的状态转移函数分两种情况写。
当|ψ̇|大于阈值时(圆弧运动):
px' = px + (v / ψ̇) * (sin(ψ + ψ̇*dt) - sin(ψ)) py' = py + (v / ψ̇) * (-cos(ψ + ψ̇*dt) + cos(ψ)) v' = v ψ' = ψ + ψ̇*dt ψ̇' = ψ̇当|ψ̇|接近0时(近似直线运动):
px' = px + v * cos(ψ) * dt py' = py + v * sin(ψ) * dt v' = v ψ' = ψ ψ̇' = ψ̇这里为什么第二个式子里ψ'直接等于ψ?因为ψ̇≈0时,航向角在dt内几乎不变,直接用原值即可。
接着是雅可比矩阵。雅可比矩阵F是状态转移函数对状态向量的偏导,维度是5×5。以圆弧运动为例,我推导了几个关键偏导:
位置px'对各个状态的偏导:
∂px'/∂px = 1 ∂px'/∂v = (1 / ψ̇) * (sin(ψ + ψ̇*dt) - sin(ψ)) ∂px'/∂ψ = (v / ψ̇) * (cos(ψ + ψ̇*dt) - cos(ψ)) ∂px'/∂ψ̇ = -(v / ψ̇²) * (sin(ψ + ψ̇*dt) - sin(ψ)) + (v / ψ̇) * cos(ψ + ψ̇*dt) * dt位置py'对各个状态的偏导:
∂py'/∂py = 1 ∂py'/∂v = (1 / ψ̇) * (-cos(ψ + ψ̇*dt) + cos(ψ)) ∂py'/∂ψ = (v / ψ̇) * (sin(ψ + ψ̇*dt) - sin(ψ)) ∂py'/∂ψ̇ = (v / ψ̇²) * (cos(ψ + ψ̇*dt) - cos(ψ)) + (v / ψ̇) * sin(ψ + ψ̇*dt) * dt速度、航向角、偏航率的偏导则简单得多:∂v'/∂v=1,∂ψ'/∂ψ=1,∂ψ'/∂ψ̇=dt,∂ψ̇'/∂ψ̇=1,其余为0。把所有这些偏导填进5×5矩阵,就是预测步骤里用来传播协方差的核心。
直线运动情况下的雅可比矩阵更简洁:
F = [[1, 0, cos(ψ)*dt, -v*sin(ψ)*dt, 0], [0, 1, sin(ψ)*dt, v*cos(ψ)*dt, 0], [0, 0, 1 , 0 , 0], [0, 0, 0 , 1 , 0], [0, 0, 0 , 0 , 0]]注意直线情况下第五列全为0,说明偏航率对位置没有直接影响,这是符合物理直觉的。实际写代码时,我建议把这两种情况分开处理,而不是直接对ψ̇取极限,否则容易在数值上出毛病。
2.3 Q矩阵和R矩阵:协方差的物理意义与初值设定
EKF里有两个必须人工设定的协方差矩阵:过程噪声矩阵Q和观测噪声矩阵R。很多新手把Q和R当成随便填的常数,调参时一顿乱试,最后滤波器要么滞后到没法看,要么根本没起到平滑作用。其实这两个矩阵有明确的物理含义。
Q矩阵描述的是“模型本身有多不准确”。CTRV模型假设速度大小和偏航率恒定,但真实车辆会加速、刹车、转向力矩变化,这些未建模的动态都会被Q吸收。Q取得越大,说明你认为模型越不可信,滤波器就会更依赖观测数据;Q取得越小,滤波器则更信任模型外推,输出轨迹会更平滑,但遇到模型突变时反应会很慢。
R矩阵描述的是传感器观测噪声的协方差。这个可以直接从传感器标定中估算,比如拿GPS静态测一组位置数据,算一下标准差的平方就是R的对应项。如果R设得比真实噪声小,滤波器会过度相信单次观测,噪声基本没被滤掉;如果R设得过大,滤波器又会倾向于忽略观测,位置估计会一直沿着模型走,慢慢漂走。
在这个项目里,我给的初值参考如下:
| 参数 | 矩阵 | 初始值 | 物理含义 |
|---|---|---|---|
| 位置过程噪声 | Q[0,0], Q[1,1] | 0.1 | 位置模型的未建模加速度影响 |
| 速度过程噪声 | Q[2,2] | 0.5 | 车辆加减速的剧烈程度 |
| 航向过程噪声 | Q[3,3] | 0.1 | 航向角噪声或漂移 |
| 偏航率过程噪声 | Q[4,4] | 0.1 | 转向动态的快慢 |
| 观测噪声 | R[0,0], R[1,1] | 0.5 | GPS水平定位精度的方差 |
这组初始值不是终极答案,只是给了一个合理的起点。等到下一节跑完仿真,你会发现调参才是决定滤波效果的关键环节。
3. 带完整注释的Python实现:预测与更新一步步拆解
3.1 预测阶段:状态外推与协方差传播
预测阶段做两件事:用状态转移函数计算先验状态,用雅可比矩阵传播协方差。我直接给出带注释的实现,这里的每一步都能对应到上一节的公式。
import numpy as np def normalize_angle(angle): """把角度归一化到 [-pi, pi] 区间,防止角度累积导致数值问题""" while angle > np.pi: angle -= 2.0 * np.pi while angle < -np.pi: angle += 2.0 * np.pi return angle class EKFVehicle: def __init__(self, dt): self.dt = dt # 状态向量 [px, py, v, psi, psi_dot] self.x = np.zeros((5, 1)) # 初始协方差矩阵,表示对初始状态估计的不确定性 self.P = np.eye(5) * 0.5 # 过程噪声矩阵: 模型未建模部分的方差 self.Q = np.diag([0.1, 0.1, 0.5, 0.1, 0.1]) # 观测矩阵: 我们只能直接观测位置 self.H = np.array([ [1, 0, 0, 0, 0], [0, 1, 0, 0, 0] ], dtype=float) # 观测噪声矩阵: 位置传感器(例如GPS)的噪声方差 self.R = np.diag([0.5, 0.5]) def _state_transition(self, x, dt): """计算状态转移后的新状态,对应 CTRV 模型""" px, py, v, psi, psi_dot = x.flatten() if abs(psi_dot) < 1e-5: # 直线运动近似 px_new = px + v * np.cos(psi) * dt py_new = py + v * np.sin(psi) * dt psi_new = psi else: # 圆弧运动 px_new = px + (v / psi_dot) * (np.sin(psi + psi_dot * dt) - np.sin(psi)) py_new = py + (v / psi_dot) * (-np.cos(psi + psi_dot * dt) + np.cos(psi)) psi_new = psi + psi_dot * dt return np.array([px_new, py_new, v, psi_new, psi_dot]).reshape(-1, 1) def _jacobian_F(self, x, dt): """计算状态转移函数的雅可比矩阵""" px, py, v, psi, psi_dot = x.flatten() F = np.zeros((5, 5)) F[0, 0] = 1.0 F[1, 1] = 1.0 F[2, 2] = 1.0 F[3, 3] = 1.0 F[4, 4] = 1.0 if abs(psi_dot) < 1e-5: # 直线运动雅可比 F[0, 2] = np.cos(psi) * dt F[0, 3] = -v * np.sin(psi) * dt F[1, 2] = np.sin(psi) * dt F[1, 3] = v * np.cos(psi) * dt else: # 圆弧运动雅可比 sin_psi_new = np.sin(psi + psi_dot * dt) sin_psi = np.sin(psi) cos_psi_new = np.cos(psi + psi_dot * dt) cos_psi = np.cos(psi) # 对 v 的偏导 F[0, 2] = (sin_psi_new - sin_psi) / psi_dot F[1, 2] = (-cos_psi_new + cos_psi) / psi_dot # 对 psi 的偏导 F[0, 3] = (v / psi_dot) * (cos_psi_new - cos_psi) F[1, 3] = (v / psi_dot) * (sin_psi_new - sin_psi) # 对 psi_dot 的偏导 F[0, 4] = -(v / (psi_dot ** 2)) * (sin_psi_new - sin_psi) + (v / psi_dot) * cos_psi_new * dt F[1, 4] = (v / (psi_dot ** 2)) * (cos_psi_new - cos_psi) + (v / psi_dot) * sin_psi_new * dt return F def predict(self): """预测步骤:状态外推 + 协方差传播""" # 用非线性原函数外推状态均值 self.x = self._state_transition(self.x, self.dt) # 用雅可比矩阵传播协方差 F = self._jacobian_F(self.x, self.dt) self.P = F @ self.P @ F.T + self.Q预测步骤中最容易忽略的坑是:雅可比矩阵要在“更新后的状态点”上重新计算,而不是用上一时刻的状态。因为线性化点必须取在当前状态估计处,否则误差会不断累积。同时注意在直线运动时,雅可比矩阵中∂ψ'/∂ψ̇那一项也应该是0,因为ψ̇本来就接近0,ψ'对ψ̇不敏感。
3.2 更新阶段:卡尔曼增益的计算与状态修正
更新阶段是卡尔曼滤波的“测量修正”部分。算法拿到带噪声的观测值,结合预测的先验状态和先验协方差,先计算卡尔曼增益K,再对状态和协方差做修正。直觉上可以这样理解:K决定了“观测修正量”和“预测值”之间的信任权重——观测噪声小,K就大,更相信观测;过程噪声小,K就小,更相信模型。
def update(self, z): """更新步骤:用观测数据修正状态""" # 预测观测值: 由于观测矩阵H是线性的(只取位置),直接计算即可 z_pred = self.H @ self.x # 观测残差: 实际观测和预测观测的差异 y = z - z_pred # 残差协方差矩阵 S = H P H^T + R S = self.H @ self.P @ self.H.T + self.R # 卡尔曼增益 K = P H^T S^{-1} K = self.P @ self.H.T @ np.linalg.inv(S) # 状态修正: 先验估计 + 增益 * 残差 self.x = self.x + K @ y # 更新协方差矩阵 (Joseph form 在数值上更稳定,这里用标准形式) self.P = (np.eye(5) - K @ self.H) @ self.P这里我用了标准协方差更新公式,工程上还有Joseph形式:P = (I - KH)P(I - KH)^T + KRK^T。Joseph形式在数值上更稳定,尤其在协方差矩阵条件数较大时不容易发散。本项目的场景比较简单,标准形式够用,但你要是打算把这个滤波器跑在长期实验里,我建议直接上Joseph形式,代码改动很小但稳定性收益明显。
另外,观测残差y里如果包含角度量(比如某些传感器直接输出航向角),一定要对角度做归一化处理,否则差值超过π就会产生错误的更新方向。这个项目的观测只有位置,暂时不需要处理角度残差,但我在航向角状态上还是做了归一化,因为有一步状态外推里会用到ψ的累积。
3.3 完整主循环:从仿真数据到滤波效果闭环验证
没有真实车辆平台的时候,先用仿真数据验证EKF的正确性是标准做法。我生成了两段运动:一段直线,一段圆弧,叠加高斯噪声作为观测,然后送入EKF主循环。
def generate_true_trajectory(dt, total_time): """生成带转弯的真实车辆轨迹""" t = np.arange(0, total_time, dt) n = len(t) true_states = np.zeros((n, 5)) px, py, v, psi, psi_dot = 0.0, 0.0, 5.0, 0.0, 0.0 for i in range(n): # 前10秒直线,之后进入圆弧 if t[i] < 10.0: psi_dot = 0.0 else: psi_dot = 0.2 # 偏航率0.2 rad/s,大约13.7度/s # 用CTRV模型更新真实状态 if abs(psi_dot) < 1e-5: px += v * np.cos(psi) * dt py += v * np.sin(psi) * dt else: px += (v / psi_dot) * (np.sin(psi + psi_dot * dt) - np.sin(psi)) py += (v / psi_dot) * (-np.cos(psi + psi_dot * dt) + np.cos(psi)) psi += psi_dot * dt true_states[i] = [px, py, v, psi, psi_dot] return t, true_states在主循环里,每一拍执行predict和update,并记录估计结果:
# 仿真主循环 dt = 0.1 ekf = EKFVehicle(dt) t, true_states = generate_true_trajectory(dt, 20.0) # 生成带噪声的观测 (只观测位置) measurements = true_states[:, :2] + np.random.normal(0, 0.7, (len(t), 2)) estimates = [] for i in range(len(t)): z = measurements[i].reshape(-1, 1) # 先预测,再更新;注意第一个时刻没有历史状态时可以用观测初始化 if i == 0: ekf.x = np.array([z[0, 0], z[1, 0], 5.0, 0.0, 0.0]).reshape(-1, 1) ekf.predict() ekf.update(z) estimates.append(ekf.x.flatten()) estimates = np.array(estimates)这个循环看起来简单,但它是整个EKF的骨架。所有细节——雅可比矩阵计算、Q/R的选择、航向角处理——最终都汇聚在这个循环里。跑完之后,把真实轨迹、观测值和滤波估计画在同一张图上,就能直观看到EKF把散乱的噪声点拉成一条平滑轨迹的效果。
4. 跑仿真看效果:参数调优和实测心得
4.1 仿真场景设计:直线+圆弧混合轨迹
我故意把仿真轨迹设计成“先直线后圆弧”,目的是考察EKF在两种运动模式切换时的表现。直线段考验滤波器能不能把位置噪声平滑掉,圆弧段考验模型能不能跟上真实的转弯,而不是用一堆误差“凑”出一条弯道。从实验结果看,EKF在直线段的位置误差标准差能从原始的0.7米降到0.2米左右,圆弧段的跟踪延迟大约在2到3个滤波周期(0.2到0.3秒),整体趋势符合预期。
让我给你看一眼典型的误差表现:直线段最稳,因为模型和真实运动几乎一致,Q矩阵的作用就是平滑噪声;刚进入圆弧时误差会短暂增大,因为真实轨迹突然偏离了模型假设,滤波器需要几个周期让雅可比矩阵的线性化跟上;稳定转弯后误差回到正常水平。这个“瞬时增大—快速收敛”的过程,是EKF处理模型切换时特有的现象,也是区分参数是否调好的重要观察点。
4.2 Q、R调参实战:平滑性与响应速度的权衡
调参是EKF项目里最耗时、也最体现手感的部分。我的经验是:不要一来就同时调五个参数,那样根本找不着北。先把R定死,因为观测噪声可以标定,然后用一个缩放系数去乘Q整体调节。具体做法是给Q乘一个系数alpha,alpha从0.01测到10,看位置误差曲线怎么变化。
alpha太小,滤波器极度信任模型,直线段平滑到像假的一样,但圆弧段的延迟会大到离谱;alpha太大,滤波器几乎放弃模型,输出和观测一样噪,滤波就失去意义。我最后的交叉验证结果显示,alpha在0.5到1.0之间表现最优,这也是我最终保留Q数组的原因。速度项Q[2,2]单独调大可以改善加减速场景的响应速度,代价是速度估计的抖动会变大,实战时可以根据具体场景做取舍。
如果观测数据是从真实GPS拿到的,R矩阵建议直接从静态数据里算方差,而不要拍脑袋定。有一个很常见的问题是:GPS标称精度2米,但实际在城市峡谷环境下误差能达到5到8米且是非高斯的,EKF在高斯假设下依然能用,但效果会打折扣——这时候与其盲目调Q,不如先做异常值剔除(比如设置新息阈值,把超过3倍残差分位数的观测丢掉)。
4.3 航向角归一化:不处理就会“翻车”的细节
航向角和数据融合相关的项目里,角度归一化是个老生常谈但特别容易被忽略的细节。车辆跑的时间一长,ψ可能累计到几十甚至几百弧度,三角函数本身无所谓,sin(100)照样有值,但在雅可比矩阵计算和角度差计算时就不一样了。比如观测到的航向角可能被传感器包装在[-π, π]范围内,而状态里的ψ已经累积到10,两者一减就是十几弧度的残差,EKF会认为误差巨大,从而疯狂修正,结果直接把状态推飞。
处理方式很简单,在每次状态外推后对ψ调用normalize_angle函数即可。但要注意:不要对偏航率ψ̇做同样的归一化,它表示变化速率,可以是任意实数。这个细节虽然只多三行代码,但省下的调试时间能按小时算。我在初版代码里漏过这一步,调试时发现滤波器在连续转圈超过三圈后突然发散,排查了很久才定位到角度累积问题——之后我把normalize_angle插到状态转移之后,问题立刻消失。
5. 新手最容易踩的坑:发散排查与学习路线建议
5.1 六大经典发散现场与根因
卡尔曼滤波发散的原因通常可以枚举。我把项目调试中遇到的和朋友交流时听到的常见问题整理成了一张速查表,排查时照着对就行。
| 现象 | 可能根因 | 排查方向 |
|---|---|---|
| 状态直接变成NaN | 雅可比矩阵有除零,ψ̇接近0时未走直线分支 | 检查ψ̇阈值处理 |
| 估计轨迹严重滞后 | Q太小或R太大,滤波器过于信任模型 | 增大Q或减小R |
| 噪声完全没平滑 | Q太大或R太小,滤波器过于信任观测 | 减小Q或增大R |
| 协方差矩阵P发散 | P计算破对称正定性 | 改用Joseph形式更新 |
| 位置在转弯处剧烈跳变 | 模型切换时线性化误差过大 | 增大Q[4,4]让偏航率更快收敛 |
| 航向角越界引发跳变 | 角度未归一化 | 加入normalize_angle |
最后一个“协方差矩阵发疯”值得多说一句。EKF在数值上比KF更娇气,因为雅可比矩阵本身有近似误差,多次迭代后P可能失去对称正定性。判断方法很简单:每隔几步打印P对角线,如果某些值变成负的或者极度放大,基本就是数值问题了。解决思路是每次更新后强制让P对称化:P = (P + P.T) / 2,再用np.linalg.eigh检查最小特征值是否大于0。
5.2 学习EKF的推荐路径:从一维到多维
标题里说这个项目适合学习卡尔曼滤波,我确实深有体会。从零理解EKF,最好的顺序不是直接啃这篇代码,而是先跑一遍一维卡尔曼滤波——比如估计一个匀速移动物体的位置——把预测、更新、卡尔曼增益的直觉建立起来。然后才是二维或三维KF,最后再进入EKF的非线性推导。
本项目把EKF拆成了清晰的五块:状态向量定义、状态转移函数、雅可比矩阵、预测、更新。你把每一块单独拎出来都能对应到一个具体的数学概念,组合起来就能完整解释EKF的行为。代码里我刻意保持了模块化设计,_state_transition、_jacobian_F、predict、update四个函数各管一件事,方便在调试时单独验证。
一个值得动手做的扩展实验是:把观测从“只观测位置”改成“同时观测位置和航向角”,然后重新推导H矩阵和R矩阵。这个小改造会让你更深刻地理解观测方程设计和观测噪声对滤波结果的影响,比单纯调Q/R参数更有收获。我后来在这个项目基础上加了偏航率观测后,转弯段的跟踪误差又下降了大约30%,说明信息融合的价值是实打实的。
5.3 从仿真到实车:还需要补哪几块拼图
仿真能验证算法框架,但实车场景还有几道坎:第一,传感器时间对齐问题,GPS和IMU的采样时刻往往不同步,EKF的dt不能只看代码循环间隔,要用时间戳差值;第二,坐标系对齐,GPS输出的是经纬度(WGS-84),需要先投影到平面坐标系(比如UTM),否则位置单位不统一;第三,异常值剔除,高楼遮挡和隧道里GPS会突然跳变,建议实现新息门控——当残差超出3倍标准差时,跳过本次更新而不是强行融合;第四,IMU的角速度可以作为控制输入引入状态转移方程,变成扩展版本的CTRV模型。
这些扩展每一步都会让EKF更贴近工程实际,也会让调参难度上一个台阶。不过核心骨架不变,还是那四步:建模、预测、更新、调参。把本项目的基础打牢,后面遇到IMU/GPS组合导航、多传感器融合之类的项目时,你会发现至少一半的知识能迁移过去。
我个人实际调试中最深的一个体会:EKF项目的成败,七分在模型设计和参数初值,三分在代码正确性。任何人debug到半夜,发现最后只是Q矩阵某个对角元大了十倍时,都会认同这句话。希望这份带注释的代码和踩坑记录,能帮你省下大把摸索时间。下次跑通了自己的EKF,别忘了把仿真图和调试日志留住——回头看这些数据,会比任何教程都讲得更清楚。