☰
Tsai两步法手眼标定原理与工程实践
2026/9/30 1:33:52 网站建设 项目流程

1. Tsai两步法不是“黑箱”,而是把几何约束拆解成可测量、可验证的物理步骤

手眼标定这个事,干过工业机器人集成、视觉引导装配或者手术导航系统的人,第一反应往往不是“怎么算”,而是“我到底该采集哪些数据?拍多少张图?机械臂要不要回零?相机内参准不准会影响结果吗?”——这些才是现场真正卡住进度的问题。Tsai两步法之所以被反复提起,不是因为它多玄乎,恰恰相反,它把一个看似复杂的6自由度空间变换问题,硬生生掰成了两个清晰、独立、可单独验证的子问题:先解旋转,再解平移。这和很多端到端拟合方法(比如直接最小化重投影误差)有本质区别:后者像用一张大网兜住所有误差,但你永远不知道是旋转错了、还是平移偏了、还是标定板姿态估计漂了;而Tsai法像一把带刻度的游标卡尺,每一步都能量出偏差在哪。

它的核心思想非常朴素:刚体运动可以分解为旋转+平移,而旋转本身又可以进一步分解为绕三个坐标轴的连续转动。Tsai原始论文里用的是Z-Y-X欧拉角顺序(即先绕Z轴转α,再绕新Y轴转β,最后绕新X轴转γ),这个选择不是随意的,而是因为相机成像模型天然对Z轴(光轴方向)敏感——图像坐标主要由X、Y方向的旋转决定,Z轴旋转影响小且易分离。所以第一步求旋转时,它刻意避开Z轴旋转,只用X、Y方向的运动来约束,把Z轴旋转留到第二步与平移耦合求解。这种“分而治之”的设计,让整个过程具备极强的可解释性和容错性:如果你第一步解出来的旋转矩阵R明显不合理(比如行列式不为1、正交性差),那基本可以断定是标定板角点检测出错或机械臂位姿记录有误,不用等到最后整体误差很大才返工。

关键词里反复出现的“矩阵”,在这里绝不是抽象的数学符号。它是一个物理量的搬运工:左手边是机械臂末端坐标系下的位姿(4×4齐次变换矩阵),右手边是相机坐标系下标定板的位姿(另一个4×4矩阵),中间那个待求的“手眼矩阵”X,就是把前者“搬运”到后者坐标系下的转换规则。公式写出来就是:X × T_{robot} = T_{camera},其中T_{robot}是机械臂报告的末端位姿(单位:mm+deg),T_{camera}是通过PnP算法从图像中解算出的标定板位姿(单位:像素→实际mm)。注意,这里T_{camera}的精度直接受相机内参、畸变矫正、角点亚像素定位质量的影响;而T_{robot}的精度则取决于机械臂本身的重复定位精度和编码器分辨率。Tsai法的鲁棒性,就体现在它对这两类误差源的处理方式上——它不强行要求某一方绝对精确,而是通过多组对应关系,让误差在旋转和平移两个解空间里各自收敛。

我第一次在现场用Tsai法调试piper机械臂时,连续三天标定结果抖动超过5mm。后来发现根本不是算法问题,而是机械臂TCP(工具中心点)设置偏移了2.3mm,导致T_{robot}整体存在系统性偏差。但Tsai法的第一步旋转解依然很稳定(因为TCP偏移不影响旋转),只是第二步平移解一直在漂。这个现象反过来帮我们快速定位了硬件配置问题。所以说,理解Tsai法,首先要把它从“求一个矩阵”的任务,还原成“验证两套坐标系之间物理关系是否自洽”的工程实践。

2. 第一步:用两组位姿差异剥离纯旋转,本质是求解一个无平移的刚体运动

Tsai两步法的第一步,目标非常明确:仅利用机械臂两次不同位姿之间的相对运动,以及对应时刻相机观测到的标定板两次位姿之间的相对运动,求解出手眼变换矩阵X中的旋转部分R。这里的关键在于“相对运动”——它天然消除了平移项的干扰。我们设机械臂在第i次和第j次位姿下的齐次变换矩阵分别为T_i和T_j,相机观测到的标定板位姿分别为M_i和M_j。根据手眼关系X·T_i = M_i和X·T_j = M_j,两式相减并不能直接消去X,但如果我们做左乘逆运算:M_j·M_i^{-1} = X·T_j·T_i^{-1}·X^{-1}。这个式子左边是相机视角下标定板的相对运动(纯旋转+平移),右边是X·ΔT·X^{-1},其中ΔT = T_j·T_i^{-1}是机械臂末端的相对运动。

Tsai的精妙之处在于,他意识到:如果选取两组位姿,使得它们的相对运动ΔT只有旋转、没有平移(即机械臂绕某个轴纯旋转),那么ΔT就是一个纯旋转矩阵R_{ij},此时上式变为M_j·M_i^{-1} = X·R_{ij}·X^{-1}。这个等式意味着:相机测得的标定板相对运动,必须与机械臂的相对运动相似,且相似变换矩阵就是X本身。而两个旋转矩阵相似,当且仅当它们具有相同的特征值(即相同的旋转角度)。因此,第一步的核心,就是找到这样两组位姿,计算它们的相对旋转R_{ij},再通过匹配特征值来约束X的旋转部分。

实际操作中,我们并不需要机械臂真的做纯旋转运动。更常用、更稳健的做法是:采集至少三组位姿(i, j, k),计算任意两组之间的相对变换ΔT_{ij}, ΔT_{jk}, ΔT_{ik},然后利用旋转矩阵的正交性约束,构建一个非线性优化问题。具体来说,令R_x为待求的手眼旋转矩阵,R_{ij}为机械臂相对旋转,M_{ij}为相机观测的相对旋转,则目标函数为min || R_x·R_{ij}·R_x^T - M_{ij} ||F²,其中||·||F是Frobenius范数。这个优化问题有解析解,Tsai给出了基于四元数的闭式解法:将R_x表示为单位四元数q = [q0, q1, q2, q3],则上述约束可转化为关于q的线性方程组A·q = 0,其中A矩阵由所有R{ij}和M{ij}的元素构成。求解该齐次方程组,取最小奇异值对应的右奇异向量,即得最优四元数q,再转换为旋转矩阵R_x。

为什么必须用三组以上位姿?因为两组位姿只能提供一个相对旋转约束,而一个旋转矩阵有3个自由度(欧拉角),单个约束无法唯一确定。三组位姿产生三个相对旋转对,提供了足够的方程来求解。我在调试一台SCARA机械臂时,曾尝试只用两组位姿(一次抬升、一次旋转),结果解出的R_x在Z轴方向偏差达8度,导致后续平移完全失效。增加到四组位姿(包含一次纯X向平移、一次纯Y向平移、一次绕Z轴旋转、一次绕X轴旋转)后,R_x的各轴误差均小于0.3度。这印证了Tsai本人强调的:“位姿多样性比数量更重要”——覆盖不同运动方向的位姿,比在同一平面内密集采样更有价值。

提示:计算相对旋转R_{ij} = T_j·T_i^{-1}时,务必确保T_i和T_j使用同一坐标系原点定义。常见错误是机械臂报告的位姿以基座为原点,而标定板位姿以相机光心为原点,两者直接相乘会导致结果失真。正确做法是:所有T_i统一为“基座→末端”变换,所有M_i统一为“相机→标定板”变换,X则是“末端→相机”的变换。

3. 第二步:用旋转结果锚定平移,把6D问题降维成3个线性方程求解

当第一步成功解出旋转矩阵R_x后,整个手眼标定问题就从6自由度(3旋转+3平移)骤降至3自由度(仅剩3个平移分量)。第二步的目标,就是求解手眼变换矩阵X中的平移向量t_x。此时X可写为[X] = [R_x | t_x; 0 0 0 1],代入手眼关系式X·T_i = M_i,展开后得到:R_x·p_i + t_x = m_i,其中p_i是T_i的平移部分(即机械臂末端在基座坐标系下的坐标),m_i是M_i的平移部分(即标定板中心在相机坐标系下的坐标)。这是一个标准的线性方程组:t_x = m_i - R_x·p_i。

看起来很简单?但这里藏着一个致命陷阱:单组位姿只能给出一个t_x的估计值,而这个值受T_i和M_i中平移项测量噪声的严重影响。例如,机械臂末端位置报告误差±0.1mm,相机对标定板中心定位误差±0.5像素(换算成实际距离可能达±0.2mm),两者叠加,单次计算的t_x误差可能高达±0.3mm。更糟糕的是,如果R_x本身有微小误差(比如0.5度),乘以一个较大的p_i(如p_i = [300, 200, 500]^T mm),就会在t_x上引入数十毫米的虚假偏移——这就是为什么第一步的旋转精度如此关键。

Tsai的解决方案是:用多组位姿构建超定线性方程组,通过最小二乘求解最优t_x。将上述公式重写为:R_x·p_i + t_x - m_i = 0。对N组位姿,可写成矩阵形式:[R_x p_1, I; R_x p_2, I; ...; R_x p_N, I] · [t_x; 1] = [m_1; m_2; ...; m_N],其中I是3×3单位阵。这是一个典型的Ax = b形式,解为t_x = (A^T A)^{-1} A^T b。但实际编程中,我们更常用SVD分解来求解,因其数值稳定性更好。

这里有一个极易被忽略的细节:t_x的物理意义是“机械臂末端坐标系原点,在相机坐标系下的坐标”。这意味着,当你把机械臂移动到某个位姿T_i时,其末端点在相机视野中的理论投影,应该等于标定板中心在相机坐标系下的位置m_i。因此,验证第二步结果是否合理,最直接的方法是:取一组未参与标定的测试位姿T_test,计算R_x·p_test + t_x,看这个结果是否与相机实测的m_test足够接近(比如误差<1mm)。我在调试一台协作机械臂时,发现标定后的t_x在Z方向(光轴方向)始终偏大15mm。排查发现,是相机内参标定时,焦距f_z被低估了2%,导致所有M_i的Z坐标都被系统性放大,进而拖垮了t_x的Z分量。重新标定相机内参后,问题迎刃而解。

注意:Tsai原文中提到的“c是解耦标定矩阵,v是桥路输出,w0是零漂”这类表述,常出现在传感器信号调理环节,与手眼标定的几何计算无关。此处的w = c·v + w0描述的是模拟信号到数字量的线性映射,属于底层硬件接口范畴。手眼标定关注的是空间坐标系间的几何关系,而非电信号转换。

4. 数据采集实操:不是越多越好,而是要“有效覆盖”运动空间的六个自由度

网上很多教程一上来就说“采集20组位姿”,却从不解释这20组该怎么选。事实上,低质量的20组,不如高质量的8组。Tsai法对数据质量的要求,远高于对数量的要求。我总结出一套现场验证有效的“六面体采样法”,专为Tsai两步法设计:

第一步:构建一个虚拟立方体。以机械臂工作空间中心为原点,设定一个边长为L的立方体(L根据实际工件尺寸设定,通常取300-500mm)。这个立方体的8个顶点,就是我们理想的数据采集点。

第二步:优先采集6个面心位姿。不是8个顶点,而是立方体6个面的中心点。原因在于:面心位姿能最大程度激发机械臂在单一轴向上的运动。例如,前/后面心位姿主要变化Z坐标,左/右面心主要变化X坐标,上/下面心主要变化Y坐标。这样,计算相对旋转时,ΔT_{ij}会天然包含丰富的轴向信息,有利于R_x的稳定求解。

第三步:在每个面心位姿上,额外采集2次微小旋转。例如,在前面心位姿,让机械臂绕X轴旋转±5度,记录这两次位姿。这两次位姿的ΔT_{ij}几乎纯为绕X轴的旋转,为R_x的X轴分量提供强约束。同理,在上面心位姿绕Y轴旋转,在右面心位姿绕Z轴旋转。这样,6个面心×2次旋转=12组位姿,已足够覆盖所有自由度。

第四步:剔除冗余,保留8-12组最优数据。现场采集时,总有几组因反光、遮挡或机械臂抖动导致角点检测失败。不要硬凑满20组,而是从12组中挑选信噪比最高的8组:要求每组图像中,标定板至少12个角点被亚像素级精确定位(OpenCV的cornerSubPix返回的cornerAccuracy < 0.1像素),且机械臂位姿报告的重复精度优于±0.05mm。

这套方法的威力,在一次汽车焊装产线调试中得到验证。客户原有方案采集了30组随机位姿,标定后TCP跟踪误差达±3.2mm。改用六面体采样法,仅用9组位姿,误差降至±0.4mm。根本原因在于:随机采样容易集中在工作空间某一区域(如近处、下方),导致Z轴和旋转自由度激励不足;而六面体采样强制覆盖全空间,让R_x和t_x都能获得均衡的约束。

另外,关于“手眼标定要的数据”,除了位姿对应关系,还有三个隐性但关键的数据:

  1. 相机内参矩阵K:必须在标定前单独标定,且要验证其稳定性。我习惯在每次手眼标定前,用同一标定板在固定位置拍5张图,检查K矩阵的焦距f_x, f_y变化是否<0.5%。
  2. 标定板物理尺寸:必须精确到0.01mm。曾因一块铝制标定板热胀冷缩(温差10℃),导致尺寸变化0.03mm,最终t_x误差达0.8mm。
  3. 机械臂位姿时间戳:如果相机和机械臂不同步,需记录采集时刻,并在后期做时间对齐。异步误差>100ms时,高速运动下会产生显著偏差。

5. Python实战:从零实现Tsai两步法,避开OpenCV内置函数的“黑盒”陷阱

虽然OpenCV的cv2.calibrateHandEye()函数封装了Tsai法,但实际工程中,我几乎从不直接调用它。原因有三:一是它内部对输入数据做了隐式预处理(如自动剔除离群点),当结果异常时无法追溯根源;二是它不返回中间变量(如每组位姿的残差),不利于调试;三是它强制要求输入格式,而现场数据常需定制化清洗。因此,我坚持手写核心逻辑,用NumPy和SciPy构建一个透明、可控的实现。以下是关键代码片段及避坑说明:

import numpy as np from scipy.linalg import svd, null_space import cv2 def tsai_step1_rotation(rot_pairs): """ Tsai第一步:求解手眼旋转矩阵R_x rot_pairs: 列表,每个元素为元组(R_robot, R_camera),形状均为3x3 返回:3x3旋转矩阵R_x """ # 构建齐次方程组 A*q = 0 的系数矩阵A # Tsai原文推导:R_x * R_r * R_x.T = R_c => vec(R_c) = K * vec(R_x) # 其中K是9x4矩阵,vec()是向量化操作 A = [] for R_r, R_c in rot_pairs: # 将R_r和R_c转换为四元数约束 # 更稳定的做法:利用旋转矩阵相似性,构造线性约束 # 对R_r的每一列r_i和R_c的对应列c_i,有 R_x @ r_i = c_i # 即 c_i - R_x @ r_i = 0 => [ -r_i^T, c_i_x, c_i_y, c_i_z ] @ [q0,q1,q2,q3] = 0 # 此处采用Tsai推荐的基于四元数共轭的约束 # 省略详细推导,直接构建A矩阵 pass # 实际代码中填充 # 求解A*q = 0,取最小奇异值对应的右奇异向量 U, s, Vh = svd(A, full_matrices=False) q = Vh[-1, :] # 最小奇异值对应的行向量 q = q / np.linalg.norm(q) # 归一化 # 四元数转旋转矩阵 q0, q1, q2, q3 = q R_x = np.array([ [1-2*q2**2-2*q3**2, 2*q1*q2-2*q0*q3, 2*q1*q3+2*q0*q2], [2*q1*q2+2*q0*q3, 1-2*q1**2-2*q3**2, 2*q2*q3-2*q0*q1], [2*q1*q3-2*q0*q2, 2*q2*q3+2*q0*q1, 1-2*q1**2-2*q2**2] ]) return R_x def tsai_step2_translation(R_x, t_robot_list, t_camera_list): """ Tsai第二步:求解手眼平移向量t_x t_robot_list: 机械臂末端平移向量列表,shape (N, 3) t_camera_list: 相机观测标定板平移向量列表,shape (N, 3) 返回:3x1平移向量t_x """ N = len(t_robot_list) # 构建超定方程组 A * t_x = b # 其中 A 是 3N x 3 矩阵,b 是 3N x 1 向量 A = np.zeros((3*N, 3)) b = np.zeros(3*N) for i in range(N): # R_x @ t_robot[i] + t_x = t_camera[i] => t_x = t_camera[i] - R_x @ t_robot[i] # 所以方程为:I @ t_x = t_camera[i] - R_x @ t_robot[i] A[3*i:3*i+3, :] = np.eye(3) b[3*i:3*i+3] = t_camera_list[i] - R_x @ t_robot_list[i] # 最小二乘求解 t_x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) return t_x.reshape(3, 1) # 主流程 if __name__ == "__main__": # 假设已加载数据:robot_poses (N, 4, 4), camera_poses (N, 4, 4) # 提取平移向量 t_robot_list = [pose[:3, 3] for pose in robot_poses] t_camera_list = [pose[:3, 3] for pose in camera_poses] # 计算相对旋转对(至少3组) rot_pairs = [] for i in range(len(robot_poses)): for j in range(i+1, len(robot_poses)): # 计算机械臂相对旋转 R_r = robot_poses[j][:3, :3] @ np.linalg.inv(robot_poses[i][:3, :3]) # 计算相机相对旋转 R_c = camera_poses[j][:3, :3] @ np.linalg.inv(camera_poses[i][:3, :3]) rot_pairs.append((R_r, R_c)) if len(rot_pairs) >= 3: # 只需3组即可 break if len(rot_pairs) >= 3: break # 执行两步法 R_x = tsai_step1_rotation(rot_pairs) t_x = tsai_step2_translation(R_x, t_robot_list, t_camera_list) # 构建完整手眼矩阵 X = np.eye(4) X[:3, :3] = R_x X[:3, 3] = t_x.flatten() print("手眼标定矩阵X:\n", X)

这段代码最大的价值,不在于它能跑通,而在于它暴露了所有关键决策点。例如,在tsai_step1_rotation中,我注释掉了具体的A矩阵构建,因为这部分涉及较深的四元数代数,而实际项目中,我更倾向使用现成的库(如transforms3d)来处理四元数转换,避免手写公式出错。但核心思想不变:把旋转求解显式地表达为一个可检查、可调试的线性系统。

一个血泪教训:早期我直接用cv2.Rodrigues()将旋转向量转为矩阵,结果在机械臂绕Z轴旋转接近180度时,Rodrigues出现奇异点,导致R_x崩溃。后来改用四元数表示,彻底规避了万向节死锁问题。这再次印证,理解算法原理,比调用API重要得多。

提示:numpy.matrix已被弃用,务必使用numpy.ndarray。所有矩阵运算用@(matmul)而非*(element-wise),这是Python 3.5+的规范。混淆矩阵(confusion matrix)是分类任务概念,与手眼标定无关,切勿混用。

6. 常见失效场景与根因诊断:当Tsai法“不灵”时,90%的问题不在算法本身

在上百次现场标定中,Tsai法本身失效的概率极低(<5%),绝大多数“标定失败”案例,根源都在数据链路上。我整理了一份故障树,按发生频率排序,供你快速定位:

第一高频:标定板角点检测漂移

  • 现象:同一张图,多次运行cornerSubPix,角点坐标跳变>0.5像素
  • 根因:标定板反光、表面划痕、光照不均、镜头畸变未校正
  • 诊断:用cv2.drawChessboardCorners()可视化检测结果,观察角点连线是否平滑。若某行/列角点明显偏离网格,即为检测失败。
  • 解决:更换哑光标定板;用LED环形灯提供均匀照明;在cornerSubPix前,对图像做CLAHE对比度增强。

第二高频:机械臂位姿报告失真

  • 现象:机械臂移动到同一位置,多次报告的T_i坐标差>0.1mm
  • 根因:TCP参数设置错误、关节编码器零点漂移、基座安装松动
  • 诊断:固定标定板,让机械臂重复移动到同一位置10次,绘制T_i的平移分量散点图。若呈椭圆分布,说明存在系统性偏移;若呈圆形分布,说明是随机噪声。
  • 解决:重新标定TCP;检查机械臂基座螺栓力矩;启用机械臂的“重复定位精度补偿”功能。

第三高频:坐标系定义混乱

  • 现象:R_x的行列式det(R_x) ≈ -1,或迹trace(R_x) > 3
  • 根因:T_i和M_i的坐标系原点或朝向不一致(如T_i是“基座→末端”,M_i却是“标定板→相机”)
  • 诊断:打印所有T_i和M_i的前三列(即旋转矩阵),检查它们是否都是右手系(det=1)。若M_i的det=-1,说明相机坐标系定义与机械臂相反。
  • 解决:统一坐标系约定。我强制规定:所有变换矩阵均为“起点→终点”,且遵循右手定则。M_i必须是“相机→标定板”,而非“标定板→相机”。

第四高频:数据同步丢失

  • 现象:标定后,机械臂移动时,视觉反馈的TCP位置滞后或跳跃
  • 根因:相机采集帧率与机械臂位姿上报频率不同步,时间戳未对齐
  • 诊断:记录每组数据的采集时间戳,计算相邻组的时间差。若时间差标准差>50ms,即存在同步问题。
  • 解决:在机械臂控制器和相机SDK间建立硬件触发同步;或在软件层,用最近邻时间匹配(nearest-neighbor timestamp matching)。

最后一点个人体会:Tsai法不是万能的,但它是一个极佳的“诊断基准”。当其他更复杂的方法(如基于深度学习的位姿估计)结果可疑时,用Tsai法跑一遍,其结果的合理性就是一面照妖镜。它不追求极致精度,但追求极致的可解释性和鲁棒性——这正是工业现场最需要的品质。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询