ROS2 AGV导航从零到一与改进A星路径规划实战
导读:矿山爆破场景下的AGV,用ROS2构建控制系统,Navigation2实现自主导航,改进A+APF融合算法做路径规划。本文围绕阿克曼运动学、ros2_control控制器框架、改进A算法、势场法融合、Navigation2集成四大模块展开,附完整YAML配置和Python代码。
一、ROS2导航框架演进与架构特征
1.1 为什么要迁移到Navigation2
ROS1的move_base(2010年设计)存在若干问题:
| 问题 | ROS1 move_base | ROS2 Navigation2 |
|---|---|---|
| 架构 | 中心化,单点故障 | 生命周期管理,可热插拔 |
| 规划器 | 插件化但接口不一致 | 统一插件接口,行为树调度 |
| 计算图 | ROS Master中心化 | 去中心化,DDS分布式 |
| 实时性 | 无确定性保证 | 支持QoS配置 |
Navigation2把导航看做行为树执行——根据当前状态(充电/导航/等待)切换不同的规划/控制策略,而不是一个简单的"给目标点就导航"的黑盒。
1.2 系统整体架构
AGV系统架构分为感知层、控制层、规划层、执行层四层:
二、ros2_control控制器框架与硬件抽象
2.1 为什么用ros2_control
ros2_control是ROS2的硬件抽象层,提供统一的接口让上层算法控制各种执行器。相比直接写串口驱动,选择ros2_control的原因:
| 对比 | 直接串口驱动 | ros2_control |
|---|---|---|
| 代码复用 | 每台机器人重写 | 通用控制器+硬件接口 |
| 生命周期 | 无 | Managed lifecycle |
| 插件化 | 无 | 标准化接口 |
| 调试工具 | 自研 | rqt_controller_manager |
2.2 阿克曼运动学
阿克曼转向的特点是内轮转向角比外轮转向角更大,这是前后轴中心位置不同导致的。
汽车转弯时,前轮需要指向不同的角度,内侧轮比外侧轮转得更多,这样四个轮子才能绕同一个瞬时转向中心转动。
| 符号 | 含义 |
|---|---|
| 内轮转向角 | 转向时靠弯内侧的轮子转角 |
| 外轮转向角 | 转向时靠弯外侧的轮子转角 |
| 轴距 | 前后轴之间的距离 |
| 轮距 | 左右轮之间的距离 |
运动学模型(bicycle model)核心实现:
import numpy as np
class AckermannKinematics:
"""阿克曼运动学模型 (bicycle model)"""
def __init__(self, wheelbase: float, max_steer: float = 0.5):
self.L = wheelbase
self.max_steer = max_steer
def inverse_kinematics(
self,
v: float,
omega: float,
dt: float = 0.01
) -> dict:
"""逆运动学: 期望速度 → 电机/舵机指令"""
if abs(omega) < 1e-6:
return {"steer_angle": 0.0, "linear_vel": v}
kappa = omega / v
steer_angle = np.arctan(kappa * self.L)
steer_angle = np.clip(steer_angle, -self.max_steer, self.max_steer)
return {
"steer_angle": float(steer_angle),
"linear_vel": float(v),
"curvature": float(kappa),
}
def forward_kinematics(self, v: float, steer_angle: float) -> dict:
"""正运动学: 电机/舵机指令 → 机器人速度"""
kappa = np.tan(steer_angle) / self.L
omega = kappa * v
return {"vx": v, "omega": omega}
def compute_wheel_angles(self, steer_angle: float, W: float) -> tuple:
"""计算左右轮独立转向角 (cot(ai) - cot(ao) = L/W)"""
tan_s = np.tan(steer_angle)
tan_ai = (W + self.L * tan_s) / (self.L - W * tan_s)
return float(np.arctan(tan_s)), float(np.arctan(tan_ai))
# 使用示例
kin = AckermannKinematics(wheelbase=0.5, max_steer=0.6)
cmd = kin.inverse_kinematics(v=0.5, omega=0.3)
print(f"舵机角度: {np.degrees(cmd['steer_angle']):.1f}°")
2.3 ros2_control配置
controller_manager:
ros__parameters:
update_rate: 100 # Hz
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
ackermann_controller:
type: diff_drive_controller/DiffDriveController
ackermann_controller:
ros__parameters:
wheelbase: 0.5 # 轴距 m
wheel_radius: 0.1 # 轮子半径 m
max_steering_angle: 0.6 # 最大转向角 rad
linear:
max: 2.0
min: -0.5
angular:
max: 2.0
odom_frame: odom
base_frame_id: base_link
enable_odom_tf: true
velocity_rolling_window_size: 10
三、改进A星算法与全局路径规划
3.1 标准A星的缺陷分析
| 缺陷 | 标准A* | 在矿山环境的危害 |
|---|---|---|
| 启发式函数 | 简单距离估算 | 未考虑坡度/障碍密度 |
| 搜索方式 | 4邻域或8邻域 | 矿山巷道狭窄,容易卡死 |
| 势场震荡 | 无局部极小处理 | APF在狭窄通道震荡 |
针对上述缺陷,对启发函数、搜索邻域、势场震荡进行改进。
3.2 改进A星实现
import numpy as np
import heapq
from typing import Optional
class ImprovedAStar:
"""改进A*算法:密度加权启发+斜向惩罚+路径平滑"""
DIRS_5 = [
(0, 1, 1.0), # 上
(0, -1, 1.0), # 下
(1, 0, 1.0), # 右
(-1, 0, 1.0), # 左
(1, 1, 1.414), # 右上
(1, -1, 1.414), # 右下
(-1, 1, 1.414), # 左上
(-1, -1, 1.414) # 左下
]
def __init__(self, grid: np.ndarray, resolution: float = 0.1):
self.grid = grid
self.resolution = resolution
self.rows, self.cols = grid.shape
self.obstacle_density = self._compute_density()
def _compute_density(self) -> np.ndarray:
"""计算障碍物密度图 (3x3窗口平滑)"""
from scipy.ndimage import uniform_filter
return uniform_filter(
(self.grid > 0.5).astype(float),
size=3,
mode='constant'
)
def heuristic(self, node: tuple, goal: tuple, alpha: float = 0.3) -> float:
"""改进启发函数: 欧氏距离 × (1 + α × 障碍密度)"""
dist = np.sqrt((node[0] - goal[0])**2 + (node[1] - goal[1])**2)
density = self.obstacle_density[
min(node[0], self.rows-1), min(node[1], self.cols-1)
]
return dist * (1.0 + alpha * density)
def search(self, start: tuple, goal: tuple, alpha: float = 0.3) -> Optional[list]:
"""A*搜索,返回路径点列表"""
open_set = []
heapq.heappush(open_set, (0.0, start))
came_from = {start: None}
g_score = {start: 0.0}
visited = set()
while open_set:
_, current = heapq.heappop(open_set)
if current in visited:
continue
visited.add(current)
if current == goal:
return self._reconstruct_path(came_from, current)
for dr, dc, cost in self.DIRS_5:
nr, nc = current[0] + dr, current[1] + dc
if not (0 <= nr < self.rows and 0 <= nc < self.cols):
continue
if self.grid[nr, nc] > 0.5:
continue
if (nr, nc) in visited:
continue
# 斜向移动遇相邻障碍则加大cost
move_cost = cost
if abs(dr) == 1 and abs(dc) == 1:
if self.grid[current[0]+dr, current[1]] > 0.5 or \
self.grid[current[0], current[1]+dc] > 0.5:
move_cost *= 2.5
tentative_g = g_score[current] + move_cost
if (nr, nc) not in g_score or tentative_g < g_score[(nr, nc)]:
came_from[(nr, nc)] = current
g_score[(nr, nc)] = tentative_g
f = tentative_g + self.heuristic((nr, nc), goal, alpha)
heapq.heappush(open_set, (f, (nr, nc)))
return None
def _reconstruct_path(self, came_from: dict, current: tuple) -> list:
path = [current]
while current in came_from and came_from[current] is not None:
current = came_from[current]
path.append(current)
path.reverse()
return path
def smooth_path(self, path: list) -> list:
"""路径平滑: 移除共线点"""
if len(path) <= 2:
return path
smoothed = [path[0]]
for i in range(1, len(path) - 1):
d1 = (path[i][0] - path[i-1][0], path[i][1] - path[i-1][1])
d2 = (path[i+1][0] - path[i][0], path[i+1][1] - path[i][1])
if d1 != d2:
smoothed.append(path[i])
smoothed.append(path[-1])
return smoothed
四、改进APF势场法与局部避障
4.1 传统APF的缺陷
| 缺陷 | 表现 | 矿山场景危险 |
|---|---|---|
| 局部极小 | 在U形障碍间振荡 | 巷道尽头无法逃脱 |
| 目标不可达 | 障碍物附近斥力>引力 | 爆破点无法到达 |
| 震荡 | 靠近障碍物时路径抖动 | 碰撞风险 |
4.2 改进斥力函数
改进的核心是距离阈值分段势场——斥力只在一定距离范围内生效,超过就归零,避免在远距离产生过大的排斥力导致震荡。
import numpy as np
class ImprovedAPF:
"""改进人工势场法:分段斥力+目标可达修复+速度平滑"""
def __init__(
self,
k_att: float = 5.0,
k_rep_max: float = 15.0,
d0: float = 0.5,
d_threshold: float = 2.0
):
self.k_att = k_att
self.k_rep_max = k_rep_max
self.d0 = d0
self.d_threshold = d_threshold
self.prev_force = None
def attractive_force(self, pos: np.ndarray, goal: np.ndarray) -> np.ndarray:
"""引力: F_att = k_att × (goal - pos)"""
return self.k_att * (goal - pos)
def repulsive_force(self, pos: np.ndarray, obs: np.ndarray) -> np.ndarray:
"""分段改进斥力:d>d_threshold时为0,d0<d<=d_threshold平滑衰减"""
diff = pos - obs
d = np.linalg.norm(diff) + 1e-8
if d > self.d_threshold:
return np.zeros(2)
if d <= self.d0:
factor = self.k_rep_max * (1.0/d - 1.0/self.d0)**2
else:
d_ratio = (self.d_threshold - d) / (self.d_threshold - self.d0)
factor = self.k_rep_max * d_ratio**2 * (1.0/d - 1.0/self.d0)
return factor * diff / d
def compute_force(
self,
pos: np.ndarray,
goal: np.ndarray,
obstacles: list,
prev_force: np.ndarray = None,
smooth_alpha: float = 0.3
) -> tuple:
"""合力计算(含速度平滑)"""
F_att = self.attractive_force(pos, goal)
F_rep = np.zeros(2)
for obs in obstacles:
F_rep += self.repulsive_force(pos, np.array(obs))
F_total = F_att + F_rep
if prev_force is not None and smooth_alpha > 0:
F_total = (1 - smooth_alpha) * F_total + smooth_alpha * prev_force
return F_total, np.linalg.norm(F_total)
def navigation_step(
self,
pos: np.ndarray,
goal: np.ndarray,
obstacles: list,
dt: float = 0.1,
max_speed: float = 0.5
) -> np.ndarray:
"""导航一步,返回更新后的位置"""
F, mag = self.compute_force(pos, goal, obstacles, self.prev_force)
if mag > 1e-6:
direction = F / mag
velocity = min(max_speed, mag * 0.1)
new_pos = pos + direction * velocity * dt
else:
new_pos = pos.copy()
self.prev_force = F
return new_pos
五、Navigation2集成与核心服务器
5.1 Navigation2核心服务器架构
Navigation2通过行为树调度规划、控制、平滑、恢复四类服务器,配合代价地图与生命周期管理:
5.2 nav2_params.yaml 关键配置
amcl:
ros__parameters:
use_sim_time: False
alpha1: 0.2
alpha2: 0.2
alpha3: 0.2
alpha4: 0.2
alpha5: 0.2
base_frame_id: base_link
beam_skip_distance: 0.5
bt_navigator:
ros__parameters:
use_sim_time: True
global_frame_id: map
robot_base_frame: base_link
default_nav_to_pose_bt_xml: $(find-pkg-share nav2_bt_navigator)/behavior_trees/navigate_to_pose_via_points.xml
plugin_lib_names:
- nav2_compute_path_to_pose_action_bt_node
- nav2_follow_path_action_bt_node
- nav2_rate_controller_bt_node
- nav2_recovery_node_bt_node
- nav2_back_up_action_bt_node
- nav2_spin_action_bt_node
- nav2_wait_action_bt_node
controller_server:
ros__parameters:
use_sim_time: True
controller_frequency: 20.0
FollowPath:
plugin: dwb_plugins::FollowPedestrianPath
min_vel_x: -0.5
max_vel_x: 0.5
max_vel_theta: 1.0
cost_weight: 1.0
heading_weight: 0.5
planner_server:
ros__parameters:
use_sim_time: True
planner_plugin_types: ["nav2_navfn_planner/NavfnPlanner"]
planner_plugin_names: ["grid_based"]
use_astar: true
allow_unknown: true
tolerance: 0.25
六、Gazebo仿真验证
实验平台架构(Gazebo矿山巷道仿真 → ROS2 Humble → Navigation2 → ros2_control阿克曼控制器 → Arduino下位机通讯 → 物理样机验证):
七、开源仓库参考
| 仓库 | 内容 |
|---|---|
| navigation2 | ROS2官方导航框架,Lifecycle管理,行为树调度 |
| nav2_smac_planner | Hybrid-A*改进版,多分辨率搜索 |
| ros2_controllers | ROS2控制器生态 |
| nav2_minimal | 最小化Navigation2配置示例 |
| turtlebot3 | ROS2 TurtleBot3仿真 |
| ackermann_msgs | 阿克曼车辆标准消息类型 |
八、总结
| 模块 | 要点 |
|---|---|
| ros2_control | 生命周期管理,控制器插件化,HardwareInterface抽象 |
| 阿克曼运动学 | Bicycle model,逆运动学求转向角 |
| 改进A* | 障碍密度启发函数+斜向搜索惩罚+路径平滑 |
| 改进APF | 分段斥力+目标可达修复+速度平滑 |
| Navigation2 | Lifecycle管理+行为树调度+代价地图+规划/控制分离 |
口诀:导航框架看Nav2,行为树调度四服务;A星加密度权重,APF分段斥力稳;阿克曼逆解转向角,ros2_control插件化。