3. 手眼标定(眼在手上)

任务目标

在眼在手上(Eye-in-Hand)构型中,相机刚性安装在机械臂末端法兰上,随机械臂一起运动。标定板固定在世界坐标系中,通过机械臂运动到多个位姿拍摄标定板图像,利用已知的机械臂末端位姿与检测到的标定板角点,求解相机坐标系到机械臂末端坐标系的固定变换矩阵 X(即手眼矩阵)。

核心原理与求解方程

对第 i 次拍摄,记:

  • Ei:机械臂末端到基座的齐次变换矩阵(由机械臂正运动学给出,已知)
  • B:标定板到相机坐标系的齐次变换矩阵(由标定板角点检测与PnP求解,已知)
  • X:相机到机械臂末端的齐次变换矩阵(待求,固定不变)

由坐标变换链 Ei · X · Bi = 常量(标定板在基座坐标系中固定),对两次拍摄 i、j 消去常量可得:

A · X = X · B

其中 A = Ei-1 · Ej,B = Bi · Bj-1。该方程为经典手眼标定 AX = XB 形式,采用 Tsai-Lenz 两步法求解:先由旋转矩阵求旋转轴与旋转角,再线性最小二乘求平移向量。

环境依赖

  • Python 3.8+,OpenCV(opencv-contrib-python ≥ 4.5),NumPy
  • 标定板:7×10 棋盘格(内角点 6×9),方格边长 30 mm
  • 机械臂:提供正运动学接口,可输出末端位姿(本案例使用仿真数据验证算法正确性)

关键完整代码

import cv2
import numpy as np

def tsai_lenz_hand_eye(A_list, B_list):
    """求解 AX = XB,A_list/B_list 为 N 组齐次变换矩阵 (4x4)"""
    n = len(A_list)
    # ---- 旋转部分:构造线性方程组 ----
    P = []  # 9 x 3 系数块
    Q = []  # 9 x 1 常数块
    for i in range(n):
        Ra, ta = A_list[i][:3,:3], A_list[i][:3,3]
        Rb, tb = B_list[i][:3,:3], B_list[i][:3,3]
        # 旋转轴(罗德里格斯)
        rvec_a, _ = cv2.Rodrigues(Ra)
        rvec_b, _ = cv2.Rodrigues(Rb)
        P.append(np.kron(np.eye(3), rvec_a.reshape(-1,1)).T)
        Q.append(rvec_b.reshape(-1,1))
    P = np.vstack(P)
    Q = np.vstack(Q)
    # 最小二乘解旋转轴
    Pg, _, _, _ = np.linalg.lstsq(P, Q, rcond=None)
    Pg = Pg.flatten()
    # 由旋转轴构造旋转矩阵
    theta = np.linalg.norm(Pg)
    if theta < 1e-9:
        raise ValueError("旋转角接近零,无法标定")
    rvec = Pg / theta * theta  # 实际为 Pg 本身
    R, _ = cv2.Rodrigues(Pg)
    # ---- 平移部分:线性最小二乘 ----
    C = []
    D = []
    for i in range(n):
        Ra, ta = A_list[i][:3,:3], A_list[i][:3,3]
        Rb, tb = B_list[i][:3,:3], B_list[i][:3,3]
        C.append(np.eye(3) - Ra)
        D.append(ta - R @ tb)
    C = np.vstack(C)
    D = np.vstack(D)
    t, _, _, _ = np.linalg.lstsq(C, D, rcond=None)
    X = np.eye(4)
    X[:3,:3] = R
    X[:3,3] = t.flatten()
    return X

# ============ 仿真数据生成(验证算法正确性) ============
def generate_synthetic_data(num_poses=15):
    """生成仿真手眼标定数据:已知真实 X,反推 A/B"""
    # 真实手眼矩阵(相机到末端)
    rvec_true = np.array([0.1, -0.2, 0.3])
    R_true, _ = cv2.Rodrigues(rvec_true)
    t_true = np.array([0.05, -0.02, 0.1])
    X_true = np.eye(4)
    X_true[:3,:3] = R_true
    X_true[:3,3] = t_true

    A_list, B_list = [], []
    # 固定标定板在基座中的位姿
    board_in_base = np.eye(4)
    board_in_base[:3,3] = [0.3, 0.0, 0.2]

    for _ in range(num_poses):
        # 随机末端位姿(绕不同轴旋转)
        rvec_e = np.random.uniform(-0.5, 0.5, 3)
        R_e, _ = cv2.Rodrigues(rvec_e)
        t_e = np.random.uniform([0.3,-0.2,0.1], [0.6,0.2,0.4])
        E = np.eye(4)
        E[:3,:3] = R_e
        E[:3,3] = t_e
        # 相机看到标定板的位姿 B = X^-1 · E^-1 · board_in_base
        X_inv = np.linalg.inv(X_true)
        E_inv = np.linalg.inv(E)
        B = X_inv @ E_inv @ board_in_base
        A_list.append(E)
        B_list.append(B)
    return A_list, B_list, X_true

# ============ 主程序 ============
if __name__ == "__main__":
    # 生成仿真数据(实际使用时替换为真实测量值)
    A_list, B_list, X_true = generate_synthetic_data(15)

    # 求解手眼矩阵
    X_est = tsai_lenz_hand_eye(A_list, B_list)

    # 验证:计算误差
    err_rot = np.linalg.norm(X_est[:3,:3] - X_true[:3,:3])
    err_trans = np.linalg.norm(X_est[:3,3] - X_true[:3,3])
    print(f"旋转矩阵误差 (Frobenius范数): {err_rot:.6f}")
    print(f"平移向量误差 (L2范数): {err_trans:.6f}")

    # 验证 AX = XB 残差
    max_residual = 0.0
    for i in range(len(A_list)):
        lhs = A_list[i] @ X_est
        rhs = X_est @ B_list[i]
        max_residual = max(max_residual, np.linalg.norm(lhs - rhs))
    print(f"AX=XB 最大残差: {max_residual:.6e}")

运行方式

  1. 将上述代码保存为 hand_eye_calib.py
  2. 终端执行:python hand_eye_calib.py

真实数据替换说明

实际机械臂标定时,将 generate_synthetic_data() 替换为真实采集流程:

  • 控制机械臂运动 10~15 个不同位姿(保证旋转轴方向差异足够大,避免退化)
  • 每个位姿记录:机械臂末端位姿 Ei(4×4 矩阵)与拍摄图像
  • 对每张图像使用 cv2.findChessboardCorners 检测角点,结合相机内参用 cv2.solvePnP 得到标定板到相机的变换 Bi
  • 将 Ei 与 Bi 成对传入 tsai_lenz_hand_eye()

验证方法与通过标准

  • 仿真验证:运行代码后,旋转矩阵误差应 < 1e-6,平移误差 < 1e-6,AX=XB 残差 < 1e-8,证明算法实现正确
  • 真实数据验证:标定完成后,控制机械臂运动到新位姿,拍摄标定板图像,利用标定结果将标定板角点从相机坐标转换到基座坐标,各角点在基座坐标系中的位置应基本重合(重投影误差 < 2~3 mm 即合格)

常见问题

  • 旋转轴退化:所有拍摄位姿的旋转轴方向过于接近时,方程病态。应保证各次运动的旋转轴方向差异明显
  • 噪声敏感:末端位姿精度对标定结果影响大,建议使用重复定位精度高的机械臂,并尽量增大各拍摄位姿间的角度差
  • 内参影响:相机内参需预先单独标定,内参误差会直接传递到手眼标定结果中