ROS MoveIt!中添加障碍物:从rviz可视化到规划生效的完整实践

发布时间:2026/7/21 4:53:23
ROS MoveIt!中添加障碍物:从rviz可视化到规划生效的完整实践
1. 项目概述这不是“加个模型”那么简单而是理解机器人空间认知的第一课如果你刚接触ROSRobot Operating System下的运动规划框架看到“在rviz中为PR2增加场景物体”这个标题第一反应可能是“不就是拖个STL文件进去点几下鼠标”——我当年也是这么想的直到第一次让PR2的机械臂在规划路径时一头撞进自己刚放上去的咖啡杯里仿真直接崩溃终端刷出一屏红色报错。那一刻我才真正意识到rviz里那个看似静态的3D视图根本不是“画布”而是一张实时更新的空间语义地图你添加的每一个盒子、每一面墙、每一只茶杯都不是视觉装饰而是被MoveIt!底层规划器如OMPL反复读取、建模、碰撞检测的刚体物理实体。它参与的是整个运动学求解链路从目标位姿反推关节角到判断该路径是否与环境中任何物体发生几何干涉再到生成满足速度/加速度约束的安全轨迹。PR2作为ROS早期标杆级双臂移动机器人其URDF模型自带完整的碰撞体积collision geometry、惯性参数inertial和视觉外观visual而你在rviz中添加的场景物体必须通过PlanningSceneInterface或moveit_msgs/AttachedCollisionObject消息以完全一致的物理语义层级接入这个系统。这意味着哪怕你只是想放一个0.1米见方的立方体当障碍物也必须明确指定它的尺寸、位姿相对于哪个坐标系base_linkodommap、是否可移动、是否附着于机器人本体……漏掉任何一个字段MoveIt!就可能忽略它、误判它或者干脆拒绝加载。所以这节教程的本质是带你亲手搭建机器人对“我在哪、周围有什么、哪些地方不能去”这一基础空间认知能力的最小可行验证环境。它面向的不是已经调通抓取pipeline的老手而是刚编译完moveit_setup_assistant、对着空荡荡的rviz界面发呆、需要从“看见障碍物”到“真正避开它”迈出第一步的ROS初学者。你不需要会写C节点但必须理解TF坐标系树、URDF基本结构、以及rviz插件与MoveIt!后台服务之间的通信契约。2. 核心设计思路与方案选型为什么不用“插入模型”按钮而要写代码2.1 rviz的“Insert Model”按钮为何是条死胡同rviz界面右下角那个显眼的“Add”按钮点开后选择“RobotModel”或“Grid”很直观但当你想找“Add Obstacle”时会发现列表里根本没有。这是因为rviz本身只是一个可视化前端Visualization Frontend它不存储、不管理、也不参与任何碰撞检测逻辑。它只做一件事接收并渲染来自ROS话题如/move_group/monitored_planning_scene或服务如/get_planning_scene的数据流。那些你通过rosrun rviz rviz启动后看到的机器人模型、坐标系网格、甚至激光点云全都是由后台运行的move_group节点MoveIt!的核心规划服务器持续发布出来的。换句话说rviz是“观众”move_group才是“导演道具组灯光师”。你如果用rviz自带的“Insert Model”功能实际是rviz::InteractiveMarker的简易封装添加的只是一个孤立的、没有TF坐标、没有碰撞属性、不发布到/planning_scene话题的纯视觉标记Visual Marker。它在rviz里看起来像一块砖但在MoveIt!的规划器眼里它根本不存在。我试过直接拖拽一个DAE模型进去然后运行move_group的plan_kinematic_path服务结果规划器完美地让PR2的末端执行器穿过了那块“砖”——因为它压根没看见。这是新手踩坑率最高的地方也是本教程必须绕开的第一个陷阱。2.2 两种合法接入方式的深度对比Python脚本 vs C节点MoveIt!官方文档给出了两种向规划场景Planning Scene注入物体的标准方法一种是使用Python APImoveit_commander另一种是编写C节点调用moveit::planning_interface::PlanningSceneInterface。对于入门者我强烈推荐Python方案原因有三开发迭代成本极低写一个.py脚本保存chmod xrosrun moveit_tutorials add_scene_object.py5秒内就能看到效果。而C需要修改CMakeLists.txtcatkin_make链接moveit_ros_planning_interface库一个拼写错误就能卡住编译十分钟。我带过的实习生里有三分之一在C的#include moveit/planning_interface/planning_interface.h这行就放弃了。调试信息更友好Python的rospy.loginfo()输出直接打在终端配合roslaunch pr2_moveit_config move_group.launch的详细日志你能清晰看到“Adding box to planning scene... OK”还是“Failed to add object: Invalid pose”。C的ROS_INFO虽然也能打印但一旦涉及std::shared_ptr生命周期或moveit_msgs::PlanningScene消息序列化失败错误堆栈往往晦涩难懂。与教程生态无缝衔接ROS官方moveit_tutorials仓库里的所有入门示例包括本节对应的add_collision_objects.py全部采用Python。这意味着你可以直接git clonerosdep install然后一行命令跑起来无需额外配置。而C示例分散在moveit_core源码的test目录里对新手不友好。提示有人会问“Python性能不够实时性差”。这是个典型误区。向规划场景添加静态障碍物是一次性初始化操作发生在任务开始前耗时通常在毫秒级。真正的实时性瓶颈在于后续的运动规划OMPL求解和轨迹执行FollowJointTrajectoryaction server这部分由C核心完成与你用什么语言添加障碍物无关。把精力花在优化添加障碍物的代码上就像给自行车换F1轮胎——方向错了。2.3 为什么必须用PR2换其他机器人行不行标题里明确写着PR2这不是历史包袱而是工程必然。PR2的URDFUnified Robot Description Format文件是ROS社区最完整、最经受考验的范本之一。它的pr2_description包里不仅定义了41个自由度的连杆link和关节joint还为每个连杆精心标注了visual用于rviz渲染的Mesh或基本几何体cylinder, boxcollision用于碰撞检测的简化几何体通常比visual更粗略如把复杂机械臂外壳简化为几个长方体且明确指定了origin偏移确保碰撞体与视觉体在空间上严格对齐inertial质量、质心、转动惯量这对动力学仿真至关重要。当你运行roslaunch pr2_moveit_config demo.launch时move_group节点会自动加载pr2_moveit_config中的srdfSemantic Robot Description Format文件该文件定义了PR2的运动学组arm_left, arm_right, torso等、禁用的碰撞对如左手不要和左胸碰撞、以及默认的规划约束。这一切构成了一个开箱即用的、经过充分验证的“空间认知基座”。如果你换成一个自定义的URDF机器人很可能因为collision体缺失、origin偏移错误、或srdf中未定义运动学组导致move_group启动失败或者添加的障碍物虽然显示出来了但规划器始终报告“no solution found”因为你机器人的“身体”自己就在和自己碰撞。所以本教程强制使用PR2不是怀旧而是给你一个零干扰的、能100%复现的基准环境。等你彻底搞懂PR2这套流程后迁移到其他机器人只需替换URDF/SRDF路径逻辑完全一致。3. 核心细节解析与实操要点从坐标系、位姿到碰撞体的全链路拆解3.1 坐标系战争base_link、world、odom你到底该用哪一个在ROS中坐标系Frame不是可选项而是空间描述的基石。PR2的TF树是一个典型的分层结构/map→/odom→/base_link→/r_gripper_palm_link…… 每一层都通过一个tf::Transform平移旋转关联。当你想在PR2面前放一个0.2m×0.2m×0.1m的盒子当障碍物时必须明确回答“这个盒子的位置和朝向是相对于哪个坐标系定义的”答案是必须是/base_link或其子坐标系。原因如下move_group节点内部维护的规划场景Planning Scene其所有物体的位姿pose字段默认且唯一接受的参考坐标系是/base_link。这是由moveit::core::PlanningScene类的设计决定的。如果你强行指定frame_id mapmove_group会尝试将其转换到/base_link但如果/map到/base_link的TF变换在当前时刻不可用比如robot_state_publisher没启动或amcl定位没收敛转换就会失败物体添加直接报错。base_link是PR2机器人本体的原点位于两轮轴心正上方、底盘中心。所有传感器如/head_mount_kinect/rgb/image_raw和执行器如/r_arm_controller/command的坐标系最终都要通过TF链路汇聚到这里。以它为基准意味着你的障碍物位置是“相对于机器人自身”的这符合绝大多数应用场景机器人在未知环境中探索它感知到的障碍物自然是以自己为参照的。那/world或/odom呢它们主要用于全局导航navigation stack描述机器人在更大环境中的绝对位置。但在单次运动规划motion planning中move_group并不关心机器人在世界地图上的绝对坐标它只关心“我的手臂现在要从A点移动到B点中间有没有东西挡着”。这个“有没有东西挡着”的判断是在/base_link坐标系下进行的几何计算。注意/base_link的Z轴指向正上方X轴指向前方机器人前进方向Y轴指向左侧遵循右手定则。所以如果你想在PR2正前方1米、地面以上0.5米处放一个盒子它的position应该是(x1.0, y0.0, z0.5)而不是(x0.0, y0.0, z1.0)。我见过太多人在这里栽跟头把X和Z轴搞反结果盒子被“埋”在地板下面或者“飞”到天花板上rviz里根本看不见。3.2 位姿Pose的魔鬼细节四元数不是玄学是避免万向节死锁的工程选择在ROS中描述一个物体在3D空间中的朝向有两种主流方式欧拉角Euler Anglesroll/pitch/yaw和四元数Quaternion。MoveIt!的API无论是Python还是C强制要求使用四元数。为什么因为欧拉角存在著名的“万向节死锁Gimbal Lock”问题当pitch角接近±90度时roll和yaw会失去一个自由度导致朝向无法唯一确定。想象PR2的机械臂抬到头顶正上方此时如果用欧拉角微小的yaw变化可能导致roll剧烈跳变规划器会认为这是一个完全不同的、不可达的姿态。四元数q (x, y, z, w)是一个四维单位向量它用一种更数学、更稳定的方式编码旋转。但对新手来说手写四元数等于自杀。幸运的是ROS提供了完美的解决方案tf.transformations模块。你只需要记住一个核心函数quaternion_from_euler(roll, pitch, yaw)。例如你想让盒子保持水平不倾斜即roll0, pitch0, yaw0那么from tf.transformations import quaternion_from_euler q quaternion_from_euler(0, 0, 0) # 返回 [0.0, 0.0, 0.0, 1.0]这个[0,0,0,1]就是单位四元数代表“无旋转”。如果你想让盒子绕Z轴垂直轴旋转90度π/2弧度就用q quaternion_from_euler(0, 0, 3.14159/2) # 返回约 [0.0, 0.0, 0.707, 0.707]关键点在于quaternion_from_euler的输入单位是弧度radians不是角度degrees。3.14159/2是90度3.14159是180度。我建议你永远用math.pi来计算避免手算错误import math q quaternion_from_euler(0, 0, math.pi/2) # 清晰、准确、不易错3.3 碰撞体Collision Object的三种形态Box、Sphere、Mesh选哪个MoveIt!支持向规划场景添加三种基本类型的碰撞体BOX用shape_msgs::SolidPrimitive定义需指定dimensions [size_x, size_y, size_z]。这是最常用、最轻量、计算最快的选择。一个长方体盒子足够模拟桌子、墙壁、箱子。SPHERE同样用SolidPrimitivedimensions [radius]。适合模拟球形障碍物如篮球、水果。计算也极快。MESH用shape_msgs::Mesh需提供一个STL或DAE格式的3D模型文件路径。这是最真实、但也最重、最易出错的选择。Mesh的顶点数过多会拖慢碰撞检测路径错误相对路径/绝对路径混淆会导致加载失败Mesh的坐标系原点如果不与模型几何中心对齐会导致物体“飘”在空中或“沉”入地下。对于入门教程无条件选择BOX。理由非常实在PR2的pr2_moveit_config默认配置中所有内置的碰撞体如table、wall都是用BOX定义的保证了兼容性。BOX的dimensions参数是三个浮点数输入简单不易出错。而MESH需要你额外准备一个文件并在代码中正确指定路径file://orpackage://新手很容易卡在这一步。碰撞检测算法如FCL对BOX的相交测试是O(1)复杂度对MESH是O(n)甚至O(n²)在实时规划中毫秒级的差异就是成功与失败的区别。实操心得我曾经为了“炫技”坚持用一个高精度的咖啡杯STL模型。结果发现由于模型内部有大量冗余三角面片每次规划前的碰撞检测预处理耗时高达300ms导致PR2的规划频率从10Hz暴跌到2Hz动作僵硬得像机器人得了帕金森。后来换成一个简单的圆柱体CYLINDERMoveIt!也支持耗时降到15ms一切恢复正常。记住在机器人学里“够用”永远比“好看”重要。4. 完整实操过程与核心环节实现从零开始一行一行敲出可运行的代码4.1 环境准备确保你的ROS工作空间已正确配置在开始写代码前必须确认你的ROS环境是干净且完整的。这不是可选步骤而是成败的关键。请按顺序执行以下命令并仔细观察每一步的输出# 1. 启动ROS核心 roscore # 2. 检查PR2相关包是否已安装Ubuntu 16.04 ROS Kinetic apt list --installed | grep pr2 # 你应该看到 pr2-description, pr2-moveit-config, pr2-simulator 等 # 如果没有运行sudo apt-get install ros-kinetic-pr2-* # 3. 创建并初始化你的工作空间假设你还没有 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src catkin_init_workspace cd ~/catkin_ws catkin_make source devel/setup.bash # 4. 关键检查moveit_tutorials是否可用 roscd moveit_tutorials # 如果提示 No such package/stack说明没装。运行 sudo apt-get install ros-kinetic-moveit-tutorials # 或者从源码编译更推荐版本最新 cd ~/catkin_ws/src git clone https://github.com/ros-planning/moveit_tutorials.git cd ~/catkin_ws rosdep install --from-paths src --ignore-src --rosdistro kinetic -y catkin_make source devel/setup.bash提示catkin_make后务必执行source devel/setup.bash。这是ROS的“魔法咒语”它把你的工作空间路径加入ROS_PACKAGE_PATH环境变量。没有这一步rosrun永远找不到你写的脚本。我见过太多人反复重装ROS却忘了这行命令浪费数小时。4.2 编写核心脚本add_scene_object.py的逐行解析现在让我们创建这个改变你对rviz认知的脚本。打开你最喜欢的编辑器gedit,vim,vscode新建文件~/catkin_ws/src/moveit_tutorials/doc/pr2_tutorials/planning/scripts/add_scene_object.py并填入以下内容。我会在每一行后面用# -- 注释解释其作用#!/usr/bin/env python # 1. 导入ROS和MoveIt!核心库 import rospy import sys import copy import moveit_commander # MoveIt! Python接口主模块 import moveit_msgs.msg # 所有MoveIt!消息类型定义 import geometry_msgs.msg # Pose, Point, Quaternion等基础几何消息 from tf.transformations import quaternion_from_euler # 四元数转换工具 import math # 数学常量和函数 # 2. 初始化ROS节点。节点名必须唯一这里叫add_scene_object # anonymousTrue 确保即使多次运行也不会因重名而冲突 rospy.init_node(add_scene_object, anonymousTrue) # 3. 初始化MoveIt!命令行接口。这是与move_group通信的桥梁 # move_group 是PR2的move_group节点默认名称 moveit_commander.roscpp_initialize(sys.argv) group_name arm_right # 选择右臂运动学组。也可以是arm_left move_group moveit_commander.MoveGroupCommander(group_name) # 4. 获取PlanningSceneInterface实例。这是专门用来增删场景物体的接口 scene moveit_commander.PlanningSceneInterface() # 5. 设置一个短暂的等待确保rviz和move_group已完全启动 # 这是经验之谈move_group启动需要时间加载URDF/SRDF太早操作会失败 rospy.sleep(2) # 6. 定义障碍物一个0.2m x 0.2m x 0.1m的盒子放在PR2正前方0.8米处 box_name obstacle_box # 6.1 创建一个CollisionObject消息 co moveit_msgs.msg.CollisionObject() co.id box_name # 物体唯一标识符后续删除时要用 co.header.frame_id base_link # 关键参考坐标系必须是base_link # 6.2 定义物体的几何形状一个长方体 box_pose geometry_msgs.msg.Pose() # 创建位姿对象 box_pose.position.x 0.8 # 在base_link前方0.8米 box_pose.position.y 0.0 # 在base_link正前方Y0 box_pose.position.z 0.1 # 离地0.1米盒子高度0.1m所以底面在z0 # 6.3 设置朝向绕Z轴旋转0度水平放置 q quaternion_from_euler(0, 0, 0) # 得到四元数 [0,0,0,1] box_pose.orientation.x q[0] box_pose.orientation.y q[1] box_pose.orientation.z q[2] box_pose.orientation.w q[3] # 6.4 将位姿赋给CollisionObject co.pose box_pose # 6.5 定义几何体一个SolidPrimitive长方体 box_primitive shape_msgs.msg.SolidPrimitive() # 注意这里需要导入shape_msgs box_primitive.type box_primitive.BOX box_primitive.dimensions [0.2, 0.2, 0.1] # x, y, z尺寸 # 6.6 将几何体添加到CollisionObject的primitives列表中 co.primitives.append(box_primitive) # 6.7 将位姿对应的坐标系base_link添加到primitive_poses列表中 # 这是必须的告诉MoveIt!这个几何体的位姿是相对于哪个frame co.primitive_poses.append(box_pose) # 6.8 最关键一步将CollisionObject发布到/planning_scene话题 # 这行代码执行后rviz里才会出现这个盒子 scene.add_object(co) # 7. 添加第二个物体一个半径0.05米的球体放在PR2右侧0.3米处 sphere_name obstacle_sphere so moveit_msgs.msg.CollisionObject() so.id sphere_name so.header.frame_id base_link sphere_pose geometry_msgs.msg.Pose() sphere_pose.position.x 0.0 sphere_pose.position.y -0.3 # Y轴负方向是右侧PR2坐标系Y向左为正 sphere_pose.position.z 0.5 q_sphere quaternion_from_euler(0, 0, 0) sphere_pose.orientation.x q_sphere[0] sphere_pose.orientation.y q_sphere[1] sphere_pose.orientation.z q_sphere[2] sphere_pose.orientation.w q_sphere[3] so.pose sphere_pose sphere_primitive shape_msgs.msg.SolidPrimitive() sphere_primitive.type sphere_primitive.SPHERE sphere_primitive.dimensions [0.05] # 半径 so.primitives.append(sphere_primitive) so.primitive_poses.append(sphere_pose) scene.add_object(so) # 8. 日志输出确认成功 rospy.loginfo(Added box %s and sphere %s to the planning scene., box_name, sphere_name) # 9. 保持节点运行以便rviz持续订阅 # 如果不加这个脚本执行完就退出物体可能一闪而过 rospy.spin()注意上面代码中shape_msgs.msg.SolidPrimitive的导入在标准ROS Kinetic中是存在的但有时需要显式声明。如果运行时报错ImportError: No module named shape_msgs.msg请在文件开头添加import shape_msgs.msg4.3 运行与验证如何确认你的障碍物真的“生效”了写完脚本别急着运行。先做两件事赋予可执行权限chmod x ~/catkin_ws/src/moveit_tutorials/doc/pr2_tutorials/planning/scripts/add_scene_object.py启动PR2的MoveIt!演示环境roslaunch pr2_moveit_config demo.launch这个命令会启动rviz、move_group节点、robot_state_publisher并加载PR2的完整URDF和SRDF。你会看到一个蓝色的PR2模型出现在rviz中央旁边有各种交互式控件Interactive Markers。现在打开一个新的终端运行你的脚本rosrun moveit_tutorials add_scene_object.py预期现象终端会输出Added box obstacle_box and sphere obstacle_sphere to the planning scene.。rviz窗口中PR2模型前方0.8米处应该出现一个红色的线框长方体右侧0.3米、离地0.5米处应该出现一个绿色的线框球体。注意它们是“线框”不是实心这是MoveIt!的默认渲染风格表示它们是“规划用的碰撞体”而非“视觉用的模型”。终极验证让规划器“看见”它们在rviz的MotionPlanning面板中找到Planning标签页。点击Select Start State选择Current让起始状态为PR2当前姿态。点击Select Goal State然后点击PR2右臂末端r_wrist_roll_link的交互式标记一个小十字拖动它尝试把它放到红色盒子的正后方即让末端从盒子后面绕过去。点击Plan按钮。如果一切正常rviz会显示一条平滑、弯曲、完美避开红色盒子和绿色球体的蓝色轨迹线。如果轨迹线直直地穿过了盒子说明障碍物没加载成功回去检查frame_id和sleep时间。实操心得第一次成功看到那条蓝色轨迹绕开你亲手添加的障碍物时那种感觉就像教孩子第一次独立跨过门槛——所有前期的枯燥配置、坐标系纠结、四元数折磨都在那一刻有了意义。这就是机器人“空间智能”的诞生瞬间。5. 常见问题与排查技巧实录那些让你抓狂半小时的“小问题”5.1 问题速查表症状、原因与一招解决症状可能原因快速解决rviz里什么都没出现脚本终端也没报错demo.launch没启动或move_group节点没运行。add_object()需要后台服务响应。运行rosnode listrviz里出现了物体但规划器完全无视它轨迹直接穿过CollisionObject的header.frame_id不是base_link或者primitive_poses列表为空。检查代码第6.1和6.7行。确保co.header.frame_id base_link且co.primitive_poses.append(box_pose)被执行。脚本报错ImportError: No module named shape_msgs.msgshape_msgs包未被正确导入或未安装。在脚本开头添加import shape_msgs.msg。如果仍报错运行sudo apt-get install ros-kinetic-shape-msgs。rviz里物体显示位置完全错误如在天花板上position.z值过大或坐标系理解错误把base_link的Y轴当成前进方向。用rostopic echo /tf查看/base_link的实时坐标。PR2的base_link原点在底盘中心Z0是地面。确保z值在0.0~0.8之间。添加物体后move_group节点疯狂刷屏报错Failed to update planning sceneCollisionObject的id重复了。比如你之前运行过一次脚本没删掉物体又运行第二次ID相同。在脚本开头添加删除逻辑scene.remove_world_object(box_name)。或者重启move_group节点。5.2 超实用避坑技巧老手才懂的“潜规则”技巧1用rostopic echo监听规划场景做“上帝视角”调试不要只依赖rviz。打开一个新终端运行rostopic echo /move_group/monitored_planning_scene这会实时打印move_group发布的完整规划场景消息。当你运行add_scene_object.py后你会在滚动的日志中清晰地看到world.collision_objects列表里多出了你定义的obstacle_box并能看到它的pose、primitives等所有字段。这是最权威的验证方式比rviz更底层、更可靠。技巧2remove_world_object()不是可选而是必做很多人写完添加脚本就以为万事大吉。但下次你想换一个位置放盒子或者测试不同尺寸就必须先删掉旧的。否则add_object()会因为ID冲突而静默失败。在你的脚本里养成习惯在add_object()之前加上scene.remove_world_object(box_name) scene.remove_world_object(sphere_name) rospy.sleep(0.5) # 给删除操作一点时间这样每次运行脚本都是一个干净的、可预测的起点。技巧3sleep()时间不是玄学是经验值代码里的rospy.sleep(2)为什么是2秒不是1秒或5秒因为move_group启动后需要时间做三件事1) 加载URDF2) 解析SRDF3) 建立与robot_state_publisher的TF连接。在虚拟机或老旧电脑上这个过程可能长达3秒。我建议你把sleep时间设为3秒并在生产代码中用rospy.wait_for_service(/get_planning_scene)来替代硬编码的sleep这样更健壮。技巧4rviz的“Fixed Frame”必须是base_link这是个极其隐蔽的坑。打开rviz看左下角Global Options里的Fixed Frame。如果它不是base_link而是map或odom那么你添加的、以base_link为参考系的物体会在rviz里显示在错误的位置甚至完全看不见。因为rviz不知道如何把base_link下的坐标转换到你当前的Fixed Frame下。务必手动把它改成base_link。5.3 进阶思考从“添加物体”到“构建真实场景”的跃迁掌握了添加单个盒子下一步就是构建一个完整的、有意义的作业环境。比如你想让PR2从桌子上拿起一个杯子。这需要一张桌子用一个大的BOXdimensions[1.2, 0.8, 0.75]z0.75是桌面高度。一个杯子用一个小的CYLINDERdimensions[0.04, 0.1]半径0.04m高度0.1m放在桌子position(0.3, 0.0, 0.750.05)杯子底面在桌面之上0.05m。一个抓取目标设置Goal State为杯子顶部的某个点并启用Allow External Joint Limits让规划器知道手腕可以旋转。所有这些都建立在你今天学会的add_object()这个原子操作之上。没有对单个物体的精准控制就不可能构建复杂的场景。所以不要小看这个“入门教程”。它不是终点而是你通往机器人自主操作世界的、第一块稳固的基石。我至今记得第一次让PR2成功避开我添加的障碍物然后稳稳地把一个虚拟的杯子放到另一个虚拟的托盘上时那种混合着疲惫与狂喜的感觉——那不是代码的胜利而是人类对空间、对逻辑、对机器的一次微小而确凿的征服。