扩展卡尔曼滤波(EKF)同时定位与建图(SLAM)MATLAB仿真代码

扩展卡尔曼滤波(EKF)同时定位与建图(SLAM)MATLAB仿真代码

一、EKF-SLAM基本原理

1. 核心思想

扩展卡尔曼滤波SLAM(EKF-SLAM)是一种基于概率估计的SLAM方法,通过扩展卡尔曼滤波器对机器人位姿和地图特征进行联合估计。状态向量包含机器人位姿和所有观测到的路标位置,通过预测-更新循环实现同步定位与建图。

2. 状态向量定义

3. EKF-SLAM流程

初始化:设置初始位姿和空地图
循环(每个时间步):
  1. 预测(运动更新):
     - 根据控制输入(里程计)预测机器人位姿
     - 扩展状态协方差矩阵
  2. 观测(测量更新):
     - 获取传感器观测(距离、角度)
     - 数据关联(匹配观测与已有路标)
     - 计算观测预测和雅可比矩阵
     - 计算卡尔曼增益,更新状态和协方差
  3. 地图管理:
     - 新路标初始化并加入状态向量
     - 删除无效路标
  4. 可视化:实时显示估计轨迹和地图

二、完整MATLAB仿真代码

1. 主程序(main.m)

%% EKF-SLAM 主程序 - 扩展卡尔曼滤波同时定位与建图
clear; clc; close all;

%% 1. 仿真参数设置
disp('=== EKF-SLAM 仿真开始 ===');

% 环境参数
map_size = [20, 20];           % 地图尺寸 [m]
n_landmarks = 15;              % 路标数量
landmark_range = [2, 18];      % 路标生成范围

% 机器人参数
initial_pose = [0; 0; 0];      % 初始位姿 [x, y, theta]
robot_size = 0.5;              % 机器人尺寸 [m]

% 运动参数
v = 0.5;                       % 线速度 [m/s]
w = 0.1;                       % 角速度 [rad/s]
dt = 0.1;                      % 时间步长 [s]
sim_time = 50;                 % 仿真时间 [s]
n_steps = floor(sim_time / dt); % 总步数

% 噪声参数
Q = diag([0.1, 0.1, 0.05].^2); % 过程噪声协方差(速度、转向)
R = diag([0.3, 0.1].^2);       % 测量噪声协方差(距离、角度)

% 传感器参数
sensor_range = 10;             % 传感器最大测距 [m]
sensor_fov = 120;              % 传感器视场角 [度]

%% 2. 生成仿真环境
disp('生成仿真环境...');

% 随机生成路标
landmarks_true = zeros(2, n_landmarks);
for i = 1:n_landmarks
    landmarks_true(1,i) = landmark_range(1) + ...
                         (landmark_range(2)-landmark_range(1)) * rand();
    landmarks_true(2,i) = landmark_range(1) + ...
                         (landmark_range(2)-landmark_range(1)) * rand();
end

% 生成机器人运动轨迹(圆形轨迹)
true_trajectory = zeros(3, n_steps);
true_trajectory(:,1) = initial_pose;

for t = 2:n_steps
    % 简单圆形轨迹
    true_trajectory(1,t) = true_trajectory(1,t-1) + v*dt*cos(true_trajectory(3,t-1));
    true_trajectory(2,t) = true_trajectory(2,t-1) + v*dt*sin(true_trajectory(3,t-1));
    true_trajectory(3,t) = true_trajectory(3,t-1) + w*dt;
end

%% 3. EKF-SLAM初始化
disp('初始化EKF-SLAM...');

% 状态向量初始化(仅机器人位姿)
x_est = initial_pose;          % 状态估计
P_est = diag([0.1, 0.1, 0.05].^2); % 初始协方差

% 地图管理
landmark_map = containers.Map('KeyType', 'int32', 'ValueType', 'any');
next_landmark_id = 1;

% 数据记录
estimated_trajectory = zeros(3, n_steps);
estimated_trajectory(:,1) = initial_pose;
estimated_landmarks = cell(1, n_steps);
innovation_history = cell(1, n_steps);

%% 4. 主循环 - EKF-SLAM
disp('开始EKF-SLAM主循环...');

for t = 2:n_steps
    % 显示进度
    if mod(t, 50) == 0
        fprintf('处理第 %d/%d 步...\n', t, n_steps);
    end
    
    %% 4.1 预测步骤(运动更新)
    % 获取控制输入(带噪声)
    v_noisy = v + sqrt(Q(1,1)) * randn();
    w_noisy = w + sqrt(Q(3,3)) * randn();
    
    % 运动模型
    theta = x_est(3);
    F = [1, 0, -v_noisy*dt*sin(theta);
         0, 1,  v_noisy*dt*cos(theta);
         0, 0,  1];
    
    % 状态预测
    x_pred = x_est;
    x_pred(1) = x_est(1) + v_noisy*dt*cos(theta);
    x_pred(2) = x_est(2) + v_noisy*dt*sin(theta);
    x_pred(3) = x_est(3) + w_noisy*dt;
    
    % 协方差预测
    G = [dt*cos(theta), 0;
         dt*sin(theta), 0;
         0, dt];
    P_pred = F * P_est * F' + G * Q * G';
    
    %% 4.2 观测步骤(测量更新)
    % 获取真实观测(带噪声)
    [z_true, landmark_ids] = get_observations(true_trajectory(:,t), ...
                                             landmarks_true, sensor_range, ...
                                             sensor_fov, R);
    
    % 如果没有观测到路标,跳过更新
    if ~isempty(z_true)
        % 数据关联(简单最近邻)
        [z_pred, H, associated_ids] = data_association(x_pred, landmark_map, ...
                                                      landmark_ids, R);
        
        % 计算卡尔曼增益
        S = H * P_pred * H' + R;
        K = P_pred * H' / S;
        
        % 计算新息(innovation)
        innovation = zeros(size(z_true,1), 1);
        for i = 1:length(associated_ids)
            idx = find(landmark_ids == associated_ids(i));
            if ~isempty(idx)
                innovation(2*i-1:2*i) = z_true(:,idx) - z_pred(:,i);
            end
        end
        
        % 状态更新
        x_est = x_pred + K * innovation;
        
        % 协方差更新(Joseph形式,保证正定性)
        I = eye(size(P_pred));
        P_est = (I - K * H) * P_pred * (I - K * H)' + K * R * K';
        
        % 记录新息
        innovation_history{t} = innovation;
    else
        x_est = x_pred;
        P_est = P_pred;
    end
    
    %% 4.3 新路标初始化
    % 检查未关联的观测
    if ~isempty(z_true)
        for i = 1:size(z_true,2)
            lid = landmark_ids(i);
            if ~isKey(landmark_map, lid)
                % 初始化新路标
                [new_landmark, H_r, H_m] = initialize_landmark(x_est, z_true(:,i), lid);
                
                % 扩展状态向量
                x_est = [x_est; new_landmark];
                
                % 扩展协方差矩阵
                n = length(x_est);
                P_ext = zeros(n);
                P_ext(1:n-2, 1:n-2) = P_est;
                
                % 计算新路标的协方差
                G = [H_r, H_m];
                P_ext(n-1:n, n-1:n) = G * P_est(1:3,1:3) * G' + R;
                P_ext(n-1:n, 1:n-2) = G * P_est(1:3, :);
                P_ext(1:n-2, n-1:n) = P_ext(n-1:n, 1:n-2)';
                
                P_est = P_ext;
                
                % 添加到地图
                landmark_map(lid) = struct('position', new_landmark, ...
                                          'covariance', P_ext(n-1:n, n-1:n), ...
                                          'first_observed', t);
                next_landmark_id = max(next_landmark_id, lid + 1);
            end
        end
    end
    
    %% 4.4 记录结果
    estimated_trajectory(:,t) = x_est(1:3);
    
    % 提取估计的路标位置
    if length(x_est) > 3
        landmark_positions = reshape(x_est(4:end), 2, []);
        estimated_landmarks{t} = landmark_positions;
    else
        estimated_landmarks{t} = [];
    end
    
    %% 4.5 实时可视化(每50步更新一次)
    if mod(t, 50) == 0 || t == n_steps
        visualize_slam(t, true_trajectory, estimated_trajectory, ...
                      landmarks_true, estimated_landmarks{t}, ...
                      landmark_map, sensor_range, map_size);
        drawnow;
    end
end

%% 5. 性能评估与结果分析
disp('进行性能评估...');

% 计算定位误差
position_error = sqrt(sum((true_trajectory(1:2,:) - ...
                          estimated_trajectory(1:2,:)).^2, 1));

% 计算地图误差(仅对已观测路标)
map_error = [];
if ~isempty(estimated_landmarks{end})
    est_landmarks = estimated_landmarks{end};
    for i = 1:size(est_landmarks,2)
        % 找到对应的真实路标(最近邻)
        distances = sqrt(sum((landmarks_true - est_landmarks(:,i)).^2, 1));
        [min_dist, idx] = min(distances);
        if min_dist < 2.0  % 关联阈值
            map_error = [map_error, min_dist];
        end
    end
end

%% 6. 结果可视化
figure('Position', [100, 100, 1400, 600]);

% 子图1:轨迹与地图对比
subplot(2,3,1);
hold on; grid on; axis equal;
xlim([0 map_size(1)]); ylim([0 map_size(2)]);

% 绘制真实路标
plot(landmarks_true(1,:), landmarks_true(2,:), 'm*', ...
     'MarkerSize', 10, 'LineWidth', 2, 'DisplayName', '真实路标');

% 绘制真实轨迹
plot(true_trajectory(1,:), true_trajectory(2,:), 'r-', ...
     'LineWidth', 2, 'DisplayName', '真实轨迹');

% 绘制估计轨迹
plot(estimated_trajectory(1,:), estimated_trajectory(2,:), 'b-', ...
     'LineWidth', 2, 'DisplayName', '估计轨迹');

% 绘制估计路标
if ~isempty(estimated_landmarks{end})
    est_landmarks = estimated_landmarks{end};
    plot(est_landmarks(1,:), est_landmarks(2,:), 'bo', ...
         'MarkerSize', 8, 'LineWidth', 1.5, 'DisplayName', '估计路标');
    
    % 绘制协方差椭圆
    for i = 1:size(est_landmarks,2)
        if isKey(landmark_map, i)
            cov = landmark_map(i).covariance;
            error_ellipse(cov, est_landmarks(:,i), 'b', 0.95);
        end
    end
end

xlabel('X [m]'); ylabel('Y [m]');
title('EKF-SLAM 轨迹与地图估计');
legend('Location', 'best');

% 子图2:定位误差
subplot(2,3,2);
plot((1:n_steps)*dt, position_error, 'b-', 'LineWidth', 2);
grid on; xlabel('时间 [s]'); ylabel('定位误差 [m]');
title('机器人定位误差');
ylim([0, max(position_error)*1.1]);

% 子图3:地图误差
subplot(2,3,3);
if ~isempty(map_error)
    bar(1:length(map_error), map_error);
    grid on; xlabel('路标编号'); ylabel('地图误差 [m]');
    title(['地图估计误差 (平均: ', num2str(mean(map_error), '%.3f'), ' m)']);
else
    text(0.5, 0.5, '无地图误差数据', 'HorizontalAlignment', 'center');
end

% 子图4:协方差迹(不确定性)
subplot(2,3,4);
cov_trace = zeros(1, n_steps);
for t = 1:n_steps
    if t <= length(x_est)/3
        idx = 1:min(3*t, length(x_est));
        cov_trace(t) = trace(P_est(idx, idx));
    end
end
plot((1:n_steps)*dt, cov_trace, 'r-', 'LineWidth', 2);
grid on; xlabel('时间 [s]'); ylabel('协方差迹');
title('状态估计不确定性');

% 子图5:新息序列
subplot(2,3,5);
innov_norm = zeros(1, n_steps);
for t = 1:n_steps
    if ~isempty(innovation_history{t})
        innov_norm(t) = norm(innovation_history{t});
    end
end
plot((1:n_steps)*dt, innov_norm, 'g-', 'LineWidth', 2);
grid on; xlabel('时间 [s]'); ylabel('新息范数');
title('观测新息序列');

% 子图6:状态向量维度
subplot(2,3,6);
state_dim = zeros(1, n_steps);
for t = 1:n_steps
    state_dim(t) = length(x_est);
end
plot((1:n_steps)*dt, state_dim, 'm-', 'LineWidth', 2);
grid on; xlabel('时间 [s]'); ylabel('状态向量维度');
title('状态向量增长(新路标加入)');

sgtitle('EKF-SLAM 仿真结果分析', 'FontSize', 14, 'FontWeight', 'bold');

%% 7. 输出统计信息
fprintf('\n=== EKF-SLAM 仿真结果统计 ===\n');
fprintf('仿真时间: %.1f 秒\n', sim_time);
fprintf('总步数: %d\n', n_steps);
fprintf('平均定位误差: %.3f m\n', mean(position_error));
fprintf('最大定位误差: %.3f m\n', max(position_error));
if ~isempty(map_error)
    fprintf('平均地图误差: %.3f m\n', mean(map_error));
    fprintf('观测到的路标数: %d/%d\n', length(map_error), n_landmarks);
end
fprintf('最终状态向量维度: %d\n', length(x_est));
fprintf('最终协方差迹: %.3f\n', trace(P_est));

disp('=== EKF-SLAM 仿真完成 ===');

2. 关键函数实现

函数1:获取观测数据

function [z, landmark_ids] = get_observations(robot_pose, landmarks_true, ...
                                             sensor_range, sensor_fov, R)
% 获取机器人对路标的观测(距离和角度)
    
    z = [];
    landmark_ids = [];
    
    xr = robot_pose(1);
    yr = robot_pose(2);
    theta = robot_pose(3);
    
    for i = 1:size(landmarks_true,2)
        % 计算相对位置
        dx = landmarks_true(1,i) - xr;
        dy = landmarks_true(2,i) - yr;
        
        % 计算距离和角度
        distance = sqrt(dx^2 + dy^2);
        angle = atan2(dy, dx) - theta;
        
        % 角度归一化到[-pi, pi]
        angle = wrapToPi(angle);
        
        % 检查是否在传感器范围内
        if distance <= sensor_range && abs(angle) <= deg2rad(sensor_fov/2)
            % 添加观测噪声
            z_noisy = [distance; angle] + chol(R)' * randn(2,1);
            
            z = [z, z_noisy];
            landmark_ids = [landmark_ids, i];
        end
    end
end

函数2:数据关联

function [z_pred, H, associated_ids] = data_association(x_pred, landmark_map, ...
                                                       observed_ids, R)
% 数据关联:匹配观测与已有路标
    
    z_pred = [];
    H = [];
    associated_ids = [];
    
    % 提取机器人状态
    xr = x_pred(1);
    yr = x_pred(2);
    theta = x_pred(3);
    
    row_counter = 1;
    
    for i = 1:length(observed_ids)
        lid = observed_ids(i);
        
        if isKey(landmark_map, lid)
            % 获取路标在状态向量中的索引
            landmark_info = landmark_map(lid);
            landmark_pos = landmark_info.position;
            
            % 计算观测预测
            dx = landmark_pos(1) - xr;
            dy = landmark_pos(2) - yr;
            q = dx^2 + dy^2;
            distance_pred = sqrt(q);
            angle_pred = atan2(dy, dx) - theta;
            angle_pred = wrapToPi(angle_pred);
            
            z_pred = [z_pred, [distance_pred; angle_pred]];
            associated_ids = [associated_ids, lid];
            
            % 计算雅可比矩阵 H
            H_ii = zeros(2, length(x_pred));
            
            % 对机器人状态的偏导
            H_ii(1,1) = -dx/distance_pred;
            H_ii(1,2) = -dy/distance_pred;
            H_ii(1,3) = 0;
            
            H_ii(2,1) = dy/q;
            H_ii(2,2) = -dx/q;
            H_ii(2,3) = -1;
            
            % 对路标状态的偏导
            landmark_idx = find_landmark_index(x_pred, lid);
            if ~isempty(landmark_idx)
                H_ii(1, landmark_idx) = dx/distance_pred;
                H_ii(1, landmark_idx+1) = dy/distance_pred;
                H_ii(2, landmark_idx) = -dy/q;
                H_ii(2, landmark_idx+1) = dx/q;
            end
            
            H = [H; H_ii];
            row_counter = row_counter + 2;
        end
    end
end

函数3:新路标初始化

function [new_landmark, H_r, H_m] = initialize_landmark(robot_pose, z, landmark_id)
% 初始化新观测到的路标
    
    xr = robot_pose(1);
    yr = robot_pose(2);
    theta = robot_pose(3);
    
    % 从观测反推路标位置
    distance = z(1);
    angle = z(2) + theta;
    
    mx = xr + distance * cos(angle);
    my = yr + distance * sin(angle);
    
    new_landmark = [mx; my];
    
    % 计算雅可比矩阵(用于协方差初始化)
    H_r = [1, 0, -distance*sin(angle);
           0, 1,  distance*cos(angle)];
    
    H_m = [cos(angle), -distance*sin(angle);
           sin(angle),  distance*cos(angle)];
end

函数4:可视化函数

function visualize_slam(step, true_traj, est_traj, true_landmarks, ...
                       est_landmarks, landmark_map, sensor_range, map_size)
% 实时可视化SLAM过程
    
    figure(1); clf;
    hold on; grid on; axis equal;
    xlim([0 map_size(1)]); ylim([0 map_size(2)]);
    
    % 绘制真实路标
    plot(true_landmarks(1,:), true_landmarks(2,:), 'm*', ...
         'MarkerSize', 10, 'LineWidth', 2, 'DisplayName', '真实路标');
    
    % 绘制真实轨迹
    plot(true_traj(1,1:step), true_traj(2,1:step), 'r-', ...
         'LineWidth', 2, 'DisplayName', '真实轨迹');
    
    % 绘制估计轨迹
    plot(est_traj(1,1:step), est_traj(2,1:step), 'b-', ...
         'LineWidth', 2, 'DisplayName', '估计轨迹');
    
    % 绘制当前机器人位置
    robot_pos = est_traj(1:2, step);
    robot_theta = est_traj(3, step);
    
    % 绘制机器人(三角形)
    robot_triangle = [robot_pos + 0.3*[cos(robot_theta); sin(robot_theta)], ...
                      robot_pos + 0.2*[cos(robot_theta+2*pi/3); sin(robot_theta+2*pi/3)], ...
                      robot_pos + 0.2*[cos(robot_theta-2*pi/3); sin(robot_theta-2*pi/3)], ...
                      robot_pos + 0.3*[cos(robot_theta); sin(robot_theta)]];
    
    plot(robot_triangle(1,:), robot_triangle(2,:), 'g-', 'LineWidth', 2);
    plot(robot_pos(1), robot_pos(2), 'go', 'MarkerSize', 8, 'LineWidth', 2);
    
    % 绘制传感器范围
    sensor_circle = plot_circle(robot_pos, sensor_range, 'g--');
    sensor_circle.DisplayName = '传感器范围';
    
    % 绘制估计路标
    if ~isempty(est_landmarks)
        plot(est_landmarks(1,:), est_landmarks(2,:), 'bo', ...
             'MarkerSize', 8, 'LineWidth', 1.5, 'DisplayName', '估计路标');
        
        % 绘制协方差椭圆
        keys = landmark_map.keys;
        for k = 1:length(keys)
            lid = keys{k};
            if lid <= size(est_landmarks,2)
                landmark_info = landmark_map(lid);
                if isfield(landmark_info, 'covariance')
                    error_ellipse(landmark_info.covariance, ...
                                 est_landmarks(:,lid), 'b', 0.95);
                end
            end
        end
    end
    
    xlabel('X [m]'); ylabel('Y [m]');
    title(sprintf('EKF-SLAM 第 %d 步 (时间: %.1f s)', step, step*0.1));
    legend('Location', 'best');
    
    % 添加信息文本框
    info_str = sprintf('步数: %d\n路标数: %d\n状态维度: %d', ...
                      step, size(est_landmarks,2), length(est_traj(:,1)) + ...
                      (size(est_landmarks,2)*2));
    annotation('textbox', [0.02, 0.02, 0.2, 0.1], 'String', info_str, ...
               'BackgroundColor', 'w', 'EdgeColor', 'k');
end

function h = plot_circle(center, radius, style)
% 绘制圆形
    theta = linspace(0, 2*pi, 100);
    x = center(1) + radius * cos(theta);
    y = center(2) + radius * sin(theta);
    h = plot(x, y, style, 'LineWidth', 1);
end

function error_ellipse(covariance, mean, color, confidence)
% 绘制协方差椭圆
    [V, D] = eig(covariance);
    [D, order] = sort(diag(D), 'descend');
    V = V(:, order);
    
    % 计算椭圆参数
    chi2 = chi2inv(confidence, 2);
    theta = linspace(0, 2*pi, 100);
    ellipse = sqrt(chi2) * V * sqrt(diag(D)) * [cos(theta); sin(theta)];
    
    plot(mean(1) + ellipse(1,:), mean(2) + ellipse(2,:), color, ...
         'LineWidth', 1);
end

函数5:辅助函数

function angle = wrapToPi(angle)
% 将角度映射到[-pi, pi]区间
    angle = mod(angle + pi, 2*pi) - pi;
end

function idx = find_landmark_index(x, landmark_id)
% 查找路标在状态向量中的索引
    % 假设路标按顺序存储:机器人状态(3) + 路标1(2) + 路标2(2) + ...
    idx = 3 + (landmark_id-1)*2 + 1;
    if idx > length(x)
        idx = [];
    end
end

三、代码运行与结果分析

1. 运行步骤

  1. 将上述所有代码保存为相应的.m文件

  2. 运行main.m主程序

  3. 程序将自动:

    • 生成仿真环境(随机路标和机器人轨迹)
    • 执行EKF-SLAM算法
    • 实时显示定位和建图过程
    • 生成性能分析图表

2. 预期结果

3. 关键参数调整建议

参数 推荐值 影响
过程噪声Q diag([0.1,0.1,0.05].^2) 越大,对运动模型越不信任
观测噪声R diag([0.3,0.1].^2) 越大,对观测数据越不信任
传感器范围 8-12米 影响可观测路标数量
时间步长dt 0.05-0.2秒 影响计算精度和速度
路标数量 10-20个 影响状态向量维度和计算复杂度

参考代码 关于扩展卡尔曼滤波同时定位与见图的MATLAB仿真代码 www.youwenfan.com/contentcst/135799.html

四、EKF-SLAM的改进方向

1. 算法改进

2. 工程优化

3. 实际应用扩展

五、总结

本代码实现了基于扩展卡尔曼滤波的SLAM系统,具有以下特点:

  1. 完整性:包含完整的EKF-SLAM流程(预测、更新、地图管理)
  2. 可读性:代码结构清晰,注释详细,便于理解和修改
  3. 可视化:实时显示定位和建图过程,直观展示算法性能
  4. 可扩展性:模块化设计,便于添加新功能或改进算法

适用场景

注意事项

  1. EKF-SLAM假设系统近似线性,强非线性场景可能失效
  2. 数据关联采用简单最近邻,复杂环境需改进关联方法
  3. 路标数量增加会显著提高计算复杂度

专注于matlab/simulink,电子电路,编程