社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Git

【机器人轨迹优化算法】高动态环境下的轨迹规划Local-Segment-AStar-Replanner算法【Github开源】【ROS2】【C++】

机器人规划与控制研究所 • 2 周前 • 79 次点击  
.cpp机器人规划与控制研究所 ——机器人/自动驾驶规划与控制方向综合、全面、专业的平台。4万人订阅的微信大号。点击标题下蓝字“机器人规划与控制研究所”关注,我们将为您提供有价值、有深度的延伸阅读。



作者序


今天是2026年1月18日,又到星期天了。最近业余时间在研究集群机器人自主绕障技术,目前尚未取得突破。

因为上周分享了这篇文章【机器人轨迹优化基础】从原理到实现:支持动态膨胀的机器人A*局部路径规划开源算法【Github开源】【ROS2】【C++】。A*算法生成的规划路线是这样的:


我觉得不太合理,因为如果是人的话,会直沿着直线走,遇到障碍物再去绕开它,我想着应该是下图这样:


应该紧紧贴着障碍物去绕障,这其实与浙大开源的ego-planner算法有相似之处,我想到了我2020年在哈工大读硕士时发表的一篇改进A*算法的论文(Path Planning Using an Improved A-star Algorithm ),就使用了这个思路,我今天下午对其做了c++代码实现,上图也是本次要介绍的Local-Segment-AStar-Replanner开源的算法规划的路线,开源链接,请访问我的github仓库:

https://github.com/JackJu-HIT/Local-Segment-AStar-Replanner/tree/master

算法提出了一种改进的 A* 逻辑,专注于在复杂环境下通过分段重规划实现高效避障,有如下优势:

  • 精细化分段处理:不同A* 盲目地重新进行全局寻路,本算法仅锁定受障碍物影响的“碰撞段”进行手术式修复,极大保留了原始路径的意图。

  • 卓越的参考线保持能力:算法原生具备“趋线”特性,强制要求绕障段在越过障碍后以最短代价回归原始参考轨迹,有效避免了A*寻路算法在大空间下容易大幅偏离预设轨道的现象。

  • 极致的计算实时性:由于搜索空间被限制在局部的障碍物交段区域,算法的计算量不再随全局地图的增大而指数级上升,能够以极高的频率响应动态环境变化。

  • 智能“缝合”逻辑:通过记录轨迹点索引并进行端点对齐,算法将 A* 生成的局部绕障曲线与全局参考直线无缝拼接,确保了整条路径在几何上的连续性与控制上的平稳性。

  • 鲁棒的收敛特性:本算法特别强化了避障后的“回归速度”,确保机器人在执行绕障任务后,能以最高效率收敛回参考轨道,适用于巡检、固定线运输等强参考线约束场景。

本算法核心的实现思路:

(1)首先,通过连接起点与终点生成原始直线参考轨迹,并将其高精度离散化为密集的路径点序列。

(2)其次,系统自动扫描离散轨迹,识别出所有与障碍物相交的冲突区间并记录其索引。

(3)随后,执行合并优化逻辑,将物理距离邻近的多个障碍段整合为统一的重规划区域。

(4)最后,针对每个障碍区间调用A*算法搜索局部绕障路径,并将其与原始轨迹中的安全段进行无缝缝合,从而生成既能有效避障又最大限度贴合原始参考线的最终导航路径。

下午一点开始编写这个算法的c++实现,截止目前刚完成github上传。那么,为什么要尽量回到原来参考轨迹上?比如说,在一些清扫洗地机器人场景下,提前对整个清扫区域生成了全覆盖路径,如果遇到障碍物你就绕大圈,那么你的清扫面积就变小了,清扫效率和效果就降低了。另一个我想的是,对于室内机器人仅仅用Local-Segment-AStar-Replanner+TEB或者Local-Segment-AStar-Replanner+MPC就能cover住室内移动机器人场景。之前我做的开源项目Fast-Planner-2D-ROS2和Ego-Planner-2D-ROS2其实是主要面向户外无人车的,规划路线较长,需要几十米的,而且A*不考虑车体轮廓规划的线不合理,混合A*考虑了轮廓但速度太慢,不能满足实时性要求,因此可以考虑这两个算法。


这是伪代码的实现,实际工程落地会与此有些出入,但核心算法一致,代码实现更加侧重工程化,这是为了能在实际项目中使用,我开源的这个项目也是:

https://github.com/JackJu-HIT/Local-Segment-AStar-Replanner/tree/master

内部纯c++实现,核心算法在manager.cpp文件里,可以很容易移植到自己的项目中去。

最近也是高产,写了好几个开源c++轨迹优化项目,可能是要对2025告别了,未来主题将不再是单机自主绕障技术了,而是集群自主绕障技术。

今天也是一直编写代码到现在,中间也没休息,中午和晚上也没吃饭,有点累,不说了。毕竟:

Talking Is Cheap,Show Me Your  Code!

下面开始正文了。

2026年1月18日 18:29:01

Jack  Ju 于沈阳




1软件框架


local_segment_astar_replanner   ├── CMakeLists.txt   ├── GridMap2D      ├── CMakeLists.txt      ├── include      └── src   ├── include      └── trajectory_obstacles_publisher.h   ├── LICENSE   ├── manager      ├── CMakeLists.txt      ├── include      └── src   ├── package.xml   ├── path_searching      ├── CMakeLists.txt      ├── include      ├── package.xml      └── src   └── src       └── trajectory_publisher.cpp


这个框架是一个典型的高度模块化、层次分明的 ROS 2 机器人规划系统。它采用了“核心算法-逻辑管理-ROS 2 封装”三层架构。

以下是对该框架结构的详细分析:

1. 层次化架构设计

该框架主要分为四个核心模块,每个模块各司其职:

A. 逻辑调度层:manager/

  • 地位:系统的“大脑”。
  • 功能:实现了你之前提到的“分段、检测、合并、缝合”核心逻辑。它不直接处理 ROS 话题,而是负责协调地图数据和搜索算法。
  • 核心类manager。它封装了 makePlan 函数,将复杂的局部重规划流程高度抽象化。

B. 环境感知层:GridMap2D/

  • 地位:数据基础。
  • 功能:负责维护环境的地图信息。提供欧几里得距离场(EDT)、膨胀(Inflation)以及占据格网查询。
  • 关键点:它为 manager 提供了 check_collision 的底层支持,决定了避障的精度和安全性。

C. 算法实现层:path_searching/

  • 地位:工具箱。
  • 功能:纯粹的路径搜索算法实现(A* 或其变体)。它与 ROS 解耦,只负责在给定的代价地图中寻找从起点到终点的最优路径。
  • 复用性:这种独立设计使得该算法可以轻松替换为 D* Lite、RRT* 等其他算法而不需要改动其他模块。

D. ROS 2 通信封装层:src/ & include/ (顶级目录)

  • 地位:对外的窗口。
  • 功能
    • 节点包装:在 trajectory_publisher.cpp 中定义 ROS 2 Node。
    • 接口转换:将 ROS 2 的消息(如  PoseStampedOccupancyGrid)转换为内部模块能识别的 PathPoint 或 Eigen 数据。
    • 可视化:发布可视化话题(如 nav_msgs/Path)供 RViz 观察。

2. 模块间的调用关系

系统的运行逻辑是从外向内调用,数据从内向外传递

  1. 输入端 trajectory_publisher.cpp 接收到目标点话题。
  2. 触发规划trajectory_publisher 调用 manager::makePlan
  3. 环境查询manager 通过 GridMap2D 检测全局直线上的障碍物交点。
  4. 算法计算:对于每个障碍区间,manager 调用  path_searching 计算绕障曲线。
  5. 结果返回manager 完成缝合后返回完整轨迹给 trajectory_publisher
  6. 输出端trajectory_publisher 将轨迹发布到 ROS 话题。

3. 该框架的优点

  • 高内聚低耦合:地图模块、搜索模块和逻辑管理模块相互独立。如果你以后想把 2D 扩展到 3D,只需要更换 GridMap2D 模块,而  manager 的分段缝合逻辑基本不需要大改。
  • 标准 CMake 结构:通过子目录(add_subdirectory)管理多个组件,符合大型工程标准,易于维护和扩展。
  • 易于集成:核心业务逻辑在 manager 中,不依赖于特定的 ROS 消息格式,可以很方便地移植到不同的机器人平台或中间件中。



2 核心Local-Segment-AStar-Replanner


在作者序里提到了,Local-Segment-Astar-Replanner核心算法都在manager.cpp文件里,下面是我封装的纯C++调用接口,规划算法只要弄懂如何调用即可轻松部署到项目里。

/** *@file       manager.h * @brief     Local-Segment-AStar-Replanner manager class  * @author    juchunyu@qq.com * @date      2026-01-18 13:31:01 * @copyright Copyright (c) 2026, Institute of Robotics Planning and Control (irpc). All rights reserved. */#pragma once#include #include "plan_env/grid_map.h"


    
#include "dyn_a_star.h"struct PathPoint{    float x;    float y;    float z;    float v;};struct ObstacleInfo{    float x;    float y;    float z;};struct intersectionPoints{    PathPoint start;    PathPoint goal;    int startIndex;    int goalIndex;};class manager{    private:        std::shared_ptr map_;        PathPoint start_;        PathPoint goal_;        std::vector global_plan_traj_;        std::vector final_path_;        std::vector intersection_points_;        std::shared_ptr planner_;        float merge_threshold_ = 2.0// 合并阈值,单位米    private:        void discretize_trajectory(const std::vector& original_trajectory,                                    std::vector& discrete_trajectory,                                    double interval);        void calculate_intersection_points();        bool check_collision(const PathPoint& p);        void merge_adjacent_segments();        void replan_and_combine_path();        bool search_astar(const PathPoint& start, const PathPoint& goal, std::vector& result);        double distance(const PathPoint& p1, const PathPoint& p2);    public:        manager();        ~manager();        void init(std::shared_ptr map ,float merge_threshold);        void makePlan(PathPoint start, PathPoint goal, std::vector &planned_traj, bool &success);        void getOrginalAstarPath(std::vector &path);
};

其实挺清晰的,这里我再对manager.cpp源代码分析下。

//@file manager.cpp#include "manager.h"manager::manager(){}manager::~manager(){}void manager::init(std::shared_ptr map ,float merge_threshold){    map_ = map;    merge_threshold_ = merge_threshold;    planner_ = std::make_shared();     Eigen::Vector2i map_size(200,200);    planner_->initGridMap(map_,map_size); }/** * 生成路径规划 * @param start 起点 * @param goal 终点 * @param planned_traj 输出的规划路径 * @param success 规划是否成功 */void manager::makePlan(PathPoint start, PathPoint goal, std::vector &planned_traj, bool &success){    start_ = start;    goal_  = goal;    std::cout   << start_.x     std::cout   << goal_.x     //1.连接起点和终点,并离散成路径点    std::vector global_plan_traj_temp;    global_plan_traj_temp.push_back(start_);    global_plan_traj_temp.push_back(goal_);    discretize_trajectory(global_plan_traj_temp, global_plan_traj_, 0.1);    std::cout  << global_plan_traj_.size() << std::endl;    //2.确定交接点    calculate_intersection_points();    //3.检测交接点是否有合并的必要行性    merge_adjacent_segments();    //4.开始针对于每段进行A*搜索并组合路径    replan_and_combine_path();    planned_traj = std::move(final_path_);    std::cout  << planned_traj.size() << std::endl;     std::cout  << intersection_points_.size() << std::endl;    // for(int i = 0 ; i < planned_traj.size(); i++)    // {    //     std::cout << "[manager] planned_traj[" << i << "]: " << planned_traj[i].x << ", " << planned_traj[i].y << std::endl;    // }       if(planned_traj.size() > 0)        success = true;    else        success = false;}/** * 获取原始A*路径 */void manager::getOrginalAstarPath(std::vector &path){    path = global_plan_traj_;}/** * 计算路径与障碍物的交点 * 将交点存储在 intersection_points_ 成员变量中 */void manager::calculate_intersection_points(){    intersection_points_.clear();    if (global_plan_traj_.empty()) return;    bool is_inside_obstacle = false;    intersectionPoints current_segment;
    if  (global_plan_traj_.size() 7    {        std::cout  << std::endl;        return;    }    for (int i = 3; i < (int)global_plan_traj_.size() - 3; ++i)    {        const auto& current_point = global_plan_traj_[i];        bool collision = check_collision(current_point); // 你的碰撞检测逻辑        if (!is_inside_obstacle && collision)        {            // 进入障碍物:记录起点坐标和索引            is_inside_obstacle = true;            current_segment.start = global_plan_traj_[i - 3];            current_segment.startIndex = i - 3;        }        else if (is_inside_obstacle && !collision)        {            // 离开障碍物:记录上一个点为终点坐标和索引            is_inside_obstacle = false;            current_segment.goal = global_plan_traj_[i + 3];            current_segment.goalIndex = i + 3;            intersection_points_.push_back(current_segment);        }    }    // 处理轨迹末尾仍在障碍物内的情况    if (is_inside_obstacle)    {        return;        current_segment.goal = global_plan_traj_.back();        current_segment.goalIndex = (int)global_plan_traj_.size() - 1;        intersection_points_.push_back(current_segment);    }}/** * @brief 合并相邻障碍物段 * @param merge_dist_threshold— 距离阈值(两段之间的缝隙距离) */void manager::merge_adjacent_segments(){    if (intersection_points_.size() 2return;    std::vector merged_results;
    // 初始化:取第一段作为当前待处理段    intersectionPoints current_proc = intersection_points_[0];    for (size_t i = 1; i < intersection_points_.size(); ++i)    {        const auto& next_seg = intersection_points_[i];        // 计算当前段终点与下一段起点之间的欧氏距离        float dx = current_proc.goal.x - next_seg.start.x;        float dy = current_proc.goal.y - next_seg.start.y;        float dz = current_proc.goal.z - next_seg.start.z;        float gap_dist = std::sqrt(dx*dx + dy*dy + dz*dz);        if (gap_dist <= merge_threshold_)        {            // 满足合并条件:            // 1. 起点(start)和起点索引(startIndex)保持不变            // 2. 终点(goal)和终点索引(goalIndex)更新为下一段的值            current_proc.goal = next_seg.goal;            current_proc.goalIndex = next_seg.goalIndex;
            // 注:此时不 push_back,因为下一段可能还能和再下一段合并        }        else        {            // 不满足合并条件:            // 保存当前已完成合并的段            merged_results.push_back(current_proc);            // 将下一段作为新的待处理段            current_proc = next_seg;        }    }    // 压入最后处理的一段    merged_results.push_back(current_proc);    // 更新成员变量    intersection_points_ = std::move(merged_results);
}/** * @brief 执行避障路径重规划并组合最终路径 */void manager::replan_and_combine_path(){    // 最终生成的完整路径    final_path_.clear();
    // 记录原始轨迹中上一次处理到的索引位置    int last_processed_idx = 0;    // 遍历所有识别出的障碍物段    for (const auto& segment : intersection_points_)    {        // 第一步:添加原始轨迹中 [上一个终点, 当前障碍物起点) 之间的安全点        for (int i = last_processed_idx; i < segment.startIndex; ++i)        {             final_path_.push_back(global_plan_traj_[i]);        }        // 第二步:针对当前障碍物段,调用 A* 算法进行绕障搜索        std::vector astar_segment;        bool success = search_astar(segment.start, segment.goal, astar_segment);        if (success)        {            // 如果 A* 寻路成功,将绕障路径点加入最终路径            // 注意:astar_segment 的起点和终点通常就是 segment.start 和 segment.goal            final_path_.insert(final_path_.end(), astar_segment.begin(), astar_segment.end());        }        else        {            // 如果 A* 失败,说明该障碍物无法绕过            // 这里可以采取策略:比如停止、或者仍然保留原路径(会撞)并报警            std::cout                        << segment.startIndex  << segment.goalIndex << std::endl;        }        // 更新索引:下一次从障碍物的 goalIndex 之后开始接原始点        last_processed_idx = segment.goalIndex + 1;    }    // 第三步:添加最后一个障碍物之后到轨迹末尾的所有点    for (int i = last_processed_idx; i < (int)global_plan_traj_.size(); ++i)    {        final_path_.push_back(global_plan_traj_[i]);    }}/** * @brief A* 搜索实现 * @param start 起点 * @param goal 终点 * @param[out] result 避障路径结果 * @return 是否成功找到路径 */bool manager::search_astar(const PathPoint& start, const PathPoint& goal, std::vector& result){    result.clear();    Eigen::Vector2d start_pt(start.x, start.y);      Eigen::Vector2d end_pt(goal.x,goal.y);    bool success = planner_->AstarSearch(0.1, start_pt, end_pt);  // 步长=分辨率    if (success)     {        result.clear();        std::vector<:vector2d> path = planner_->getPath();  // 注意:getPath需改为返回Vector2d        std::cout ;        for (const auto& pt : path)         {            PathPoint temp;            temp.x = pt.x() ;            temp.y = pt.y();            temp.z = 0;            result.push_back(temp);            // std::cout << "(" << pt.x() << ", " << pt.y() << ")\n";        }        return true;    }     else    {        std::cout ;        return false;    }}/** * 检查路径点是否与障碍物碰撞 * @param p 路径点 * @return 是否碰撞 */bool manager::check_collision(const PathPoint& p){    return map_->getInflateOccupancy(Eigen::Vector2d(p.x, p.y));}// 计算两点之间的欧氏距离(单位:米)double manager::distance(const PathPoint& p1, const PathPoint& p2) {    double dx = p2.x - p1.x;    double dy = p2.y - p1.y;    return std::sqrt(dx*dx + dy*dy);}/** * 将轨迹离散为均匀间隔的点(间隔10cm) * @param original_trajectory 原始轨迹(由多个顶点组成的折线)  * @param discrete_trajectory 输出的离散轨迹 * @param interval 间隔距离(单位:米,默认0.1米即10cm) */void manager::discretize_trajectory(const std::vector& original_trajectory,                                                            std::vector& discrete_trajectory,                                                            double interval) {    if (original_trajectory.size() 2) {        std::cerr  << std::endl;        return;    }    discrete_trajectory.clear();    // 添加轨迹起点    // discrete_trajectory.push_back(cur_pose_);    discrete_trajectory.push_back(original_trajectory[0]);    // 遍历原始轨迹的每一段线段    for (size_t i = 0; i < original_trajectory.size() - 1; ++i) {        const PathPoint& start = original_trajectory[i];        const PathPoint& end = original_trajectory[i+1];        double seg_length = distance(start, end);  // 线段总长度        if (seg_length 1e-6) {  // 跳过长度接近0的线段(避免除零)            continue;        }        // 计算当前线段需要插入的点数(不含起点,含终点)        int num_points = static_cast<int>(seg_length / interval);        // 最后一个点到终点的距离(避免累积误差)        double last_interval = seg_length - num_points * interval;        // 生成线段上的离散点        for (int j = 1; j <= num_points; ++j) {            double ratio;            if (j < num_points) {                // 前num_points-1个点:按均匀间隔计算                ratio = (j * interval) / seg_length;            } else {                // 最后一个点:直接对齐到线段终点(避免累积误差)                ratio = 1.0;            }            // 线性插值计算点坐标            PathPoint p;            p.x = start.x + ratio * (end.x - start.x);            p.y = start.y + ratio * (end.y - start.y);            discrete_trajectory.push_back(p);        }    }}


这份源代码实现了一个分段式路径重规划器。它的核心逻辑是:“只在有障碍物的地方动手术,没障碍物的地方走直线”

下面我按功能块对代码进行详细解释:

1. 初始化与主控流程

init 函数:系统装配

void manager::init(std::shared_ptr mapfloat merge_threshold) {
    map_ = map// 注入地图
    merge_threshold_ = merge_threshold; // 合并距离阈值
    planner_ = std::make_shared(); // 实例化底层A*算法
    Eigen::Vector2i map_size(200,200);
    planner_->initGridMap(map_, map_size); // 让A*知道地图边界
}
  • 解释:这是“装配”阶段。它将地图指针传递给管理器,并初始化 A* 规划器。

makePlan 函数:核心流水线

这是算法的顶层逻辑,它严格执行四个步骤:

  1. 离散化:把起点和终点的直线连线切成每段 10cm 的小点(discretize_trajectory)。
  2. 找交点:找出哪些小点撞到了障碍物(calculate_intersection_points)。
  3. 合并:如果两个障碍物离得很近,就把它们当成一个大障碍物处理(merge_adjacent_segments)。
  4. 缝合:对障碍区段跑 A,然后把 A 结果和原有的直线段拼在一起(replan_and_combine_path)。

2. 障碍物区间检测

calculate_intersection_points

这是最精妙的部分,它不仅找碰撞,还做了缓冲处理

for (int i = 3; i < (int)global_plan_traj_.size() - 3; ++i) {
    bool collision = check_collision(global_plan_traj_[i]);
    if (!is_inside_obstacle && collision) {
        // 发现障碍物起点
        is_inside_obstacle = true;
        current_segment.start = global_plan_traj_[i - 3]; // 向后多退3个点(30cm)
        current_segment.startIndex = i - 3;
    } elseif (is_inside_obstacle && !collision) {
        // 发现障碍物终点
        is_inside_obstacle = false;
        current_segment.goal = global_plan_traj_[i + 3]; // 向前多跨3个点(30cm)
        current_segment.goalIndex = i + 3;
        intersection_points_.push_back(current_segment);
    }
}
  • 源代码解释
    •  索引保护:当检测到 i 点碰撞时,它记录  i-3 为 A* 的起点。
    • 为什么要多退/多进 3 个点? 为了给 A* 留出“起跑空间”。如果 A* 的起点紧贴着障碍物,搜索算法往往会因为没有合法邻域而失败。这 30cm 的缓冲大大提高了避障成功率。

3. 区间合并(减少碎片化)

merge_adjacent_segments

if (gap_dist <= merge_threshold_) {
    // 距离太近,执行合并
    current_proc.goal = next_seg.goal;
    current_proc.goalIndex = next_seg.goalIndex;
}
  • 源代码解释:如果第一段障碍物的结尾到第二段障碍物的开始距离小于 merge_threshold_,则不结束第一段,而是直接把终点拉到第二段的结尾。这能防止机器人在两个很近的障碍物之间反复切换规划状态。

4. 路径缝合(核心拼接逻辑)

replan_and_combine_path

这是实现“无缝衔接”的关键。

int last_processed_idx = 0;
for (const auto& segment : intersection_points_) {
    // 1. 放入障碍物之前的安全直线段
    for (int i = last_processed_idx; i < segment.startIndex; ++i) {
        final_path_.push_back(global_plan_traj_[i]);
    }

    // 2. 放入 A* 搜索出来的绕障曲线段
    search_astar(segment.start, segment.goal, astar_segment);
    final_path_.insert(final_path_.end(), astar_segment.begin(), astar_segment.end());

    // 3. 更新游标,跳过刚刚处理过的障碍区
    last_processed_idx = segment.goalIndex + 1;
}
// 4. 放入最后一段安全段
for (int i = last_processed_idx; i < global_plan_traj_.size(); ++i) {
    final_path_.push_back(global_plan_traj_[i]);
}
  • 源代码解释
    • 使用 last_processed_idx 像缝纫机的针头一样走位。
    • 安全段 + A*段 + 安全段。通过索引(Index)精准控制,保证了路径点在内存中是连续的,没有重叠也没有遗漏。

5. 辅助算法:离散化与插值

discretize_trajectory

int num_points = static_cast<int>(seg_length / interval);
for (int j = 1; j <= num_points; ++j) {
    double ratio = (j == num_points) ? 1.0 : (j * interval) / seg_length;
    p.x = start.x + ratio * (end.x - start.x);
    // ...
    discrete_trajectory.push_back(p);
}
  • 源代码解释
    • 均匀采样:保证直线路径上每 10cm 就有一个探测点,确保不会因为间距太大而漏掉细小的障碍物。
    • 误差补偿:当 j == num_points 时强制 ratio = 1.0,确保离散后的终点绝对等于目标的 goal 位置。

总结:

  1. 索引驱动 (Index-Driven)**:利用 startIndex 和  goalIndex 快速切分路径,比通过坐标查找点要快得多,复杂度为 
  2. 空间安全裕度:在计算交点时主动外扩 3 个点,体现了对实际机器人动力学的理解(A* 需要一定距离来转弯)。
  3. 高度解耦manager 类只负责逻辑流程,底层的 A* 搜索和地图查询都是通过接口完成的。
  4. 缝合思想:完美解决了传统 A* 规划后“回不到原轨迹”的问题。它通过强制指定 A* 的终点为直线上的某个点,实现了路径的迅速收敛。




3 一些细节的讨论


首先有个比较重要的参数merge_threshold_:表示合并阈值,单位米。

void manager::init(std::shared_ptr map ,float merge_threshold){    map_ = map;    merge_threshold_ = merge_threshold;    planner_ = std::make_shared();     Eigen::Vector2i map_size(200,200);    planner_->initGridMap(map_,map_size); }

怎么去在实际机器人工程中使用这个参数,首先理解这个参数的含义,他表示两段障碍物距离有多近时,我们将它合并为一个起点和和终点。这里就有说法了。首先,你要对你的机器人了解透彻,如果两个障碍物之间只有2米距离,你要去哦确认你的机器人是否能走这个曲率较大的曲线,毕竟,最终的规划轨迹还是要靠控制去执行的,如果走不了,你就把这个阈值放大些。

我担心我没说清楚,借助ai再解释下。

在实际机器人工程中,merge_threshold_(合并阈值)绝不仅仅是一个简单的距离数值,它是路径规划算法机器人物理特性(动力学/运动学)之间的桥梁。

理解并配置好这个参数,是确保规划轨迹“可执行”的关键。以下是从工程实践角度对该参数的深度解析和使用指南:

1. 核心含义:防止“过早回归”导致的路径振荡

在你的算法中,如果不合并(Gap > Threshold),机器人会尝试在两个障碍物之间 “缝合”一段直线回归原始轨道

  • 如果阈值过小:机器人刚避开第一个障碍物,还没来得及完全回归稳态,就要立刻执行第二个避障动作。这会产生一个连续的“S”形大曲率路径。
  • 如果阈值合理:算法会将两个近距离障碍物连同它们之间的空隙视为一个“大障碍物”,规划一条顺滑的包络曲线一次性绕过。

2. 如何根据机器人特性确定阈值?

在配置 merge_threshold_ 时,你需要考量以下三个核心维度:

A. 最小转弯半径(Minimum Turning Radius)

这是最重要的物理指标。

  • 阿克曼转向/车式机器人:具有明确的最小转弯半径 。如果两个障碍物之间的距离小于 (粗略估算:两个转弯动作所需的距离),机器人根本无法在两段之间收敛回直线。
  • 工程建议:对于此类机器人,merge_threshold_ 通常应设置为机器人轴距的 3-5 倍,或不小于最小转弯半径的 2 倍。

B. 机器人的物理尺寸(宽与长)

  • 如果机器人体型巨大(如无人运输车),在狭窄的空隙中频繁切换姿态会导致车体扫掠区域(Swept Area)增大,反而更容易碰撞。
  • 工程建议:阈值至少应大于机器人的车身长度。

C. 运行速度与控制精度

  • 速度(Velocity):速度越高,控制器的响应窗口越窄。高速下频繁的曲率切换会导致严重的路径跟踪偏差(Tracking Error)甚至侧翻。
  • 控制稳定性:如果你的控制器(如 pure_pursuit 或 MPC)在处理 S 型弯道时有明显的滞后或振荡,就必须通过增大阈值来“牺牲”路径的长度,换取路径的“平滑度”。

3. 实际工程中的调参逻辑

你可以遵循以下步骤进行实车调试:

  1. 初始值设定

  • 高灵活性机器人(如差速/全向轮):可设小些,建议 1.0m - 2.0m
  • 低灵活性机器人(如无人车、乘用车):建议从 5.0m 甚至更高开始。
  • 观测现象与对策

    • 原因:阈值过大,把不相干的障碍物强行合并了。
    • 对策减小merge_threshold_
    • 原因:阈值过小,算法试图进入每个微小的缝隙。
    • 对策增大merge_threshold_
    • 现象 1:机器人频繁摆头,执行轨迹呈锯齿状
    • 现象 2:明明中间有很大的空地,机器人却绕了一个巨大的远路
  • 计算公式参考(工程经验): 一个比较通用的经验公式是:

    • : 机器人最高速度
    • : 控制器从扰动恢复到稳态的时间(通常取 1-2 秒)
    • : 机器人车长

    4. 总结:工程哲学的权衡

    在机器人工程中,“能走通”远比“走得近”重要

    • 调大 merge_threshold_:是一种保守策略。它生成的路径曲率更小、更顺滑,对控制器的要求低,乘客(或负载)感觉更舒适,但会稍微多绕一点路。
    • 调小 merge_threshold_:是一种激进策略。它追求极限的路径利用率,但极度考验机器人的动力学极限和控制器的跟踪能力。

    你的代码中提供了这个参数,实际上是给了下游控制器一个“容错空间”。在复杂的工业或室外场景下,建议优先给一个较大的值,以保证系统的整体鲁棒性。


    另外一个需要注意的是地图的膨胀参数设置,setInflateRadius就是设置地图膨胀参数。

    void TrajectoryAndObstaclesPublisher::init_planner_base(){    Eigen::Vector2i map_size(map_x_size_,map_y_size_);    map_     = std::make_shared(map_resolution_,map_size);    map_->setInflateRadius(0.5);    planner_manager_ =  std::make_shared();    planner_manager_->init(map_,1.0);}


    最后一个是关于a*算法的,之前在这篇文章讨论过:【机器人轨迹优化基础】从原理到实现:支持动态膨胀的机器人A*局部路径规划开源算法【Github开源】【ROS2】【C++】

    就是下面的map_size大小的设置, 这里的map_size(200,200);中200表示起点和终点的范围是20米,也就说,A*最大只能规划20米长的路径,一般来说,针对于目前这个场景够用,如需更大可以对这个参数进行调整。

    void manager::init(std::shared_ptr map ,float merge_threshold){    map_ = map;    merge_threshold_ = merge_threshold;    planner_ = std::make_shared();     Eigen::Vector2i map_size(200,200);    planner_->initGridMap(map_,map_size); }

    好了,至此写完了。


    我建了一个轨迹优化与运动控制方向技术交流群,欢迎各位同行专家加入交流讨论!扫描下方二维码,添加作者微信,然后拉进群。


    Python社区是高质量的Python/Django开发社区
    本文地址:http://www.python88.com/topic/191892