全局路径规划的核心任务,就是让移动机器人从起点A安全高效地到达终点B。听起来简单,但实际操作中会遇到几个关键难题:
环境建模:真实世界是连续的,但计算机需要离散化处理。栅格地图(Grid Map)是最常用的环境表示方法,把空间划分为均匀的单元格,每个格子标记为障碍物或可通行区域。这种方法的优势是直观且计算简单,但分辨率选择很关键——太粗糙会丢失细节,太精细又会增加计算负担。
路径质量评价:什么样的路径算"好"?通常考虑三个指标:
实时性要求:在动态环境中,规划算法必须在有限时间内给出解,这对计算复杂度提出了硬性约束。一个在理论上完美但计算耗时的算法,可能还不如一个次优但快速的方案实用。
提示:栅格地图中,单元格大小通常取机器人半径的1.5-2倍。例如对于直径30cm的机器人,使用15-20cm的栅格既能保证安全余量,又不会过度增加计算量。
Dijkstra是确定性算法的基石,保证找到最短路径。其核心思想是从起点开始,逐步向外"扩散",直到触及终点。以下是C++实现的关键部分:
cpp复制struct Node {
int x, y;
float cost;
bool operator>(const Node& other) const {
return cost > other.cost;
}
};
void dijkstra(const GridMap& map, Node start, Node goal) {
priority_queue<Node, vector<Node>, greater<Node>> pq;
vector<vector<float>> cost_so_far(map.height, vector<float>(map.width, INFINITY));
pq.push(start);
cost_so_far[start.y][start.x] = 0;
while (!pq.empty()) {
Node current = pq.top();
pq.pop();
if (current.x == goal.x && current.y == goal.y) break;
for (const auto& [dx, dy] : directions) {
Node next = {current.x + dx, current.y + dy};
if (!map.isValid(next.x, next.y)) continue;
float new_cost = cost_so_far[current.y][current.x] + map.getCost(next.x, next.y);
if (new_cost < cost_so_far[next.y][next.x]) {
cost_so_far[next.y][next.x] = new_cost;
pq.push({next.x, next.y, new_cost});
came_from[next.y][next.x] = {current.x, current.y};
}
}
}
}
优化点:
A*在Dijkstra基础上加入启发式函数h(n),引导搜索方向。关键实现细节:
cpp复制float heuristic(Node a, Node b) {
// 曼哈顿距离适用于栅格环境
return abs(a.x - b.x) + abs(a.y - b.y);
}
void a_star(const GridMap& map, Node start, Node goal) {
// ...初始化与Dijkstra类似...
while (!pq.empty()) {
Node current = pq.top();
pq.pop();
if (current.x == goal.x && current.y == goal.y) break;
for (const auto& [dx, dy] : directions) {
Node next = {current.x + dx, current.y + dy};
if (!map.isValid(next.x, next.y)) continue;
float new_cost = cost_so_far[current.y][current.x] + map.getCost(next.x, next.y);
if (new_cost < cost_so_far[next.y][next.x]) {
cost_so_far[next.y][next.x] = new_cost;
float priority = new_cost + heuristic(next, goal); // A*的核心区别
pq.push({next.x, next.y, priority});
came_from[next.y][next.x] = {current.x, current.y};
}
}
}
}
启发式函数选择原则:
针对栅格地图的特性优化,跳过大量对称路径。核心思想是识别"跳跃点"——路径中必须经过的关键转折点:
cpp复制vector<Node> find_successors(Node p, const GridMap& map) {
vector<Node> successors;
for (const auto& [dx, dy] : directions) {
Node next = jump(p, dx, dy, map);
if (next.x != -1) successors.push_back(next);
}
return successors;
}
Node jump(Node current, int dx, int dy, const GridMap& map) {
Node next = {current.x + dx, current.y + dy};
if (!map.isValid(next.x, next.y)) return {-1, -1};
if (next.x == goal.x && next.y == goal.y) return next;
// 强制邻居检查
if (has_forced_neighbor(next, dx, dy, map)) return next;
// 对角线移动的特殊处理
if (dx != 0 && dy != 0) {
if (jump(next, dx, 0, map).x != -1 || jump(next, 0, dy, map).x != -1) {
return next;
}
}
return jump(next, dx, dy, map);
}
适用场景:
当处理超大地图时,可以采用分层策略:
cpp复制class HierarchicalPlanner {
public:
Path plan(const Pose& start, const Pose& goal) {
// 顶层规划
auto coarse_path = topo_planner.plan(start.region, goal.region);
// 逐段细化
Path final_path;
for (int i = 0; i < coarse_path.size()-1; ++i) {
auto segment = grid_planner.plan(coarse_path[i].entry, coarse_path[i+1].exit);
final_path.insert(final_path.end(), segment.begin(), segment.end());
}
return final_path;
}
private:
TopologicalPlanner topo_planner;
GridPlanner grid_planner;
};
对于二值地图(仅有障碍/自由两种状态),可以用bitset大幅减少内存占用:
cpp复制class CompactGridMap {
public:
bool isObstacle(int x, int y) const {
return bitset[y * width + x];
}
void setObstacle(int x, int y, bool value) {
bitset[y * width + x] = value;
}
private:
vector<bool> bitset; // 或使用更紧凑的位操作
int width, height;
};
利用现代CPU多核特性并行计算:
cpp复制void parallel_a_star(const GridMap& map, Node start, Node goal) {
vector<thread> workers;
vector<Path> partial_results(num_threads);
auto worker_func = [&](int thread_id) {
// 每个线程处理地图的一个区域
auto local_goal = adjust_goal_for_thread(goal, thread_id);
partial_results[thread_id] = a_star(map, start, local_goal);
};
for (int i = 0; i < num_threads; ++i) {
workers.emplace_back(worker_func, i);
}
// ...合并部分结果...
}
优先级队列的键选择:
浮点数比较陷阱:
cpp复制// 危险!
if (new_cost < cost_so_far[next.y][next.x])
// 安全做法
if (new_cost + epsilon < cost_so_far[next.y][next.x])
地图边界处理遗漏:
在1000x1000栅格上的测试结果(i7-11800H, 单线程):
| 算法 | 平均时间(ms) | 路径长度 | 扩展节点数 |
|---|---|---|---|
| Dijkstra | 1250 | 最优 | 980,000 |
| A*(曼哈顿) | 420 | 最优 | 320,000 |
| A*(欧几里得) | 380 | 最优 | 290,000 |
| JPS | 85 | 最优 | 45,000 |
原始算法生成的路径往往存在锯齿,可通过以下方法优化:
拉直(String Pulling):
cpp复制Path smooth_path(const Path& original) {
Path smoothed = {original.front()};
int anchor = 0;
for (int i = 2; i < original.size(); ++i) {
if (lineOfSight(smoothed.back(), original[i])) continue;
smoothed.push_back(original[i-1]);
}
smoothed.push_back(original.back());
return smoothed;
}
B样条平滑:
cpp复制BSpline create_spline(const Path& path) {
vector<Point> control_points = extract_key_points(path);
return BSpline(control_points, 3); // 三次样条
}
通过动态调整启发式权重,平衡最优性和速度:
cpp复制float adaptive_weight(Node current, Node goal) {
float progress = heuristic(start, current) / heuristic(start, goal);
return 1.0 + 4.0 * progress; // 随接近目标逐渐降低权重
}
允许任意角度移动,减少不必要的转折:
cpp复制void theta_star_update(Node current, Node neighbor) {
if (lineOfSight(parent[current], neighbor)) {
// 尝试绕过障碍物的更直接路径
float new_cost = cost_so_far[parent[current].y][parent[current].x] +
lineCost(parent[current], neighbor);
if (new_cost < cost_so_far[neighbor.y][neighbor.x]) {
// ...更新路径...
}
} else {
// 标准A*更新
}
}
为轮式/履带机器人设计的变种,考虑转向半径等限制:
cpp复制struct HybridNode {
Pose pose; // 包含位置和朝向
float g, h;
vector<Pose> path_segment;
};
vector<HybridNode> get_motion_primitives(const HybridNode& current) {
vector<HybridNode> primitives;
for (float angle : {-30deg, 0deg, 30deg}) {
for (float dist : {0.5m, 1.0m}) {
auto path = simulate_steering(current.pose, angle, dist);
if (!collides(path)) {
primitives.push_back({path.back(),
current.g + path_length(path),
heuristic(path.back(), goal),
path});
}
}
}
return primitives;
}
将规划器封装为ROS插件:
cpp复制class MyGlobalPlanner : public nav_core::BaseGlobalPlanner {
public:
void initialize(std::string name, costmap_2d::Costmap2DROS* costmap_ros) override {
costmap_ = costmap_ros->getCostmap();
// 初始化地图转换等
}
bool makePlan(const geometry_msgs::PoseStamped& start,
const geometry_msgs::PoseStamped& goal,
std::vector<geometry_msgs::PoseStamped>& plan) override
{
GridMap grid = convertCostmap(*costmap_);
Node start_node = convertPose(start);
Node goal_node = convertPose(goal);
auto path = a_star(grid, start_node, goal_node);
plan = smooth_path(convertPath(path));
return !plan.empty();
}
};
// 注册插件
PLUGINLIB_EXPORT_CLASS(MyGlobalPlanner, nav_core::BaseGlobalPlanner)
应对动态障碍物的典型架构:
cpp复制class Replanner {
public:
void updateObstacles(const ObstacleMap& obstacles) {
grid_.update(obstacles);
if (need_replan()) {
std::lock_guard<std::mutex> lock(plan_mutex_);
current_plan_ = planner_.plan(current_pose_, goal_, grid_);
}
}
Path getCurrentPlan() const {
std::lock_guard<std::mutex> lock(plan_mutex_);
return current_plan_;
}
private:
mutable std::mutex plan_mutex_;
GridMap grid_;
Path current_plan_;
Planner planner_;
};
xml复制<node pkg="rviz" type="rviz" name="rviz" args="-d $(find my_planner)/config/planner.rviz" />
<!-- planner.rviz 片段 -->
<Global Options>
<Fixed Frame>map</Fixed Frame>
</Global Options>
<Displays>
<Grid name="Costmap" topic="/global_costmap/costmap" />
<Path name="Global Plan" topic="/global_plan" color="0;1;0" />
<MarkerArray name="Search Tree" topic="/planner/search_tree" />
</Displays>
使用gperftools进行CPU分析:
bash复制# 安装
sudo apt install google-perftools libgoogle-perftools-dev
# 运行分析
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libprofiler.so \
CPUPROFILE=planner.prof ./planner_node
# 查看结果
pprof --web ./planner_node planner.prof
使用GTest验证算法正确性:
cpp复制TEST(AStarTest, FindsPathInSimpleMap) {
GridMap map(10, 10);
map.setObstacle(5, 0, 5, 9); // 垂直障碍墙
auto path = AStar().plan({0, 0}, {9, 9}, map);
ASSERT_FALSE(path.empty());
EXPECT_NEAR(path_length(path), 18.0, 0.1);
}
TEST(JumpPointSearchTest, HandlesDeadEnds) {
// 构建死胡同地图
// 验证是否能正确返回无解
}
使用CUDA并行计算启发式值:
cpp复制__global__ void compute_heuristics(Node* nodes, Node goal, int count) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < count) {
nodes[idx].h = abs(nodes[idx].x - goal.x) + abs(nodes[idx].y - goal.y);
}
}
void precompute_heuristics(vector<Node>& open_set, Node goal) {
Node* d_nodes;
cudaMalloc(&d_nodes, open_set.size() * sizeof(Node));
cudaMemcpy(d_nodes, open_set.data(), open_set.size() * sizeof(Node), cudaMemcpyHostToDevice);
int threads = 256;
int blocks = (open_set.size() + threads - 1) / threads;
compute_heuristics<<<blocks, threads>>>(d_nodes, goal, open_set.size());
cudaMemcpy(open_set.data(), d_nodes, open_set.size() * sizeof(Node), cudaMemcpyDeviceToHost);
cudaFree(d_nodes);
}
使用HLS(高层次综合)实现优先级队列:
cpp复制#pragma HLS pipeline II=1
void update_queue(Node new_node, Node queue[MAX_QUEUE], int& queue_size) {
// 硬件优化的插入排序
int i = queue_size - 1;
while (i >= 0 && queue[i].cost > new_node.cost) {
queue[i+1] = queue[i];
i--;
}
queue[i+1] = new_node;
queue_size++;
}
地图一致性检查:
异常处理策略:
cpp复制try {
path = planner.plan(current_pose, goal);
} catch (const NoPathException& e) {
// 尝试放宽约束条件
adjustConstraints();
path = planner.plan(current_pose, goal);
}
能耗管理:
使用神经网络预测启发式函数:
cpp复制class NeuralHeuristic {
public:
float predict(const Node& node, const Node& goal) {
tensorflow::Tensor input(tensorflow::DT_FLOAT, {1, 4});
auto input_map = input.tensor<float, 2>();
input_map(0, 0) = node.x; input_map(0, 1) = node.y;
input_map(0, 2) = goal.x; input_map(0, 3) = goal.y;
std::vector<tensorflow::Tensor> outputs;
session->Run({{"input", input}}, {"output"}, {}, &outputs);
return outputs[0].scalar<float>()();
}
};
冲突-Based搜索(CBS)算法框架:
cpp复制class CBS {
public:
Solution findSolution(const vector<Agent>& agents) {
while (!open_list.empty()) {
auto node = open_list.pop();
auto conflict = findFirstConflict(node.solution);
if (!conflict) return node.solution;
for (const auto& constraint : generateConstraints(conflict)) {
auto new_node = node;
new_node.constraints.add(constraint);
replanAgent(new_node, constraint.agent_id);
open_list.push(new_node);
}
}
return {}; // 无解
}
};
概率路线图(PRM)在动态环境中的变种:
cpp复制class DynamicPRM {
void adaptToChanges(const ChangeSet& changes) {
for (const auto& change : changes) {
if (change.type == OBSTACLE_ADDED) {
removeNodesNear(change.position, change.radius);
}
// 其他变化类型处理...
}
addRandomNodes(1000); // 补充新节点
}
};