做视觉引导抓取项目,最让人头大的一幕往往是这样:相机明明清清楚楚看到了工件位置,把像素坐标发给机器人,机器人却死活抓不准,偏几个毫米甚至偏了一两个厘米。换了镜头、调了光源、升级了相机,问题依旧。直到有人问了你一句“你做手眼标定了吗”,你才意识到,压根没建立相机坐标和机器人坐标之间的关系。这篇就是围绕Halcon里的九点标定法,把这套关系从原理到代码、从现场操作到精度排查完整讲一遍。无论你是刚接手视觉项目的自动化工程师,还是正在为机器人抓取定位发愁的技术爱好者,看完都能自己动手标出能用的结果。
1. 先解决两个坐标系的问题:手眼标定的本质和两种部署方式
1.1 视觉给的是像素,机器人要的是坐标,中间缺个“翻译”
先想清楚一个最基础的问题:相机看到的图像,本质上是一个二维的像素矩阵。图像里一个工件中心在(1358, 726),这只是说这个点在图像第726行、第1358列,和机器人该往哪个方向走多少毫米没有任何直接关系。
而机器人运动时用的是自己的机械坐标系,单位是毫米,原点通常在机器人底座或某个固定的工具零点。要把图像的像素位置变成机器人能用的(X, Y, Z)坐标,必须知道两个坐标系之间怎么换算。这个换算关系,就是手眼标定要解的问题。
在Halcon语境下,九点标定法做的事情很直白:在一张覆盖工作区域的图像上取9个已知位置的特征点,一边记录它们的像素坐标,一边对应记录机器人实际到达这些点时的机械坐标,然后让Halcon用一个变换矩阵把这两组坐标拟合起来。这个矩阵就是那个“翻译官”。
1.2 眼在手外还是眼在手上,直接影响算法选型
手眼系统的部署方式大体分两种,九点标定法适用的是其中一种。
眼在手外(固定相机):相机装在机械臂外面,比如架在工位正上方或斜上方,相机不动。这种情况下,相机看到的工作平面基本不变,成像模型可以近似成一个二维平面映射。九点标定法正是针对这个场景设计的,简单、稳定、见效快。
眼在手上(随动相机):相机装在机械臂末端,随着机械臂一起动。这种情况下相机每换一个位姿看到的视野都不同,单纯做二维平面映射是搞不定的,需要解一个AX = XB形式的手眼方程。Halcon里对应的是hand_eye_calibration算子,配合create_calib_data创建'hand_eye_moving_cam'类型的数据模型。
两种方式的差异,我整理成了下面这张表:
| 对比项 | 眼在手外(固定相机) | 眼在手上(随动相机) |
|---|---|---|
| 相机安装 | 固定在支架/机台上 | 装在机械臂末端法兰 |
| 成像稳定性 | 视野固定,不会因机械臂运动变化 | 视野随末端位姿改变 |
| 标定方式 | 九点标定法为主 | hand_eye_calibration系列算子 |
| 典型应用 | 固定工位抓取、定位、测量 | 移动式巡检、机器人引导装配 |
| 难度 | 低,入门首选 | 高,需要同时处理相机位姿和机器人位姿 |
如果你刚开始接触手眼标定,我强烈建议先用固定相机的九点标定法把整套流程跑通,理解了坐标变换的本质之后,再去看眼在手上的完整手眼标定,会轻松很多。
2. 九点标定的数学底牌:不要被“仿射变换”四个字吓到
2.1 一次搞定旋转、平移和缩放
九点标定法求解的本质上是一个二维仿射变换矩阵,Halcon里用vector_to_hom_mat2d算子求,得到了一个2×3的齐次变换矩阵:
[ x ] [ a b tx ] [ u ] [ y ] = [ c d ty ] [ v ] [ 1 ] [ 0 0 1 ] [ 1 ]其中(u, v)是像素坐标,(x, y)是机器人坐标。矩阵里的a, b, c, d负责旋转、缩放和切变,tx, ty负责平移。展开来看就是两个简单的线性方程:
x = a * u + b * v + tx y = c * u + d * v + ty你可以把它理解为Photoshop里对图层做“自由变换”操作:把一张图在平面上旋转一个角度,缩放一个比例,再平移一段距离。九点标定做的就是把像素平面“变换”到机器人平面,只不过这里的变换参数由9组已知点反推出来。
2.2 为什么是9个点,而不是3个或者4个
从数学上看,仿射变换有6个未知参数,理论上找到3个不共线的对应点就能解出唯一解。但现实中,像素坐标提取有误差,机器人走位有误差,示教器读数也有误差。如果你只用3个点,任何一点的小误差都会被毫无保留地放大进矩阵,标定结果就是“看起来能对上,实际工作起来偏得离谱”。
9个点解决的是冗余问题。6个参数,9组方程,这是一个超定方程组,Halcon内部会用最小二乘法求解,让所有点的整体误差平方和最小。相当于你在现场量了9次“零件的尺寸”,取了一个加权平均的最优结果,而不是只信单次读数。
点位的空间分布也很有讲究。哪怕你放20个点,如果全部挤在相机视野中央一个很小的区域,得到的矩阵在视野边缘的外推误差依然会很大。正确做法是让9个点尽量分散,铺满相机实际工作范围。
2.3 什么时候九点标定法不够用
这里必须说清楚九点标定法的边界,避免有人拿它硬套复杂场景。
第一,它假设相机光轴垂直于工作平面,成像模型接近正投影。如果安装时相机斜得厉害,工件在视野不同位置的高度差又大,仿射变换就会吃力。这时可以考虑用vector_to_proj_hom_mat2d求透视变换矩阵,它有8个参数,能处理一定的透视变形。
第二,它只能解决二维平面问题。如果项目要求机器人抓取有高度差、需要俯仰角补偿的工件,就得回到完整的手眼标定,用标定板和机械臂位姿数据一起解算三维空间关系。
第三,镜头畸变不能太夸张。廉价广角镜头在视野边缘的桶形畸变明显,仿射变换是线性模型,纠正不了这种非线性变形。这种情况下要么先做相机内参标定并用undistort_image矫正图像,要么换畸变小的镜头。
3. Halcon九点标定代码实战:从圆点提取到矩阵生成
3.1 入门方案:圆形Mark点提取坐标
这是现场用得最多、也最容易理解的方案。相机一次拍下一张包含9个圆形Mark点的图像,通过阈值分割找到所有圆点,计算圆心坐标作为像素坐标。
完整代码流程如下:
* 关闭窗口实时更新,加快执行速度 dev_update_off () * 读取标定图像 read_image (Image, 'E:/calib/points_9.png') * 如果采集的是彩色图,先转灰度 rgb1_to_gray (Image, GrayImage) * 阈值分割,提取暗色圆点区域 threshold (GrayImage, Regions, 0, 100) * 将连通区域拆开 connection (Regions, ConnectedRegions) * 筛选出面积合适、圆度达标的区域 select_shape (ConnectedRegions, SelectedRegions, ['area', 'circularity'], 'and', [800, 0.7], [10000, 1.0]) * 按顺序排序,保证和机器人坐标对应 sort_region (SelectedRegions, SortedRegions, 'character', 'true', 'row') * 求每个圆点的面积、中心行列坐标 area_center (SortedRegions, Area, Rows, Columns) * 注意:Halcon中Rows是纵坐标,Columns是横坐标 * 后面做变换时把Columns作为Px,Rows作为Py这一段跑完,Rows和Columns两个数组里就是9个圆心的像素坐标。这里有个新手最容易踩的坑:sort_region按行排序时,如果9个点不是严格的九宫格布局,排序结果可能和你预期的不一样。稳妥的做法是在标定时让9个点从左到右、从上到下依次排列,标完肉眼核对一遍对应顺序。
机器人端对应的9个坐标,可以通过示教器手动记录,也可以通过PLC或上位机自动读,整理成两个数组:
* 机器人坐标系下的X坐标数组 RobotX := [100.0, 150.0, 200.0, 100.0, 150.0, 200.0, 100.0, 150.0, 200.0] * 机器人坐标系下的Y坐标数组 RobotY := [50.0, 50.0, 50.0, 100.0, 100.0, 100.0, 150.0, 150.0, 150.0]然后就是重头戏,用vector_to_hom_mat2d求解变换矩阵:
* 求仿射变换矩阵 * 参数依次是:源点X、源点Y、目标点X、目标点Y vector_to_hom_mat2d (Columns, Rows, RobotX, RobotY, HomMat2D)3.2 把标定结果用起来:在线坐标转换
标定完矩阵不是拿来收藏的,最终要用于在线运行时的坐标换算。当视觉算法在图像上找到一个工件的中心点(TargetRow, TargetColumn)后,把它丢给affine_trans_point_2d:
* 在线定位时,将一个像素坐标转为机器人坐标 affine_trans_point_2d (HomMat2D, TargetColumn, TargetRow, RobotTargetX, RobotTargetY)affine_trans_point_2d的输出就是机器人可以直接执行的X和Y坐标。如果你发现转换出来的坐标和机器人实际到位位置有固定偏差,多半是像素坐标和机器人坐标的对应顺序搞反了,或者是机器人工具坐标系的参考点没有和标定针尖对齐。
3.3 进阶方案:标定板+find_calib_object
如果项目对精度要求高,或者想同时做完整的相机标定,推荐用Halcon标准标定板配合find_calib_object提取特征点。这个方法能拿到亚像素精度的特征点坐标,远好过阈值分割求圆心。
* 使用Halcon标准标定板描述文件 * 根据实际标定板规格选择,常见的有30mm、10mm等 find_caltab (Image, Caltab, 'caltab_30mm.descr', 3, 112, 5) * 如果做过相机内参标定,可以创建标定数据模型做更精确的特征点提取 create_calib_data ('calibration_object', 1, 1, CalibDataID) set_calib_data_calib_object (CalibDataID, 0, 'caltab_30mm.descr') * 这里要填入相机内参,来自之前的内参标定结果 set_calib_data_cam_param (CalibDataID, 0, 'area_scan_division', [FocalLength, Kappa, Sx, Sy, Cx, Cy, ImageWidth, ImageHeight]) * 提取标定板特征点 find_calib_object (Image, CalibDataID, 0, 0, 0, [], []) get_calib_data (CalibDataID, 'image', 0, 'x', Px) get_calib_data (CalibDataID, 'image', 0, 'y', Py)提取到特征点后,同样根据标定板原点在机器人坐标系中的位置,构造对应的RobotX、RobotY数组,再调用vector_to_hom_mat2d。
3.4 验证代码:标定结果好不好,先回代检验
矩阵求完之后,第一件事不是拿工件去试,而是用原来的9个点回代,看看理论输出和实际记录的机器人坐标差多少:
* 回代验证,计算每个点的定位误差 NumPoints := |RobotX| MaxErr := 0.0 for I := 0 to NumPoints - 1 by 1 affine_trans_point_2d (HomMat2D, Columns[I], Rows[I], CheckX, CheckY) DiffX := RobotX[I] - CheckX DiffY := RobotY[I] - CheckY Err := sqrt(DiffX * DiffX + DiffY * DiffY) if (Err > MaxErr) MaxErr := Err endif endforMaxErr就是这组标定点里的最大回代误差。如果这个值超过项目精度要求的1/3左右,说明标定过程有某个环节出了问题,不要急着上线,先回看像素提取和机器人坐标的对应关系。
4. 现场操作全流程:从摆放标定板到记录机器人坐标
4.1 标定点位布局:覆盖作业面比点位密度更重要
很多人在实验室标定效果很好,一到现场就拉胯,最常见的原因就是标定点只覆盖了视野中央一小块区域。九点标定的点位布局,应该遵循“工作区域全覆盖”原则:9个点分布成一个覆盖整个作业范围的九宫格,点位越靠近实际生产时工件可能出现的位置,在线转换精度越好。
举个例子,如果相机视野是 600mm × 500mm,而工件通常出现在视野右侧,那标定点的分布就应尽量把右侧区域加密,甚至可以适当超出实际作业范围,避免出现“外推”情况。标定是内插法精度高、外推法精度低,这个规律在视觉引导里十分显著。
4.2 机器人坐标怎么记录:示教器与上位机两种方式
记录机器人坐标有两种主流方式。
第一种,示教器手动记录。把机器人切换到手动模式,用示教器把工具末端移动到某个圆点中心,记录当前坐标值。这种方式适合点位少、无上位机通信的项目,缺点是容易记错、效率低。
第二种,上位机自动记录。在程序里通过TCP/IP、Modbus或Profinet与机器人控制器通信,机器人每到达一个标定点就把当前坐标发给上位机。这种方式效率高,还能避免人工抄录的错误,但需要提前把通信协议调通。
无论用哪种方式,有一个核心前提必须确认:记录的坐标值对应的参考点,和视觉定位的参考点必须一致。比如视觉算的是工件中心,那机器人记录的也必须是工具中心TCP到达工件中心时示教器显示的值,而不是机器人法兰盘中心。
4.3 像素坐标和机器人坐标的顺序必须一一对应
这是九点标定里最隐蔽也最致命的坑:像素坐标提取出了9个点,机器人坐标也记录好了9个值,但两者顺序对不上。
发生顺序错乱的常见原因有两个:
第一,图像上9个圆点的提取顺序和机器人记录顺序不一致。比如机器人记录顺序是按“从左到右、从上到下”,但Halcon提取圆点时因为光照不均或圆点缺损,导致某个点没被提取到,所有后续点的顺序整体错位。
第二,sort_region排序模式选错。'character'模式适合比较规整的网格布局,一旦点在视觉上有轻微旋转或倾斜,排序结果可能把不同行的点混排。
我自己的做法是:标定时在机器人示教器上按固定路线依次走一遍9个点,每个点对应的图像位置也严格按同样顺序排列,走位路线的第一个点固定选在视野左上角。这样就可以确保第N张图像对应的像素特征点和第N条机器人坐标记录一一对应。再配合回代误差检查,一旦有某个点对错位,最大回代误差会非常夸张,立刻能发现问题。
4.4 现场必做检查清单
- 相机固定是否牢靠,有没有可能的微小位移;
- 标定板或Mark点是否平整贴合工作平面;
- 镜头表面是否干净,光照是否均匀,圆点边缘有没有严重阴影;
- 机器人TCP是否标定准确,末端工具是否松动;
- 每个记录点的机器人坐标是否和像素点一一对应。
不要嫌这些检查啰嗦,我在项目里见过太多次最后查出来是“螺丝松了”“示教器读数抄串行”这种低级问题。
5. 标定完怎么知道准不准:交叉验证与误差排查清单
5.1 回代误差与额外验证点
回代误差只能检验拟合结果和标定点之间的自洽性,如果标定点本身取错了,回代误差再小也没意义。所以我习惯在标定完成后再额外取3~5个“验证点”——不参与矩阵求解,只用来做最终检验。
验证点取法:在工作范围内随便选几个位置,机器人走到位,记录机械坐标,图像上记录对应像素坐标,再用标定矩阵把像素坐标转成机械坐标,和实测机械坐标对比。如果偏差在项目允许范围内,这套手眼关系才算真正可靠。
5.2 误差来源排查顺序
一旦验证点偏差超差,按下面这个顺序排查,比盲目重标定有效得多:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 所有点偏差接近且成固定向量 | 机器人坐标记录参考点选错,或工具中心偏移 | 检查TCP标定 |
| 回代误差小,验证点误差大 | 标定点范围覆盖不足,存在外推区域 | 扩大标定范围重新标定 |
| 边缘误差明显大于中心 | 镜头畸变或相机光轴倾斜 | 做内参标定并畸变矫正,或调整安装角度 |
| 某几个点误差异常大 | 像素点顺序与机器人坐标顺序错位 | 逐点核对对应关系 |
| 整体误差偏大且没有规律 | 特征点提取精度差、光照不佳 | 改善光照,换用标定板提取亚像素点 |
5.3 提升标定精度的几个进阶做法
如果项目要求的精度在亚毫米级,仅仅用阈值分割提取圆心是不够的。
一个做法是改用Halcon标准标定板配合find_calib_object,这个算子在内部做了亚像素边缘提取,特征点精度比普通阈值分割高一到两个数量级。
另一个做法是先用create_calib_data做一次完整的相机内参标定,得到镜头畸变系数,然后在采集标定图像时先用undistort_image矫正图像,再提取特征点。这个做法能有效消除镜头畸变在视野边缘带来的非线性误差。
此外,如果相机安装角度无法保证光轴绝对垂直工作台,可以考虑用vector_to_proj_hom_mat2d求透视变换矩阵替代仿射变换。它比仿射变换多两个参数,对透视变形有更好的拟合能力,代价是需要至少在4个位置放置标定点,特征点数量多时依然用最小二乘。
6. 换成我上项目时踩过的坑:九点标定容易被忽视的六个细节
6.1 Halcon环境问题能卡住大半天
新手最容易卡住的反而不是算法本身,而是环境。Halcon分试用版和带加密狗的商业版,没有license的情况下算子会直接报错。另外Halcon安装路径别带中文,导出的C++/C#程序在目标机器上部署时记得把运行时DLL一起带上。记得有一次我在现场调试,Halcon程序在自己电脑上跑得好好的,到了客户工控机上闪退,排查了半天发现是少了hdevengine.dll。
6.2 九点标定的结果和相机固定位置是绑定的
标定矩阵只有在相机的安装位置、角度完全不变时才有效。调过一次光路、拧动过相机支架、哪怕只是重新装了镜头,之前的标定结果都作废,必须重新标定。所以现场维护流程里一定要写上“相机位移必重新标定”这条规则,否则项目上线后出现漂移问题,排查起来极其痛苦。
6.3 不要用九点标定强行做三维场景
九点标定法本质上是“平面近似”思路,适合的工作条件是工件基本在一个平面上运动,比如传送带上平面抓取、桌面定位贴合。如果工件有多层的堆叠、或者机械臂需要在不同高度抓取,老老实实去用完整手眼标定,create_calib_data ('hand_eye_moving_cam', ...)或者'hand_eye_stationary_cam'类型,再配合hand_eye_calibration求解,虽然复杂但方向是对的。
6.4 标定矩阵保存与自动加载
标定结果不是一次性成果,项目重启、程序重启后都要复用。Halcon里可以把矩阵用write_tuple保存到文件,程序初始化时用read_tuple加载,这样就不需要每次开机都重新标定一遍。
* 保存标定矩阵和对应的时间戳 write_tuple (HomMat2D, 'E:/calib/hom_mat_2d.tup') write_tuple (CalibDate, 'E:/calib/calib_date.tup') * 程序启动时加载 read_tuple ('E:/calib/hom_mat_2d.tup', HomMat2D)保存时顺手写一个calib_info.txt,记录标定日期、相机高度、镜头型号、操作人。后期如果出现精度漂移,至少能判断是不是因为期间有人动过相机。
6.5 记得在程序里加“标定状态”开关
上线程序尽量保留一个“标定模式”和“运行模式”的切换开关。标定模式下,程序只负责特征点提取和坐标展示,方便现场调试人员看清楚每个点位是否提取正确;运行模式下再执行完整流程。这个习惯能省很多联调时间。
6.6 多次标定取平均是有效但常被忽略的做法
如果现场条件允许,同一套点位做两次或三次标定,每次相机和机器人重新回零,然后对比几组矩阵的差异。差异很小说明系统重复性好,差异大说明某个环节不稳定。实际应用时取多组标定的平均结果,通常比单次标定更稳。
手眼标定这件事,说难不难,说简单也不简单。它不像深度学习那样需要调大量的网络结构,更多是考验你对坐标系变换的理解和现场操作的细致程度。我做了这么多视觉引导项目,最深的一个体会就是:标定结果不好,九成不是因为Halcon算法不行,而是前期的安装固定、点对记录、光路环境这些“笨功夫”没做到位。把这篇文章里的坑都避掉,按流程一步一步来,你也能得到一个稳定可靠的手眼关系。