机器人路径规划算法实践总结 (Path Planning Practical Guide)
March 2021
目录
基于搜索的路径规划 (Search-Based)
基于采样的路径规划 (Sampling-Based)
局部规划与轨迹优化 (Local & Trajectory)
理论实现 vs 工业级代码库
工程选型矩阵与实战避坑指南
附录:核心代码工具库
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 Sampling 、Bridge 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
};
}
三层验证体系
仿真验证层 (Gazebo / Isaac Sim)
注入传感器噪声、执行器延迟、通信丢包
测试边界场景:窄通道、动态障碍突袭、目标突变
使用 ros2 bag 录制数据,复现偶发问题
半实物层 (ROS 2 + 真实控制器)
验证通信延迟对规划频率的影响
测试 fallback 机制:主规划器超时时的降级策略
检查内存占用、CPU 负载、实时性抖动
实车压力测试
长时间运行内存泄漏检测(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,理解工业框架设计
针对业务痛点(如窄通道、动态障碍)定制代价函数或采样策略
建立自动化测试集,覆盖边界场景,避免回归问题