🧭 第17课:路径规划(全局)

导航进阶 ✅ Docker验证通过

📋 课程目标

🧠 什么是全局路径规划?

全局路径规划(Global Planner)是在已知地图上,从起点到终点计算出一条无碰撞的最优路径。它是导航系统的"大脑",决定了机器人要走的大方向。

🔑 全局规划 vs 局部规划

📐 经典路径规划算法

┌─────────────── 路径规划算法分类 ────────────────┐ │ 基于搜索(确定性) │ 基于采样(概率性) │ │ Dijkstra/A*/D*Lite │ RRT/RRT*/PRM │ │ ✅ 最优性保证 │ ✅ 高维空间 │ │ ❌ 维度灾难 │ ❌ 次优路径 │ │──────────────────────┼──────────────────────────│ │ 基于势场 │ 优化方法 │ │ APF/RRT+APF │ CHOMP/STOMP/TrajOpt │ │ ✅ 实时性好 │ ✅ 平滑路径 │ │ ❌ 局部极小 │ ❌ 计算量大 │ └──────────────────────────────────────────────────┘
算法类型最优性Nav2支持
Dijkstra搜索✅ 全局最优NavFn
A*搜索+启发✅ 全局最优NavFn
Hybrid-A*搜索+运动学🟡 近似最优SmacPlanner
RRT*采样🟡 渐近最优-
State Lattice搜索+运动学✅ 全局最优SmacPlanner

🔧 Nav2全局规划器配置

1. NavFn Planner(经典A*)

# nav2_params.yaml - NavFn配置
planner_server:
  ros__parameters:
    expected_planner_frequency: 1.0
    planner_plugins: ["GridBased"]
    GridBased:
      plugin: "nav2_navfn_planner/NavfnPlanner"
      tolerance: 0.5
      use_astar: true
      allow_unknown: true

2. SmacPlannerHybrid(Hybrid-A*)

# Hybrid-A*配置 - 考虑运动学约束
planner_server:
  ros__parameters:
    planner_plugins: ["GridBased"]
    GridBased:
      plugin: "nav2_smac_planner/SmacPlannerHybrid"
      allow_unknown: true
      max_iterations: 1000000
      max_planning_time: 5.0
      motion_model_for_search: "REEDS_SHEPP"
      minimum_turning_radius: 0.4
      reverse_penalty: 2.0
      non_straight_penalty: 1.2
      cost_penalty: 2.0
      analytic_expansion_max_length: 3.0
NavFn A* vs Hybrid-A* 路径对比: NavFn: S ────┐ ████ ┌─── G (尖角, 不可执行) Hybrid: S ────╲ ████ ╱─── G (平滑, 可直接执行)

🐍 Python:A*规划器实现

#!/usr/bin/env python3
# A*路径规划器 - 2D栅格地图
import heapq, math, numpy as np

class AStarPlanner:
    def __init__(self, grid, resolution, origin):
        self.grid = grid
        self.resolution = resolution
        self.origin = origin
        self.h, self.w = grid.shape
        self.occ_thresh = 50

    def w2g(self, wx, wy):
        return int((wx-self.origin[0])/self.resolution), int((wy-self.origin[1])/self.resolution)

    def g2w(self, gx, gy):
        return gx*self.resolution+self.origin[0]+self.resolution/2, \
               gy*self.resolution+self.origin[1]+self.resolution/2

    def heuristic(self, a, b):
        return math.hypot(a[0]-b[0], a[1]-b[1])

    def neighbors(self, node):
        dirs = [((0,1),1.),((0,-1),1.),((1,0),1.),((-1,0),1.),
                ((1,1),1.414),((1,-1),1.414),((-1,1),1.414),((-1,-1),1.414)]
        result = []
        for (dx,dy),c in dirs:
            nx,ny = node[0]+dx, node[1]+dy
            if 0<=nx<self.w and 0<=ny<self.h and self.grid[ny][nx]<self.occ_thresh:
                result.append(((nx,ny), c*self.resolution))
        return result

    def plan(self, start_w, goal_w):
        start, goal = self.w2g(*start_w), self.w2g(*goal_w)
        if not (0<=start[0]<self.w and 0<=start[1]<self.h): return None
        if not (0<=goal[0]<self.w and 0<=goal[1]<self.h): return None
        open_set = [(0., start)]; came_from = {}; g = {start: 0.}; closed = set()
        while open_set:
            _, cur = heapq.heappop(open_set)
            if cur == goal:
                path = []
                while cur in came_from: path.append(self.g2w(*cur)); cur = came_from[cur]
                path.append(self.g2w(*start)); path.reverse(); return path
            closed.add(cur)
            for nb, mc in self.neighbors(cur):
                if nb in closed: continue
                tg = g[cur] + mc
                if tg < g.get(nb, float('inf')):
                    came_from[nb] = cur; g[nb] = tg
                    heapq.heappush(open_set, (tg+self.heuristic(nb,goal), nb))
        return None

# 测试
grid = np.zeros((20,20), dtype=np.int8)
grid[8:12,5:15] = 100
p = AStarPlanner(grid, 0.05, (-0.5,-0.5))
path = p.plan((0.,0.), (0.5,0.5))
print(f"{'✅ Found' if path else '❌ Failed'}: {len(path) if path else 0} points")

🐍 Python:ROS2全局规划器节点

#!/usr/bin/env python3
# 自定义Nav2全局规划器节点
import rclpy, numpy as np
from rclpy.node import Node
from nav_msgs.msg import Path, OccupancyGrid
from geometry_msgs.msg import PoseStamped

class GlobalPlannerNode(Node):
    def __init__(self):
        super().__init__('custom_global_planner')
        self.declare_parameter('tolerance', 0.5)
        self.map_sub = self.create_subscription(
            OccupancyGrid, '/global_costmap/costmap', self._map_cb, 10)
        self.path_pub = self.create_publisher(Path, '/plan', 10)
        self.goal_sub = self.create_subscription(
            PoseStamped, '/goal_pose', self._goal_cb, 10)
        self.current_map = None
        self.get_logger().info('🧭 自定义全局规划器已启动')

    def _map_cb(self, msg): self.current_map = msg

    def _goal_cb(self, msg):
        if not self.current_map: return
        grid = np.array(self.current_map.data).reshape(
            self.current_map.info.height, self.current_map.info.width)
        planner = AStarPlanner(grid, self.current_map.info.resolution,
            (self.current_map.info.origin.position.x,
             self.current_map.info.origin.position.y))
        path_pts = planner.plan((0.,0.), (msg.pose.position.x, msg.pose.position.y))
        if not path_pts:
            self.get_logger().error('规划失败'); return
        pm = Path(); pm.header.frame_id = 'map'
        pm.header.stamp = self.get_clock().now().to_msg()
        for px,py in path_pts:
            ps = PoseStamped(); ps.header = pm.header
            ps.pose.position.x = px; ps.pose.position.y = py
            ps.pose.orientation.w = 1.0; pm.poses.append(ps)
        self.path_pub.publish(pm)
        self.get_logger().info(f'✅ 规划完成: {len(pm.poses)}点')

def main(args=None):
    rclpy.init(args=args); rclpy.spin(GlobalPlannerNode()); rclpy.shutdown()

🔧 C++:Hybrid-A*运动学核心

// hybrid_astar_core.cpp
#include <cmath><vector><memory>
struct Pose2D { double x,y,theta; };
class HybridAStarCore {
    double min_r_, step_, max_steer_=0.5;
    std::vector<double> steers_;
public:
    HybridAStarCore(double r, double s) : min_r_(r), step_(s) {
        steers_ = {-max_steer_,-max_steer_/2,0.,max_steer_/2,max_steer_};
    }
    std::vector<Pose2D> successors(const Pose2D& c) {
        std::vector<Pose2D> res;
        for(auto st:steers_) for(bool fwd:{true,false})
            res.push_back(simulate(c,st,fwd));
        return res;
    }
private:
    Pose2D simulate(const Pose2D& s, double steer, bool fwd) {
        Pose2D e; double d=step_*(fwd?1:-1);
        if(std::abs(steer)<1e-6) {
            e.x=s.x+d*std::cos(s.theta); e.y=s.y+d*std::sin(s.theta);
            e.theta=s.theta;
        } else {
            double tr=min_r_/std::tan(std::abs(steer)), a=d/tr;
            if(steer>0){e.x=s.x+tr*(std::sin(s.theta+a)-std::sin(s.theta));
                        e.y=s.y-tr*(std::cos(s.theta+a)-std::cos(s.theta));}
            else{e.x=s.x-tr*(std::sin(s.theta-a)+std::sin(s.theta));
                 e.y=s.y+tr*(std::cos(s.theta-a)-std::cos(s.theta));}
            e.theta=s.theta+(steer>0?a:-a);
        }
        return e;
    }
};

📊 规划器选型与调优

场景推荐理由
室内简单NavFn速度快、配置简单
大型楼层SmacPlanner2D降采样、内存优化
停车场Hybrid-A*运动学约束、可倒车
狭窄通道Hybrid-A*精确转弯半径
问题参数调优方向
规划超时max_iterations增大或简化地图
路径贴墙cost_penalty增大,远离高代价区域
倒车过多reverse_penalty增大(2.0→5.0)
目标不可达tolerance增大容差

🎯 练习题

📝 练习1:规划器对比实验

同一地图分别用NavFn和SmacPlanner2D,对比规划时间和路径长度。

📝 练习2:Hybrid-A*调优

调整reverse_penalty和minimum_turning_radius,观察路径变化。

📝 练习3:自定义启发函数

实现欧氏/曼哈顿/对角距离启发函数,对比搜索效率。

🏆 成就解锁

🏅 全局规划专家

经验值:+250 XP