机器人路径规划算法实践总结 (Path Planning Practical Guide)


目录

  1. 基于搜索的路径规划 (Search-Based)
  2. 基于采样的路径规划 (Sampling-Based)
  3. 局部规划与轨迹优化 (Local & Trajectory)
  4. 理论实现 vs 工业级代码库
  5. 工程选型矩阵与实战避坑指南
  6. 附录:核心代码工具库

1. 基于搜索的路径规划 (Search-Based)

1.1 核心思想:把连续空间离散化,用"启发式代价"导航

将机器人的构型空间 (C-Space) 离散为网格或图结构,维护一个待探索节点集合 (Open List)。每次挑选综合代价最小的节点进行扩展,直到触及目标。

$$ f(n) = g(n) + h(n) $$
符号 含义 工程要点
$g(n)$ 从起点到当前节点的实际累积代价 需考虑地形代价、转向惩罚、时间成本
$h(n)$ 启发式预估代价 必须可采纳(Admissible) 且一致(Consistent) 才能保证最优性
$f(n)$ 综合优先级 优先队列排序依据

启发函数设计准则

// 可采纳性:h(n) ≤ 实际最短代价 → 保证找到最优解
// 一致性:h(n) ≤ cost(n,n') + h(n') → 保证节点只扩展一次,提升效率

// 对角启发式(8连通网格,满足一致性)
double diagonalHeuristic(int x1, int y1, int x2, int y2, 
                         double cost_straight = 1.0, 
                         double cost_diag = M_SQRT2) {
    int dx = abs(x1 - x2), dy = abs(y1 - y2);
    int min_d = std::min(dx, dy);
    int max_d = std::max(dx, dy);
    // 对角步数×对角代价 + 直线步数×直线代价
    return cost_diag * min_d + cost_straight * (max_d - min_d);
}

// Tie-Breaker:当f值相同时,优先选择h更小的节点,减少不必要的探索
node->f = node->g + node->h * (1.0 + 1e-4);  // 微小扰动打破平局

1.2 算法流程与工业级 C++ 实现 (A*)

⚠️ 教学代码常见陷阱

// 问题1:priority_queue 不支持 decrease-key,重复插入导致内存泄漏
if (!open_set_contains(neighbor, open_set)) {  // O(N) 查找!破坏整体复杂度
    open_set.push(neighbor);
}

// 问题2:g值更新后,queue 中旧节点仍按旧 f 值排序,导致错误扩展
neighbor->g = tentative_g;  // 优先级未同步更新

应对策略:双映射 + 惰性删除策略

// 核心数据结构
struct Node {
    int x, y;
    double g = INFINITY, h = 0, f = INFINITY;
    Node* parent = nullptr;
    bool operator>(const Node& other) const { return f > other.f; }
    void reset() { g = h = f = INFINITY; parent = nullptr; }  // 对象复用必备
};

// 坐标编码工具(用于哈希映射)
inline uint64_t encodeCoord(int x, int y, int width) {
    return (static_cast<uint64_t>(x) << 32) | static_cast<uint32_t>(y);
}

// A* 规划器核心类
class AStarPlanner {
private:
    // 坐标 → 节点最新状态的映射(避免重复创建)
    std::unordered_map<uint64_t, Node*> node_registry;
    // 优先队列(存储原始指针,通过 registry 验证有效性)
    std::priority_queue<Node*, std::vector<Node*>, std::greater<Node*>> open_set;
    // 内存池:避免频繁 new/delete
    ObjectPool<Node> node_pool;
    
public:
    std::vector<Point> plan(const GridMap& map, const Point& start, const Point& goal) {
        // 初始化起点
        Node* start_node = acquireNode(start.x, start.y);
        start_node->g = 0;
        start_node->h = diagonalHeuristic(start.x, start.y, goal.x, goal.y);
        start_node->f = start_node->g + start_node->h;
        open_set.push(start_node);
        
        while (!open_set.empty()) {
            Node* current = popValidNode();  // 惰性删除:跳过废弃节点
            if (!current) break;
            
            // 目标检测
            if (current->x == goal.x && current->y == goal.y) {
                auto path = reconstructPath(current);
                cleanup();  // 释放内存池
                return path;
            }
            
            // 扩展邻居(8连通)
            for (const auto& [nx, ny, cost] : getNeighbors8(map, current)) {
                if (map.isOccupied(nx, ny)) continue;
                
                double tentative_g = current->g + cost;
                Node* neighbor = acquireNode(nx, ny);
                
                // 发现更优路径则更新
                if (tentative_g < neighbor->g) {
                    neighbor->parent = current;
                    neighbor->g = tentative_g;
                    neighbor->h = diagonalHeuristic(nx, ny, goal.x, goal.y);
                    neighbor->f = neighbor->g + neighbor->h * 1.001;  // Tie-Breaker
                    open_set.push(neighbor);  // 新状态入队,旧状态惰性丢弃
                }
            }
        }
        cleanup();
        return {};  // 无解
    }
    
private:
    Node* acquireNode(int x, int y) {
        uint64_t key = encodeCoord(x, y, map_width);
        if (auto it = node_registry.find(key); it != node_registry.end()) {
            return it->second;  // 复用已有节点
        }
        Node* node = node_pool.acquire();  // 从内存池获取
        node->x = x; node->y = y;
        node_registry[key] = node;
        return node;
    }
    
    Node* popValidNode() {
        while (!open_set.empty()) {
            Node* top = open_set.top(); open_set.pop();
            uint64_t key = encodeCoord(top->x, top->y, map_width);
            // 惰性删除:只处理与registry中g值一致的节点
            if (node_registry.count(key) && node_registry[key]->g == top->g) {
                return top;
            }
        }
        return nullptr;
    }
    
    void cleanup() {
        for (auto& [key, node] : node_registry) {
            node_pool.release(node);  // 归还内存池
        }
        node_registry.clear();
        while (!open_set.empty()) open_set.pop();
    }
};

1.3 配置空间(C-Space)障碍物膨胀原理

为什么需要膨胀?机器人有物理尺寸,不能简化为质点。将障碍物在C-Space中"膨胀"机器人半径,可将规划问题转化为质点导航。

// 距离变换 + 阈值膨胀(比逐点卷积更高效)
void inflateObstacles(GridMap& map, double robot_radius, double safety_margin) {
    double inflation_dist = robot_radius + safety_margin;
    int inflation_cells = ceil(inflation_dist / map.resolution);
    
    // 多源BFS计算每个格子到最近障碍物的距离
    std::queue<std::pair<int,int>> q;
    const int dx[] = {1,-1,0,0,1,1,-1,-1};
    const int dy[] = {0,0,1,-1,1,-1,1,-1};
    
    // 初始化
    for (int i = 0; i < map.height; i++) {
        for (int j = 0; j < map.width; j++) {
            if (map.isOccupied(i, j)) {
                map.dist[i][j] = 0;
                q.push({i, j});
            } else {
                map.dist[i][j] = INFINITY;
            }
        }
    }
    
    // BFS传播距离
    while (!q.empty()) {
        auto [x, y] = q.front(); q.pop();
        for (int k = 0; k < 8; k++) {
            int nx = x + dx[k], ny = y + dy[k];
            if (!map.inBounds(nx, ny)) continue;
            double step_cost = (k < 4) ? 1.0 : M_SQRT2;
            double new_dist = map.dist[x][y] + step_cost * map.resolution;
            if (new_dist < map.dist[nx][ny]) {
                map.dist[nx][ny] = new_dist;
                q.push({nx, ny});
            }
        }
    }
    
    // 根据距离设置代价值(指数衰减,越近代价越高)
    for (int i = 0; i < map.height; i++) {
        for (int j = 0; j < map.width; j++) {
            if (map.dist[i][j] <= inflation_dist) {
                // 代价值:0(自由) ~ 254(危险),255=障碍物
                double ratio = map.dist[i][j] / inflation_dist;
                map.cost[i][j] = static_cast<uint8_t>(254 * (1.0 - ratio));
            }
        }
    }
}

1.4 工程痛点与稳定性考量

痛点维度 具体表现 工业级应对策略
计算量与内存 网格分辨率翻倍,节点数呈平方/立方增长,Open List 膨胀导致 $O(N \log N)$ 超时 采用多分辨率网格JPS跳跃点搜索、或切换至采样法;使用内存池避免频繁分配
启发函数设计 $h(n)$ 高估导致非最优;$h(n)$ 低估导致退化为 Dijkstra,探索面积爆炸 使用对角启发式 + Tie-Breaker;动态环境用加权A* ($f=g+w·h, w>1$) 加速
路径生硬 输出呈"锯齿状",直接下发给底盘会导致电机高频抖动 必做后处理平滑:B-Spline 拟合、TEB 弹性带优化、或衔接 Frenet 多项式规划
动态环境失效 障碍物移动需全图重规划,频率过高导致 CPU 满载 采用**增量式搜索 *(D Lite)、或降级为局部规划器处理高频动态障碍

2. 基于采样的路径规划 (Sampling-Based)

2.1 核心思想:不显式建图,随机撒点"长树"

放弃离散网格,在连续空间中随机采样。通过碰撞检测将自由空间的采样点连成树状结构,逐步逼近目标。

算法 核心特点 最优性 适用场景
RRT 快速探索,偏重目标采样 ❌ 不保证 高维空间、运动学约束复杂
RRT* 引入 **Rewire **(重连) 机制 ✅ 渐进最优 对路径质量有要求的场景
Informed RRT* 椭圆启发域采样,收敛更快 ✅ 渐进最优 已知起终点,需快速收敛
PRM 预建路网,支持多查询 ✅ 渐进最优 静态环境、多任务规划

2.2 算法流程与工业级 C++ 实现 (RRT*)

运动学感知的 RRT* 核心逻辑

// 状态定义(含航向角,适用于阿克曼/差速底盘)
struct State {
    double x, y, theta;  // 位置 + 航向
    double g = INFINITY; // 累积代价
    State* parent = nullptr;
    void reset() { g = INFINITY; parent = nullptr; }
};

// 运动学约束的 Steer 函数(以阿克曼转向为例)
State steerKinodynamic(const State& from, const Point& target, 
                       double step_size, double min_turn_radius) {
    // 1. 计算期望航向角
    double target_heading = atan2(target.y - from.y, target.x - from.x);
    
    // 2. 考虑最大曲率约束(最小转弯半径)
    double max_turn_angle = step_size / min_turn_radius;  // 单步最大转向角
    double angle_diff = normalizeAngle(target_heading - from.theta);
    double new_heading = from.theta + clamp(angle_diff, -max_turn_angle, max_turn_angle);
    
    // 3. 按新航向生成新状态
    return {
        from.x + step_size * cos(new_heading),
        from.y + step_size * sin(new_heading),
        new_heading,
        INFINITY, nullptr
    };
}

// 连续碰撞检测(避免高速运动漏检)
bool checkCollisionContinuous(const State& s1, const State& s2, 
                              const std::vector<Obstacle>& obs,
                              double robot_radius, double step_ratio = 0.1) {
    double dist = hypot(s2.x - s1.x, s2.y - s1.y);
    int steps = std::max(2, static_cast<int>(dist / (robot_radius * step_ratio)));
    
    for (int i = 1; i < steps; i++) {
        double t = static_cast<double>(i) / steps;
        State mid = lerpState(s1, s2, t);  // 线性插值状态
        if (checkCollisionDiscrete(mid, obs, robot_radius)) return true;
    }
    return false;
}

// RRT* 核心扩展与重连逻辑
class RRTStarPlanner {
private:
    std::vector<State*> tree;
    KDTree* kd_tree;  // 加速最近邻搜索
    ObjectPool<State> state_pool;
    
public:
    std::vector<State> plan(const PlanningContext& ctx, int max_iter = 10000) {
        // 初始化起点
        State* root = state_pool.acquire();
        *root = ctx.start; root->g = 0;
        tree.push_back(root);
        kd_tree->insert(root);
        
        for (int iter = 0; iter < max_iter; iter++) {
            // 1. 采样(可结合目标偏置)
            Point q_rand = (rand01() < 0.05) ? ctx.goal : sampleFreeSpace(ctx.bounds);
            
            // 2. 最近邻搜索(KD-Tree O(log N))
            State* q_near = kd_tree->nearest(q_rand);
            
            // 3. 运动学感知的扩展
            State q_new = steerKinodynamic(*q_near, q_rand, ctx.step_size, ctx.min_turn_radius);
            if (checkCollisionContinuous(*q_near, q_new, ctx.obstacles, ctx.robot_radius)) {
                continue;  // 碰撞则丢弃
            }
            
            // 4. 创建新节点
            State* q_new_node = state_pool.acquire();
            *q_new_node = q_new;
            q_new_node->parent = q_near;
            q_new_node->g = q_near->g + distance(*q_near, q_new);
            tree.push_back(q_new_node);
            kd_tree->insert(q_new_node);
            
            // 5. RRT* 核心:Rewire(带邻域衰减)
            double radius = std::min(ctx.max_rewire_radius, 
                                   ctx.gamma * pow(log(tree.size()) / tree.size(), 1.0/2.0));
            auto neighbors = kd_tree->radiusSearch(q_new, radius);
            
            for (State* q_near_neighbor : neighbors) {
                if (q_near_neighbor == q_near) continue;
                
                double cost_via_new = q_new_node->g + distance(*q_new_node, *q_near_neighbor);
                if (cost_via_new < q_near_neighbor->g && 
                    !checkCollisionContinuous(*q_new_node, *q_near_neighbor, 
                                            ctx.obstacles, ctx.robot_radius)) {
                    // 仅更新直接邻居,子树代价延迟计算(Lazy Propagation)
                    q_near_neighbor->parent = q_new_node;
                    q_near_neighbor->g = cost_via_new;
                    // 注意:实际工业实现中,g值传播可延迟到查询时计算
                }
            }
            
            // 6. 目标检测(带容差)
            if (distance(q_new, ctx.goal) < ctx.goal_tolerance) {
                return reconstructPath(q_new_node);
            }
        }
        return {};  // 超时未找到
    }
};

2.3 窄通道优化:自适应采样策略

// Gaussian 采样:在障碍物边界附近增加采样概率
Point gaussianSample(const PlanningContext& ctx, double sigma) {
    while (true) {
        Point p1 = sampleUniform(ctx.bounds);
        Point p2 = sampleGaussian(p1, sigma);  // 以p1为中心的高斯扰动
        
        // 接受条件:一个点在自由空间,一个点在障碍物内(边界附近)
        bool in1 = isInFreeSpace(p1, ctx.obstacles);
        bool in2 = isInFreeSpace(p2, ctx.obstacles);
        if (in1 != in2) {
            return in1 ? p1 : p2;
        }
    }
}

// Bridge Test:专门针对窄通道的采样
Point bridgeTestSample(const PlanningContext& ctx, double max_len) {
    while (true) {
        Point center = sampleUniform(ctx.bounds);
        Point direction = sampleUnitVector();
        Point p1 = center + direction * max_len/2;
        Point p2 = center - direction * max_len/2;
        
        // 接受条件:两端在障碍物内,中间在自由空间(穿过窄通道)
        if (!isInFreeSpace(p1, ctx.obstacles) && 
            !isInFreeSpace(p2, ctx.obstacles) &&
            isInFreeSpace(center, ctx.obstacles)) {
            return center;
        }
    }
}

2.4 工程痛点与计算量分析

痛点维度 具体表现 工业级应对策略
最近邻搜索耗时 朴素遍历 $O(N)$,节点破万后严重拖慢实时性 强制使用 FLANN / KD-Tree / Ball Tree,将查询压至 $O(\log N)$
**窄通道 **(Narrow Passage) 随机采样极难命中狭窄缝隙,探索效率骤降 采用 Gaussian SamplingBridge Test 或结合启发式采样 (Informed RRT*)
重连计算爆炸 RRT* 的 Rewire 需遍历邻域+碰撞检测,计算量呈指数上升 限制邻域半径随节点数衰减;或改用 Informed RRT* 仅采样椭圆启发域
结果随机性 同一起终点多次运行路径差异大,不利于产线调试 固定随机种子调试;生产环境结合 Anytime RRT* 持续优化已有路径

3. 局部规划与轨迹优化 (Local & Trajectory)

3.1 DWA (动态窗口法) 核心逻辑与参数自适应

“看眼前几步怎么走最安全、最舒服”

算法流程

// DWA 主循环(20-50Hz)
Twist computeDWACommand(const RobotState& state, 
                        const Costmap& costmap,
                        const Point& local_goal,
                        const DWAParams& params) {
    
    // 1. 动态窗口计算:可达速度空间
    VelocityWindow Vd = computeDynamicWindow(
        state.velocity, 
        params.max_vel, params.min_vel,
        params.max_accel, params.max_omega_accel,
        params.dt  // 仿真步长
    );
    
    // 2. 轨迹预测与采样
    std::vector<Trajectory> trajectories;
    for (double v : sample(Vd.v_range, params.v_resolution)) {
        for (double w : sample(Vd.omega_range, params.omega_resolution)) {
            Trajectory traj = simulateTrajectory(state, v, w, params.dt, params.sim_time);
            if (isValidTrajectory(traj, costmap, params.robot_radius)) {
                trajectories.push_back(traj);
            }
        }
    }
    
    // 3. 代价评估(支持动态权重)
    double obs_weight, goal_weight, vel_weight;
    std::tie(obs_weight, goal_weight, vel_weight) = 
        adaptiveWeights(costmap, state, params);  // 根据环境拥挤度调整
    
    Trajectory* best = nullptr;
    double best_score = -INFINITY;
    
    for (auto& traj : trajectories) {
        double score = 
            obs_weight * obstacleCost(traj, costmap) +
            goal_weight * goalCost(traj, local_goal) +
            vel_weight * velocityCost(traj, params.preferred_vel);
        
        if (score > best_score) {
            best_score = score;
            best = &traj;
        }
    }
    
    // 4. 执行最优 + 安全兜底
    if (best) {
        return {best->v_end, best->omega_end};
    } else {
        // 无可行轨迹:紧急制动
        return {0, 0};
    }
}

// 动态权重调整:根据局部障碍物密度
std::tuple<double, double, double> adaptiveWeights(
    const Costmap& costmap, const RobotState& state, const DWAParams& params) {
    
    double density = computeLocalDensity(costmap, state.pos, params.eval_radius);
    
    if (density > 0.7) {
        // 拥挤环境:优先避障,降低速度追求
        return {params.obs_weight * 2.0, params.goal_weight * 0.3, params.vel_weight * 0.5};
    } else if (density < 0.2) {
        // 开阔环境:优先趋近目标,允许更高速度
        return {params.obs_weight * 0.5, params.goal_weight * 2.0, params.vel_weight * 1.5};
    }
    return {params.obs_weight, params.goal_weight, params.vel_weight};
}

3.2 Frenet 坐标系规划:多项式轨迹生成

“把道路拆成纵向跟车和横向换道,分别用多项式拟合”

核心数学模型

// Frenet 轨迹生成器
class FrenetPlanner {
public:
    // 纵向规划:四次多项式 s(t) = a0 + a1*t + a2*t² + a3*t³ + a4*t⁴
    Polynomial4 planLongitudinal(const LonBoundary& bound, double target_vel) {
        // 边界条件:位置、速度、加速度、加加速度(Jerk)
        // 优化目标:最小化 Jerk + 速度跟踪误差
        return solveQP(bound, target_vel, {JERK_MIN, VEL_TRACK});
    }
    
    // 横向规划:五次多项式 d(s) = b0 + b1*s + b2*s² + b3*s³ + b4*s⁴ + b5*s⁵
    Polynomial5 planLateral(const LatBoundary& bound, double road_width) {
        // 边界条件:横向位置、导数、曲率
        // 约束:道路边界、最大曲率、曲率变化率
        return solveQP(bound, road_width, {CURV_LIMIT, BOUNDARY_PENALTY});
    }
    
    // 轨迹评估与选择
    FrenetTrajectory selectBest(const std::vector<FrenetTrajectory>& candidates) {
        FrenetTrajectory* best = nullptr;
        double min_cost = INFINITY;
        
        for (auto& traj : candidates) {
            if (!isFeasible(traj)) continue;  // 可行性检查
            
            double cost = 
                params.k_jerk * traj.jerk_cost +
                params.k_time * traj.time_cost +
                params.k_lat_dev * traj.lateral_deviation +
                params.k_collision * traj.collision_risk;
            
            if (cost < min_cost) {
                min_cost = cost;
                best = &traj;
            }
        }
        return best ? *best : FrenetTrajectory::emergencyStop();
    }
};

工程优化技巧

// 预计算查找表 (LUT) 加速多项式求值
class PolynomialLUT {
    std::vector<double> coeff_lut;  // 系数预计算
    std::vector<double> time_grid;  // 时间网格
    
public:
    void build(const Polynomial4& poly, double t_min, double t_max, int resolution) {
        time_grid = linspace(t_min, t_max, resolution);
        coeff_lut.resize(resolution);
        for (int i = 0; i < resolution; i++) {
            coeff_lut[i] = poly.evaluate(time_grid[i]);  // 预计算
        }
    }
    
    double query(double t) {
        // 二分查找 + 线性插值,比直接多项式求值快 3-5 倍
        int idx = binarySearch(time_grid, t);
        return lerp(coeff_lut[idx], coeff_lut[idx+1], 
                   (t - time_grid[idx]) / (time_grid[idx+1] - time_grid[idx]));
    }
};

3.3 实时性瓶颈与动态避障陷阱

痛点维度 具体表现 工业级应对策略
局部极小值 DWA 在 U 型障碍或狭窄死胡同中反复横跳、原地打转 引入全局势场引导、切换至 **TEB **(Timed Elastic Band) 支持倒车/拓扑切换
参考线依赖 Frenet 严重依赖高质量 Reference Line,弯道突变易导致规划失败 前端增加参考线平滑 (QP 优化)、加入曲率约束道路边界惩罚项
高频执行压力 局部规划需 20~50Hz 运行,多项式求解+代价计算易超时 提前生成 **LUT **(查找表)、使用 Eigen 矩阵库 SIMD 加速、限制采样数量
轨迹振荡 连续帧规划结果跳变,导致底盘控制不稳定 增加轨迹平滑约束、使用**模型预测控制 **(MPC) 统一优化、添加输出滤波

4. 理论实现 vs 工业级代码库 (关键差异) ⚙️

在实际项目中,极少从零手写底层搜索/采样逻辑。学术代码用于理解原理,工业代码用于保证鲁棒性、实时性与可维护性。

4.1 架构差异对比

维度 教学/自研实现 (Python/C++ 标准库) 工业级部署 (OMPL / Nav2 / Apollo / MoveIt 2)
数据结构 std::vector, std::priority_queue 自定义内存池 (Memory Pool)、对象复用、避免频繁 new/delete
碰撞检测 逐点网格查询或简单几何相交 **延迟碰撞检测 **(Lazy)、GPU 加速、**体素八叉树 **(OctoMap)、**连续碰撞检测 **(CCD)
并行化 单线程串行 多线程采样、路径并行评估、代价地图多线程更新、CUDA 加速
增量更新 障碍物变化需全量重算 支持 D Lite / RRTx* 局部修剪、仅重规划受影响分支
容错机制 崩溃或卡死无降级 内置 Fallback Controller、安全急停、超时强制输出次优路径、看门狗监控
参数管理 硬编码或配置文件 动态参数服务器、在线调参、参数自适应策略
可观测性 打印日志 指标埋点 (Prometheus)、轨迹可视化 (rviz2)、性能剖析 (tracy/perf)

4.2 主流工业栈推荐

场景 推荐组合 核心库/模块 说明
**AGV / 服务机器人 **(2D) SmacPlanner2D (Hybrid A*) + DWB / TEB ROS 2 Nav2 开箱即用,支持代价地图动态膨胀、多约束优化、插件化架构
自动驾驶 / 泊车 Lattice Planner / Hybrid A* + Frenet 优化 Apollo / Autoware 严格满足运动学约束,支持多路径并行评估、行为预测耦合
机械臂 / 高自由度 RRT* / PRM + STOMP / CHOMP 优化 OMPL / MoveIt 2 支持关节空间规划、自碰撞检测、轨迹平滑、逆运动学集成
无人机 / 高速系统 Kinodynamic RRT* + Minimum Snap 优化 fast_planner / bspline_opt 绑定动力学约束,输出可直接下发飞控,支持在线重规划

💡 核心结论
“大框架一致,但魔鬼在细节”。工业级代码库将算法逻辑封装为可配置的 Plugin,底层处理了内存管理、实时调度、传感器噪声滤波与异常恢复。开发者应聚焦于代价函数设计、参数整定、参考线质量与系统集成,而非重复造轮子。


5. 工程选型矩阵与实战避坑指南

5.1 多维度算法对比矩阵

算法族 时间复杂度 空间复杂度 最优性保证 动态障碍适应性 参数敏感度 典型延迟 适用维度
A / JPS* $O(N \log N)$ $O(N)$ (网格) ✅ 全局最优 ❌ 需全量重规划 中 (启发函数/分辨率) 10~50ms 2D/3D 离散
Hybrid A* $O(N^3)$ $O(N^3)$ (含 $\theta$) ⚠️ 次优 ❌ 静态为主 高 (转向/倒车惩罚) 50~200ms 2D+ 航向
RRT $O(N \log N)$ $O(N)$ (树) ❌ 不保证 ⚠️ 需增量更新 低 (步长/采样率) 20~100ms 高维连续
RRT* $O(N^2)$ (含重连) $O(N)$ ✅ 渐进最优 ❌ 计算过重 中高 (邻域半径) 100~500ms 高维连续
Informed RRT* $O(N \log N)$ $O(N)$ ✅ 渐进最优 ❌ 静态为主 中 (椭圆参数) 50~200ms 高维连续
PRM $O(N \log N)$ 预建 $O(N)$ ✅ 渐进最优 ❌ 静态环境 低 (采样密度) 查询<10ms 高维静态
DWA $O(K \cdot T)$ $O(1)$ ❌ 局部最优 ✅ 极佳 高 (代价权重) <5ms 2D 速度空间
Frenet $O(M \cdot N)$ $O(M)$ ⚠️ 依赖参考线 ⚠️ 中等 高 (多项式阶数/权重) 10~30ms 道路跟随

5.2 实战调优 Checklist

5.3 选型决策流

graph TD
    A[开始选型] --> B{环境维度?}
    B -->|2D 平面 | C{动态障碍频率?}
    B -->|3D/高维 | D[采样法: RRT*/PRM]
    
    C -->|低频/静态 | E[搜索法: A*/JPS]
    C -->|高频动态 | F[分层: 全局 A* + 局部 DWA/TEB]
    
    E --> G{有运动学约束?}
    G -->|是 | H[Hybrid A* / State Lattice]
    G -->|否 | I[标准 A* + 后处理平滑]
    
    D --> J{多查询 or 单查询?}
    J -->|多查询 | K[PRM + 懒惰碰撞检测]
    J -->|单查询 | L[Informed RRT* + 椭圆采样]
    
    F --> M{需要倒车/复杂机动?}
    M -->|是 | N[TEB / MPPI]
    M -->|否 | O[标准 DWA]
    
    H --> P[添加轨迹平滑: B-Spline/TEB]
    L --> P
    N --> P
    O --> P

    style A fill:#e1f5fe,stroke:#01579b
    style P fill:#fff9c4,stroke:#fbc02d
    style N fill:#e8f5e9,stroke:#2e7d32
    style L fill:#e8f5e9,stroke:#2e7d32k

5.4 常见坑点与解决方案

坑点现象 根本原因 解决方案
规划路径"贴墙"行驶 膨胀半径 < 机器人半径,或代价衰减过快 增大 inflation_radius,调整代价衰减曲线为指数型
动态障碍避不及时 局部规划频率过低,或预测时域太短 提升 DWA 频率至 30Hz+,增加 sim_time 至 3-5s
窄通道无法通过 采样法概率低,或搜索法分辨率不足 使用 Gaussian 采样 + Bridge Test,或切换多分辨率网格
路径频繁跳变 代价地图更新延迟,或参考线不平滑 增加代价地图滤波,参考线使用 QP 平滑 + 曲率约束
内存占用持续增长 节点/状态对象未复用,或缓存未清理 使用对象池 + 显式 reset(),定期清理历史轨迹缓存
实车振荡/抖动 规划输出未平滑,或控制频率不匹配 规划后必做 B-Spline 平滑,确保规划频率 ≥ 控制频率 2 倍

附录:核心代码工具库

A.1 内存池实现 (ObjectPool)

// 通用对象池:预分配 + 对象复用,避免频繁 new/delete
template<typename T>
class ObjectPool {
private:
    std::vector<T*> free_list;
    std::vector<std::unique_ptr<T>> storage;
    size_t chunk_size = 1024;
    
public:
    T* acquire() {
        if (free_list.empty()) {
            // 批量分配新块,减少系统调用
            for (size_t i = 0; i < chunk_size; i++) {
                storage.emplace_back(std::make_unique<T>());
                free_list.push_back(storage.back().get());
            }
        }
        T* obj = free_list.back();
        free_list.pop_back();
        obj->reset();  // 🔑 关键:重置对象状态,避免脏数据
        return obj;
    }
    
    void release(T* obj) {
        if (!obj) return;
        obj->reset();  // 清理状态
        free_list.push_back(obj);
    }
    
    void clear() {
        free_list.clear();
        storage.clear();  // 如需彻底释放内存
    }
    
    size_t size() const { return storage.size() - free_list.size(); }
};

A.2 KD-Tree 最近邻搜索封装

// 简化版 KD-Tree 接口(实际建议使用 nanoflann / FLANN)
class KDTree {
public:
    void insert(const Point& p, void* data) {
        points_.push_back(p);
        data_.push_back(data);
        dirty_ = true;
    }
    
    void* nearest(const Point& query) {
        rebuildIfNeeded();
        int best_idx = -1;
        double best_dist = INFINITY;
        // 实际实现:递归剪枝搜索,复杂度 O(log N)
        searchRecursive(root_, query, best_idx, best_dist);
        return data_[best_idx];
    }
    
    std::vector<void*> radiusSearch(const Point& query, double radius) {
        rebuildIfNeeded();
        std::vector<void*> results;
        searchRadiusRecursive(root_, query, radius, results);
        return results;
    }
    
private:
    void rebuildIfNeeded() {
        if (dirty_) {
            buildTree();  // O(N log N) 构建
            dirty_ = false;
        }
    }
    bool dirty_ = true;
    std::vector<Point> points_;
    std::vector<void*> data_;
};

A.3 连续碰撞检测 (CCD) 工具

// 线段 - 圆形障碍物连续碰撞检测
bool segmentCircleCCD(const Point& p1, const Point& p2, 
                      const Circle& obs, double robot_radius) {
    // 参数化线段: P(t) = p1 + t*(p2-p1), t∈[0,1]
    Vec2 d = p2 - p1;
    Vec2 f = p1 - obs.center;
    
    // 二次方程: ||P(t) - obs.center||² = (robot_radius)²
    double a = d.dot(d);
    double b = 2 * f.dot(d);
    double c = f.dot(f) - obs.radius * obs.radius;
    
    double discriminant = b*b - 4*a*c;
    if (discriminant < 0) return false;  // 无实根,不相交
    
    double t1 = (-b - sqrt(discriminant)) / (2*a);
    double t2 = (-b + sqrt(discriminant)) / (2*a);
    
    // 检查交点是否在线段参数范围内
    return (t1 >= 0 && t1 <= 1) || (t2 >= 0 && t2 <= 1);
}

// 状态插值工具(用于运动学约束的连续检测)
State lerpState(const State& s1, const State& s2, double t) {
    return {
        s1.x + t * (s2.x - s1.x),
        s1.y + t * (s2.y - s1.y),
        normalizeAngle(s1.theta + t * normalizeAngle(s2.theta - s1.theta)),
        INFINITY, nullptr
    };
}

三层验证体系

  1. 仿真验证层(Gazebo / Isaac Sim)
    • 注入传感器噪声、执行器延迟、通信丢包
    • 测试边界场景:窄通道、动态障碍突袭、目标突变
    • 使用 ros2 bag 录制数据,复现偶发问题
  2. 半实物层(ROS 2 + 真实控制器)
    • 验证通信延迟对规划频率的影响
    • 测试 fallback 机制:主规划器超时时的降级策略
    • 检查内存占用、CPU 负载、实时性抖动
  3. 实车压力测试
    • 长时间运行内存泄漏检测(valgrind/ASan)
    • 极端场景:光照变化/地面打滑/通信丢包/传感器遮挡
    • A/B 测试:不同参数组合的通过率、舒适度、效率对比

调试工具链

工具 用途 集成方式
rviz2 + marker 可视化规划过程、代价热力图、候选轨迹 ROS 2 publisher
plotjuggler 实时绘制速度、曲率、代价值等时序曲线 ROS 2 topic 订阅
tracy / perf 性能剖析,定位热点函数与阻塞点 C++ 埋点 + 系统采样
optuna + ros2param 自动参数搜索,找到最优代价权重组合 Python 脚本 + 参数服务
valgrind / ASan 内存泄漏与越界检测,保障长期稳定性 编译时开启 + 测试脚本

总结与建议
“算法决定可能性,工程决定可用性”
教学代码关注正确性,工业代码关注鲁棒性
不要过早优化,但必须预留性能监控埋点
所有"理论上可行"的算法,都需经过噪声、延迟、故障三重考验才能上线
先用 Nav2 / MoveIt 2 跑通 baseline,理解工业框架设计
针对业务痛点(如窄通道、动态障碍)定制代价函数或采样策略
建立自动化测试集,覆盖边界场景,避免回归问题