发布于 

手眼标定与 3D 抓取:眼手协同实战

产线上机械臂抓不准的锅,90% 要扣在标定头上——不是相机不行,是"手"(机械臂)和"眼"(相机)的关系没量准。相机看到的是"目标在相机坐标系下的坐标",机械臂要的是"目标在基座坐标系下的坐标",中间隔着的那个变换没标准,视觉坐标换算到机器人坐标就会整体跑偏,重复精度 0.02 mm 的机械臂照样一把抓空。

这篇的主线只有一句话:**相机装在手上还是装在旁边,决定标定方程的两种解法,也决定误差往哪走。**想通这一句,标定流程、TF 结构、排查思路就全都顺理成章了。

一、先分清两种眼睛

手眼标定(Hand-Eye Calibration)解的是经典的 AX = XB 方程:已知若干组"机器人自己动了多少"和"相机看到标定板动了多少",求出相机与机械臂之间那个固定的未知变换 X。数学上它是同一个方程,但两种安装方式会让 A 和 B 取完全不同的东西:

  • Eye-in-Hand(眼在手上):相机固定在法兰/工具上,跟着机械臂一起动。标定板静止在桌面上,动的是机械臂。此时 A = T_toolA_toolB⁻¹(两次末端位姿之差),B = T_base_board(基座到标定板),解出的 X 是 T_tool0_camera(tool0 到 camera_link)。手动机、眼随手动。
  • Eye-to-Hand(眼在旁边):相机固定在三脚架或支架上俯视工作区,机械臂带着标定板动。此时 A = T_base_toolA_toolB_base(基座系下末端位移),B = T_boardA_boardB(标定板位移),解出的 X 是 T_base_camera(基座到相机)。手动机、眼旁观。

不用管公式怎么推,记住"谁动机理"就够了:动的是相机,解"手→眼";动的是标定板,解"地→眼"。

维度 Eye-in-Hand Eye-to-Hand
视野 随臂移动,越伸越看得清 固定视野,看得远
精度 工作距离短,放大误差小 靠分辨率撑,远距易糊
标定难度 高,需同步采集位姿 中,支架易保持稳定
典型场景 无序抓取、多工位转移 上料下料、固定工位

**安装方式还决定误差往哪走:**Eye-in-Hand 的相机离目标近,深度误差被工作距离放大的比例小,但机械臂关节误差直接混进相机位姿;Eye-to-Hand 固定不动、无关节耦合,可视野是死的,距离一远像素误差换算成毫米就放大。所以近距离精细抓取优先眼在手上,大场景监管优先眼在旁边——没有更好的安装,只有更合适的安装。

安装环节还有两个常被忽略的物理细节,踩过的人都懂:一是相机支架的刚性——悬臂太长或钣金太薄,机械臂急停时的振动会让相机相对法兰微移,标定结果悄悄失效,表现为"设备运行一周后抓取精度缓慢劣化",重启又能好一阵,其实是支架松了;二是线缆的走线——拖链里未固定的网线反复弯折拉扯相机接头,同样会造成微米到毫米级的位移。标定精度的前提是"手眼关系刚性不变",这个前提是结构件给的,不是算法给的。

二、标定流程:采集是九成功夫

有了方程,剩下的全是让方程"可解且解得准"的活。以 easy_handeye2 的流程为例(MoveIt2 手眼标定插件是同一套底层逻辑,只是包在 planning 里):

  1. 相机内参标定(前置):棋盘格内参,重投影误差控制在 0.5 px 以内才算合格,内参歪了后面全歪;
  2. 采样 15–20 个姿态:机械臂带着相机(或标定板)摆不同位姿,每个位姿记录 T_base_tool 和检测到的棋盘角点;
  3. 求解 AX = XB:easy_handeye2 内置 Tsai-Lenz、Park、Daniilidis 等经典算法,推荐先用 Tsai-Lenz 跑通;
  4. 残差验证:看每个采样点的重投影误差散点,剔除离群点后重解。
  • 采样姿态的讲究比算法选择重要得多:

  • 角度必须拉开:三个旋转轴各至少 ±20°,全是小角度微调会让 AX = XB 的旋转部分接近零,方程病态、解不稳定;

  • 不能全共面:所有姿态的棋盘格中心不能落在同一平面附近,否则平移分量欠约束,Z 方向会漂;

  • 棋盘格充满视野 1/3 以上:角点检测亚像素精度才有保证,太远时角点成像只有几个像素,误差直接翻倍。

摆姿态的实操口诀:想象棋盘格法线指向一个球面的不同经纬度,15 个姿态尽量均匀撒在球面上,同时机械臂保持在舒适工作区间内、棋盘格不出视野不脱焦。easy_handeye2 的采样界面会实时显示已采姿态的分布,盯着球面点撒匀即可,比背参数表管用。

**重投影误差是及格线:**求解完成后用标出的 X 把所有位姿的棋盘格投影回图像,平均误差 < 2 px 及格,< 1 px 良好;单点 > 4 px 的采样直接踢掉重摆。别迷信求解器——垃圾进垃圾出,15 个好姿态胜过 40 个烂姿态。

求解算法本身怎么选?Tsai-Lenz 1989 年的经典做法是先把旋转和平移分开解:用旋转部分的性质解出 X 的旋转,再回代解平移,两步走、速度快,对姿态分布要求高;Park 和 Daniilidis 是后续改进,用李群/对偶四元数把旋转平移联合求解,姿态分布稍差时更稳。**工程上的诚实建议:姿态采得好,三个算法结果差在 0.1 mm 以内;姿态采得差,换哪个算法都救不回来。**所以调算法不如多摆五个姿态。

标定频率也常被问:多久重标一次?判断标准不是日历而是事件——发生碰撞、换过相机/镜头、支架受过冲击、抓取偏差出现"随位姿变化"的模式,四者任一命中就重标。产线例行公事的月度重标,多数时候是在给心理安全感打工。

三、TF 树固化:把标定结果焊死

标定出的 X 是一个静态变换,写进 TF 树的方式就是 static_transform_publisher。TF 树的结构取决于安装方式:

Eye-in-Hand 的 TF 链:
world
└── base_link            (机械臂基座)
    └── link_1 ... link_6
        └── tool0        (法兰)
            └── camera_link    ← 标定解 X = T_tool0_camera
                └── camera_optical_frame

Eye-to-Hand 的 TF 链:
world
├── base_link            (机械臂基座)
│   └── link_1 ... link_6
│       └── tool0
└── camera_link          ← 标定解 X = T_base_camera
    └── camera_optical_frame

launch 文件里固化(数值替换成标定输出):

<!-- Eye-in-Hand:tool0 -> camera_link -->
<node pkg="tf2_ros" exec="static_transform_publisher"
      args="--x 0.052 --y -0.031 --z 0.118
            --qx 0.707 --qy 0.0 --qz 0.0 --qw 0.707
            --frame-id tool0 --child-frame-id camera_link" />
# Python 写法(lifecycle 场景更灵活)
from tf2_ros.static_transform_broadcaster import StaticTransformBroadcaster
from geometry_msgs.msg import TransformStamped

msg = TransformStamped()
msg.header.frame_id = "tool0"
msg.child_frame_id = "camera_link"
msg.transform.translation.x = 0.052
msg.transform.translation.y = -0.031
msg.transform.translation.z = 0.118
# 旋转同理填 quaternion,来自标定结果的旋转矩阵转四元数
StaticTransformBroadcaster(node).sendTransform(msg)

两个工程细节:四元数用 scipy.spatial.transform.Rotation.from_matrix(T[:3, :3]).as_quat() 转换;camera_optical_frame 的光学坐标系朝向是 Z 向前、X 向右、Y 向下,与 ROS 的 X 前 Y 左 Z 上相反,漏了这层旋转,点云会整体镜像,表现为"抓取点总在真实位置对称侧"——这是新手排查半天的经典坑。

TF 树的正确性验证也有懒人办法:RViz 里把所有 frame 打开,肉眼检查三条铁律——camera_link 必须长在 tool0(或 base_link)上不能悬空;机械臂动的时候点云必须"钉"在场景里不动(动的是相机,点云世界里物体不动);用 TF2 的 lookup_transform 算一遍 tool0 到 camera_link 的距离,和卷尺量的支架尺寸对得上。三条全过,TF 这关才算焊死。

TF 树固化之后,还有一件小事顺手做掉:把标定日期、重投影误差、采样数写进 launch 文件注释或一个 calibration_info.yaml。半年后抓取精度飘了,第一件事就是翻出这份档案——上次标定误差是多少、什么时候标的,心里立刻有底。工业系统的可维护性,一半靠日志和档案,不靠老师傅的记忆。

四、3D 感知抓取:从点云到抓取位姿

深度相机的选型直接决定点云质量的上限,三条主流路线各有脾气:

  • 结构光(RealSense D435/D455):近距 0.3–3 m 精度毫米级,但红外结构光在阳光下或多台同用时互相干扰;
  • ToF:测距范围大、帧率高,多机互不干扰,但角落处的多路径反射会出鬼影点;
  • 主动红外双目:折中方案,弱纹理物体上靠红外散斑补特征。

室内产线无日照直射、工作距离 1 m 以内,结构光是性价比默认解;户外物流场景换 ToF。点云管线的滤波再好,也救不回传感器物理上限之外的噪声。

标定焊死之后,视觉管线才能跑起来。深度相机输出的原始点云几十万点,直接喂给位姿估计既慢又脆,标准管线是"滤波 → 法线 → 位姿"三步:

import pclpy
from pclpy import pcl

# 1. 直通滤波:只留工作台上方 0.2~0.8 m 的点云
cloud = pcl.io.loadPCDFile("scene.pcd")
crop = cloud.make_cropbox()
crop.set_min(-1.0, -1.0, 0.2)
crop.set_max(1.0, 1.0, 0.8)
work_cloud = crop.filter()

# 2. 体素降采样:5 mm 格子,点数砍一个数量级
vox = work_cloud.make_voxel_grid_filter()
vox.set_leaf_size(0.005, 0.005, 0.005)
down = vox.filter()

# 3. 统计离群点去除:邻域均值滤波踢掉飞点
sor = down.make_statistical_outlier_filter()
sor.set_mean_k(50)
sor.set_std_dev_mul_thresh(1.0)
clean = sor.filter()

# 4. 表面法线:pcl_normal_estimation
ne = clean.make_normal_estimation()
ne.set_k_search(30)
normals = ne.compute()

# 5. 估计抓取位姿:法线反向即接近方向,SACSegmentation 拟合平面
seg = clean.make_segmenter()
seg.set_model_type(pcl.SACMODEL_PLANE)
seg.set_method_type(pcl.SAC_RANSAC)
seg.set_distance_threshold(0.01)
inliers, coefficients, _ = seg.segment()

得到目标位姿后,把环境喂给 MoveIt2 的 planning scene,让规划器知道哪里是桌面、哪里是障碍:

from moveit.planning import MoveItPy
from moveit.core.robot_state import RobotState

moveit = MoveItPy("moveit_py")
panda = moveit.get_planning_component("panda_arm")
scene = moveit.get_planning_scene_monitor()

# 生成 OctoMap 碰撞环境:点云投影成占据栅格
scene.apply_planning_scene_msg_with_octomap_from_point_cloud(
    point_cloud_msg, frame_id="camera_optical_frame"
)

# 设置抓取目标位姿(来自位姿估计)
panda.set_start_state_to_current_state()
panda.set_goal_state(
    pose_goal={
        "panda_hand": target_pose_stamped,  # 视觉估出的抓取位姿
    }
)
plan = panda.plan()

OctoMap 的价值在于"实时":每帧点云更新占据栅格,工件被挪走、新料箱进场,碰撞环境跟着变,规划器不会按五分钟前的旧地图撞上去。滤波管线的顺序也有讲究——先直通裁剪再降采样,把算力花在工作区内的点上;统计滤波放最后,因为离群点在降采样后更容易被识别成孤立小簇。

位姿估计的输出也要做一层校验再交给机械臂,裸信视觉是产线大忌。三个实用闸门:抓取位姿与重力方向夹角——法线估计出的接近方向若偏离竖直 > 30°,多半是点云噪声或遮挡导致的假平面,丢弃;点云支撑数——拟合出平面的内点数少于阈值(比如 200 点),说明目标太小或太远,结果不可信;与上一帧一致性——工件理论上静止,位姿帧间跳变 > 10 mm 必有其一出错,触发重测。三个闸门把"视觉偶尔抽风"挡在机械臂动起来之前,代价只是几行判断代码。

五、抗遮挡与柔顺抓取:抓偏了抓歪了怎么办

实际产线不会像实验室那样摆得端端正正。遮挡的对策是提前把托盘边缘、料箱壁写进碰撞环境(planning scene 里的 collision object),规划时自然绕开;柔顺的对策是笛卡尔空间路径规划——不走关节空间插值,而是直线/圆弧轨迹精确控制末端姿态。选笛卡尔的根本原因:关节空间插值出来的末端轨迹是条看不出规律的曲线,伸进料箱抓工件时这条曲线可能扫到箱壁;笛卡尔直线则保证下探路径全程竖直,行为可预期。代价是计算量大(逆运动学逐点求解)且可能碰到关节极限,所以只在最后十几厘米的下探段用笛卡尔,长距离移动仍走关节空间规划,各取所长。

from pymoveit2 import MoveIt2Servo
from moveit.planning import cartesian_path

# 笛卡尔直线插值:竖直下探抓取,避免斜插碰翻相邻工件
(cart_plan, fraction) = panda.compute_cartesian_path(
    position_target=grasp_position,
    orientation_target=grasp_orientation,
    max_step=0.01,          # 1 cm 步长
    jump_threshold=0.0,
)
if fraction < 0.95:
    node.get_logger().warn(f"只完成 {fraction:.0%},路径被遮挡截断")

最后是绕不开的排查环节——抓取偏差三个嫌疑人,按排查成本从低到高:

  1. 标定嫌疑:抓取点偏移量随机械臂位姿变化而变化(换个工作位置偏差方向就变)→ 手眼标定漂了,重跑第二节流程;
  2. 手眼延迟嫌疑:偏差方向和传送带/工件运动方向一致,工件速度一快偏差就大 → 相机触发到图像可用之间延迟没补,参考多传感器硬件时间同步里的硬件触发方案;
  3. 夹爪回差嫌疑:每次闭合姿态都差一点,但偏差量固定不随位姿变 → 齿轮回差/气压不稳,机械问题,换标定排查思路。

实际排查可以更快:拿一个工件放在视野中心正下方,让机械臂竖直下去抓,再移到视野边缘重复一次。中心准、边缘偏 → 标定嫌疑(视野边缘畸变放大);两个位置都偏同一个方向和量 → 夹爪嫌疑;工件静止时准、放在动平台上偏 → 延迟嫌疑。两分钟三次抓取,嫌疑人基本锁定,再针对性深挖。

**抓歪了抓空了,先看偏差的模式,再动标定。**三个嫌疑人的"作案特征"完全不同,无脑重标定只会浪费半天还找不着真凶。另一个隐性事实是夹爪本身也有重复精度:气动平行夹爪 0.1–0.5 mm、电动伺服夹爪 < 0.05 mm,预算允许时上电动夹爪,能把第三嫌疑人直接从排查名单里划掉。

六、软硬联动

手眼标定是"眼"这头的地基,机器人系统的另外两块拼图在:

参考文献

  1. Tsai, R. Y., & Lenz, R. K. "A New Technique for Fully Autonomous and Efficient 3D Robotics Hand/Eye Calibration." IEEE Transactions on Robotics and Automation, vol. 5, no. 3, pp. 345–358, 1989.(AX = XB 闭式解经典)
  2. Hörmmann, C., et al. "easy_handeye2: Simple, Straight-Forward Hand-Eye Calibration for ROS 2." GitHub Repository, 2023.(本篇采样与求解流程参考)
  3. Rusu, R. B., & Cousins, S. "3D is Here: Point Cloud Library (PCL)." IEEE International Conference on Robotics and Automation (ICRA), 2011.(滤波/法线/分割模块)
  4. MoveIt2 Documentation. "MoveIt Py Tutorial and Planning Scene Tutorial." MoveIt.ros.org, 2024.(planning scene 与 OctoMap 接口)
  5. Chen, H., et al. "A Survey on Hand-Eye Calibration for Robot Vision." Robotics and Computer-Integrated Manufacturing, 2021.(两种安装方式的误差分析对比)