.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* 逻辑,专注于在复杂环境下通过分段重规划实现高效 避障,有如下优势:
本算法核心的实现思路:
(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 于沈阳
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 的消息(如
PoseStamped , OccupancyGrid )转换为内部模块能识别的 PathPoint 或 Eigen 数据。 可视化 :发布可视化话题(如 nav_msgs/Path )供 RViz 观察。 2. 模块间的调用关系 系统的运行逻辑是 从外向内调用,数据从内向外传递 :
输入端 :
trajectory_publisher.cpp 接收到目标点话题。 触发规划 : trajectory_publisher 调用 manager::makePlan 。 环境查询 : manager 通过 GridMap2D 检测全局直线上的障碍物交点。 算法计算 :对于每个障碍区间, manager 调用
path_searching 计算绕障曲线。 结果返回 : manager 完成缝合后返回完整轨迹给 trajectory_publisher 。 输出端 : 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源代码分析下。
# 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 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; calculate_intersection_points (); merge_adjacent_segments (); replan_and_combine_path (); planned_traj = std::move (final_path_); std::cout << planned_traj.size () << std::endl; std::cout << intersection_points_.size () << 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 () 2 ) return ; 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_) { current_proc.goal = next_seg.goal; current_proc.goalIndex = next_seg.goalIndex; } 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]); } std::vector astar_segment; bool success = search_astar (segment.start, segment.goal, astar_segment); if (success) { final_path_.insert (final_path_.end (), astar_segment.begin (), astar_segment.end ()); } else { std::cout << segment.startIndex << segment.goalIndex << std::endl; } 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 (); std::cout ; for (const auto & pt : path) { PathPoint temp; temp.x = pt.x () ; temp.y = pt.y (); temp.z = 0 ; result.push_back (temp); } 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 (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 ) { 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) { 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 map , float 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 函数:核心流水线 这是算法的 顶层逻辑 ,它严格执行四个步骤:
离散化 :把起点和终点的直线连线切成每段 10cm 的小点( discretize_trajectory )。
找交点 :找出哪些小点撞到了障碍物( calculate_intersection_points )。 合并 :如果两个障碍物离得很近,就把它们当成一个大障碍物处理( merge_adjacent_segments )。 缝合 :对障碍区段跑 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 ; } else if (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 位置。 总结: 索引驱动 (Index-Driven)**:利用 startIndex 和
goalIndex 快速切分路径,比通过坐标查找点要快得多,复杂度为 。 空间安全裕度 :在计算交点时主动外扩 3 个点,体现了对实际机器人动力学的理解(A* 需要一定距离来转弯)。 高度解耦 : manager 类只负责逻辑流程,底层的 A* 搜索和地图查询都是通过接口完成的。 缝合思想 :完美解决了传统 A* 规划后“回不到原轨迹”的问题。它通过强制指定 A* 的终点为直线上的某个点,实现了路径的迅速收敛。
首先有个比较重要的参数 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.0m - 2.0m 。 低灵活性机器人(如无人车、乘用车) :建议从 5.0m 甚至更高开始。 现象 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 ); } 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); } 我建了一个轨迹优化与运动控制方向技术交流群,欢迎各位同行专家加入交流讨论!扫描下方二维码,添加作者微信,然后拉进群。