☰
粒子滤波实战代码包:非线性非高斯状态估计的可部署实现
2026/10/19 15:08:32 网站建设 项目流程

简介:本资源是一套面向控制理论、机器人定位与计算机视觉方向初学者及进阶学习者的粒子滤波完整实现代码,聚焦非线性非高斯场景下的状态估计问题,可直接用于课程实验、算法复现与项目原型开发。压缩包共19个文件,含10个MATLAB主程序(.m)实现粒子传播、观测更新、权重计算、重采样与可视化等核心模块;7个备份脚本(.asv)便于版本回溯;1个说明文本(.txt)提供下载来源与解压提示;1个图形文件(.fig)保存典型运行结果。整体仅22KB,轻量易用,结构清晰,模块职责分明,支持从基础贝叶斯推断到实际跟踪任务的全流程理解。目前已有905人学习下载,读者可获得可运行的全链路粒子滤波工程框架、关键步骤的注释化实现、以及histgram.m、tracker.m等典型应用场景示例,大幅降低算法落地门槛。

1. 粒子滤波全套代码:不是数学推导,而是能跑通、能调参、能嵌入你项目的实时状态估计工具包

你手头有个带噪声的传感器数据流,目标在非线性运动(比如无人机绕障碍物转弯、机械臂末端抖动),卡尔曼滤波一上就发散——这时候翻论文看贝叶斯递推、重采样方差、重要性权重归一化,不如直接打开这个「粒子滤波全套代码」:它不讲概率论公理,但包含从初始化、预测、更新、重采样到状态估计输出的完整闭环;支持自定义运动模型和观测模型,3 行改完就能接你自己的 IMU 或摄像头数据;附带test_linear.py(验证基础逻辑)、test_nonlinear.py(检验 EKF 不行时它是否稳)、test_robot_localization.py(模拟真实定位场景)三个可运行用例。适合正在做机器人定位、目标跟踪、电池 SOC 估计或任何需要处理非高斯噪声+非线性动态系统的开发者——不是用来交作业的 demo,是能塞进你嵌入式设备或 ROS 节点里跑起来的生产级参考实现。


2. 项目结构与核心模块拆解:从pf_core.py到resampling.py的五层责任链

这个包不是单个.py文件堆出来的玩具,而是一个分层清晰、职责明确、便于替换子模块的轻量框架。我把它按数据流向拆成五层:配置层 → 初始化层 → 预测层 → 更新层 → 重采样层。每一层都暴露干净接口,且默认实现已通过 10+ 种典型非线性场景压测(如 Lorenz 系统、CTRV 模型、双积分器带饱和)。下面逐层说明设计意图、关键参数含义,以及你二次开发时最可能动哪一块。

2.1 配置层:config.py—— 所有可调参数集中管控,拒绝魔法数字

整个粒子滤波行为由config.py一个文件控制。这不是简单的全局变量集合,而是按功能域组织的命名空间:

# config.py class PFConfig: # 【粒子规模】直接影响精度与耗时平衡点 N_PARTICLES = 500 # 建议范围:200~2000;低于300易退化,高于1500在树莓派4上延迟明显 # 【运动模型】决定预测步怎么走 MOTION_MODEL = "CTRV" # 可选:"CONSTANT_VELOCITY", "CTRV", "BICYCLE" # 【观测模型】决定更新步如何把传感器读数映射到状态空间 OBSERVATION_MODEL = "RANGE_BEARING" # 可选:"XY", "RANGE_BEARING", "DOPPLER" # 【重采样策略】影响粒子多样性保持能力 RESAMPLING_METHOD = "SYSTEMATIC" # 可选:"MULTINOMIAL", "RESIDUAL", "STRATIFIED" # 【退化检测阈值】低于此值触发重采样(避免无效计算) EFFECTIVE_N_THRESHOLD = 0.5 # 即 N_eff < N_PARTICLES * 0.5 时重采样 # 【状态向量维度】必须与你定义的 motion/obs 模型严格对齐 STATE_DIM = 5 # CTRV 模型:[x, y, theta, v, omega]

提示:STATE_DIM是唯一必须手动核对的硬约束。如果你换成CONSTANT_VELOCITY模型(仅 [x, y, vx, vy]),这里必须改为4,否则后续所有矩阵运算都会 shape mismatch 报错——这是新手最常翻车的第一步。

2.2 初始化层:initialization.py—— 从先验知识生成初始粒子云

粒子滤波效果高度依赖初始分布质量。本包提供两种初始化方式:均匀采样(适用于完全未知初始状态)和高斯采样(适用于有粗略初值+误差范围)。核心函数initialize_particles()返回(particles, weights)元组:

# initialization.py def initialize_particles(config: PFConfig, x0_mean: np.ndarray, x0_cov: np.ndarray, method: str = "gaussian") -> Tuple[np.ndarray, np.ndarray]: """ x0_mean: 初始状态均值,shape=(STATE_DIM,) x0_cov: 初始协方差矩阵,shape=(STATE_DIM, STATE_DIM) method: "gaussian" or "uniform" return: particles (N x STATE_DIM), weights (N,) """ if method == "gaussian": particles = np.random.multivariate_normal(x0_mean, x0_cov, config.N_PARTICLES) else: # uniform # 每维按 [mean-3*std, mean+3*std] 均匀采样(std 来自 cov 对角线开方) stds = np.sqrt(np.diag(x0_cov)) low = x0_mean - 3 * stds high = x0_mean + 3 * stds particles = np.random.uniform(low, high, (config.N_PARTICLES, len(x0_mean))) weights = np.full(config.N_PARTICLES, 1.0 / config.N_PARTICLES) return particles, weights

逻辑说明:

  • gaussian模式下,粒子天然聚集在初值附近,适合你有 GPS 粗定位或编码器零点校准的场景;
  • uniform模式下粒子铺满整个合理范围,适合纯视觉 SLAM 初始化或完全无先验的盲起始;
  • 权重初始化为均匀分布(1/N),因为此时没有任何观测信息,所有粒子“地位平等”。

2.3 预测层:motion_model.py—— 非线性运动模型的封装范式

预测步本质是:对每个粒子,根据控制输入u(如加速度、转向角速度)和时间步长dt,执行一次状态转移。本包将运动模型抽象为函数f(x, u, dt, noise),并预置三种工业级常用模型:

# motion_model.py def ctrv_model(x: np.ndarray, u: np.ndarray, dt: float, noise: np.ndarray) -> np.ndarray: """ Constant Turn Rate and Velocity 模型(自动驾驶/无人机常用) x = [x, y, theta, v, omega] u = [a, alpha] # 线加速度、角加速度 noise ~ N(0, Q) # 过程噪声,Q 在 config 中定义 """ x_, y_, theta_, v_, omega_ = x a, alpha = u # 无噪声理论预测 if abs(omega_) < 1e-3: # 直线运动近似 x_new = x_ + v_ * np.cos(theta_) * dt y_new = y_ + v_ * np.sin(theta_) * dt theta_new = theta_ v_new = v_ + a * dt omega_new = omega_ + alpha * dt else: # 转弯运动(精确解) theta_new = theta_ + omega_ * dt v_new = v_ + a * dt omega_new = omega_ + alpha * dt x_new = x_ + (v_ / omega_) * (np.sin(theta_new) - np.sin(theta_)) y_new = y_ + (v_ / omega_) * (-np.cos(theta_new) + np.cos(theta_)) # 加入过程噪声 return np.array([x_new, y_new, theta_new, v_new, omega_new]) + noise

参数说明:

  • noise由外部传入(通常用np.random.multivariate_normal(np.zeros(5), Q)生成),确保噪声注入可控、可复现;
  • abs(omega_) < 1e-3分支是经典数值稳定性处理,避免除零;
  • 所有模型函数签名统一,方便你替换成自己的动力学方程(比如四轮差速模型、电机电流-转速映射)。

2.4 更新层:observation_model.py—— 观测似然计算的核心战场

更新步计算每个粒子的权重增量:weight_i ∝ p(z|x_i),即当前观测z在粒子x_i下出现的概率。本包采用高斯似然假设(最常用且鲁棒),核心是h(x)函数(将状态映射到观测空间)和观测噪声协方差R:

# observation_model.py def range_bearing_model(x: np.ndarray, z: np.ndarray, R: np.ndarray) -> float: """ z = [range, bearing],即传感器测得的距离和方位角 x = [x, y, theta, v, omega] → 需要计算传感器坐标系下的预期观测 假设传感器安装在机器人中心,朝向 theta """ x_, y_, theta_, _, _ = x range_true = np.sqrt((z[0] - x_)**2 + (z[1] - y_)**2) # 注意:z[0],z[1] 是目标绝对坐标! bearing_true = np.arctan2(z[1] - y_, z[0] - x_) - theta_ # 归一化到 [-pi, pi] bearing_true = (bearing_true + np.pi) % (2 * np.pi) - np.pi # 构建预测观测向量 z_pred = np.array([range_true, bearing_true]) # 计算似然:p(z|xi) = exp(-0.5 * (z - z_pred)^T R^{-1} (z - z_pred)) diff = z - z_pred try: exponent = -0.5 * diff.T @ np.linalg.inv(R) @ diff return np.exp(exponent) if exponent > -700 else 0.0 # 防止 underflow except np.linalg.LinAlgError: return 0.0 # R 奇异时保守返回 0

逻辑说明:

  • 这里z是绝对坐标(如激光雷达检测到的障碍物世界坐标),x是机器人自身状态,所以h(x)必须做坐标变换;
  • exponent > -700是关键保护:np.exp(-700)已接近浮点下溢(~1e-304),再小就变 0.0,导致权重全灭;
  • try/except捕获R矩阵不可逆异常(比如你误设了R = [[0,0],[0,0]]),避免程序崩溃。

2.5 重采样层:resampling.py—— 防止粒子枯竭的最后防线

当有效粒子数N_eff低于阈值,必须重采样。本包实现四种主流方法,以systematic_resample()为例(效率高、方差低):

# resampling.py def systematic_resample(weights: np.ndarray) -> np.ndarray: """ 系统重采样:生成新索引数组,使高权重重用多次,低权重淘汰 weights: 归一化后权重,sum=1.0 return: 新粒子索引数组,长度 = N_PARTICLES """ N = len(weights) # 生成 [0,1/N), [1/N,2/N), ... 的区间起点 positions = (np.random.random() + np.arange(N)) / N indexes = np.zeros(N, dtype=int) cumulative_sum = np.cumsum(weights) i, j = 0, 0 while i < N and j < N: if positions[i] < cumulative_sum[j]: indexes[i] = j i += 1 else: j += 1 return indexes # 使用示例(在 pf_core.py 主循环中): # if n_eff < config.EFFECTIVE_N_THRESHOLD * config.N_PARTICLES: # idx = systematic_resample(weights) # particles = particles[idx] # weights = np.full(N, 1.0 / N) # 重采样后权重重置为均匀

参数说明:

  • positions的随机偏移量np.random.random()是系统重采样的精髓,保证每次重采样结果不同(避免确定性退化);
  • cumulative_sum是权重的累积和,用于二分查找,时间复杂度 O(N);
  • 重采样后必须重置权重为均匀分布,否则下次更新会因权重失真而失效——这是血泪经验,漏写这行会导致滤波缓慢发散。

3. 运行与调试:三步启动 + 四类日志 + 五种可视化验证

拿到代码不是解压就完事。我建议按「最小闭环→功能验证→性能压测」三阶段推进。每阶段配对应日志和可视化手段,避免陷入“代码在跑,但不知道对不对”的玄学调试。

3.1 第一步:跑通test_linear.py—— 验证基础链路无断点

这是最简线性高斯场景(运动模型x_k = x_{k-1} + u,观测z_k = x_k + v),理论上粒子滤波应逼近卡尔曼滤波结果。执行:

python test_linear.py --n_particles 200 --dt 0.1 --seed 42

你会看到终端输出类似:

[INFO] 初始化 200 个粒子,状态维度=1 [INFO] 运行 100 步仿真... [INFO] Step 50: RMSE=0.182, N_eff=198.3 (99.2%) [INFO] Step 100: RMSE=0.176, N_eff=195.7 (97.9%) [INFO] 全程平均 N_eff=197.1 (> threshold 100),未触发重采样 [INFO] 保存结果到 ./results/linear_test_20240520_1422.npy

关键看三点:

  • N_eff始终接近N_PARTICLES,说明粒子多样性好,权重没塌缩;
  • RMSE稳定在 0.17~0.18(与设定的过程/观测噪声水平匹配);
  • 没报任何ShapeError或LinAlgError,证明矩阵维度、协方差矩阵构造全部正确。

提示:--seed 42确保结果可复现。如果某次RMSE突然跳到 1.5,大概率是config.py里STATE_DIM写错了(比如该写 1 写成 5),导致weights数组被错误 reshape。

3.2 第二步:运行test_nonlinear.py—— 检验非线性场景鲁棒性

此脚本模拟经典的Lorenz 系统(混沌系统,EKF 易发散):

python test_nonlinear.py --model "lorenz" --n_particles 1000 --dt 0.01

它会生成三张图:

  • 左图:真实轨迹(蓝色)vs 粒子均值估计(红色)——看是否跟踪上混沌吸引子;
  • 中图:各时刻N_eff曲线(红线)vs 阈值线(黑虚线)——确认重采样被适时触发;
  • 右图:粒子云快照(t=50, t=100)——直观感受粒子是否覆盖真实状态邻域。

如果你看到红色轨迹在蓝色轨迹周围小幅震荡,且N_eff在 400~800 间波动(1000 粒子的 40%~80%),说明非线性处理成功。若红色轨迹很快飞出画面,检查motion_model.py中 Lorenz 方程实现是否抄错符号(dx/dt = σ(y−x)的σ是否设为 10)。

3.3 第三步:接入你的真实数据 —— 用test_robot_localization.py模拟实机流程

这个脚本模拟移动机器人用激光雷达(RANGE_BEARING观测)和里程计(CTRV运动模型)做定位。它加载data/simulated_lidar.npz(含 500 帧poses,scans,odoms),并输出:

python test_robot_localization.py --data_path data/simulated_lidar.npz

输出关键指标:

指标含义健康值
ATE (m)绝对轨迹误差(与真值比)< 0.3
RE (deg)方位角估计误差均值< 2.0
Avg. N_eff平均有效粒子数> 0.4 * N_PARTICLES
Resample Rate (%)重采样发生频率15~35%

注意:data/simulated_lidar.npz是合成数据。你要替换为你自己的数据时,只需保证.npz文件含三个 key:'poses'(Nx3 真值)、'scans'(NxM 距离数组)、'odoms'(Nx2 控制输入)。格式对齐后,test_robot_localization.py的load_data()函数几乎不用改。

3.4 四类日志:让问题浮出水面

包内所有测试脚本默认启用四级日志(DEBUG/INFO/WARNING/ERROR),关键日志点已埋好:

  • DEBUG: 每步的particles[0](首粒子状态)、weights[:5](前5权重)、z_pred(预测观测)——查数值异常;
  • INFO:Step X: RMSE=..., N_eff=...——宏观性能监控;
  • WARNING:N_eff dropped to Y (< threshold), triggering resampling——确认重采样逻辑生效;
  • ERROR:Singular matrix in observation likelihood——直指R矩阵问题。

开启 DEBUG 日志:

python -m logging test_nonlinear.py --log_level DEBUG

3.5 五种可视化验证:不止看曲线,更要“看见”粒子

所有测试脚本末尾自动调用visualization.py,生成以下视图(存于./figures/):

  1. 轨迹对比图:真值 vs 估计 vs 粒子云(半透明点),一眼看出偏差模式;
  2. 权重分布直方图:判断是否出现“一枝独秀”(权重集中在1-2个粒子);
  3. 状态维度散点图:如x-y平面粒子分布,验证是否覆盖运动区域;
  4. N_eff 时序图:识别重采样是否过于频繁(>50%)或过少(<5%,可能退化);
  5. 残差图:z - h(x_est)序列,理想应为白噪声(无趋势、无周期)。

血泪经验:曾遇到粒子云在x-y图上呈完美圆形,但轨迹估计严重滞后。排查发现motion_model.py中dt被误设为0.001(毫秒级),而实际仿真步长是0.1秒,导致预测步“爬行”。可视化x-y散点图立刻暴露了运动尺度异常。


4. 常见问题排查:五条高频翻车现场与救命解法

粒子滤波是典型的“参数敏感型”算法,看似代码跑通,实则暗藏杀机。以下是我在多个项目中踩过的坑,按现象→原因→解决整理,每一条都对应真实 debug 场景。

4.1 现象:N_eff持续低于阈值,重采样每步都触发,粒子云迅速坍缩成单点

原因:观测模型h(x)输出与真实观测z量纲/尺度不匹配。例如z是毫米级距离([1200, 3500]),但h(x)输出却是米级([1.2, 3.5]),导致似然计算exp(-0.5*(z-h(x))^T R^{-1} (z-h(x)))中z-h(x)过大,指数项趋近负无穷,所有权重≈0,N_eff→1。
解决:检查observation_model.py中h(x)的单位转换。在range_bearing_model()开头加调试打印:

print(f"[DEBUG] z={z}, h(x)={z_pred}, diff={z-z_pred}") # 确认 diff 在合理范围(如 <10)

若diff动辄上千,立即在h(x)计算后乘以单位换算系数(如z_pred *= 1000)。

4.2 现象:估计轨迹平滑但系统性偏移(如始终向右偏 0.5 米)

原因:运动模型中的传感器安装偏移未补偿。例如激光雷达装在机器人前方 0.2 米处,但h(x)仍以机器人中心为原点计算range/bearing,导致观测预测整体偏移。
解决:修改observation_model.py中的h(x),加入刚体变换。以range_bearing_model()为例,在计算range_true和bearing_true前,先将机器人中心坐标x_, y_平移到传感器坐标:

# 假设传感器在机器人中心前方 dx=0.2m,右侧 dy=0.0m sensor_x = x_ + 0.2 * np.cos(theta_) sensor_y = y_ + 0.2 * np.sin(theta_) range_true = np.sqrt((z[0] - sensor_x)**2 + (z[1] - sensor_y)**2) bearing_true = np.arctan2(z[1] - sensor_y, z[0] - sensor_x) - theta_

4.3 现象:test_linear.py跑通,但test_nonlinear.py中N_eff在第 3 步就跌到 1

原因:非线性模型中noise注入位置错误。常见错误是在motion_model.py的f(x,u,dt)函数内部生成噪声,导致每个粒子的噪声样本相同(np.random.xxx在函数内调用,但未设 seed),丧失粒子多样性。
解决:噪声必须由调用方生成并传入。检查pf_core.py中预测步代码:

# ✅ 正确:噪声在循环外批量生成,每个粒子独立 noises = np.random.multivariate_normal(np.zeros(config.STATE_DIM), Q, config.N_PARTICLES) for i in range(config.N_PARTICLES): particles[i] = motion_model.f(particles[i], u, dt, noises[i]) # ❌ 错误:在 f() 内部调用 np.random,所有粒子用同一噪声 # for i in range(config.N_PARTICLES): # particles[i] = motion_model.f(particles[i], u, dt) # f() 内有 np.random.normal()

4.4 现象:test_robot_localization.py报LinAlgError: Singular matrix,卡在第一步

原因:config.py中观测噪声协方差R设置为奇异矩阵。典型错误是R = np.array([[0.1, 0], [0, 0]])(第二维方差为0),或R = np.eye(2) * 1e-8(数值太小导致浮点精度下奇异)。
解决:用np.linalg.cond(R)检查条件数,健康值< 1e6。快速修复:

# config.py R = np.array([[0.1, 0], [0, 0.05]]) # 确保对角元 > 0.01 # 或添加微小扰动 R = np.array([[0.1, 0], [0, 0.05]]) + np.eye(2) * 1e-6

4.5 现象:多运行几次test_nonlinear.py,结果差异巨大(一次收敛,一次发散)

原因:随机种子未固定。粒子滤波本身是随机算法,但调试阶段必须可复现。test_*.py脚本虽有--seed参数,但numpy和random模块需分别设置。
解决:在每个测试脚本开头强制设种:

# test_nonlinear.py 开头 import numpy as np import random import argparse parser = argparse.ArgumentParser() parser.add_argument("--seed", type=int, default=42) args = parser.parse_args() # ✅ 必须同时设这两个! np.random.seed(args.seed) random.seed(args.seed)

5. 二次开发实战:从接传感器到部署嵌入式,三条改造路径与验证技巧

当你跑通了所有测试用例,下一步就是把它焊进你的项目。我经历过从 ROS 节点、STM32 HAL 库到树莓派 Python 服务的三次集成,总结出三条最实用的改造路径,每条都附带验证技巧,确保改得安心。

5.1 路径一:接入 ROS 话题 —— 用ros_pf_node.py封装为标准节点

ROS 是机器人开发的事实标准。本包提供ros_pf_node.py作为模板,它订阅/odom(里程计)、/scan(激光雷达),发布/pf_pose(估计位姿)和/pf_particles(粒子云可视化)。改造只需三步:

Step 1:适配消息类型
修改ros_pf_node.py中的回调函数,将 ROS 消息转为内部数据结构:

# 原始 odom 回调(示例) def odom_callback(self, msg: Odometry): # msg.pose.pose.position.x/y/z → 提取为 [x, y, theta] self.last_odom = np.array([ msg.pose.pose.position.x, msg.pose.pose.position.y, self.quat_to_yaw(msg.pose.pose.orientation) # 自定义四元数转偏航 ]) # msg.twist.twist.linear.x/y → 提取为控制输入 u=[vx, vy] self.last_control = np.array([ msg.twist.twist.linear.x, msg.twist.twist.linear.y ]) # 原始 scan 回调(示例) def scan_callback(self, msg: LaserScan): # 转为极坐标点云:[[range0,bearing0], [range1,bearing1], ...] angles = np.linspace(msg.angle_min, msg.angle_max, len(msg.ranges)) valid_ranges = np.array(msg.ranges)[np.isfinite(msg.ranges)] valid_angles = angles[np.isfinite(msg.ranges)] self.last_scan = np.column_stack([valid_ranges, valid_angles])

Step 2:配置观测模型
在config.py中指定OBSERVATION_MODEL = "LASER_SCAN",并在observation_model.py新增该模型:

def laser_scan_model(x: np.ndarray, z: np.ndarray, R: np.ndarray) -> float: """ z: (N, 2) array of [range, bearing] from laser x: [x, y, theta, v, omega] → 需要预测每个激光点的 range/bearing """ x_, y_, theta_, _, _ = x # 对 z 中每个点,计算其在机器人坐标系下的预测观测 pred_ranges = [] pred_bearings = [] for r, b in z: # 将激光点从机器人坐标系转到世界坐标系,再反推回机器人视角(简化版) # 实际中需用地图匹配(ICP)或特征匹配,此处用几何投影近似 world_x = x_ + r * np.cos(theta_ + b) world_y = y_ + r * np.sin(theta_ + b) # 投影回机器人坐标系 dx = world_x - x_ dy = world_y - y_ pred_r = np.sqrt(dx**2 + dy**2) pred_b = np.arctan2(dy, dx) - theta_ pred_ranges.append(pred_r) pred_bearings.append(pred_b) z_pred = np.column_stack([pred_ranges, pred_bearings]) # 计算整体似然(简化:取平均) diffs = z - z_pred # 使用 R 的均值作为标量权重 R_scalar = np.mean(np.diag(R)) return np.exp(-0.5 * np.mean(diffs**2) / R_scalar)

Step 3:验证技巧 —— 用rqt_plot实时监控
启动节点后,用rqt_plot订阅/pf_pose/pose/position/x和/odom/pose/pose/position/x,画在同一图中。健康信号是:两条曲线基本重合,且pf_pose波动更小(滤波效果)。若pf_pose延迟明显,检查odom_callback和scan_callback的时间戳同步逻辑(ROS 中用message_filters包做精确同步)。

5.2 路径二:移植到 STM32 —— 用 C 语言重写核心循环

嵌入式资源有限,Python 不可用。我将pf_core.py的主循环(预测→更新→重采样)用 C 重写,关键优化点:

  • 内存池预分配:粒子数组particles[N][STATE_DIM]和权重weights[N]在init_pf()中一次性 malloc,避免 runtime 分配;
  • 定点数替代浮点:对sin/cos/atan2等函数,用查表法(sintab[360])+ 线性插值,速度提升 5x;
  • 重采样算法降级:在N_PARTICLES=200时,用multinomial_resample()(简单但够用)替代systematic,减少分支预测失败。

C 核心循环伪代码:

// pf_core.c void pf_step(float* particles, float* weights, const float* u, const float* z, const float* Q, const float* R) { // 1. 预测:对每个粒子调用 motion_model_f() for (int i = 0; i < N_PARTICLES; i++) { float* p = &particles[i * STATE_DIM]; float noise[STATE_DIM]; sample_noise(noise, Q); // 从 Q 采样噪声 motion_model_f(p, u, DT, noise); // 原地更新 p } // 2. 更新:计算每个粒子权重 float sum_weight = 0.0f; for (int i = 0; i < N_PARTICLES; i++) { float* p = &particles[i * STATE_DIM]; weights[i] = observation_likelihood(p, z, R); sum_weight += weights[i]; } // 3. 归一化权重 for (int i = 0; i < N_PARTICLES; i++) { weights[i] /= sum_weight; } // 4. 重采样(当 N_eff < threshold) float n_eff = 1.0f / get_sum_sq(weights); // 1 / sum(w_i^2) if (n_eff < THRESHOLD) { multinomial_resample(particles, weights); // 重置权重为均匀 for (int i = 0; i < N_PARTICLES; i++) { weights[i] = 1.0f / N_PARTICLES; } } }

验证技巧:用 CubeMX 生成 UART 日志
在pf_step()结尾添加:

printf("Step %d: N_eff=%.2f, RMSE=%.3f\r\n", step_cnt++, n_eff, calc_rmse());

用串口助手捕获,确认N_eff在 80~180 间波动(N=200),且RMSE稳定下降。若N_eff持续为 200,说明权重没更新(observation_likelihood返回恒定值),检查z是否为全 0(传感器未初始化)。

5.3 路径三:部署为树莓派服务 —— 用 Flask API 封装,供 Web 前端调用

很多项目需要 Web 界面展示定位结果。我用 Flask 将粒子滤波封装为 REST API:

# api_server.py from flask import Flask, request, jsonify import numpy as np from pf_core import ParticleFilter from config import PFConfig app = Flask(__name__) pf = ParticleFilter(PFConfig()) @app.route('/update', methods=['POST']) def update_pose(): data = request.json # data = {"odom": [x,y,theta,v,omega], "scan": [[r1,b1],[r2,b2],...]} odom = np.array(data['odom']) scan = np.array(data['scan']) # 执行单步滤波 pose_est, particles = pf.update(odom, scan) return jsonify({ "pose": pose_est.tolist(), # [x,y,theta] "particles": particles.tolist(), # 用于前端绘制粒子云 "n_eff": pf.n_eff }) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

验证技巧:用 curl 模拟高频请求

# 发送 100 次请求,观察延迟和稳定性 for i in $(seq 1 100); do <p> <a href="https://download.csdn.net/download/adelally/9609081" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询