简介:面向导航定位与滤波算法学习者的粒子滤波实现资料,重点展示粒子滤波在非线性、非高斯条件下的状态估计与高精度定位方法,适用于GPS信号弱、城市峡谷、室内等复杂场景,也可作为SLAM、组合导航等方向的重要前置参考。压缩包内共2个文件,包含一个.m脚本文件(实现粒子滤波核心流程)和一个txt说明文件,整体仅2KB,代码结构精简,适合逐行阅读与修改调试。已有355人学习下载,非常适合正在学习粒子滤波原理、需要快速验证算法效果或开展课程实验的研究生和工程师。借助这份代码,读者可直接运行定位示例,理解粒子初始化、权重更新、重采样等关键步骤,并在此基础上扩展多传感器融合或对比卡尔曼滤波,更高效地完成导航定位课题与项目。
1. 粒子滤波导航定位:为什么机器人导航先把“我在哪”搞清楚
机器人导航的第一步不是规划路径,而是回答一个朴素的问题:我在哪、朝哪个方向。很多团队建图建得漂亮、路径规划也没问题,机器人一上自主导航就翻车,原因多半不是算法不行,而是定位——它连自己从哪出发都不知道,后续所有决策全建立在错误前提上。粒子滤波(particle filter)就是解决这类导航定位问题的常用方法:用一堆带权重的随机粒子去逼近机器人位姿的后验概率,擅长处理“开机不知道在哪”“环境对称导致多个可能位置”这类场景。这篇笔记面向做导航定位的工程师和学习者,从原理拆起,给出一套可运行的 Python 最小实现,再落到 ROS2/Nav2 中 AMCL 的实际配置与常见坑,希望能让你照着做、少走弯路。
2. 粒子滤波原理:把贝叶斯后验变成一堆可计算的随机猜测
2.1 贝叶斯后验与蒙特卡洛采样:粒子就是“带权重的猜测”
机器人定位在数学上是一个贝叶斯滤波问题:已知地图、控制量和传感器观测,求当前位姿 x=(x,y,θ) 的概率分布 p(x | z_{1:t}, u_{1:t})。这个分布严格写出来是个积分,但对于二维平面环境里的非高斯观测,大多数时候没有解析解。
卡尔曼滤波要求线性高斯假设,EKF 退而求其次对非线性做线性化,但依然要求噪声高斯、后验单峰。粒子滤波换了一条完全不同的路:用 N 个粒子表示 N 个候选位姿,每个粒子附带权重 w_i,权重大小表示“这个候选位姿和当前观测匹配的程度”。从数学角度讲,就是用这 N 个带权样本逼近真实后验分布,属于蒙特卡洛方法在滤波问题里的直接应用。
N 个粒子最初可以随机撒满整个地图空间,这就是“全局定位”的来源。随着观测不断到来,权重会自然集中在真实位姿附近的粒子上,分布从“平摊”变成“尖峰”。粒子数量越大逼近越精确,但计算量也线性上涨,所以工程上要做权衡。
2.2 为什么坚持用粒子滤波而不是 EKF:多峰分布、全局定位与噪声鲁棒性
遇到这三类情况,EKF 会非常吃力。
第一类是多峰后验。环境对称时,一串同样的激光观测可能同时匹配多个位置——两排货架几乎一样的仓库走廊里,机器人在不同通道位置都能拿到相似的距离数据。EKF 用单个高斯峰描述后验,只能锁定一个峰;如果锁错了,后续观测也拉不回来,因为均值被错误峰“绑架”了。粒子滤波可以同时维护多个峰,每个峰对应一组粒子,靠权重动态决定当前更相信哪个位置。
第二类是全局重定位。机器人开机时对自身位置一无所知,EKF 必须有初始猜测,给错了就发散。粒子滤波把粒子随机撒到整个可行区域,靠观测逐步筛出正确位置,不需要人工干预。
第三类是传感器噪声模型。激光在玻璃、黑体、镜面附近会出现明显反射和吸收误差,误差分布不是高斯。EKF 的观测更新公式要求高斯噪声,一旦真实噪声带长尾,滤波效果明显下降。粒子滤波的观测似然函数可以自由定义,本质上是采样和权重归一化,不依赖分布闭式解。
| 维度 | EKF | 粒子滤波 |
|---|---|---|
| 状态分布假设 | 高斯单峰 | 任意分布,可多峰 |
| 全局定位 | 需要初始猜测 | 粒子随机撒布即可 |
| 传感器噪声模型 | 要求高斯 | 任意似然函数 |
| 计算开销 | 恒定、低 | 随粒子数线性增加 |
| 典型工程产物 | robot_pose_ekf | AMCL、gmcl |
但粒子滤波也不是银弹。它比 EKF 慢、需要调参、粒子退化是常态。如果机器人已经在建好的地图里完成过初始定位、环境不对称、激光噪声干净,用 EKF 会更划算;但导航场景多半要面对全局定位和重定位,所以 AMCL 这种粒子滤波实现成了事实上的标配。
2.3 预测-更新-重采样:粒子滤波的三个阶段
每个控制周期,粒子滤波跑三件事。
预测:拿到里程计或 IMU 控制量 u=(v, ω),用运动模型把每个粒子向前推一个时间步。差分驱动机器人在小时间间隔 dt 内近似直线运动,就做x += v·dt·cosθ、y += v·dt·sinθ、θ += ω·dt,并叠加过程噪声。过程噪声很关键,它模拟轮子打滑、地面不平带来的不确定性;不加这步,粒子会在重采样后越挤越紧,最后丧失多样性。
更新:把粒子位置对应的“预测观测”和真实传感器观测 z_t 做比较。对激光雷达来说,就是在粒子位置用光线投射或距离场插值算出到最近障碍物的距离,再与真实测距值做高斯似然计算。每个粒子的权重乘上这个似然,再整体归一化,得到后验权重分布。
重采样:权重归一化后,低权重粒子对后验几乎没有贡献,再多跑几轮权重会被少数高权重粒子垄断,这叫粒子退化。重采样按权重从现有粒子中随机抽取 N 个,权重高的被复制几份,权重低的被丢弃,把计算资源重新分配到可信区域。注意,重采样本身会损失多样性,工程上不会每帧都做,而是看退化指标——有效粒子数 Neff = 1/Σw_i²,当 Neff 掉到粒子总数的一半以下才重采样。
三阶段合起来就是一次典型迭代,后面第 3 章的代码会把这套流程落成一个能跑的脚本。
3. 用 Python 从零实现粒子滤波定位:一套能跑通的最小代码
3.1 粒子初始化与运动模型:先让一千个“假设的人”迈开脚步
先不纠结高深的数学。定位器说白了就是把 N 个“假设的机器人位置”抱在手里,不断推着它们走、看观测打分,再挑选好位置。地图用 20×20 米空间、四个角落放四个地标,模拟激光能检测到的柱体。N 默认取 1000,粒子字段就是 x、y、θ 和权重 w。
import numpy as np # 地图地标(单位:米),模拟激光可扫到的柱体 LANDMARKS = np.array([ [2.0, 2.0], [18.0, 2.0], [18.0, 18.0], [2.0, 18.0], ]) class ParticleFilter: def __init__(self, num_particles=1000): self.n = num_particles self.reset() def reset(self): # 全局初始化:粒子在整个地图范围内随机撒布 self.x = np.random.uniform(0, 20, self.n) self.y = np.random.uniform(0, 20, self.n) self.th = np.random.uniform(-np.pi, np.pi, self.n) self.w = np.ones(self.n) / self.n def predict(self, v, omega, dt): # 运动模型:小时间步内近似先转后走 dist = v * dt dth = omega * dt self.th += dth self.x += dist * np.cos(self.th) self.y += dist * np.sin(self.th) # 过程噪声:模拟轮子打滑、地面不平 self.x += np.random.normal(0, 0.02, self.n) self.y += np.random.normal(0, 0.02, self.n) self.th += np.random.normal(0, 0.01, self.n)逻辑说明:reset()用全局均匀分布撒粒子,这是全局定位的核心。predict()传的是线速度 v、角速度 omega 和步长 dt,注意顺序是先转后走。代码里的噪声项必不可少,没有它粒子会因重采样而退化,最终在一个错误位置自我感觉良好。
参数说明:过程噪声标准差 0.02 米/0.01 弧度按普通室内机器人里程计精度估。履带或重型底盘可以放宽到 0.05 米;全向轮要看地面摩擦系数,建议先接 rosbag 跑一遍离线数据再定。
3.2 观测模型与权重更新:用激光距离给每个粒子打分
机器人这帧的真实观测是到四个地标的距离,我们简化成全向测距:每个粒子对每个地标算一个高斯似然,乘起来作为权重乘数。
def update(self, z, sigma=0.15): # z 是长度为 4 的真实测距数组 for i, lm in enumerate(LANDMARKS): # 粒子到地标 i 的期望距离 d_pred = np.sqrt((self.x - lm[0])**2 + (self.y - lm[1])**2) # 高斯似然:期望距离与真实距离越近,得分越高 likelihood = np.exp(-0.5 * ((d_pred - z[i]) / sigma) ** 2) self.w *= likelihood # 防止所有权重下溢到 0 self.w = np.maximum(self.w, 1e-300) # 归一化,让权重之和为 1 self.w /= self.w.sum()逻辑说明:这段代码的核心是“先用地图构造差异,再用差异的似然打分”。likelihood 越大,权重乘得越多,最终归一化后权重越高。np.maximum那行很重要,四个地标似然连乘后数值很容易低于浮点下限,必须截断并重归一。sigma 越大,粒子的“视野”越宽,收敛越平滑,但也更容易被错误峰吸引。
参数说明:sigma 对应激光测距标定误差,室内 2D 激光一般取 0.05~0.2 米。如果雷达长距离噪声偏大,可以在工程里把 sigma 随距离动态放大。我更建议老老实实标定传感器,而不是靠调大 sigma 掩盖问题。
3.3 重采样与有效粒子数:防止粒子群退化的关键一步
粒子滤波最容易从“感觉能跑”变成“翻车”的就是退化:几十轮之后权重只剩几个粒子非零,其余粒子报废。退化指标是有效粒子数 Neff。
def neff(self): # 有效粒子数:权重越均匀该值越接近 n return 1.0 / np.sum(self.w ** 2) def resample(self): # 按权重做有放回抽样 idx = np.random.choice(self.n, size=self.n, p=self.w) self.x = self.x[idx] self.y = self.y[idx] self.th = self.th[idx] self.w = np.ones(self.n) / self.n def mean_pose(self): # 加权平均位姿(角度用圆均值求) return (float(np.sum(self.x * self.w)), float(np.sum(self.y * self.w)), float(np.arctan2(np.sum(np.sin(self.th) * self.w), np.sum(np.cos(self.th) * self.w))))逻辑说明:neff 返回的有效粒子数越大说明权重越均衡;接近 1 说明只剩一个粒子承载全部信任,必须立刻重采样。resample 用np.random.choice按权重做有放回抽样,高权重粒子被复制多份、低权重粒子消失,权重重新平均到 1/N。mean_pose 是给用户汇报的“当前位姿”,x、y 做加权平均,θ 要用圆均值,直接对角度求算术平均会在 ±π 附近错得离谱。
参数说明:我一般把重采样阈值写成neff < self.n * 0.5,即权重均匀性低于一半时再触发;阈值设太高频发重采样,浪费多样性,设太低粒子退化又压不住。
3.4 最小仿真主循环:在合成地图里看粒子如何收敛
下面把上面的类拼起来做合成仿真。真实机器人从 (5,5) 出发,以 0.5 m/s 线速度、0.1 rad/s 角速度跑 10 秒,每个周期加高斯噪声生成“真实观测”,再喂给粒子滤波。
# 仿真参数 dt, T = 0.1, 100 gt = np.array([5.0, 5.0, 1.0]) # 真实位姿:x, y, theta pf = ParticleFilter(num_particles=1000) for t in range(T): v, omega = 0.5, 0.1 # 更新真实位姿 gt[0] += v * dt * np.cos(gt[2]) gt[1] += v * dt * np.sin(gt[2]) gt[2] += omega * dt # 生成带噪声的观测:地标距离 z = np.sqrt((gt[0] - LANDMARKS[:, 0])**2 + (gt[1] - LANDMARKS[:, 1])**2) z += np.random.normal(0, 0.05, len(LANDMARKS)) # 粒子滤波:预测 -> 更新 -> 按需重采样 pf.predict(v, omega, dt) pf.update(z) if pf.neff() < pf.n * 0.5: pf.resample() est = pf.mean_pose() print("估计位姿:", est) print("真实位姿:", gt) print("位置误差(米):", round(np.linalg.norm(np.array(est[:2]) - gt[:2]), 4))逻辑说明:主循环对应真实机器人的控制周期。gt是按运动学公式推进的“上帝视角”,z模拟激光测距并加入 0.05 米高斯噪声。粒子滤波每个周期先按同样的控制量预测,再用观测更新权重,当有效粒子数跌破一半时重采样。“按需重采样”很重要:刚开始粒子均匀分布,更新后有效粒子数骤降,触发多次重采样;粒子收敛后有效粒子数回升,重采样次数自动减少。
参数说明:粒子数 1000 在 20×20 米地图、4 个地标的简单场景下够用;地图扩大到 100×100 米或地标退化时,粒子数低于 300 基本不收敛。可以每帧打印 neff 观察退化节奏:连续 30 帧 neff 都大于 600,说明观测信息充足,可以降粒子数省算力。
4. 在 ROS2/Nav2 中落地粒子滤波:AMCL 参数调优与边界手感
4.1 AMCL 在导航栈里做什么:从静态地图到实时位姿变换
Nav2 导航栈里,AMCL(Adaptive Monte Carlo Localization)是最常用的粒子滤波定位实现。它的输入端是静态地图(通常是建图阶段保存的二维占用栅格地图)、TF 变换(odom→base_footprint)、激光 scan,输出端是不断修正的 odom→map 坐标变换。导航的整体链路大致是:map_server 发布地图,AMCL 维护“map 原点相对机器人当前位姿”的修正矩阵,planner 基于 AMCL 发布的位姿做路径规划。也就是说,AMCL 是导航栈里的感知定位模块。
这里要记住一个边界:AMCL 不是建图工具。它假设地图已经存在且保持不变,只负责回答“实时位姿是什么”。如果地图本身带误差(比如建图时扫描匹配没做好),AMCL 再调参也救不回来,这属于输入质量问题。
4.2 必调参数:粒子数、KLD 误差、重采样间隔与里程计噪声
一份比较稳的 AMCL 参数模板长这样,参数名遵循 Nav2 的 amcl 配置,注释是我按现场手感加的:
# amcl.yaml min_particles: 500 max_particles: 2000 # KLD 采样:误差上限和置信度下限 kld_err: 0.02 kld_z: 0.99 # 位姿移动多少距离/角度后触发一次观测更新 update_min_d: 0.25 update_min_a: 0.2 # 重采样间隔:每 1 帧执行一次按需重采样 resample_interval: 1 # TF 与控制器之间的时间容差,单位秒 transform_tolerance: 1.0 # 恢复重采样策略:slow/fast alpha recovery_alpha_slow: 0.001 recovery_alpha_fast: 0.1 # 激光模型:likelihood_field_prob 抗噪声性更好 laser_model_type: likelihood_field_prob laser_max_range: 30.0参数说明:
- min_particles/max_particles:粒子数量上下界。AMCL 会根据 KLD 采样自动增减粒子,但上下界别设太极端。室内平地建议 (500, 2000),仓库大空间可以上 (2000, 8000)。我一般不会盲目把上限拉高,粒子数从 1000 加到 3000,定位精度提升很小,CPU 占用却涨了 3 倍。
- kld_err/kld_z:这两个决定“自动停止增加粒子”的统计阈值。kld_err 越小,要求粒子越多越精细;kld_z 越大,置信度要求越高也需要更多粒子。默认 0.02/0.99 已经够用,不追求极致精度不用动。
- update_min_d/update_min_a:机器人移动 0.25 米或转 0.2 弧度以上才处理一帧观测。设太大粒子跟不上快速转弯,设太小浪费 CPU 处理重复帧。
- 激光模型:likelihood_field_prob 比 beam 模型抗噪声,但对长走廊“假峰”更敏感。如果机器人在玻璃环境跑,beam 模型的短波噪声表现反而好一些。
- recovery_alpha_slow/fast:这是 AMCL 的“后悔药”机制。fast 系数大于 slow 时,如果短时间观测匹配分数骤降,说明可能被“绑架”,系统会随机补撒少量粒子。默认 0.001/0.1 能用,但我在对称环境里会把两个都调成 0,避免频繁补撒造成抖动。
4.3 Nav2 下 AMCL 的启动与 TF 衔接细节
Nav2 里 AMCL 通常由 nav2_bringup 启动,yaml 路径在 launch 文件里指定。跑通以后最常遇到的三个衔接问题:
第一,AMCL 输出的 map→odom 变换必须有 TF 树的连续性。检查方法是用ros2 run tf2_ros tf2_echo map odom观察是否持续输出。频繁中断时,先看底盘驱动是否定期发布 odom,再检查 AMCL 的 transform_tolerance 是否小于底盘驱动延迟。
第二,base_footprint 和 base_link 的变换要正确。我见过最玄学的问题是 AMCL 定位明明正常,rviz 里机器人和地图却在视觉上错位。原因不在定位,而在底盘驱动里的 base_footprint 没接好激光雷达坐标系。TF 树缺一环,AMCL 输出再准也没用。
第三,激光雷达频率和 AMCL 更新频率不匹配。雷达 40Hz、update_min_d 设 0.25 米、移动速度 1.2m/s 时,观测更新周期约 0.2 秒,没问题;但如果你用的是 10Hz 雷达,还要把 transform_tolerance 适当加大。我见过有人把 update_min_d 调成 0.05 米,10Hz 雷达根本处理不过来,AMCL 迟到,定位反而更差。
5. 粒子滤波定位避坑:五个常见问题与排查路径
5.1 粒子全聚到“假位置”:对称环境里的多峰定位翻车
现象:机器人在两侧对称的货架间直行,AMCL 位姿每隔十几秒突然横跳一米以上,但激光匹配分数并没有明显下降。
原因:对称走廊里多个位置能拿到几乎相同的测距结果。粒子滤波后验呈双峰,重采样把粒子集中到其中一个峰;因为两侧结构完全相同,随机性让它来回切换。
解决:先确认地图对称性来源,看激光匹配得分是否真的没降。工程上两个做法:一是限制全局初始化时的粒子分布范围,例如已知机器人在某个通道区间,就不要整图撒粒子;二是检测“跳变”——把 AMCL 输出的位姿和里程计位姿做差,差值在零点几秒内突变超过 1 米,就触发一次粒子全局重新撒布,相当于重置 AMCL。
5.2 长走廊里粒子数不够:滤波发散与横向漂移
现象:机器人在 30 米长走廊内行走,定位输出的方向角没问题,但横向位置缓慢漂移,从走廊中心线偏到墙边,最后在 rviz 里看机器人已经“穿墙”。
原因:走廊纵长横窄,激光在横向的信息高度重复,观测更新对横向位置约束不足。粒子数兜不住横向的弱约束,几次重采样后粒子簇就在横向上错开。
解决:确保走廊两端有门洞、柱子这类结构点,用 max_particles 撑住横向多峰。我在 50 米走廊里把 max_particles 从 2000 拉到 5000,横向漂移明显变小,CPU 占用也上来了。如果确实没有更多结构特征,可以加反光柱辅助,或者与 IMU 横向加速度做融合。
5.3 机器人被搬走以后回不来:全局重定位失效
现象:机器人被人工挪到另一个房间再开机,AMCL 还输出旧位置的位姿,导航随即报路径不可行。
原因:AMCL 默认继承上一时刻的后验样本,新观测会把粒子从旧位置拖向新位置,但粒子数不足时可能要几十秒甚至不收敛,这就是经典的 kidnapped robot 问题。
解决:在系统里保留一个全局重定位入口。常见的做法是订阅一个重置请求,收到后重新初始化粒子云;实现麻烦的话,把 AMCL 节点重启一遍,节点加载地图后会做全局初始化。这个过程建议在 UI 里做一个按钮,交给现场操作员触发。
5.4 重采样过于频繁:粒子多样性被提前耗尽
现象:机器人静止时定位却在“跳舞”,位姿输出抖个不停,甚至偶尔跳到错误位置。
原因:resample_interval 设成 1 且 update_min_d 设得太小,机器人不动时观测几乎不变,但每帧都在重采样,粒子多样性损失后又靠过程噪声拉宽,形成震荡。
解决:把 resample_interval 提高到 3~5,update_min_d/update_min_a 调回 0.25 米/0.2 弧度。条件允许的话,在主循环里加一个“机器人速度接近零时不重采样”的判断——静止状态下粒子群不应该剧烈变化。
5.5 激光传感器噪声不匹配:玻璃墙和反光地面上的玄学问题
现象:玻璃幕墙附近,激光测距要么穿透、要么打出一个很远的异常值,粒子权重整体指向错误位置,定位“翻车”毫无预兆。
原因:likelihood_field 模型假设激光误差符合高斯分布,玻璃和镜面反射会让测距变成一段长尾噪声,超出高斯模型的表达能力。粒子滤波本身允许自定义似然,但默认模型没覆盖这种情况。
解决:做两步前处理。先在驱动层滤掉过远或过近的无效点,再对静态玻璃区域手工补全地图,把玻璃画成障碍物。如果玻璃是移动的(比如会被人推开的玻璃隔断),定位模块很难单独解决,建议改多传感器融合,或用更长时段的观测做粒子记忆。
6. 验证定位是否真的可信:用 Neff 与位姿协方差做体检
6.1 用 Neff 和协方差判断收敛与发散
粒子滤波在实机上跑起来以后,不能只看 rviz 里“看起来没问题”。AMCL 会发布 /particlecloud 话题,里面是粒子位姿数组,但通常不包含权重,所以 Neff 要结合日志或自定义发布拿;粒子云的协方差则可以直接算。
# 订阅 /particlecloud(nav_msgs/PoseArray)后取坐标算协方差 import numpy as np def particle_cloud_std(particles): # particles: N x 3,对应 x, y, theta xs = particles[:, 0] ys = particles[:, 1] cov = np.cov(np.vstack((xs, ys))) spread = np.sqrt(np.trace(cov)) return cov, spread逻辑说明:协方差的迹越小,说明粒子聚得越紧;运行一段时间后 trace 仍然大于某个阈值,代表观测信息不足、粒子还在乱晃。如果协方差很小但机器人位姿明显错了,说明粒子收敛到了错误峰——这时候要看多峰性:把粒子云聚类成两个簇,如果两个簇权重相近,就说明对称场景里后验确实多峰,不能只取一个加权平均。
6.2 数据回放与“回到充电桩”的实机校验法
最有效的验证手段不是看协方差,而是回放数据。把激光、里程计、AMCL 输出录成 rosbag,离线对同一段数据反复调参,对比不同参数下的位姿误差。这个习惯帮我避免了大量实车往返试验,比在现场一次一次跑要快得多。
实机验证也一样朴素:让机器人回到一个坐标精确已知的充电桩,对比当前打印位姿和昨天标定的位姿。偏差在 5 厘米以内说明定位闭环是健康的;偏差超过 10 厘米就要回头查里程计标定和地图精度,而不是继续调粒子数——这是很多人忽略的“原点验证法”。我自己的习惯是每次改完参数都跑一遍原点回充,如果回充对准都过不了,就不会继续测导航。
最后提醒一句:粒子滤波不是黑匣子,把 Neff、协方差、匹配分数打印出来看着它慢慢收敛,比闷头调一百次参数都管用。希望这些经验和踩过的坑能帮你把导航定位这一步走稳。
本文还有配套的精品资源,点击获取