ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Graspness+ROS2+MoveIt2:无序3D场景抓取系统实战指南

Graspness+ROS2+MoveIt2:无序3D场景抓取系统实战指南 简介这份资源面向机器人抓取方向的研究者与工程开发者聚焦无序3D场景下的6自由度抓取难题。其核心是集成Graspness推理服务完成抓取姿态预测并借助ROS2与MoveIt2打通从姿态输出到机械臂运动规划、避障与执行的全链路可用于家庭、仓库、零售等杂乱环境的物品抓取验证。压缩包共215个文件约2.2MB以37个py脚本、17个cpp与18个hpp源码、26个stl模型、14个xacro与13个yaml配置、13个dae网格及rviz、srdf等为主覆盖推理桥接、运动规划、仿真描述与手眼标定等模块。目前已有36人学习。资源附带说明文档与附赠资料包含系统设计理论、操作步骤与注意事项读者可据此理解Graspness与MoveIt2的接口衔接方式掌握路径规划、避障及机械臂与抓手协调运动的实现思路并借助示例代码与文档快速复现和二次开发。1. 无序3D场景抓取为什么Graspness值得你花时间真实世界的桌面从来不会像数据集里那样整洁。零件、工具、日用品堆叠在一起遮挡、反光、纹理缺失同时出现传统基于采样点评估的6-Dof抓取姿态预测在这里几乎必然翻车。Graspness这个概念的核心洞察很直接与其对每个候选抓取逐一打分不如先学习一个「哪里值得抓」的稠密评分场再在这个场的引导下做姿态回归。这样做的好处是推理速度和多物体泛化能力同时提升尤其适合无序3D场景。这套系统要落地需要三块拼图Graspness推理服务负责从点云直接输出6-Dof抓取姿态ROS2作为通信中间件把感知、规划、执行串起来MoveIt2负责运动规划与执行。适合已经跑通ROS2基础、想在真实机械臂上验证抓取算法的工程师也适合做具身智能方向、需要一套可复现抓取baseline的研究者。下面按「推理服务怎么接、ROS2节点怎么搭、MoveIt2怎么配、坑在哪」的顺序展开。2. Graspness推理服务从点云到6-Dof抓取姿态的最小闭环2.1 Graspness评分场到底在算什么Graspness不是直接输出抓取姿态而是先对场景中每个点预测一个「抓取友好度」标量。这个标量反映的是如果机械臂末端以某个接近方向靠近该点成功抓取的概率有多高。训练时用大量仿真抓取标注做监督推理时只需要一次前向传播就能得到稠密评分场。拿到评分场之后系统会在高分区域采样种子点对每个种子点回归一组抓取参数接近方向、夹爪开合宽度、以及绕接近轴的旋转角。这组参数就是6-Dof抓取姿态的完整描述。相比逐候选评估的方法这种「先评分再回归」的两阶段设计把计算量从O(N×M)降到O(NM)N是场景点数M是候选抓取数。实际部署时点云通常来自深度相机或激光雷达。常见做法是把点云降采样到固定点数比如20000点归一化到单位球内再送入网络。降采样用FPS最远点采样保证空间覆盖均匀。归一化时记录质心和缩放因子后续要把预测的抓取姿态变换回原始坐标系。注意评分场的分辨率直接受输入点数影响。点数太少小物体上的高分区域会被稀释点数太多推理延迟线性增长。20000点是一个在精度和速度之间比较平衡的经验值。2.2 把推理服务封装成ROS2节点推理服务本身可以用Python写加载训练好的权重暴露一个服务接口。ROS2这边建议用Service而不是Topic因为抓取请求是同步的客户端发一帧点云等服务端返回抓取姿态列表。用Topic的话还得自己维护请求ID和超时反而麻烦。先定义服务接口。在ROS2包里建一个srv文件# srv/DetectGrasps.srv sensor_msgs/PointCloud2 input_cloud --- geometry_msgs/PoseArray grasp_poses float32[] grasp_scores然后写服务端节点import rclpy from rclpy.node import Node from sensor_msgs.msg import PointCloud2 from geometry_msgs.msg import PoseArray, Pose from your_pkg.srv import DetectGrasps import numpy as np class GraspnessServer(Node): def __init__(self): super().__init__(graspness_server) self.srv self.create_service( DetectGrasps, detect_grasps, self.handle_detect) # 加载模型只加载一次 self.model self.load_model(/path/to/weights.pth) self.get_logger().info(Graspness server ready) def load_model(self, path): # 实际加载逻辑根据你的框架来 import torch model torch.load(path, map_locationcuda) model.eval() return model def handle_detect(self, request, response): points self.cloud_to_numpy(request.input_cloud) # 降采样 归一化 points_norm, centroid, scale self.preprocess(points) # 推理 grasps, scores self.model.predict(points_norm) # 变换回原始坐标系 grasps self.denormalize(grasps, centroid, scale) response.grasp_poses self.to_pose_array(grasps) response.grasp_scores scores.tolist() return response def cloud_to_numpy(self, cloud_msg): # 根据点云格式解析常见是xyz float32 import sensor_msgs_py.point_cloud2 as pc2 pts pc2.read_points_numpy( cloud_msg, field_names(x, y, z)) return pts.astype(np.float32) def preprocess(self, points): # FPS降采样到20000点 from your_utils import fps_sample sampled fps_sample(points, 20000) centroid sampled.mean(axis0) centered sampled - centroid scale np.max(np.linalg.norm(centered, axis1)) normalized centered / scale return normalized, centroid, scale def denormalize(self, grasps, centroid, scale): # grasps形状 [K, 7]前3是位置后4是四元数 grasps[:, :3] grasps[:, :3] * scale centroid return grasps def to_pose_array(self, grasps): pa PoseArray() pa.header.frame_id camera_link for g in grasps: p Pose() p.position.x, p.position.y, p.position.z g[:3] p.orientation.x, p.orientation.y, \ p.orientation.z, p.orientation.w g[3:7] pa.poses.append(p) return pa def main(): rclpy.init() node GraspnessServer() rclpy.spin(node) rclpy.shutdown()这段代码的关键点有三个。第一模型在__init__里加载一次不要每次请求都重新加载否则单次推理延迟会从几十毫秒飙到几秒。第二cloud_to_numpy用sensor_msgs_py的read_points_numpy比手动遍历点云快一个数量级。第三归一化和反归一化必须成对出现质心和缩放因子要跟着抓取姿态一起变换回去否则抓取点会飘到场景外面。参数方面FPS采样点数20000是默认值如果你的场景物体特别小比如螺丝、芯片可以提到40000但推理时间会翻倍。缩放因子用最大范数而不是标准差是为了保证所有点都在单位球内网络训练时就是这么做的推理时必须一致。2.3 点云预处理里最容易忽略的坐标系问题深度相机输出的点云通常在相机坐标系下而机械臂规划需要的是基坐标系。很多人在推理服务里直接输出相机坐标系下的抓取姿态到了MoveIt2那边才发现姿态完全不对。正确做法是在ROS2里用TF2做坐标变换而且要在推理之前把点云转到基坐标系或者在推理之后把抓取姿态转到基坐标系。我一般倾向于在推理之前转点云。原因是Graspness模型对输入点的空间分布敏感如果点云在相机坐标系下归一化的质心和缩放因子会受相机安装位置影响换一个安装角度就得重新调参。转到基坐标系后归一化参数只跟场景本身有关更稳定。TF2变换的代码大概长这样from tf2_ros import Buffer, TransformListener from tf2_sensor_msgs.tf2_sensor_msgs import do_transform_cloud class GraspnessServer(Node): def __init__(self): super().__init__(graspness_server) self.tf_buffer Buffer() self.tf_listener TransformListener(self.tf_buffer, self) def transform_cloud(self, cloud_msg, target_frame): try: transform self.tf_buffer.lookup_transform( target_frame, cloud_msg.header.frame_id, rclpy.time.Time()) return do_transform_cloud(cloud_msg, transform) except Exception as e: self.get_logger().error(fTF failed: {e}) return Nonelookup_transform的第三个参数用rclpy.time.Time()表示取最新可用的变换。如果TF树还没建好这里会抛异常所以要在服务回调里做判空。常见做法是在launch文件里确保静态TF发布节点先启动或者加一个短暂的等待。注意do_transform_cloud会保留点云的字段结构但如果你用的是自定义点云格式变换后字段可能丢失。建议统一用sensor_msgs/PointCloud2标准格式字段只保留xyz和intensity。3. ROS2节点编排让感知、规划、执行各就各位3.1 用Component和CallbackGroup控制并发抓取系统里有三个主要节点推理服务端、抓取规划客户端、执行监控节点。如果全部用独立进程进程间通信的序列化开销会拖慢整体响应。ROS2提供了Component机制可以把多个节点加载到同一个进程里配合Intra-Process通信实现零拷贝。零拷贝的前提是发布者和订阅者在同一个进程并且QoS配置为SENSOR_DATA或SYSTEM_DEFAULT。对于点云这种大消息零拷贝能省掉一次内存拷贝延迟降低很明显。CallbackGroup用来控制回调的并发行为。推理服务端的回调是计算密集型的不应该和TF监听的回调抢线程。常见做法是给推理回调分配MutuallyExclusiveCallbackGroup给TF和状态发布分配ReentrantCallbackGroup这样推理在跑的时候TF还能正常更新。from rclpy.callback_groups import ( MutuallyExclusiveCallbackGroup, ReentrantCallbackGroup) class GraspnessServer(Node): def __init__(self): super().__init__(graspness_server) self.infer_cb_group MutuallyExclusiveCallbackGroup() self.tf_cb_group ReentrantCallbackGroup() self.srv self.create_service( DetectGrasps, detect_grasps, self.handle_detect, callback_groupself.infer_cb_group) self.tf_timer self.create_timer( 0.1, self.update_tf, callback_groupself.tf_cb_group)MutuallyExclusiveCallbackGroup保证同一时间只有一个推理回调在执行避免GPU显存竞争。ReentrantCallbackGroup允许TF更新和状态发布并发执行。如果你的推理服务用了多GPU可以把MutuallyExclusive改成Reentrant但要在代码里自己做显存锁。3.2 抓取姿态从服务端到MoveIt2的完整链路推理服务返回的是PoseArray里面可能有几十个候选抓取。不能直接全部丢给MoveIt2得先做筛选和排序。筛选条件通常包括抓取姿态是否在机械臂工作空间内、接近方向是否与场景无碰撞、夹爪开合宽度是否匹配物体尺寸。我一般会写一个独立的筛选节点订阅PoseArray输出排序后的PoseArray。排序依据是Graspness评分但还要叠加一个可达性惩罚用MoveIt2的/compute_ik服务快速检查每个姿态是否有逆解没有逆解的直接降权。class GraspFilter(Node): def __init__(self): super().__init__(grasp_filter) self.sub self.create_subscription( PoseArray, grasp_candidates, self.filter_callback, 10) self.pub self.create_publisher( PoseArray, grasp_filtered, 10) self.ik_client self.create_client( ComputeIK, /compute_ik) def filter_callback(self, msg): scored [] for i, pose in enumerate(msg.poses): score msg.scores[i] if hasattr(msg, scores) else 0.0 if not self.in_workspace(pose): continue ik_ok self.check_ik(pose) if not ik_ok: score * 0.3 # 降权但不完全丢弃 scored.append((score, pose)) scored.sort(keylambda x: x[0], reverseTrue) out PoseArray() out.header msg.header out.poses [p for _, p in scored[:10]] self.pub.publish(out) def in_workspace(self, pose): # 根据你的机械臂工作空间定义 p pose.position return (0.2 p.x 0.8 and -0.4 p.y 0.4 and 0.0 p.z 0.6) def check_ik(self, pose): # 同步调用IK服务实际用异步超时 req ComputeIK.Request() req.ik_request.pose_stamped.pose pose future self.ik_client.call_async(req) rclpy.spin_until_future_complete( self, future, timeout_sec0.05) return future.result() is not Nonein_workspace里的范围要根据你的机械臂实际工作空间改不要照抄。check_ik用同步调用是为了代码简洁实际部署时建议用异步超时否则一个卡住的IK请求会阻塞整个筛选节点。筛选后的姿态列表发给MoveIt2的/move_group动作接口。MoveIt2的规划请求里要设置allowed_planning_time和num_planning_attempts无序场景下建议allowed_planning_time2.0num_planning_attempts5给规划器足够的重试机会。3.3 执行阶段的夹爪控制与力反馈MoveIt2规划出轨迹后通过FollowJointTrajectory动作发给机械臂控制器。夹爪控制通常是独立的用GripperCommand动作或自定义的ROS2服务。抓取执行时先移动到预抓取姿态再直线接近抓取姿态最后闭合夹爪。力反馈很重要。无序场景里物体可能滑动纯位置控制容易把物体推走。常见做法是在夹爪闭合时监测电流或力矩超过阈值就停止闭合记录当前开合宽度作为实际抓取宽度。如果实际宽度远小于预期说明抓空了或者物体被推开了需要重新触发推理。def execute_grasp(self, grasp_pose): # 1. 移动到预抓取姿态 pre_grasp self.offset_along_approach( grasp_pose, -0.1) self.move_to_pose(pre_grasp) # 2. 直线接近 self.move_linear(grasp_pose) # 3. 闭合夹爪监测力反馈 self.gripper.close() start self.get_clock().now() while not self.gripper.is_closed(): if self.gripper.current() self.force_threshold: self.gripper.stop() break if (self.get_clock().now() - start).nanoseconds 2e9: self.get_logger().warn(Gripper timeout) break rclpy.spin_once(self, timeout_sec0.01) # 4. 抬起 self.move_linear(pre_grasp)offset_along_approach沿着抓取姿态的接近轴反方向偏移10厘米这是预抓取位置。move_linear用笛卡尔直线规划避免关节空间规划带来的不可预测路径。力阈值根据夹爪型号定一般设在额定力矩的60%到80%。注意夹爪闭合超时不要设太长2秒足够。超时后应该抬起并重新推理而不是继续等。无序场景里物体位置可能已经被碰变了继续执行原抓取姿态大概率失败。4. MoveIt2配置与运动规划从URDF到抓取轨迹4.1 机械臂URDF和SRDF里必须检查的几项MoveIt2的配置从URDF开始。URDF里要确保每个关节的limit标签有effort和velocity否则MoveIt2的规划器会报错。夹爪的关节要单独定义并且设置mimic标签让两个手指同步运动。SRDF里要定义group至少两个arm和gripper。arm组包含所有机械臂关节gripper组包含夹爪关节。还要定义end_effector把夹爪的基座link关联到arm组的末端。常见坑是end_effector的parent_link设错了。如果设成了arm组的最后一个link但夹爪基座和这个link之间还有固定关节MoveIt2计算IK时会忽略这个固定关节的变换导致抓取姿态偏移。正确做法是把parent_link设成夹爪基座linkgroup设成arm。!-- SRDF片段 -- group namearm chain base_linkbase_link tip_linktool0/ /group group namegripper joint namefinger_joint1/ joint namefinger_joint2/ /group end_effector namegrasp_ee parent_linktool0 groupgripper parent_grouparm/chain的tip_link是tool0这是机械臂法兰盘。夹爪通过固定关节连在tool0上。end_effector的parent_link也是tool0这样MoveIt2在计算IK时会自动把夹爪的TCP变换考虑进去。4.2 规划器参数RRTConnect和CHOMP怎么选MoveIt2默认用OMPL的RRTConnect。无序场景里障碍物多RRTConnect的随机采样可能导致规划时间波动很大。如果对规划时间敏感可以换用CHOMP或STOMP这两种优化规划器在复杂环境里更稳定但需要调参。RRTConnect的关键参数是range控制每次扩展的步长。默认值0.0表示自动计算通常等于机械臂最大臂展的10%。在杂乱场景里可以调小到5%增加采样密度但规划时间会变长。CHOMP的关键参数是planning_time_limit和optimization_time_limit。前者是总时间预算后者是优化阶段的时间。无序场景建议planning_time_limit3.0optimization_time_limit1.5。CHOMP对初始轨迹敏感如果初始轨迹穿过障碍物优化可能失败。常见做法是先用RRTConnect规划一条可行轨迹再用CHOMP做平滑。# ompl_planning.yaml planner_configs: RRTConnect: type: geometric::RRTConnect range: 0.0 CHOMP: type: optimization::CHOMP planning_time_limit: 3.0 optimization_time_limit: 1.5 use_stochastic_descent: trueuse_stochastic_descent开启随机下降能跳出局部最优但每次规划结果可能不同。如果要求可复现设为false。4.3 抓取姿态的碰撞检测与场景更新MoveIt2的碰撞检测依赖PlanningScene。无序场景里除了机械臂自身还要把桌面、相机支架、以及场景中的其他物体加进去。点云可以直接作为Octomap加入PlanningScene但Octomap的分辨率要调太细会导致碰撞检测变慢太粗会漏检。常见做法是用/planning_scene话题发布Octomap分辨率设0.01米。同时把已知的静态物体桌面、支架用CollisionObject加入避免Octomap把静态物体也体素化浪费计算资源。抓取姿态的碰撞检测要单独做。MoveIt2的/check_state_validity服务可以检查一个关节状态是否碰撞但抓取姿态是笛卡尔空间下的需要先做IK。更直接的方法是用FCL库自己写碰撞检测把夹爪的碰撞模型加载进去对每个候选抓取姿态做一次碰撞查询。from moveit_msgs.srv import GetStateValidity from moveit_msgs.msg import RobotState def check_grasp_collision(self, grasp_pose): # 先做IK ik_result self.compute_ik(grasp_pose) if ik_result is None: return False # 构造RobotState state RobotState() state.joint_group_names [arm] state.joint_group_positions [ik_result] # 调用碰撞检测 req GetStateValidity.Request() req.robot_state state req.group_name arm future self.validity_client.call_async(req) rclpy.spin_until_future_complete(self, future) return future.result().validcompute_ik用MoveIt2的/compute_ik服务返回关节角度。GetStateValidity检查这个关节状态是否与PlanningScene中的物体碰撞。注意joint_group_names和joint_group_positions要对应顺序不能错。注意Octomap更新频率不要太高1Hz足够。高频更新会导致PlanningScene频繁重建规划延迟增加。如果场景变化快可以只更新变化区域而不是全图重建。5. 避坑与排查无序抓取系统上线前必须过的坎5.1 推理服务返回空列表或全零评分现象服务端正常响应但grasp_poses为空或者grasp_scores全是0。原因最常见的是点云预处理出了问题。如果点云里包含NaN或InfFPS采样会失败返回空数组。另一个可能是归一化时缩放因子为0导致所有点变成NaN。解决在cloud_to_numpy之后加一步清洗用np.isfinite过滤无效点。归一化前检查scale是否大于1e-6如果太小说明点云退化了直接返回空结果并打日志。def preprocess(self, points): mask np.isfinite(points).all(axis1) points points[mask] if len(points) 100: self.get_logger().warn(Too few valid points) return None, None, None sampled fps_sample(points, 20000) centroid sampled.mean(axis0) centered sampled - centroid scale np.max(np.linalg.norm(centered, axis1)) if scale 1e-6: self.get_logger().warn(Degenerate cloud) return None, None, None return centered / scale, centroid, scale5.2 MoveIt2规划失败但IK有解现象/compute_ik返回了有效关节角度但/move_group规划失败报Unable to find a valid plan。原因IK有解只说明目标姿态在运动学上可达不代表存在无碰撞路径。无序场景里机械臂可能被桌面或其他物体挡住从当前状态到目标状态的路径全部被堵死。解决先检查PlanningScene里的碰撞物体是否过多。Octomap分辨率太细会把噪声也体素化导致自由空间被过度压缩。把分辨率从0.01调到0.02试试。另外allowed_planning_time设大一点给规划器更多时间探索。如果还是失败可以尝试从不同的预抓取姿态接近。同一个抓取姿态接近方向可以绕接近轴旋转换一个旋转角可能就绕开了障碍物。5.3 抓取执行时物体被推走现象夹爪闭合过程中物体滑动或翻倒抓取失败。原因接近方向不对或者夹爪闭合速度太快。Graspness预测的接近方向是统计最优但实际物体表面摩擦系数、重心分布可能让这个方向不成立。解决在预抓取位置加一个微调步骤用腕部相机或力传感器确认物体位置再修正抓取姿态。夹爪闭合速度降到额定速度的30%给物体适应时间。如果还是推走说明抓取姿态本身有问题应该换下一个候选抓取。5.4 ROS2节点启动顺序导致TF查询失败现象推理服务启动时报LookupException: Frame camera_link does not exist。原因TF树还没建好推理服务就开始处理请求了。ROS2的节点启动是并行的静态TF发布节点可能比推理服务晚启动。解决在推理服务的__init__里加一个TF等待循环或者用rclpy的spin_until_future_complete等TF可用。更简单的做法是在launch文件里用TimerAction延迟启动推理服务。def wait_for_tf(self, target_frame, timeout_sec5.0): start self.get_clock().now() while rclpy.ok(): try: self.tf_buffer.lookup_transform( target_frame, camera_link, rclpy.time.Time()) return True except Exception: pass if (self.get_clock().now() - start).nanoseconds \ timeout_sec * 1e9: return False rclpy.spin_once(self, timeout_sec0.1)5.5 零拷贝没生效导致延迟高现象点云传输延迟大推理服务收到点云时已经过了几百毫秒。原因零拷贝要求发布者和订阅者在同一个进程并且QoS匹配。如果推理服务和相机驱动在不同进程零拷贝不会生效。解决用ros2 component把相机驱动和推理服务加载到同一个容器进程。QoS设成SENSOR_DATAreliability为BEST_EFFORTdurability为VOLATILE。检查ros2 topic info的QoS是否匹配。ros2 component load /ComponentManager camera_driver \ image_transport::ImageTransport ros2 component load /ComponentManager graspness_server \ your_pkg::GraspnessServer加载后用ros2 component list确认两个节点在同一个容器里。如果不在零拷贝不会生效。6. 进阶技巧用评分场热力图做在线诊断与参数自整定Graspness评分场不只能用来选抓取姿态还能当诊断工具。把评分场投影到3D空间用RViz2的PointCloud2显示颜色从蓝到红表示评分从低到高。如果热力图集中在物体边缘而不是中心说明模型对边缘特征过拟合可能需要增加中心区域的训练样本。如果热力图弥散、没有明显峰值说明点云质量太差得检查相机曝光或滤波参数。我习惯在调试时开一个独立的RViz2窗口左边显示原始点云右边显示评分场热力图。抓取失败时先看热力图如果高分区域根本不在物体上那就是感知问题不用调规划参数。如果高分区域在物体上但抓取还是失败再查IK和碰撞检测。参数自整定可以用评分场的统计量做。计算评分场的均值和方差如果均值低于0.3说明场景里没有明显可抓区域应该触发重新扫描或调整相机角度。如果方差很小说明评分场区分度不够可以适当降低FPS采样点数让评分场更集中。def diagnose_score_field(self, scores): mean_score np.mean(scores) std_score np.std(scores) if mean_score 0.3: self.get_logger().warn( Low mean score, scene may be ungraspable) return rescan if std_score 0.1: self.get_logger().warn( Low variance, consider reducing FPS points) return adjust_fps return ok这个诊断逻辑可以做成一个ROS2服务规划节点在抓取失败后调用根据返回值决定是重新推理还是调整参数。实际跑下来mean_score 0.3触发重新扫描能避免大部分无效抓取尝试std_score 0.1触发FPS调整能提升小物体的抓取成功率。还有一个技巧是用评分场做抓取顺序规划。如果场景里有多个物体先抓评分最高的抓完重新推理再抓下一个。不要一次性规划所有抓取因为抓走一个物体后场景变了原来的抓取姿态可能不再有效。这个策略在密集堆叠场景里特别有用能减少碰撞和推挤。注意评分场热力图的可视化频率不要太高1Hz足够。高频发布大点云会拖慢RViz2反而影响调试效率。最后说个血泪教训我一开始图省事把推理服务和MoveIt2放在同一台机器上结果GPU推理和规划抢CPU规划时间从2秒涨到8秒。后来把推理服务拆到带GPU的工控机上通过ROS2跨机通信规划时间回到2秒以内。跨机通信记得设ROS_DOMAIN_ID一致QoS用RELIABLE点云传输用BEST_EFFORT。这个方案值不值得做取决于你的场景是否真的无序。如果物体都是规则摆放传统方法够用如果场景像工具箱一样乱Graspness加ROS2加MoveIt2这套组合是目前比较靠谱的开源路线。希望帮到你。本文还有配套的精品资源点击获取
返回列表