尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

手眼标定后手眼矩阵怎么用?从坐标变换链到抓取点求解

手眼标定后手眼矩阵怎么用?从坐标变换链到抓取点求解 标定完成的那一刻很多人的心情是先高兴后发懵。手眼矩阵出来了屏幕上几个4x4矩阵摆在那儿接下来呢到底该拿哪一个去算抓取点乘在左边还是乘在右边顺序反了会怎样我见过不少项目卡在这一步——标定做了三轮最后发现不是标定不准而是矩阵用错了。这玩意儿说难不难但确实需要先把坐标变换链理顺否则代码写出来就是薛定谔的抓取有时候准有时候歪得离谱。这篇内容专门解决标定完之后怎么用矩阵这个问题。我会从矩阵本身的物理含义讲起把从像素坐标到机器人基座坐标的完整变换链路拆开给出可以直接抄的代码再把调试过程中最容易翻车的几个坑挨个说清楚。适合刚做完手眼标定、正在写视觉引导程序的工程师也适合被为什么我算出来的点不对折磨了一周的兄弟参考。1. 标定完你手里拿到的到底是哪个矩阵先花点时间把概念掰扯清楚。手眼标定解决的问题是相机坐标系和机器人坐标系之间的相对位姿关系。很多文档把这个关系直接叫手眼矩阵但不同库、不同工具包返回的变量名五花八门——cam2gripper、gripper2cam、base2cam、cam2baseTensorFlow机器人学工具箱、Eigen、ROS的easy_handeye、OpenCV叫法各不相同。你如果不弄清楚每个变量名背后的数学方向直接抄代码大概率要踩坑。我建议统一用这个约定来思考T_A_B表示把B坐标系下的点变换到A坐标系下也就是p_A T_A_B * p_B对应到具体场景手眼标定分两种安装方式标定结果的物理含义完全不同这是最容易出问题的地方。第一种Eye-in-Hand眼在手上相机装在机械臂末端法兰上跟着机械臂一起动。这种情况下标定得到的是末端坐标系与相机坐标系之间的变换记为T_end_cam它描述的是相机装在末端什么位置、什么姿态。使用时的变换链是像素坐标 - 相机坐标 - 末端坐标 - 基座坐标 - 工具坐标第二种Eye-to-Hand眼在手外相机固定安装在工作区上方或者侧面不跟着机械臂动。这种情况下标定得到的是基座坐标系与相机坐标系之间的变换记为T_base_cam它描述的是相机相对于机器人基座装在什么位置、什么姿态。使用时的变换链直接缩短一环像素坐标 - 相机坐标 - 基座坐标 - 工具坐标以OpenCV的cv2.calibrateHandEye()为例它输出的变量名是R_cam2gripper、t_cam2gripper。函数名看起来是从相机到夹爪但实际含义是将相机坐标系下的点变换到夹爪末端坐标系下的旋转和平移。也就是说它返回的矩阵是T_end_cam。方向一定要在代码里验证不能靠函数名猜。提示不管你用哪个库拿到手眼矩阵后的第一件事不是写抓取代码而是写一个验证函数把一个已知点从相机系变换到机器人系看看结果合不合理。这一步能省掉后面几天的排查时间。还有一个容易忽略的点标定结果通常以旋转向量平移向量的形式保存比如OpenCV里的rvec和tvec。要用的时候必须通过罗德里格斯变换cv2.Rodrigues把旋转向量转成3x3旋转矩阵再拼成4x4齐次变换矩阵。很多人直接拿rvec当矩阵用结果自然是乱的。2. 手动把坐标穿起来从像素到基座的一次完整变换理解了矩阵是什么接下来就是把整条链捋顺。手眼矩阵不是单独用的它只是坐标变换链中的一个环节。最核心的逻辑是——点当前在哪个坐标系就往目标坐标系一层一层变换每层变换用一个4x4齐次矩阵顺序不能乱方向不能反。我用Eye-in-Hand举一个最常见的完整链路假设你用的是3D相机能直接读出某个像素对应的深度值变换步骤数学表达数据来源作用像素坐标 - 相机坐标p_cam相机内参K、畸变系数D、深度值去掉镜头畸变算出点在相机坐标系下的3D位置相机坐标 - 末端坐标p_end T_end_cam * p_cam手眼标定结果把点从相机系挪到机械臂末端法兰系末端坐标 - 基座坐标p_base T_base_end * p_end机器人控制器给出的当前末端位姿把点从法兰系挪到机器人基座系基座坐标 - 工具坐标p_tool T_tool_base * p_baseTCP标定/机器人控制器把抓取点转换到工具坐标系方便下发抓取指令这里面有两个非常容易被搞反的环节。第一个是T_base_end的方向。很多机器人的API返回的是末端在基座坐标系下的位姿也就是T_base_end直接可以乘。但有的协议或者旧版本驱动返回的是基座在末端坐标系下的位姿也就是T_end_base。如果你不确定就随便读一个当前位姿看平移分量如果平移分量约等于末端法兰中心在机器人世界坐标系的坐标那就是T_base_end如果某个分量看起来明显不对比如Z轴是负的多半是反了需要取逆。第二个是整条链的乘法顺序。矩阵乘法不满足交换律T_A_B * T_B_C可以把C系下的点变到A系但如果你写成T_B_C * T_A_B结果毫无物理意义。我用一个换乘地铁的类比帮你记你在10号线车厢里相机系要出站到地面基座系必须先坐10号线到换乘站末端系再换乘1号线到地面站基座系。你不能从10号线车厢直接跳到地面必须经过换乘站。手眼矩阵就是那个换乘通道它本身不产生坐标只是把点从一个参照系搬到另一个参照系。换一个理解方式这就像穿衣服。你从里到外的顺序是内衣相机坐标- 衬衫末端坐标- 外套基座坐标。你要把内衣上的一个点映射到外套外表面的位置必须先穿衬衫再穿外套顺序反了衣服就穿不进去。在实际的固定拍照场景里很多人会做一个简化机械臂每次移到同一个拍照位然后相机拍照。因为拍照时末端位姿固定不变T_base_end就变成一个常量。这时候你可以把T_base_end * T_end_cam预先乘好得到一个相机到基座的合成矩阵运行时直接用这个合成矩阵能省一步运算。但要注意一旦机械臂换了拍照位或者末端在运动中拍照这个合成矩阵就必须换成当前时刻的T_base_end否则算出来的点全是错的。3. 代码里怎么落地抓取点的完整求解链路概念理顺了代码其实很简单。直接给一份可以当作起步模板的Python实现基于OpenCV和NumPy适配Eye-in-Hand结构。核心就三步像素去畸变得到归一化坐标结合深度得到相机系3D点然后沿着变换链一路乘到基座系。import numpy as np import cv2 def build_pose_matrix(rvec, tvec): 把旋转向量平移向量拼成4x4齐次矩阵 R, _ cv2.Rodrigues(rvec) T np.eye(4) T[:3, :3] R T[:3, 3] np.asarray(tvec).reshape(3) return T def pixel_to_robot(u, v, depth, camera_matrix, dist_coeffs, T_end_cam, T_base_end_current): 将像素坐标(u, v) 深度值depth转换为机器人基座坐标系下的3D点。 参数说明: camera_matrix: 相机内参矩阵 K3x3 dist_coeffs: 畸变系数 [k1, k2, p1, p2, k3, ...] T_end_cam: 手眼标定结果相机系-末端系 T_base_end_current: 拍摄该图像时机器人末端在基座系下的位姿 # 1. 像素坐标 - 去畸变后的归一化相机坐标 # 注意undistortPoints返回的是归一化平面坐标无内参 pts np.array([[[float(u), float(v)]]], dtypenp.float32) undist cv2.undistortPoints(pts, camera_matrix, dist_coeffs, Pcamera_matrix) x_norm, y_norm undist[0][0] # 2. 结合深度值得到相机坐标系下的3D点 # 这个深度值必须沿着相机光轴方向单位与标定板/机器人一致 p_cam np.array([x_norm * depth, y_norm * depth, depth, 1.0]) # 3. 相机系 - 末端系 p_end T_end_cam p_cam # 4. 末端系 - 基座系 # 如果T_base_end_current已经是基座看到末端的位姿直接乘 # 如果你手里是末端看到基座的位姿T_end_base需要取逆 p_base T_base_end_current p_end return p_base[:3]这段代码里有一个容易被忽略的细节cv2.undistortPoints的第二个参数是畸变系数第三个参数P如果是相机内参矩阵返回的就是去畸变后的像素坐标如果不传P默认返回归一化平面坐标。上面代码里我传了Pcamera_matrix但后面又直接乘了depth——这里有个陷阱。让我把逻辑再理一遍。如果Pcamera_matrix返回的是去畸变后的像素坐标那x_norm其实还是像素值直接乘深度不对。更稳妥的写法是不传P拿归一化坐标再乘深度。修正后的代码应该是undist cv2.undistortPoints(pts, camera_matrix, dist_coeffs) # 不传P得到归一化坐标 x_norm, y_norm undist[0][0] # 相机系下的3D点注意这里假设深度就是Z值 p_cam np.array([x_norm * depth, y_norm * depth, depth, 1.0])为什么要这样因为3D相机读出的深度值本质上是物体表面到相机光心沿光轴的距离也就是相机坐标系下的Z坐标。归一化坐标(x_norm, y_norm)乘上Z才能还原出相机系下的X和Y。如果你直接拿像素坐标乘深度结果会带上内参的缩放整个点在相机系下的位置就是错的后面再怎么变都救不回来。另一个常见问题是单位。手眼标定时用的标定板尺寸单位是毫米还是米机器人位姿的单位是毫米还是米深度值又是毫米还是米这三者必须统一。很多项目就死在单位上——深度读出来是毫米机器人位姿是米标定板尺寸又用了厘米最后算出来的点偏差几个数量级。我习惯全部统一成毫米因为工业机器人示教器上看到的坐标通常默认毫米。再往下写就是结合具体视觉检测的完整流程。比如你识别到一个工件的中心像素(u, v)3D相机给出该点的深度depth当前机械臂末端位姿T_base_end_current由机器人实时上报手眼矩阵T_end_cam是标定好的常量# 假设已经检测到目标像素和深度 u, v 320, 240 depth_mm 812.5 # 读取机器人当前末端位姿注意这里用你机器人SDK返回的4x4矩阵 T_base_end_current robot.get_current_end_pose() # 计算基座系下的抓取点 target_base pixel_to_robot(u, v, depth_mm, K, D, T_end_cam, T_base_end_current) print(目标点在基座坐标系下的位置:, target_base)拿到target_base之后还要做一步——如果机械臂末端装的是气动夹爪或者吸盘夹爪中心往往不等于末端法兰中心这时候需要减掉工具坐标系的偏置或者把抓取点先变换到工具坐标系再下发。这个偏置来自TCP标定是另一个标定任务但很多人把它忘了导致视觉算得准一抓就偏一个固定量。4. 最容易翻车的几个点手眼调试的排查与验证矩阵用了、代码写了但结果不对这时候怎么排查我总结了一套排查顺序按照从简单到复杂排列大部分问题都能在这个顺序里定位到。第一查手眼矩阵方向。这是最高频的翻车点。把OpenCV返回的R_cam2gripper构建成矩阵后先做个自检取一个已知的相机系坐标原点也就是p_cam [0, 0, 0, 1]乘入手眼矩阵看得到的p_end是不是等于手眼矩阵的平移向量。如果不是说明你构建矩阵时把R和t拼错了。然后用示教器把机械臂末端移动到一个已知位置读取T_base_end再用手眼矩阵把相机原点变换到基座系看这个点是否落在机械臂末端附近。如果偏得很远八成是T_end_cam和T_cam_end搞反了试试取逆再做一遍。第二查T_base_end的方向。就像前面说的有的API返回T_base_end有的返回T_end_base。判断方法很简单把当前末端位置打印出来看看平移分量的数值是否和示教器上显示的世界坐标一致。如果Z值明显有正负号问题或者X/Y对不上把矩阵取逆再试。第三查深度值的物理含义。3D相机的深度值不一定是沿光轴的Z坐标。有些相机返回的是物体到相机平面的垂直距离有些是沿着每条光线的距离还有的ToF相机有非线性误差。如果你发现近距离准、远距离偏或者图像边缘偏先检查深度单位再用一个已知高度的方块做实验看看不同距离下深度读数的偏差规律。第四查识别本身有没有误差。手眼矩阵只解决坐标映射问题不解决识别不准问题。如果你的像素坐标本身偏了5个像素那手眼矩阵算得再准也没用。算一下假设工作距离300mm相机分辨率1920x1080视场大概400mm宽那么一个像素对应的物理尺寸约0.2mm5个像素就是1mm。如果识别算法有亚像素精度问题这个误差还会更大。所以在排查时先确保你用的是一个特征非常明确的点——比如标定板角点、圆形Mark点而不是边缘模糊的工件轮廓。第五查机器人本身的绝对定位精度。这是一个很多人忽略的大坑。手眼标定假设机器人反馈的位姿是准确的但实际工业机器人的绝对定位精度通常在±0.5mm到±2mm之间取决于品牌和负载。如果你发现视觉算出来的点在某个区域准、在另一个区域偏而且偏的方向和幅度有规律很可能是机器人绝对定位误差在作怪。这时候可以做一次机器人坐标系-相机坐标系联合标定来补偿或者用相对抓取策略先移到拍照位视觉引导时只做小范围偏移调整。我还整理了一个症状对照表方便你快速定位问题现象最可能的原因检查方法抓取点整体偏移一个固定矢量TCP未标定或手眼矩阵平移方向反了用示教器对已知点比较偏移方向和大小距离越远偏差越大手眼矩阵旋转方向反了或旋转向量转矩阵时出错在近处和远处分别验证看偏差是否随距离增大只在某个方向偏相机安装松动或标定时数据采集方向单一锁紧相机重新采集包含多方向的数据标定换一个位置就乱机器人位姿用错时刻或单位不一致确认拍照时读取的末端位姿和图像帧是同一时刻近距离准、远距离不准深度相机远距离精度差或畸变模型不匹配用不同距离的标定板验证深度精度总是偏一个角度旋转矩阵与平移向量拼接错误用相机原点自检法检查矩阵构建排查的时候记住一个原则不要同时改两个变量。很多人一急就同时重新标定手眼、重新标定TCP、换识别算法结果问题反而越来越乱。一次只改一个东西改完就验证这样才能快速定位。验证方法我推荐两个简单粗暴但非常有效。第一个是针尖对点法在机械臂末端装一根尖锐的针或者用示教器自带的校准针把针尖对准一个固定尖点比如标定板角点、工件尖角记录末端位姿。然后用相机识别这个尖点的像素坐标通过你的变换链算出一个基座系坐标再让机械臂移动到算出来的位置看针尖是否落在那个尖点上。如果偏移在2mm以内说明整条链是通的。第二个是棋盘格重投影法拍照时放一块张正友标定板机械臂停在某个位置记录此时的末端位姿和图像。用标定板位姿估计cv2.solvePnP算出标定板原点在相机系下的坐标再用你的变换链把它变换到基座系和机器人示教器中标定板原点的实际位置对比。这个方法不依赖识别直接对比的是几何关系精度更高适合做定量验证。5. 从能算到能用手眼矩阵在真实场景里的进阶玩法很多项目做到了能算点就停了但实际要稳定落地还有几个进阶问题值得说。固定拍照位模式。这是工业场景最常用的省心方案。机械臂先运动到一个固定的拍照位停下相机拍照视觉系统算出目标在基座系下的坐标机械臂再过去抓。好处是拍照时机器人是静止的T_base_end恒定不变可以预先算好合成矩阵不存在帧同步问题精度最稳定。缺点是节拍变慢因为拍照需要机械臂专门跑一趟。如果节拍要求高就需要动态抓取。动态抓取的关键在帧同步。想象一下机械臂边移动边拍照或者传送带上的工件边运动边抓取。这时候手眼矩阵没变变的只是T_base_end——你必须保证读到的机器人位姿和图像曝光时刻是同一个时刻。否则机械臂已经往前走了一段你还在用之前的位姿算结果自然偏一大截。工业上常见的做法有两种一是硬同步用硬件触发信号把图像曝光时刻锁存同时从机器人控制器读出该时刻的位姿二是软同步用机器人实时位姿流时间戳插值反推出图像曝光时刻的位姿。如果用的是软同步时间戳一定要对齐这个问题排查起来特别头疼。运动补偿的两种思路。动态抓取还有一种做法是不做帧同步而是做运动补偿。先算出差值机械臂在图像帧时刻的位姿和最近上报的位姿之间存在一个小的变换把这个变换补偿到抓取点坐标上。说白了就是位置预测。传送带场景更常见的是标定一个传送带编码器到基座坐标系的变换工件在传送带上移动时通过编码器读数实时更新工件在基座系下的位置相机只负责确认工件类型和初始位置。这种方案处理得好的话节拍可以做到很快但标定和调试复杂度也明显上升。多相机场景的坐标系统一。很多工作站不只装一台相机比如双侧各装一台Eye-to-Hand相机或者顶部一台大视野相机加机械臂上一台小视野Eye-in-Hand精定位相机。每台相机都有自己的手眼矩阵视觉系统会输出各自坐标系下的结果。这时候必须在系统层面统一坐标系——要么全部转换到基座系再合并要么定义一个中间世界坐标系把每台相机的结果统一映射过去。我见过一个项目两台相机标定各自都没问题但一联动就打架后来查出来是两台相机对标定板的尺寸用了不同单位一个毫米一个厘米换算关系差了十倍。何时重新标定。手眼矩阵不是标一次就一劳永逸。相机松动、机械臂碰撞、重新拆装相机、更换相机镜头、甚至机械臂长期使用后减速机磨损导致位姿反馈漂移这些都会让手眼矩阵失效。建议每次项目导入时做一次针尖对点验证每隔一两个月或者每次现场维护后重新标定一次。如果发现抓取精度逐渐变差先检查机械部件有没有松动再考虑重新标定。记录每次标定的误差日志。标定过程本身会有重投影误差OpenCV的calibrateHandEye没有直接给出一个统一的误差值但你可以通过回代验证来计算用标定得到的手眼矩阵把每一组标定数据的相机位姿变换到末端位姿和实际记录的末端位姿对比计算位置误差和角度误差。把这个误差记录到日志里长期积累下来你会发现它能直观反映整套系统的健康状态。误差突然变大说明有东西变了值得去查。我做视觉引导项目这几年最深的体会是手眼标定本身并不难难的是标定完之后你能不能自信地让机械臂抓起来。建议你拿到手眼矩阵的第一天不要急着写完整的抓取流程先花半天时间做一次针尖对点验证把变换链上的每一步都在代码里打印出来亲眼看着误差在一个可接受的范围内后面就顺了。这个慢半拍的验证习惯能帮你建立对整套系统的直觉——哪一环出了问题你大概能猜到是哪里而不是黑盒式地算了不对重新标定试试。
返回列表