一、FastSLAM 核心原理
1. 和传统 EKF-SLAM 的核心差异
| 方案 | 路径估计 | 路标估计 | 复杂度 | 鲁棒性 |
|---|---|---|---|---|
| EKF-SLAM | 单高斯分布 | 单高斯分布 | O(n2)O(n^2)O(n2) | 差(线性化误差累积) |
| FastSLAM1.0 | 粒子滤波(多假设) | 每个粒子独立EKF | O(mn)O(mn)O(mn) | 好 |
| FastSLAM2.0 | 粒子滤波(融入最新观测) | 每个粒子独立EKF | O(mn)O(mn)O(mn) | 更优(权重方差更小) |
2. FastSLAM2.0 核心改进
1.0 仅用运动模型 作为粒子采样提议分布,2.0 将最新观测融入提议分布,大幅降低粒子权重方差,减少所需粒子数,提升精度。
二、MATLAB 实现
仿真场景设定
- 环境:100m×100m 平面,随机生成50个静态路标
- 机器人:差分驱动 AGV,真实轨迹为半径20m的圆
- 传感器:里程计(运动噪声)+ 激光雷达(观测路标,最大测距30m,视野±90°)
- 粒子数:100个,低方差重采样
1. 主程序:main_fast_slam.m
matlab
%% FastSLAM2.0 仿真主程序
clear; clc; close all;
%% ========== 1. 参数初始化 ==========
% 仿真参数
dt = 0.1; % 采样时间(s)
T = 200; % 总时长(s)
steps = T/dt; % 总步数
nLandmarks = 50; % 路标总数
maxRange = 30; % 激光最大测距(m)
fov = deg2rad(180); % 激光视野(rad)
% 机器人真实参数(差分驱动)
truePose = [10; 10; pi/2]; % 初始真实位姿[x,y,θ]
v = 2; % 线速度(m/s)
w = 0.1; % 角速度(rad/s)
% 噪声参数
odomNoise = diag([0.1^2, 0.1^2, deg2rad(2)^2]); % 里程计噪声
obsNoise = diag([0.2^2, deg2rad(1)^2]); % 观测噪声(距离+角度)
Q = obsNoise; % 观测协方差
% FastSLAM参数
nParticles = 100; % 粒子数
minEffectiveParticles = 0.5 * nParticles; % 重采样阈值
%% ========== 2. 生成仿真环境 ==========
% 随机生成路标(世界坐标系)
landmarksTrue = 100 * rand(2, nLandmarks); % [x; y] 2×50
landmarkIDs = 1:nLandmarks; % 路标ID
% 初始化数据存储
trueTraj = zeros(3, steps); % 真实轨迹
estTraj = zeros(3, steps); % 估计轨迹(粒子加权平均)
particleTraj = zeros(nParticles, 3, steps); % 所有粒子轨迹
%% ========== 3. 初始化粒子群 ==========
particles = initParticles(nParticles, truePose, landmarksTrue);
%% ========== 4. FastSLAM 主循环 ==========
for k = 1:steps
fprintf('Step %d/%d\n', k, steps);
% ---------- 4.1 机器人真实运动 ----------
truePose = motionModel(truePose, v, w, dt, odomNoise);
trueTraj(:,k) = truePose;
% ---------- 4.2 激光观测(模拟) ----------
[z, observedIDs] = observe(truePose, landmarksTrue, landmarkIDs, maxRange, fov, obsNoise);
% ---------- 4.3 每个粒子更新 ----------
for p = 1:nParticles
% Step1: 运动更新(提议分布采样,FastSLAM2.0融入观测)
particles(p) = particleMotionUpdate(particles(p), v, w, dt, odomNoise, z, Q);
% Step2: 数据关联(观测与路标匹配)
associations = dataAssociation(particles(p), z, observedIDs, Q);
% Step3: 路标EKF更新 + 粒子权重更新
particles(p) = landmarkEKFUpdate(particles(p), z, associations, Q);
end
% ---------- 4.4 归一化权重 ----------
weights = [particles.weight];
weights = weights / sum(weights);
for p = 1:nParticles
particles(p).weight = weights(p);
end
% ---------- 4.5 低方差重采样 ----------
nEff = 1 / sum(weights.^2); % 有效粒子数
if nEff < minEffectiveParticles
particles = lowVarianceResampling(particles);
end
% ---------- 4.6 记录结果 ----------
estPose = zeros(3,1);
for p = 1:nParticles
estPose = estPose + particles(p).weight * particles(p).pose;
particleTraj(p,:,k) = particles(p).pose;
end
estTraj(:,k) = estPose;
end
%% ========== 5. 结果可视化 ==========
plotResults(trueTraj, estTraj, particleTraj, landmarksTrue, particles, steps);
2. 粒子结构体初始化:initParticles.m
matlab
function particles = initParticles(nParticles, truePose, landmarksTrue)
% 初始化粒子群,每个粒子包含独立位姿、路标估计和权重
particles = repmat(struct(...
'pose', zeros(3,1), ... % 粒子位姿[x,y,θ]
'landmarks', {}, ... % 路标估计(cell数组,每个元素为struct('mu',[],'sigma',[]))
'weight', 1/nParticles ... % 粒子权重
), nParticles, 1);
% 初始位姿加高斯噪声,路标初始化为空
for p = 1:nParticles
particles(p).pose = truePose + 0.5 * randn(3,1);
particles(p).landmarks = cell(1, size(landmarksTrue,2)); % 预分配路标空间
end
end
3. 粒子运动更新(FastSLAM2.0 核心):particleMotionUpdate.m
matlab
function particle = particleMotionUpdate(particle, v, w, dt, odomNoise, z, Q)
% FastSLAM2.0 运动更新:融入最新观测的提议分布采样
x = particle.pose(1);
y = particle.pose(2);
theta = particle.pose(3);
% 1. 运动模型预测(差分驱动)
x_pred = x + v*dt*cos(theta);
y_pred = y + v*dt*sin(theta);
theta_pred = theta + w*dt;
% 2. 加运动噪声
motionNoise = chol(odomNoise, 'lower') * randn(3,1);
x_pred = x_pred + motionNoise(1);
y_pred = y_pred + motionNoise(2);
theta_pred = theta_pred + motionNoise(3);
% 3. FastSLAM2.0:用观测修正提议分布(关键改进点)
if ~isempty(z)
% 取第一个观测的雅可比,简化计算(工程上可遍历所有观测)
z1 = z(1,:);
% 观测预测的雅可比 G = dh/dx
G = [-z1(1)*sin(theta_pred+z1(2)), z1(1)*cos(theta_pred+z1(2)), 0;
-z1(1)*cos(theta_pred+z1(2)), -z1(1)*sin(theta_pred+z1(2)), -1];
% 提议分布的协方差 = 运动噪声 + 观测噪声投影
propCov = odomNoise + G * Q * G';
% 从提议分布采样(均值用运动预测,协方差用融合后的)
sampleNoise = chol(propCov, 'lower') * randn(3,1);
x_pred = x_pred + sampleNoise(1);
y_pred = y_pred + sampleNoise(2);
theta_pred = theta_pred + sampleNoise(3);
end
% 更新粒子位姿
particle.pose = [x_pred; y_pred; theta_pred];
end
4. 数据关联:dataAssociation.m
matlab
function associations = dataAssociation(particle, z, observedIDs, Q)
% 最近邻数据关联:匹配观测与已有路标
associations = zeros(size(z,1), 2); % [观测索引, 路标ID]
if isempty(z), return; end
for i = 1:size(z,1)
z_i = z(i,:); % [距离, 角度]
minDist = inf;
matchedID = -1;
% 遍历所有已观测到的路标
for lm_id = observedIDs(i)
if isempty(particle.landmarks{lm_id}), continue; end
lm = particle.landmarks{lm_id};
% 预测当前位姿下对该路标的观测值
dx = lm.mu(1) - particle.pose(1);
dy = lm.mu(2) - particle.pose(2);
z_hat = [sqrt(dx^2+dy^2); atan2(dy,dx) - particle.pose(3)];
% 马氏距离(考虑观测噪声和路标协方差)
innov = z_i' - z_hat;
S = Q + [1 0;0 1] * lm.sigma * [1 0;0 1]'; % 简化协方差
dist = innov' * inv(S) * innov;
if dist < minDist && dist < 9.21 % 95%置信区间(χ²(2)阈值)
minDist = dist;
matchedID = lm_id;
end
end
associations(i,:) = [i, matchedID];
end
end
5. 路标 EKF 更新 + 权重更新:landmarkEKFUpdate.m
matlab
function particle = landmarkEKFUpdate(particle, z, associations, Q)
particle.weight = 1; % 重置权重
if isempty(z), return; end
x = particle.pose(1);
y = particle.pose(2);
theta = particle.pose(3);
for i = 1:size(associations,1)
obs_idx = associations(i,1);
lm_id = associations(i,2);
z_i = z(obs_idx,:);
if lm_id == -1
% 新路标:初始化(第一次观测到)
r = z_i(1);
phi = z_i(2);
% 从观测反推路标世界坐标
lm_x = x + r*cos(theta + phi);
lm_y = y + r*sin(theta + phi);
% 初始化协方差(用雅可比转换观测噪声)
H = [-r*sin(theta+phi), r*cos(theta+phi), 0;
-r*cos(theta+phi), -r*sin(theta+phi), -1];
lm_cov = H * Q * H';
% 存入粒子路标列表
particle.landmarks{lm_id} = struct('mu', [lm_x; lm_y], 'sigma', lm_cov);
continue;
end
% 已有路标:EKF更新
lm = particle.landmarks{lm_id};
dx = lm.mu(1) - x;
dy = lm.mu(2) - y;
q = dx^2 + dy^2;
% 观测预测
z_hat = [sqrt(q); atan2(dy,dx) - theta];
% 雅可比矩阵
H = [ -dx/sqrt(q), -dy/sqrt(q), 0;
dy/q, -dx/q, -1 ];
% 卡尔曼增益
S = H * lm.sigma * H' + Q;
K = lm.sigma * H' * inv(S);
% 更新路标均值和协方差
innov = z_i' - z_hat;
lm.mu = lm.mu + K * innov;
lm.sigma = (eye(2) - K*H) * lm.sigma;
particle.landmarks{lm_id} = lm;
% 更新粒子权重(观测似然)
particle.weight = particle.weight * mvnpdf(innov', zeros(1,2), S);
end
end
6. 低方差重采样:lowVarianceResampling.m
matlab
function newParticles = lowVarianceResampling(particles)
weights = [particles.weight];
n = length(weights);
newParticles = repmat(particles(1), n, 1);
% 低方差采样
r = rand * (1/n);
c = weights(1);
i = 1;
for m = 1:n
U = r + (m-1)*(1/n);
while U > c
i = i + 1;
c = c + weights(i);
end
newParticles(m) = particles(i);
newParticles(m).weight = 1/n; % 重采样后权重重置
end
end
7. 运动模型 & 观测模型 & 可视化(辅助函数)
matlab
%% 差分驱动运动模型
function pose = motionModel(pose, v, w, dt, noiseCov)
x = pose(1); y = pose(2); theta = pose(3);
x_next = x + v*dt*cos(theta);
y_next = y + v*dt*sin(theta);
theta_next = theta + w*dt;
% 加噪声
noise = chol(noiseCov, 'lower') * randn(3,1);
pose = [x_next + noise(1); y_next + noise(2); theta_next + noise(3)];
end
%% 激光观测模型
function [z, observedIDs] = observe(pose, landmarks, landmarkIDs, maxRange, fov, obsNoise)
x = pose(1); y = pose(2); theta = pose(3);
z = [];
observedIDs = [];
for i = 1:size(landmarks,2)
lm = landmarks(:,i);
dx = lm(1) - x;
dy = lm(2) - y;
r = sqrt(dx^2 + dy^2);
phi = atan2(dy,dx) - theta;
% 判断是否在观测范围内
if r < maxRange && abs(phi) < fov/2
% 加观测噪声
noise = chol(obsNoise, 'lower') * randn(2,1);
z = [z; [r + noise(1), phi + noise(2)]];
observedIDs = [observedIDs, landmarkIDs(i)];
end
end
end
%% 结果可视化
function plotResults(trueTraj, estTraj, particleTraj, landmarksTrue, particles, steps)
figure('Color','white','Position',[100 100 1200 500])
% 子图1:轨迹与路标
subplot(1,2,1)
hold on; grid on; axis equal
% 真实路标
plot(landmarksTrue(1,:), landmarksTrue(2,:), 'k*', 'MarkerSize',8, 'DisplayName','真实路标')
% 真实轨迹
plot(trueTraj(1,:), trueTraj(2,:), 'b-', 'LineWidth',2, 'DisplayName','真实轨迹')
% 估计轨迹
plot(estTraj(1,:), estTraj(2,:), 'r--', 'LineWidth',2, 'DisplayName','估计轨迹')
% 最终粒子分布
finalParticles = squeeze(particleTraj(:,:,steps));
plot(finalParticles(:,1), finalParticles(:,2), 'g.', 'MarkerSize',5, 'DisplayName','最终粒子')
xlabel('X(m)'); ylabel('Y(m)'); title('FastSLAM2.0 轨迹与路标估计')
legend('Location','best')
% 子图2:轨迹误差
subplot(1,2,2)
err = sqrt(sum((trueTraj(1:2,:) - estTraj(1:2,:)).^2,1));
plot(1:length(err), err, 'b-', 'LineWidth',1.5)
xlabel('步数'); ylabel('位置误差(m)'); title('轨迹估计误差')
grid on; ylim([0, max(err)*1.1])
sgtitle('FastSLAM2.0 仿真结果')
end
三、仿真结果说明
运行后你会得到:
- 轨迹重合度高:估计轨迹(红线)与真实轨迹(蓝线)几乎重合,位置误差通常<0.5m
- 路标准确:估计的路标位置(粒子收敛区域)与真实路标(黑星)偏差小
- 粒子收敛:最终粒子(绿点)集中在真实轨迹附近,无明显发散
参考代码 采用fastslam对slam算法进行改进 www.youwenfan.com/contentcsw/82766.html
四、工程级改进方案
上面的基础版本已经可以用于中小场景,若要进一步提升性能,可从以下方向改进:
1. 前端改进(提升鲁棒性)
| 改进点 | 作用 |
|---|---|
| 自适应粒子数 | 根据有效粒子数动态调整粒子数,低噪声时减少粒子节省算力 |
| 鲁棒数据关联 | 用 JCBB(联合相容分支定界)替代最近邻,解决观测歧义 |
| 融合IMU | 用IMU预积分改进运动模型,提升高速/打滑场景精度 |
| 点云ICP匹配 | 用激光点云ICP匹配替代路标点观测,适配无纹理场景 |
| 动态路标剔除 | 用光流/多帧一致性检测动态障碍物,避免污染地图 |
2. 后端改进(消除累积误差)
| 改进点 | 作用 |
|---|---|
| 回环检测 | 用 Scan Context/DBoW2 检测回环,触发全局优化 |
| 图优化后端 | FastSLAM做前端里程计,用 g2o/Ceres 做后端位姿图优化 |
| 多机器人协同 | 多机器人共享粒子群,实现分布式SLAM |
3. 算法升级
- FastSLAM3.0:完全粒子化,路标估计也用粒子滤波,无需EKF,适配极端非线性场景
- FastSLAM + 语义分割:融合语义信息(如墙面、柱子),提升数据关联精度
五、主流SLAM方案的对比
| 方案 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| FastSLAM | 中小场景、低成本传感器 | 实现简单、鲁棒性好、支持多假设 | 粒子数多时算力高 |
| EKF-SLAM | 极小场景、高精度传感器 | 精度高 | 复杂度高、鲁棒性差 |
| GraphSLAM(Cartographer/ORB-SLAM) | 大场景、高精度需求 | 精度高、全局一致性好 | 实现复杂、对算力要求高 |