📖 第01课:SLAM概述

课程阶段:阶段1 - SLAM基础(1-6)
学习目标:理解SLAM问题的数学本质、概率框架、核心模块,掌握SLAM分类体系与代表性算法
前置知识:线性代数基础、概率论基础、Python/C++编程

一、什么是SLAM?

SLAM(Simultaneous Localization And Mapping,同时定位与建图)是机器人领域的核心问题:机器人在未知环境中,同时估计自身位姿构建环境地图

这是一个典型的"鸡与蛋"问题——定位需要地图,建图需要位姿,两者必须同时求解。SLAM的数学本质是一个高维状态估计问题,需要从不完整、带噪声的传感器数据中恢复环境结构和机器人运动。

┌──────────────────────────────────────────────────┐ │ SLAM 问题 │ │ │ │ 传感器观测 z ──→ ┌──────────┐ ──→ 位姿 x │ │ │ │ │ │ 控制输入 u ──→ │ SLAM │ ──→ 地图 m │ │ │ 求解器 │ │ │ 先验知识 ──→ │ │ ──→ 不确定性 Σ │ │ └──────────┘ │ │ │ │ 输出: p(x₀:T, m | z₁:T, u₁:T) │ └──────────────────────────────────────────────────┘

二、SLAM问题的数学形式化

2.1 状态空间定义

定义以下随机变量:

SLAM的完整概率形式:

p(x0:T, m | z1:T, u1:T)

即:给定所有观测 z1:T 和控制输入 u1:T
求机器人轨迹 x0:T 和地图 m 的联合后验分布。

这是一个高维联合分布——假设100个路标,每个路标3维,轨迹100步,
则状态空间维度 = 100×6 + 100×3 = 900维。

2.2 贝叶斯递归分解

利用贝叶斯法则和条件独立性,SLAM后验可以递归分解:

第一步:轨迹与地图分离

p(x0:T, m | z1:T, u1:T) = p(x0:T | z1:T, u1:T) · p(m | x0:T, z1:T)

第二步:轨迹的马尔可夫分解

p(x0:T | z1:T, u1:T) = η · p(x₀) · ∏t=1T p(xt | xt-1, ut) · p(zt | xt, m)

其中 η = 1/p(z1:T | u1:T) 是归一化常数。

第三步:地图的条件分布

给定轨迹,地图的后验可分解为各路标独立:
p(m | x0:T, z1:T) = ∏j=1M p(mj | x0:T, z1:T)

这是EKF-SLAM的关键:地图路标在给定轨迹后条件独立。

2.3 运动模型与观测模型

运动模型(状态转移):

xt = f(xt-1, ut) + wt,   wt ~ N(0, Rt)

其中 f(·) 是运动函数,wt 是过程噪声。
对于差速驱动:f 返回 (x+Δx, y+Δy, θ+Δθ)。

观测模型:

zt = h(xt, m) + vt,   vt ~ N(0, Qt)

其中 h(·) 是观测函数,vt 是观测噪声。
对于相机观测路标:h 将3D路标投影到2D像素坐标。

2.4 马尔可夫假设

SLAM求解依赖两个关键独立性假设:

运动马尔可夫性:
p(xt | x0:t-1, u1:t) = p(xt | xt-1, ut)

当前状态只依赖上一状态和当前控制。

观测条件独立性:
p(zt | x0:t, m, z1:t-1) = p(zt | xt, m)

当前观测只依赖当前位姿和地图。

⚠ 这些假设在实际中可能不成立!
• 运动马尔可夫性忽略打滑、惯性等时序相关性
• 观测独立性忽略动态物体、光照变化等
• 鲁棒SLAM需要处理这些违例

三、SLAM系统的四大模块

传感器数据 ──→ [前端 Front-end] ──→ [后端 Back-end] ──→ 地图输出 │ ↑ └──→ [回环检测] ────┘ │ [地图表示] ←──────┘ 前端:特征提取 → 数据关联 → 帧间运动估计 后端:状态估计 → 图优化 → 一致性维护 回环:场景识别 → 几何验证 → 约束添加 地图:稀疏/稠密/语义 → 多层次表示

3.1 前端(Front-end)

负责数据关联和帧间运动估计,是SLAM的"感知"模块:

前端的错误(错误数据关联)会传播到后端,导致地图不可用。因此鲁棒的数据关联是SLAM的基石。

3.2 后端(Back-end)

负责状态估计与全局优化,是SLAM的"推理"模块:

3.3 回环检测(Loop Closure Detection)

检测机器人是否回到曾经到过的位置,消除累积漂移:

3.4 地图表示

类型数学描述适用场景代表算法
路标地图m = {m₁,...,mM} ∈ ℝ³M视觉SLAM定位ORB-SLAM2
占据栅格p(occ | z1:t) ∈ [0,1] 网格导航避障GMapping
点云地图{pᵢ} ⊂ ℝ³,稠密3D点集重建与可视化LOAM
TSDFD(x) 截断符号距离场面重建KinectFusion
语义地图{(pᵢ, cᵢ)} 带标签的地图高级任务规划Kimera
OctoMap八叉树概率占据大规模3D导航OctoMap

四、SLAM方法分类

4.1 按后端方法分类

滤波方法 vs 优化方法

维度滤波(EKF/PF)优化(图优化/BA)
状态表示当前时刻分布 p(xt, m)全轨迹 p(x0:T, m)
线性化一阶(EKF)或采样(PF)迭代重线性化
计算量O(M²) 每步(EKF)O(N³) 但稀疏可加速
一致性线性化误差累积全局最优(局部极值内)
回环处理困难自然支持
代表算法EKF-SLAM, FastSLAMORB-SLAM, Cartographer

4.2 按传感器分类

类型传感器优点缺点代表
视觉SLAM单目/双目/RGB-D相机成本低、信息丰富受光照影响、尺度问题ORB-SLAM3
激光SLAM2D/3D LiDAR精度高、不受光照影响成本高、稀疏LOAM, Cartographer
多传感器融合相机+LiDAR+IMU鲁棒、互补标定复杂、计算量大LVI-SAM, R3LIVE

4.3 按地图表示分类

根据地图的稠密程度:

五、SLAM的概率图模型

SLAM问题可以用概率图模型优雅地表示。理解图模型是理解现代SLAM算法的基础。

5.1 动态贝叶斯网络

时刻: 0 1 2 3 ... T 控制: u₁ ──→ u₂ ──→ u₃ ──→ u₄ ──→ ... ──→ uT 位姿: x₀ ──→ x₁ ──→ x₂ ──→ x₃ ──→ ... ──→ xT │ │ │ │ │ ↓ ↓ ↓ ↓ ↓ 观测: z₁ z₂ z₃ z₄ ... zT │ │ │ │ │ ↓ ↓ ↓ ↓ ↓ 地图: m₁ m₂ m₁ m₂ m₁ m₂ m₁ m₂ ... m₁ m₂ ↕ ↕ ↕ ↕ ↕ 关联: [c₁] [c₂] [c₃] [c₄] ... [cT]

这个动态贝叶斯网络展示了SLAM的核心结构:

5.2 因子图表示

在现代SLAM中,更常用因子图来表示优化问题:

SLAM的最大后验估计等价于最小二乘:

x*0:T, m* = argmin { ∑t=1T ||xt - f(xt-1, ut)||²Rt
                     + ∑t=1Tj∈Jt ||ztj - h(xt, mj)||²Qt }

每个 ||·||²Σ = (·)T Σ-1 (·) 是马氏距离。
这个目标函数对应一个因子图:
• 先验因子:锚定 x₀
• 运动因子:连接 xt-1 → xt,信息矩阵 ΣRt
• 观测因子:连接 xt → mj,信息矩阵 ΣQt

六、SLAM的可观测性与一致性

6.1 可观测性

SLAM问题是否可解,取决于系统是否可观测

可观测性定义:

系统在轨迹 x0:T 上可观测,当且仅当:
对于任意两个不同状态 (x0:T, m) ≠ (x'0:T, m'),
存在至少一个观测 zt 使得 p(zt | xt, m) ≠ p(zt | x't, m')。

单目SLAM的不可观测自由度:
• 7-DoF:6-DoF位姿 + 1-DoF全局尺度
• 绝对尺度无法从单目观测中恢复
• 需要初始化或先验来固定尺度

6.2 一致性

SLAM估计的一致性指估计的不确定性是否真实反映了误差大小:

一致性定义:

估计 (x̂, Σ̂) 是一致的,当且仅当:
(x - x̂)T Σ̂-1 (x - x̂) 服从 χ²(n) 分布
其中 n 是状态维度。

EKF-SLAM的不一致性问题:
• 线性化误差导致协方差低估
• 长时间运行后变得过度自信
• 解决方案:UKF、迭代EKF、或切换到优化方法

七、代码实践:简单EKF-SLAM仿真

下面实现一个2D EKF-SLAM仿真,包含完整的预测和更新步骤,验证SLAM的基本原理。

import numpy as np import matplotlib.pyplot as plt # ============================================================ # 2D EKF-SLAM Simulation # State: [x, y, θ, m_x1, m_y1, ..., m_xM, m_yM] # ============================================================ class EKFSlam2D: def __init__(self, n_landmarks=10): # State: [x, y, θ, lx1, ly1, ..., lxM, lyM] self.n_lm = n_landmarks self.dim = 3 + 2 * n_landmarks # State vector and covariance self.x = np.zeros(self.dim) self.P = np.zeros((self.dim, self.dim)) # Initial pose uncertainty self.P[0:3, 0:3] = np.diag([0.01, 0.01, 0.001]) # Landmark uncertainty: large initially self.P[3:, 3:] = np.eye(2 * n_landmarks) * 1000.0 # Motion noise self.R = np.diag([0.05, 0.05, 0.01]) # [Δx, Δy, Δθ] # Observation noise self.Q = np.diag([0.1, 0.05]) # [range, bearing] # Track which landmarks have been observed self.observed = [False] * n_landmarks def predict(self, u): """EKF predict step with motion model u=[Δx, Δy, Δθ] x_new = x + R(θ) * [Δx, Δy]^T θ_new = θ + Δθ """ dx, dy, dtheta = u theta = self.x[2] # Compute Jacobian of motion model w.r.t. pose # x' = x + cos(θ)*Δx - sin(θ)*Δy # y' = y + sin(θ)*Δx + cos(θ)*Δy # θ' = θ + Δθ Fx = np.zeros((3, self.dim)) Fx[0:3, 0:3] = np.eye(3) # Jacobian of motion w.r.t. [x, y, θ] Gx = np.array([ [1, 0, -np.sin(theta)*dx - np.cos(theta)*dy], [0, 1, np.cos(theta)*dx - np.sin(theta)*dy], [0, 0, 1] ]) # Jacobian of motion w.r.t. control noise V = np.array([ [np.cos(theta), -np.sin(theta), 0], [np.sin(theta), np.cos(theta), 0], [0, 0, 1] ]) # Update state mean self.x[0] += np.cos(theta) * dx - np.sin(theta) * dy self.x[1] += np.sin(theta) * dx + np.cos(theta) * dy self.x[2] += dtheta # Update covariance: P' = G*P*G^T + V*R*V^T G = np.eye(self.dim) G[0:3, 0:3] = Gx self.P = G @ self.P @ G.T + Fx.T @ V @ self.R @ V.T @ Fx def update(self, z_list): """EKF update with landmark observations. z_list: [(landmark_id, range, bearing), ...] """ for lm_id, r_obs, b_obs in z_list: lx_idx = 3 + 2 * lm_id ly_idx = 4 + 2 * lm_id # Predicted landmark position lx = self.x[lx_idx] ly = self.x[ly_idx] dx = lx - self.x[0] dy = ly - self.x[1] q = dx**2 + dy**2 sqrt_q = np.sqrt(q) # Predicted observation: [range, bearing] r_pred = sqrt_q b_pred = np.arctan2(dy, dx) - self.x[2] # Innovation innov = np.array([r_obs - r_pred, np.arctan2(np.sin(b_obs - b_pred), np.cos(b_obs - b_pred))]) # Jacobian H = dh/dstate H = np.zeros((2, self.dim)) # dh/d[x, y, θ] H[0, 0] = -dx / sqrt_q H[0, 1] = -dy / sqrt_q H[1, 0] = dy / q H[1, 1] = -dx / q H[1, 2] = -1 # dh/d[lx, ly] H[0, lx_idx] = dx / sqrt_q H[0, ly_idx] = dy / sqrt_q H[1, lx_idx] = -dy / q H[1, ly_idx] = dx / q # Kalman gain S = H @ self.P @ H.T + self.Q K = self.P @ H.T @ np.linalg.inv(S) # State update self.x += K @ innov # Covariance update: P' = (I - K*H)*P I_KH = np.eye(self.dim) - K @ H self.P = I_KH @ self.P self.observed[lm_id] = True # ============================================================ # Simulation # ============================================================ np.random.seed(42) # Generate landmarks in a room n_landmarks = 8 landmarks = np.array([ [5, 0], [5, 5], [0, 5], [-5, 5], [-5, 0], [-5, -5], [0, -5], [5, -5] ], dtype=float) # Generate robot trajectory (circular path) T = 100 dt = 0.1 true_traj = [] for t in range(T): theta = 2 * np.pi * t / T x = 3 * np.cos(theta) y = 3 * np.sin(theta) true_traj.append([x, y, theta + np.pi/2]) true_traj = np.array(true_traj) # Initialize EKF-SLAM slam = EKFSlam2D(n_landmarks) slam.x[0:3] = true_traj[0] # Run SLAM est_traj = [] for t in range(1, T): # True motion prev = true_traj[t-1] curr = true_traj[t] dx = (curr[0] - prev[0])*np.cos(prev[2]) + (curr[1] - prev[1])*np.sin(prev[2]) dy = -(curr[0] - prev[0])*np.sin(prev[2]) + (curr[1] - prev[1])*np.cos(prev[2]) dtheta = curr[2] - prev[2] u = np.array([dx, dy, dtheta]) # Add motion noise u_noisy = u + np.random.multivariate_normal(np.zeros(3), slam.R * 0.5) # Predict slam.predict(u_noisy) # Observe landmarks within range z_list = [] for j in range(n_landmarks): dx_lm = landmarks[j, 0] - curr[0] dy_lm = landmarks[j, 1] - curr[1] r = np.sqrt(dx_lm**2 + dy_lm**2) if r < 8.0: # max observation range b = np.arctan2(dy_lm, dx_lm) - curr[2] r_noisy = r + np.random.randn() * 0.1 b_noisy = b + np.random.randn() * 0.05 z_list.append((j, r_noisy, b_noisy)) # Update if z_list: slam.update(z_list) est_traj.append(slam.x[0:3].copy()) est_traj = np.array(est_traj) # Plot results fig, axes = plt.subplots(1, 2, figsize=(14, 6)) # Trajectory comparison axes[0].plot(true_traj[:,0], true_traj[:,1], 'b-', label='Ground Truth', linewidth=2) axes[0].plot(est_traj[:,0], est_traj[:,1], 'r--', label='EKF-SLAM Estimate', linewidth=2) axes[0].scatter(landmarks[:,0], landmarks[:,1], c='green', s=100, marker='*', label='True Landmarks', zorder=5) est_lm = slam.x[3:].reshape(-1, 2) axes[0].scatter(est_lm[:,0], est_lm[:,1], c='red', s=50, marker='o', label='Estimated Landmarks', alpha=0.7) axes[0].set_xlabel('X (m)') axes[0].set_ylabel('Y (m)') axes[0].set_title('EKF-SLAM: Trajectory & Landmarks') axes[0].legend() axes[0].grid(True, alpha=0.3) axes[0].set_aspect('equal') # Position error over time pos_error = np.sqrt((true_traj[1:,0] - est_traj[:,0])**2 + (true_traj[1:,1] - est_traj[:,1])**2) axes[1].plot(pos_error, 'b-', linewidth=1) axes[1].set_xlabel('Time Step') axes[1].set_ylabel('Position Error (m)') axes[1].set_title('EKF-SLAM Localization Error') axes[1].grid(True, alpha=0.3) plt.tight_layout() plt.savefig('ekf_slam_result.png', dpi=150) plt.show() # Print landmark estimation error for j in range(n_landmarks): err = np.linalg.norm(est_lm[j] - landmarks[j]) print(f"Landmark {j}: error = {err:.3f} m")

八、ROS2集成:SLAM仿真框架

下面创建一个ROS2节点来模拟简单的2D SLAM系统,展示传感器数据发布、TF变换和地图服务。

# slam_sim_node.py - ROS2 2D SLAM Simulation Node import rclpy from rclpy.node import Node from geometry_msgs.msg import PoseStamped, Twist from nav_msgs.msg import OccupancyGrid, Odometry from sensor_msgs.msg import LaserScan from tf2_ros import TransformBroadcaster from geometry_msgs.msg import TransformStamped import numpy as np class SlamSimNode(Node): def __init__(self): super().__init__('slam_sim_node') # Robot state self.robot_pose = np.array([0.0, 0.0, 0.0]) # x, y, theta self.dt = 0.1 # 10 Hz update rate # Subscribers self.cmd_vel_sub = self.create_subscription( Twist, '/cmd_vel', self.cmd_vel_callback, 10) # Publishers self.odom_pub = self.create_publisher(Odometry, '/odom', 10) self.scan_pub = self.create_publisher(LaserScan, '/scan', 10) self.map_pub = self.create_publisher(OccupancyGrid, '/map', 10) self.pose_pub = self.create_publisher(PoseStamped, '/slam_pose', 10) # TF broadcaster self.tf_broadcaster = TransformBroadcaster(self) # Timer for simulation loop self.timer = self.create_timer(self.dt, self.simulation_step) # Simple map (walls at boundaries) self.map_resolution = 0.05 # m/pixel self.map_width = 200 self.map_height = 200 self.grid = np.zeros((self.map_height, self.map_width), dtype=np.int8) # Add boundary walls self.grid[0, :] = 100 self.grid[-1, :] = 100 self.grid[:, 0] = 100 self.grid[:, -1] = 100 self.get_logger().info('SLAM simulation node started') def cmd_vel_callback(self, msg): # Apply velocity to update robot pose v = msg.linear.x omega = msg.angular.z theta = self.robot_pose[2] # Differential drive model self.robot_pose[0] += v * np.cos(theta) * self.dt self.robot_pose[1] += v * np.sin(theta) * self.dt self.robot_pose[2] += omega * self.dt def simulation_step(self): # Publish odometry self.publish_odometry() # Publish simulated laser scan self.publish_scan() # Publish map self.publish_map() # Broadcast TF self.broadcast_tf() def publish_odometry(self): odom = Odometry() odom.header.stamp = self.get_clock().now().to_msg() odom.header.frame_id = 'odom' odom.child_frame_id = 'base_link' odom.pose.pose.position.x = float(self.robot_pose[0]) odom.pose.pose.position.y = float(self.robot_pose[1]) # Orientation from yaw from tf_transformations import quaternion_from_euler q = quaternion_from_euler(0, 0, self.robot_pose[2]) odom.pose.pose.orientation.x = q[0] odom.pose.pose.orientation.y = q[1] odom.pose.pose.orientation.z = q[2] odom.pose.pose.orientation.w = q[3] self.odom_pub.publish(odom) def publish_scan(self): scan = LaserScan() scan.header.stamp = self.get_clock().now().to_msg() scan.header.frame_id = 'laser' scan.angle_min = -np.pi scan.angle_max = np.pi scan.angle_increment = np.pi / 180 # 1 degree resolution scan.range_min = 0.1 scan.range_max = 10.0 n_beams = 360 scan.ranges = [10.0] * n_beams # default: max range # Simulate wall detections (simplified) for i in range(n_beams): angle = self.robot_pose[2] + scan.angle_min + i * scan.angle_increment # Ray-cast to boundary walls at ±5m for wall_dist in [5.0]: dx = np.cos(angle) * wall_dist dy = np.sin(angle) * wall_dist wx = self.robot_pose[0] + dx wy = self.robot_pose[1] + dy if abs(wx) >= 5.0 or abs(wy) >= 5.0: r = min(5.0/max(abs(np.cos(angle)), 1e-6), 5.0/max(abs(np.sin(angle)), 1e-6)) scan.ranges[i] = min(r, 10.0) + np.random.randn()*0.02 self.scan_pub.publish(scan) def publish_map(self): map_msg = OccupancyGrid() map_msg.header.stamp = self.get_clock().now().to_msg() map_msg.header.frame_id = 'map' map_msg.info.resolution = self.map_resolution map_msg.info.width = self.map_width map_msg.info.height = self.map_height map_msg.info.origin.position.x = -5.0 map_msg.info.origin.position.y = -5.0 map_msg.info.origin.orientation.w = 1.0 map_msg.data = self.grid.flatten().tolist() self.map_pub.publish(map_msg) def broadcast_tf(self): t = TransformStamped() t.header.stamp = self.get_clock().now().to_msg() t.header.frame_id = 'odom' t.child_frame_id = 'base_link' t.transform.translation.x = float(self.robot_pose[0]) t.transform.translation.y = float(self.robot_pose[1]) from tf_transformations import quaternion_from_euler q = quaternion_from_euler(0, 0, self.robot_pose[2]) t.transform.rotation.x = q[0] t.transform.rotation.y = q[1] t.transform.rotation.z = q[2] t.transform.rotation.w = q[3] self.tf_broadcaster.sendTransform(t) def main(args=None): rclpy.init(args=args) node = SlamSimNode() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()

8.1 Launch文件

<!-- slam_sim.launch.py --> <launch> <node pkg="slam_course" exec="slam_sim_node" name="slam_sim" output="screen"> <param name="map_resolution" value="0.05"/> <param name="max_range" value="10.0"/> </node> <node pkg="rviz2" exec="rviz2" name="rviz2" args="-d $(find-pkg-share slam_course)/config/slam.rviz"/> </launch>

九、Gazebo仿真实验

配置Gazebo仿真环境来测试SLAM算法:

<!-- gazebo_slam.world - Simple room for SLAM testing --> <?xml version="1.0"?> <sdf version="1.6"> <world name="slam_test_room"> <include> <uri>model://ground_plane</uri> </include> <include> <uri>model://sun</uri> </include> <!-- Room walls (10m x 10m) --> <model name="room"> <static>true</static> <!-- North wall --> <link name="north_wall"> <pose>0 5 1 0 0 0</pose> <collision name="collision"> <geometry><box><size>10 0.2 2</size></box></geometry> </collision> <visual name="visual"> <geometry><box><size>10 0.2 2</size></box></geometry> </visual> </link> <!-- Add South, East, West walls similarly --> </model> <!-- Obstacles --> <model name="obstacle_1"> <static>true</static> <link name="link"> <pose>2 2 0.5 0 0 0</pose> <collision name="collision"> <geometry><cylinder><radius>0.3</radius><height>1</height></cylinder></geometry> </collision> <visual name="visual"> <geometry><cylinder><radius>0.3</radius><height>1</height></cylinder></geometry> </visual> </link> </model> </world> </sdf>

十、SLAM发展历史与趋势

年代里程碑方法
1986Smith, Self, Cheeseman首次提出随机地图估计
1991Leonard, Durrant-Whyte将SLAM形式化为同时定位与建图
2002Montemerlo et al.FastSLAM: 粒子滤波+Rao-Blackwellized
2006Grisetti et al.GMapping: 自适应粒子滤波2D SLAM
2008Kuemmerle et al.g2o: 通用图优化框架
2011Engel et al.LSD-SLAM: 半稠密直接法
2015Mur-Artal et al.ORB-SLAM: 特征法视觉SLAM标杆
2016Zhang & SinghLOAM: 激光里程计与建图
2018Shan et al.LeGO-LOAM: 轻量级地面优化LOAM
2020Campos et al.ORB-SLAM3: 多地图多传感器统一框架
2021Shan et al.LVI-SAM: 激光视觉惯性融合
2023VariousGaussian Splatting SLAM: 3DGS表示

十一、练习

练习1:EKF-SLAM参数敏感性分析

修改上述EKF-SLAM仿真代码,分析以下参数对SLAM性能的影响:

  1. 将运动噪声 R 增大5倍,观察轨迹估计误差如何变化
  2. 将观测噪声 Q 增大5倍,观察路标估计误差如何变化
  3. 将观测范围从8m减小到4m,观察SLAM何时"失效"
  4. 绘制不同参数下的NEES(Normalized Estimation Error Squared)曲线,验证一致性

提示:NEES = (x - x̂)ᵀ Σ⁻¹ (x - x̂),一致时NEES应接近状态维度n

练习2:数据关联挑战

在仿真中加入数据关联错误:

  1. 以5%的概率将观测关联到错误的路标ID
  2. 观察EKF-SLAM的估计如何发散
  3. 实现一个基于马氏距离的数据关联门限(χ²检验),拒绝离群关联
  4. 比较有无门限的SLAM性能差异

练习3:ROS2 SLAM仿真

基于上面的ROS2节点:

  1. 使用ros2 run运行节点,用rviz2可视化
  2. 编写一个控制节点,让机器人沿正方形路径运动
  3. 记录/odom和/scan话题数据到rosbag
  4. 回放rosbag数据,观察SLAM输出

练习4:SLAM问题维度分析

计算以下SLAM场景的状态空间维度,并分析EKF-SLAM的计算复杂度:

  1. 100步轨迹,20个2D路标
  2. 1000步轨迹,200个2D路标
  3. 100步轨迹,50个3D路标(每个路标3维)

EKF-SLAM每步的复杂度为O(M²),其中M为路标数。分析在什么规模下EKF-SLAM不再可行。