简介:这份源码面向机器人、自动驾驶方向的ROS开发者,提供一套基于Gazebo的智能车轨迹跟踪仿真与设计实现,覆盖PID、滑模控制、模型预测控制等算法从理论到代码的落地过程。资源共74个文件,压缩包1.37MB,包含功能包配置(yaml/xml/launch)、Python控制脚本、Gazebo仿真环境(world/sdf/dae/material)以及rviz可视化配置,目录按传感器模拟、路径规划、控制器、节点通信、主程序与参数配置模块化组织,便于对照学习。目前已有199人学习下载。通过阅读源码可理解ROS节点消息传递机制,掌握如何用rospy订阅与发布话题、在Gazebo中搭建智能车模型与环境,并结合A*/Dijkstra/RRT等路径规划算法完成轨迹跟踪调试。这套资源尤其适合想深入ROS实战、将控制理论转化为可运行代码的研究者与开发者。
1. 智能车轨迹跟踪仿真:不是玄学,是 ROS 加三个算法
智能车的轨迹跟踪仿真,说玄学也不算夸张——同一个 Pure Pursuit 包,别人车压着线走,你车画龙画到赛道外,最后查出来就是前视距离系数差了个 0.1。这份基于 ROS 的智能车轨迹跟踪算法仿真与设计源码,我把包内的 launch、控制节点、Gazebo 模型逐个拆过一遍,把能直接复现的仿真流程和调参经验整理成下面的实操笔记。它适合三类人:正在备赛智能车竞赛的队员、课程设计要做 ROS 仿真交作业的同学,以及想把 ROS 路径跟踪在 Gazebo 里跑通的入门者。读完你能搞清这份资源怎么编译、怎么启动、怎么把参数调到不翻车。
2. 环境与工程结构:把源码包跑起来之前先搞清这几件事
2.1 先别急着编译:确认是 ROS1 还是 ROS2 工程
拆开这个 zip 之后,第一件事不是catkin_make,而是先看一眼根目录。我拿到的这份按 ROS1 Noetic 组织——顶层有src/,有指向/opt/ros/noetic/share/catkin/cmake/toplevel.cmake的软链,launch 文件也是roslaunch语法,不是ros2 launch。判断依据很简单:看软链指向哪个 ROS 版本,看 launch 文件是<launch>根标签还是.yaml描述。
cd ~/traj_ws ls -la # 期望看到: # CMakeLists.txt -> /opt/ros/noetic/share/catkin/cmake/toplevel.cmake # src/ # build/ 或 devel/(如果作者已经编译过) ls src # 一般会看到这些功能包: # smart_car_description 小车 URDF 模型与 meshes # traj_tracking 轨迹跟踪控制节点 # path_publisher 参考轨迹发布节点 # sim_bringup gazebo 与 rviz 的一键 launch逻辑说明:CMakeLists.txt是软链这一点很关键,它决定了你必须用catkin_make而不是colcon来编译。如果你机器上装的是 ROS2 Humble,直接catkin_make会报一堆找不到geometry_msgs之类的错,那多半不是包坏了,是你环境选错了。判断完 ROS 版本再动手,能省掉后面一整轮排查。
2.2 环境准备:Ubuntu 20.04 + ROS Noetic + Gazebo 11
这份源码默认跑在 Ubuntu 20.04 + ROS Noetic + Gazebo 11 上。装 ROS 这块,我一般直接用鱼香 ROS 一键安装脚本,比手动配源省心,装完默认带好 Gazebo 和 Rviz。装完系统 ROS 后,还有几个仿真依赖要确认在不在,常见坑是只装了ros-noetic-desktop没装 gazebo 插件。
# 检查关键依赖,缺哪个补哪个 sudo apt install ros-noetic-gazebo-ros-pkgs ros-noetic-gazebo-ros-control \ ros-noetic-robot-state-publisher ros-noetic-rviz \ ros-noetic-teleop-twist-keyboard # 进入工作空间,让 rosdep 补全剩余依赖 cd ~/traj_ws rosdep update rosdep install --from-paths src --ignore-src -r -y逻辑说明:gazebo-ros-pkgs提供spawn_model和gazebo_ros节点,负责把 URDF 模型加载进仿真世界;robot-state-publisher负责发布robot_description对应的 TF 树,Rviz 里看不到车模往往就是它没起来。teleop-twist-keyboard是用来发/cmd_vel手动验证底盘正反转的,调跟踪算法时最好关掉它,避免两个节点抢话题。
参数说明:--from-paths src表示只解析src/目录下功能包的依赖;-r表示缺失依赖直接安装;-y自动确认。如果你的源里缺了某些 ROS 包,执行后会提示ros-noetic-xxx的安装命令,照着跑就行。
2.3 编译三件套:catkin_make、source、rospack 自检
环境没问题之后,编译流程是固定的三步,顺序不要乱。先 source 系统 ROS,再catkin_make,最后 source 工作空间。这一步最容易翻车的点,是忘了在catkin_make之后重新 source,导致roslaunch找不到新编译出来的节点。
source /opt/ros/noetic/setup.bash cd ~/traj_ws catkin_make source devel/setup.bash # 自检:确认核心节点已经被 ROS 索引到 rospack find traj_tracking rospack find sim_bringup逻辑说明:catkin_make会把功能包里的 Python 节点复制到devel/lib下,并把可执行权限处理好。rospack find能查到包路径,就说明包已经被系统索引。如果rospack find报Package not found,先查echo $ROS_PACKAGE_PATH里有没有/home/用户名/traj_ws/src,没有就是 source 顺序不对。
参数说明:traj_tracking是控制节点所在包,sim_bringup是一键启动包。两者都能找到,编译这步就算过了。到这里工程就算立住了,下一步拆核心算法。
3. 核心算法拆解:Pure Pursuit 与 Stanley 的选型、参数和实现
3.1 纯追踪的几何直觉:一段圆弧追一个点
轨迹跟踪常用算法就三类:Pure Pursuit(纯追踪)、Stanley、PID 前馈。这份源码的主力控制节点是 Pure Pursuit,原因很实际:对低速智能车,它把“往目标点走”抽象成一段圆弧,物理意义清楚,整个控制器就一个前视距离ld能调,特别适合课程设计和竞赛快速出效果。
纯追踪的核心是几何关系:小车当前位姿到目标点连一条线,把这条线当作圆弧的弦,根据车辆轴距L和弦与车头方向的夹角α,算出要转的曲率半径,再换算成前轮转角。转向角的近似公式就是δ = atan( 2 * L * sin(α) / ld )。ld越大,车越“钝”,切弯越外抛;ld越小,车越“灵”,但也越容易抖。
#!/usr/bin/env python3 import rospy import math from geometry_msgs.msg import Twist from nav_msgs.msg import Path class PurePursuit: def __init__(self): self.current_pose = None self.path = None self.ld = rospy.get_param('~lookahead', 0.8) # 前视距离,单位 m self.wheelbase = rospy.get_param('~wheelbase', 0.26) # 轴距,单位 m self.linear = rospy.get_param('~linear_vel', 0.5) # 期望线速度 m/s self.cmd_pub = rospy.Publisher('/cmd_vel', Twist, queue_size=1) rospy.Subscriber('/odom', Odometry, self.odom_cb) rospy.Subscriber('/plan_path', Path, self.path_cb) def find_target_point(self): # 在路径上找一个离当前位置恰好为 ld 的点 best_idx = 0 min_diff = 1e9 for i, pose in enumerate(self.path.poses): dx = pose.pose.position.x - self.current_pose.position.x dy = pose.pose.position.y - self.current_pose.position.y dist = math.hypot(dx, dy) diff = abs(dist - self.ld) if diff < min_diff: min_diff = diff best_idx = i return self.path.poses[best_idx].pose.position def control_loop(self, event): if self.path is None or self.current_pose is None: return target = self.find_target_point() dx = target.x - self.current_pose.position.x dy = target.y - self.current_pose.position.y alpha = math.atan2(dy, dx) - self.yaw # 纯追踪转向角公式 delta = math.atan2(2.0 * self.wheelbase * math.sin(alpha), self.ld) cmd = Twist() cmd.linear.x = self.linear cmd.angular.z = delta self.cmd_pub.publish(cmd)逻辑说明:find_target_point不是取路径上最近的点,而是取“距离恰好等于ld”的那个点。这个点决定车要往哪拐,是纯追踪的灵魂。alpha是目标点相对车头方向的夹角,圆弧半径由ld / (2 * sin(alpha))决定,转角再由轴距换算成角速度。整套逻辑里ld是唯一需要常调的参数。
参数说明:lookahead设 0.8 是低速小车的常用起步值;wheelbase必须和 URDF 里前后轮中心距严格一致,差 5 厘米转弯特性就明显变样;linear_vel设 0.5,速度越高ld要跟着放大,否则高速下转弯增益过大直接发散。
3.2 Stanley 控制器:横向偏差的直接反馈
如果纯追踪在你的赛道上表现不稳,这份思路里的备选方案是 Stanley。Stanley 不找前视点,而是把车当前位置到参考路径的横向偏差e直接打进前轮转角:δ = θ_e + atan( k * e / v ),其中θ_e是车头与最近路径点切线方向的夹角。它的好处是收敛更硬、贴线更紧,坏处是路径不能太毛糙,稍有跳变就会猛打方向。
| 算法 | 核心思路 | 关键参数 | 对路径平滑度要求 | 低速表现 |
|---|---|---|---|---|
| Pure Pursuit | 前视点圆弧拟合 | 前视距离 ld | 低,路径散乱也能跑 | 稳,适合课程设计 |
| Stanley | 横向偏差反馈 | 增益 k | 高,要求路径连续 | 贴线紧,但怕跳变 |
| PID 前馈 | 偏差比例积分微分 | kp/ki/kd | 中 | 直观,容易超调 |
逻辑说明:选型时先看你的参考轨迹是怎么来的。如果路径点是随机撒的散点,建议用 Pure Pursuit,它天然对散点有平滑作用;如果路径本身是连续光滑的曲线,比如 8 字形或椭圆,Stanley 贴线精度会明显更高。这份源码里主控制器是 Pure Pursuit,但保留了一份 Stanley 的参考实现,可以改 launch 参数切换。
参数说明:Stanley 的k一般从 0.5 起步,k越大修正越猛,湿滑地面或高速下容易横向摆振。速度v太小会导致atan(k*e/v)增益爆炸,所以停车状态下 Stanley 完全不适用,这种场景 Pure Pursuit 才有解。
3.3 参数边界:什么情况下算法会彻底失效
拆完代码后最该记住的是参数失效边界。ld小于 0.3 米时,低速小车会以非常高的频率切换目标点,转向指令来回打,表现为车头高频抖动;ld大于 1.5 米时,车会贴着路径内侧走,切弯严重外抛,看起来就像在抄近道。速度每提高 0.2 m/s,建议ld同步增加 0.1~0.15 米,这是我从仿真里反复试出来的经验值,不是严格公式。
另一个容易忽视的边界是路径点的密度。如果参考路径每两个点之间距离超过ld,find_target_point会在两个点之间跳来跳去,导致转角突变。常见做法是发布轨迹前先做一次插值,把相邻点间距压到 0.1 米以内。
4. 从 launch 到复现:仿真启动、轨迹发布与调参全流程
4.1 launch 文件结构解析
这份源码的一键启动入口在sim_bringup包里。启动文件承担三件事:加载 URDF 到参数服务器、把机器人 spawn 进 Gazebo 世界、拉起跟踪控制节点和 Rviz。下面这份是精简过的主 launch,和包内结构一致:
<launch> <arg name="model" default="$(find smart_car_description)/urdf/smart_car.urdf" /> <param name="robot_description" textfile="$(arg model)" /> <!-- 把机器人模型加载进 Gazebo --> <node name="spawn_model" pkg="gazebo_ros" type="spawn_model" args="-urdf -model smart_car -param robot_description -x 0 -y 0 -z 0.05" /> <!-- 发布 TF 树,Rviz 显示必需 --> <node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher" /> <!-- 轨迹跟踪控制节点,参数在这里改 --> <node name="tracking_controller" pkg="traj_tracking" type="pure_pursuit.py" output="screen"> <param name="lookahead" value="0.8" /> <param name="wheelbase" value="0.26" /> <param name="linear_vel" value="0.5" /> </node> <node name="rviz" pkg="rviz" type="rviz" args="-d $(find sim_bringup)/rviz/tracking.rviz" /> </launch>逻辑说明:spawn_model的-x 0 -y 0 -z 0.05表示小车出生在原点,离地 5 厘米,给 Gazebo 一个下落稳定时间;robot_state_publisher读取参数服务器上的robot_description并发布 TF,没有它 Rviz 里只有坐标系没有车模;tracking_controller的output="screen"会把节点日志打到终端,调参时能直接看到有没有报错。Rviz 直接加载预设的.rviz文件,避免每次手动添加 Topic。
参数说明:所有控制参数都集中在 launch 里,比改 Python 代码方便。lookahead、wheelbase、linear_vel是三个最常动的值。注意wheelbase是从 URDF 里量出来的,不要凭感觉填。
4.2 一键启动与话题自检
编译通过后,启动是两条命令的事。第一次跑建议开两个终端,一个看 Gazebo 和控制器日志,一个用来查话题。Gz 窗口里小车如果一直原地打滑,先别急着调算法,八成是 URDF 摩擦没配好,这部分避坑章详细说。
# 终端 1:启动仿真与跟踪 source ~/traj_ws/devel/setup.bash roslaunch sim_bringup sim_tracking.launch # 终端 2:确认关键话题和 TF 都在 rostopic list | grep -E "odom|cmd_vel|plan_path" rosrun tf tf_echo odom base_link逻辑说明:rostopic list能看到/odom、/cmd_vel、/plan_path三个话题同时在,控制链路才算通。tf_echo odom base_link会以 10Hz 左右输出坐标变换关系,能看到平移量在变化就说明小车模型在仿真里真正动起来了,这时候再往下调跟踪才有意义。
4.3 发布参考轨迹:圆形与 8 字形
源码包里带了一个path_publisher节点,默认发布圆形路径。我建议第一次验证不要用圆形,用 8 字形——它能同时检验左右转弯的对称性,圆轨迹只能暴露单边问题。下面这个脚本可以直接替换包内配置,生成 8 字形参考路径:
#!/usr/bin/env python3 import rospy import math from nav_msgs.msg import Path from geometry_msgs.msg import PoseStamped rospy.init_node('figure8_publisher') pub = rospy.Publisher('/plan_path', Path, queue_size=1) rate = rospy.Rate(10) path = Path() path.header.frame_id = 'odom' # 生成 400 个点,组成两个对称的环 for i in range(400): t = i * math.pi / 60.0 p = PoseStamped() p.header.frame_id = 'odom' p.pose.position.x = 1.5 * math.sin(t) p.pose.position.y = 1.0 * math.sin(2.0 * t) path.poses.append(p) while not rospy.is_shutdown(): path.header.stamp = rospy.Time.now() pub.publish(path) rate.sleep()逻辑说明:x = 1.5 * sin(t)、y = 1.0 * sin(2t)是参数方程形式的 8 字形曲线。400 个点均匀分布在整条曲线上,相邻点间距大约 0.05 米,远小于ld,能保证纯追踪算法稳定找点。frame_id必须设成odom,和 Rviz 的固定坐标系一致,否则路径显示不出来。
参数说明:math.pi / 60.0控制的是采样步长,这个值决定每个周期采样点数。想跑得更慢更平滑,把分母从 60 改成 90,点数不变但轨迹更新变密;想让小车跑得更激进,把1.5和1.0两个幅值加大。发布频率 10Hz 足够,太高反而占用带宽。
4.4 调参的实操顺序
调参最忌讳上来乱拧。我一般按这个顺序走:先固定linear_vel = 0.5,然后从ld = 0.6开始,以 0.1 步进往上加,每一步观察两个现象——一是车是否在直道段贴线,二是弯道段是否外抛。直道贴线差就减小ld,弯道外抛就加大ld。当某个ld值下直道不抖、弯道不抛,就固定它,再去动速度。
# 运行时也可以动态改参数,省得反复重启 launch rosrun dynamic_reconfigure dynparam set /tracking_controller lookahead 0.9逻辑说明:dynamic_reconfigure是老牌动态调参工具,但前提是你的控制节点里实现了dynamic_reconfigure回调。如果这份源码的节点没写这个回调,运行会报错,那就老老实实改 launch 里的<param>重启。判断方法很简单:启动后终端里有没有出现dynparam相关的日志。
| 现象 | 优先调整参数 | 调整方向 |
|---|---|---|
| 直道段左右画龙 | lookahead | 增大 0.1~0.2 |
| 弯道外抛严重 | lookahead | 减小 0.1 左右 |
| 弯道切内侧太狠 | linear_vel | 降低 0.1 m/s |
| 车头高频抖动 | lookahead | 增大到 0.8 以上 |
| 整体贴线但滞后 | wheelbase | 核对 URDF 实际轴距 |
5. 避坑记录:仿真中小车翻车的五种典型原因与排查
5.1 Gazebo 里小车陷地或四轮弹跳
现象:启动仿真后,小车模型没有稳定停在地面上,而是反复弹跳,或者半个车体陷进地面,原地打转。原因:URDF 里车轮的碰撞标签不完整,或者<gazebo reference>里没配置摩擦系数,Gazebo 默认把轮子当光滑刚体处理。解决:给每个车轮增加摩擦参数,这是最标准的修法。
<gazebo reference="wheel_left_front"> <mu1>1.0</mu1> <mu2>1.0</mu2> <kp>100000.0</kp> <kd>10.0</kd> </gazebo>逻辑说明:mu1和mu2是纵向与侧向摩擦系数,小车轮胎设 1.0 起步比较稳,赛道竞速场景可以更高。kp和kd是接触刚度与阻尼,数值太小会导致接触变形过大,车像踩在棉花上。四个轮子都要加,缺一个就会出现单轮打滑。
5.2 直道画龙,车头高频摆动
现象:参考路径是一条直线,小车却左右画龙,摆动频率越来越高,最后冲出赛道。原因:ld设得太小,比如小于 0.3 米,目标点切换频率远高于底盘响应频率,形成正反馈振荡。解决:把ld拉回 0.6~0.8 区间,同时检查linear_vel是否过高。这里有一条血泪经验:速度每提升 0.2 m/s,ld至少要同步加 0.1,否则画龙是必然的。
5.3 里程计漂移,轨迹越跑越偏
现象:控制算法看起来没问题,小车前几圈贴线很好,跑了几圈之后路径整体偏移,误差越来越大。原因:Gazebo 的/odom如果没有配置高斯噪声,理想得反而不真实;如果配置了较大线速度噪声,长时间积分后位姿漂移会被放大。解决:先看/odom话题的协方差值,如果噪声很大,在控制节点里改用/base_pose_ground_truth话题做位姿输入,这在仿真验证算法时是常见做法。
5.4 Rviz 里看不到车模和路径
现象:Gazebo 里车在跑,Rviz 里只有坐标系和路径点,看不到小车模型。原因:robot_state_publisher没启动,或者 Rviz 的 fixed frame 设成了map而仿真里只有odom坐标系。解决:launch 里补上robot_state_publisher节点;Rviz 左上角 Global Options 里把 Fixed Frame 改成odom,并确认/tf话题有数据。排查命令是rostopic echo /tf,连续输出odom -> base_link的变换就说明 TF 正常。
5.5 多个节点抢/cmd_vel,小车不听控制
现象:刚启动跟踪时小车还正常,后来手动跑了一下键盘遥控,再切回跟踪模式,小车完全不动,或者朝一个方向猛打。原因:teleop_twist_keyboard节点还活着,它以 10Hz 往/cmd_vel发零速度或键盘指令,和控制节点争抢同一个话题,底盘节点只能收到混合后的指令。解决:启动跟踪前强制关掉键盘节点,或者做个简单的仲裁:底盘节点订阅/cmd_vel的同时检查/tracking_enable标志位,只有标志位为 true 时才执行跟踪指令。这个“多个节点抢移动指令话题”的问题在实车上更常见,仿真里养成关节点的习惯能少踩很多坑。
6. 进阶验证:用误差曲线把跟踪精度量化出来
调参不能只靠肉眼盯着 Rviz 画面,那叫感觉,不叫验证。我一般会在仿真稳定后加一个误差测量节点,订阅/odom和/plan_path,实时计算小车当前位置到参考路径的最近距离,攒成误差序列,最后输出均方根误差和最大误差。这样每次调参,都能拿到一组硬数据对比。
#!/usr/bin/env python3 import rospy import math from nav_msgs.msg import Odometry, Path class ErrorMonitor: def __init__(self): self.path = None self.errors = [] rospy.Subscriber('/odom', Odometry, self.odom_cb) rospy.Subscriber('/plan_path', Path, self.path_cb) # 每 2 秒输出一次当前平均误差 self.timer = rospy.Timer(rospy.Duration(2.0), self.report) def odom_cb(self, msg): if self.path is None: return px = msg.pose.pose.position.x py = msg.pose.pose.position.y # 遍历全部路径点,找最近距离,作为当前时刻的横向误差 d_min = min( math.hypot(px - q.pose.position.x, py - q.pose.position.y) for q in self.path.poses ) self.errors.append(d_min) def path_cb(self, msg): self.path = msg self.errors.clear() # 路径更新后重置误差统计 def report(self, event): if len(self.errors) < 10: return rmse = math.sqrt(sum(e * e for e in self.errors) / len(self.errors)) rospy.loginfo("RMSE: %.3f m, 当前误差: %.3f m", rmse, self.errors[-1]) if __name__ == '__main__': rospy.init_node('error_monitor') ErrorMonitor() rospy.spin()逻辑说明:误差计算用的是“最近路径点距离”,这是确定性的,不依赖小车在路径上的投影位置是否唯一,实现最简单且不易出错。RMSE 对大的瞬时误差敏感,能暴露偶发的转弯外抛;最大误差则要配合 Rviz 画面一起看,确认是不是在同一个弯道反复出现。
我自己的评判标准是:低速 0.5 m/s 下,RMSE 小于 0.05 米算合格,小于 0.03 米算调得不错;超过 0.1 米说明参数还有明显问题,不要急着跑高速。从那以后,我每次调前视距离都会强制记一笔当前的ld、速度和 RMSE 三项数据,复盘时一眼就能看出上一步改动带来的影响。数据比感觉可靠,这套误差测量脚本建议直接跑起来。希望帮到你。
本文还有配套的精品资源,点击获取