1、手眼标定概述

机械臂手眼标定是机器人领域中的一个基础且关键的技术环节。简单来说,它的目标是确定机械臂与相机(眼睛)之间的坐标变换关系。手眼标定要解决的问题就是:让大脑(控制系统)知道眼睛看到的物体,相对于手(机械臂末端)在什么位置,这样手才能准确地抓取或操作物体。
机械臂手眼标定一般分两种形式,一种是眼在手上,一种是眼在手外:
请添加图片描述

在机械臂系统中存在以下4个坐标系:

  • 机械臂基座坐标系
  • 机械臂夹爪坐标系
  • 相机坐标系
  • 标定板坐标系

2、眼在手上标定原理

如下图所示,眼在手上模式下,相机固定在机械臂末端,标定板固定在地面,机械臂带着相机从不同角度对这标定板拍照,目的是标定出相机坐标系和夹爪坐标系间的关系。
眼在手上模式有以下特点:

  • 标定时标定板固定不动
  • 标定板和机械臂基座之间相对静止,存在固定的转换关系
  • 相机和夹爪之间相对静止,存在固定的关系
  • 每次拍照,相机和标定板之间相对位置都会变化,但是可以通过opencv库计算出两者相对位置关系
  • 每次拍照机械臂末端与基座的相对位置会变化,但是可以通过示教器读取两者相对位置关系

在这里插入图片描述

上图表示了机械臂在两个位置拍摄标定板的情况,其中,bTt^bT_tbTt表示标定板(target)到机器人基底坐标系的关系,cTt^cT_tcTt表示标定板到相机的变换关系,gTc^gT_cgTc表示相机到夹爪坐标系的变换关系,bTg^bT_gbTg表示夹爪到到机器人基底坐标系的变换关系,通过空间点PtP_tPt通过两中不同的方式转换到基地坐标系下的PbP_bPb,可以得到:
bTt=bTggTccTt^bT_t=^bT_g^gT_c^cT_tbTt=bTggTccTt
有点链式法则的意思,bTt^bT_tbTt是固定不变的,所以多次拍照就可以建立一系列等式:
bTg(1)⋅gTc⋅cTt(1)=bTg(2)⋅gTc⋅cTt(2) ^bT_g^{(1)} \cdot ^gT_c \cdot ^cT_t^{(1)} = ^bT_g^{(2)} \cdot ^gT_c \cdot ^cT_t^{(2)} bTg(1)gTccTt(1)=bTg(2)gTccTt(2)
其中,bTg^bT_gbTg可以通过示教器读取,cTt^cT_tcTt可以通过opencv封装的库来计算,这样就转换成了一个普通的空间坐标转换问题,只要列的方程个数足够就可求解出相机和夹爪的关系。

3、夹爪坐标系和基底坐标系的关系

以夹爪坐标系和基底坐标系为例说明一下空间坐标转换的基本方式,其它几组坐标转换也是类似的关系,这里的夹爪到基底坐标系的关系是可以直接从机械臂读取的。夹爪坐标系gripper和机械臂基座坐标系base之间的关系求解如下所示:
在这里插入图片描述
若空间中某一点P在基底坐标系与夹爪坐标系下分别表示为PbP_bPbPgP_gPg, 则有:
Pb=RzRyRxPg+btgP_b = R_zR_yR_xP_g + ^bt_g Pb=RzRyRxPg+btg
其中,
Rx=[1000cos⁡θ−sin⁡θ0sin⁡θcos⁡θ] R_x = \begin{bmatrix} 1 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta \\ 0 & \sin\theta & \cos\theta \end{bmatrix} Rx=1000cosθsinθ0sinθcosθ
Ry=[0cos⁡θ−sin⁡θ0100sin⁡θcos⁡θ] R_y = \begin{bmatrix} 0 & \cos\theta & -\sin\theta \\ 0 & 1 & 0 \\ 0 & \sin\theta & \cos\theta \end{bmatrix} Ry=000cosθ1sinθsinθ0cosθ
Rz=[0cos⁡θsin⁡θ0−sin⁡θcos⁡θ001] R_z = \begin{bmatrix} 0 & \cos\theta & \sin\theta \\ 0 & -\sin\theta & \cos\theta \\ 0 & 0 & 1 \end{bmatrix} Rz=000cosθsinθ0sinθcosθ1
btg^bt_gbtg可以示教器中的X,Y,Z,RxR_xRxRyR_yRyRzR_zRz可以由示教器中的A,B,C获取,总之示教器中通常提供的6个自由度包含3个转角、3个平移量的数据就是夹爪在基座坐标系下的位姿,这个数值是已知的。值得注意的是,不同厂家的机械臂对于位姿信息的定义可能会有区别,甚至有的机械臂是Z-X-Y的机械臂,所以需要搞清楚机械臂上6个位姿信息到底是什么。

记夹爪(gripper)到机器人底座(base)的旋转矩阵是bRg^bR_gbRg,即bRg=RzRyRx^bR_g=R_zR_yR_xbRg=RzRyRxPb=bRgPg+btgP_b=^bR_gP_g + ^bt_gPb=bRgPg+btg
若空间中某一点P在基底坐标系与夹爪坐标系下的齐次坐标分别是(Xb,Yb,Zb,1)(X_b,Y_b,Z_b, 1)XbYbZb1(Xg,Yg,Zg,1)(X_g,Y_g,Z_g, 1)XgYgZg1,那么写成其次坐标与矩阵的形式,如下:

[XbYbZb1][bRgbtg0T1][XgYgZg1]=bTg[XgYgZg1] \begin{bmatrix} X_b \\ Y_b \\ Z_b \\ 1 \end{bmatrix} \begin{bmatrix} ^b\mathbf{R}_g & ^b\mathbf{t}_g \\ \mathbf{0}^T & 1 \end{bmatrix} \begin{bmatrix} X_g \\ Y_g \\ Z_g \\ 1 \end{bmatrix} = ^bT_g \begin{bmatrix} X_g \\ Y_g \\ Z_g \\ 1 \end{bmatrix} XbYbZb1[bRg0Tbtg1]XgYgZg1=bTgXgYgZg1
其中,bTg^bT_gbTg表示夹爪(gripper)到机器人底座(base)的变换矩阵,形状为[4,4]。

4、眼在手外

眼在手外模式下,相机固定在地面,标定板固定在夹爪上,机械臂带着标定板在相机视场中运动,目的是标定出相机和基底坐标系的关系。
眼在手外模式有以下特点:

  • 标定时相机固定不动
  • 标定板和机械臂夹爪之间相对静止,存在固定的转换关系
  • 相机和机械臂底座之间相对静止,存在固定的关系
  • 每次拍照,相机和标定板之间相对位置都会变化,但是可以通过opencv库计算出两者相对位置关系
  • 每次拍照机械臂夹爪与基座的相对位置会变化,但是可以通过示教器读取两者相对位置关系

gTb(1)⋅bTc⋅cTt(1)=gTb(2)⋅bTc⋅cTt(2) ^gT_b^{(1)} \cdot ^bT_c \cdot ^cT_t^{(1)} = ^gT_b^{(2)} \cdot ^bT_c \cdot ^cT_t^{(2)} gTb(1)bTccTt(1)=gTb(2)bTccTt(2)
眼在手外和眼在手上两种模式在解算难度上是相同的,只是使用的场景不同。
眼在手外适用场景:

  • 全局定位与抓取: 例如传送带上的工件抓取、托盘码垛/拆垛。相机俯视整个料箱或传送带,识别物体位置后告诉机械臂去哪里拿。
  • 环境监测与防碰撞: 监控工作区域内是否有人员闯入,或者工件是否放置到位,侧重于安全与状态监测。
  • 大尺寸工件的粗定位: 如大型板材的抓取,只需要知道工件的大概位置和角度,不需要微米级的末端精度。
  • 多机器人协同: 一个固定相机同时观测多台机械臂的工作区域,协调它们之间的作业,避免干涉。

眼在手上适用场景:

  • 精密装配与插拔: 例如电子元件的插装、轴承压入。当机械臂接近目标时,相机可以近距离对孔位或销钉进行精确对准。
  • 小物体抓取与分拣: 在料盘或传送带上抓取尺寸较小的零件,相机可以移动到物体正上方获取高分辨率图像,避免远处拍摄像素不足的问题。
  • 曲面加工与跟踪: 如打磨、焊接或涂胶,相机可以实时观察工具与工件接触点的状态,动态调整轨迹。
  • 大范围检测: 需要检测大型工件(如汽车车身)时,机械臂可以带动相机移动到不同位置拍照,合成全景图像,避免安装多个固定相机。

两种方式选型:

考量维度眼在手上 (Eye-in-Hand)眼在手外 (Eye-to-Hand)
核心任务精密作业、微调、小物体识别粗定位、抓取、全局监测、防撞
视野 (FOV)局部、可变、可放大全局、固定、范围大
精度极高(近距离作业时)中等或较低(取决于架设高度)
遮挡问题基本无遮挡(相机可调整角度)可能存在机械臂遮挡工件的风险
动态模糊易受机械臂振动影响无运动模糊,成像稳定
标定关系标定相机与末端的关系标定相机与机器人基座的关系
典型行业3C电子、精密打磨、医疗手术物流分拣、码垛、机床上下料

5、眼在手外实战相关

相机固定不动,机械臂自动抓取相机视场范围内的某个产品。

5.1标定流程

步骤 1:数据采集

需要采集 N 组(建议 ≥15 组,我采集24组) 数据,每组包含:

  1. 机械臂末端位姿(位置 + 姿态)
  2. 相机拍摄的棋盘格图像

数据采集时让机械臂带动标定板(或相机)运动到不同位置,覆盖工作空间的不同区域,包含足够的旋转和平移变化。

步骤 2:角点检测与位姿估计

对每张棋盘格图像进行角点检测,并使用 PnP 算法求解标定板到相机的位姿。

步骤 3:构建手眼标定方程

将机械臂位姿和标定板位姿分别转换为齐次变换矩阵,输入 OpenCV 的手眼标定函数。

步骤 4:求解并验证

求解得到手眼矩阵,可通过重投影误差或多组数据的一致性来验证标定精度。

5.2 OpenCV 核心函数详解

5.2.1 角点检测:cv2.findChessboardCorners

ret, corners = cv2.findChessboardCorners(
    image,              # 输入图像(灰度图)
    patternSize,        # 内角点数量 (cols, rows),如 (9, 6)
    corners=None,       # 输出角点
    flags=0             # 检测标志
)

常用 flags:

  • cv2.CALIB_CB_ADAPTIVE_THRESH: 自适应阈值,对光照不均匀更鲁棒
  • cv2.CALIB_CB_NORMALIZE_IMAGE: 直方图均衡化,增强对比度
  • cv2.CALIB_CB_FILTER_QUADS: 过滤四边形,减少误检

5.2.2 亚像素优化:cv2.cornerSubPix

corners = cv2.cornerSubPix(
    image,              # 输入图像
    corners,            # 初始角点
    winSize=(11, 11),   # 搜索窗口大小
    zeroZone=(-1, -1),  # 死区大小(-1 表示不使用)
    criteria            # 迭代终止条件
)

终止条件 criteria:

criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
  • TERM_CRITERIA_EPS: 当角点位置变化小于阈值时停止
  • TERM_CRITERIA_MAX_ITER: 最大迭代次数 30
  • 0.001: 位置变化阈值(像素)

5.2.3 PnP 求解位姿:cv2.solvePnP

ret, rvec, tvec = cv2.solvePnP(
    objectPoints,       # 3D 物体坐标 (N, 3)
    imagePoints,        # 2D 图像坐标 (N, 2)
    cameraMatrix,       # 相机内参矩阵 (3, 3)
    distCoeffs,         # 畸变系数 (5, 1) 或 (8, 1)
    rvec=None,          # 初始旋转向量
    tvec=None,          # 初始平移向量
    useExtrinsicGuess=False,
    flags=cv2.SOLVEPNP_ITERATIVE
)

参数说明:

  • objectPoints: 棋盘格角点在世界坐标系中的坐标,通常设定标定板平面 Z=0
  • imagePoints: 检测到的角点像素坐标
  • cameraMatrix:
    [fx,  0, cx]
    [ 0, fy, cy]
    [ 0,  0,  1]
    
  • distCoeffs: 径向畸变 (k1,k2,k3) + 切向畸变 (p1,p2)

返回值:

  • rvec: 旋转向量(轴角表示),需用 cv2.Rodrigues 转换为旋转矩阵
  • tvec: 平移向量

5.2.4 旋转向量转旋转矩阵:cv2.Rodrigues

R, _ = cv2.Rodrigues(rvec)  # rvec: (3,1) → R: (3,3)

罗德里格斯公式将轴角表示转换为旋转矩阵,是紧凑的旋转表示方法。

5.2.5 手眼标定:cv2.calibrateHandEye

R_cam2base, t_cam2base = cv2.calibrateHandEye(
    R_gripper2base,     # 机械臂基座到末端的旋转矩阵列表
    t_gripper2base,     # 机械臂基座到末端的平移向量列表
    R_target2cam,       # 相机到标定板的旋转矩阵列表
    t_target2cam,       # 相机到标定板的平移向量列表
    method=cv2.CALIB_HAND_EYE_TSAI
)

可用算法 method:

算法常量特点
TSAIcv2.CALIB_HAND_EYE_TSAI经典两阶段法,速度快
SHIUcv2.CALIB_HAND_EYE_SHIU封闭解,精度较高
DANIILIDIScv2.CALIB_HAND_EYE_DANIILIDIS对偶四元数法
PARKcv2.CALIB_HAND_EYE_PARK非线性优化
HORAUDcv2.CALIB_HAND_EYE_HORAUD四元数法

输入数据要求:

  • 所有旋转矩阵为 3x3 numpy 数组
  • 所有平移向量为 (3,1) 或 (3,) 数组
  • 列表长度 ≥ 3(建议 ≥ 15)

5.3 关键代码实现

5.3…1 棋盘格 3D 坐标构建

def create_chessboard_object_points(cols, rows, square_size):
    """
    创建棋盘格角点的 3D 坐标
    假设标定板在 Z=0 平面上
    """
    objp = np.zeros((cols * rows, 3), np.float32)
    objp[:, :2] = np.mgrid[0:cols, 0:rows].T.reshape(-1, 2) * square_size
    return objp

5.3.2 欧拉角转旋转矩阵

def euler_to_rotation_matrix(rx, ry, rz, order='xyz'):
    """
    XYZ 欧拉角转旋转矩阵(输入为弧度)
    """
    R_x = np.array([
        [1, 0, 0],
        [0, np.cos(rx), -np.sin(rx)],
        [0, np.sin(rx), np.cos(rx)]
    ])
    R_y = np.array([
        [np.cos(ry), 0, np.sin(ry)],
        [0, 1, 0],
        [-np.sin(ry), 0, np.cos(ry)]
    ])
    R_z = np.array([
        [np.cos(rz), -np.sin(rz), 0],
        [np.sin(rz), np.cos(rz), 0],
        [0, 0, 1]
    ])

    if order == 'xyz':
        R = R_z @ R_y @ R_x
    return R

5.3.3 齐次变换矩阵求逆

def invert_homogeneous_transform(matrix):
    """
    4x4 齐次变换矩阵求逆
    T = [R | t]
        [0 | 1]
    T_inv = [R^T | -R^T·t]
            [ 0  |    1    ]
    """
    R = matrix[:3, :3]
    t = matrix[:3, 3]

    inv_R = R.T
    inv_t = -inv_R @ t

    inv_matrix = np.eye(4)
    inv_matrix[:3, :3] = inv_R
    inv_matrix[:3, 3] = inv_t
    return inv_matrix

5.3.4 完整标定流程

def hand_eye_calibration(
    robot_poses,        # 机械臂位姿列表 [(x,y,z,rx,ry,rz), ...]
    image_paths,        # 图像路径列表
    camera_matrix,      # 相机内参
    dist_coeffs,        # 畸变系数
    pattern_size,       # 棋盘格尺寸 (cols, rows)
    square_size         # 方格边长 (mm)
):
    """
    完整的手眼标定流程
    """
    R_base2gripper_list = []
    t_base2gripper_list = []
    R_target2cam_list = []
    t_target2cam_list = []

    for pose, img_path in zip(robot_poses, image_paths):
        # 1. 机械臂位姿 → 齐次矩阵
        R_g2b = euler_to_rotation_matrix(*pose[3:])
        t_g2b = np.array(pose[:3])
        T_g2b = np.eye(4)
        T_g2b[:3, :3], T_g2b[:3, 3] = R_g2b, t_g2b

        # 2. 求逆得到 base→gripper
        T_b2g = invert_homogeneous_transform(T_g2b)
        R_base2gripper_list.append(T_b2g[:3, :3])
        t_base2gripper_list.append(T_b2g[:3, 3])

        # 3. 图像角点检测 + PnP
        image = cv2.imread(img_path)
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        objp = create_chessboard_object_points(*pattern_size, square_size)

        ret, corners = cv2.findChessboardCorners(gray, pattern_size)
        if ret:
            corners = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1),
                criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))

            ret, rvec, tvec = cv2.solvePnP(objp, corners, camera_matrix, dist_coeffs)
            if ret:
                R_target2cam, _ = cv2.Rodrigues(rvec)
                R_target2cam_list.append(R_target2cam)
                t_target2cam_list.append(tvec)

    # 4. 手眼标定求解
    R_cam2base, t_cam2base = cv2.calibrateHandEye(
        R_base2gripper_list, t_base2gripper_list,
        R_target2cam_list, t_target2cam_list,
        method=cv2.CALIB_HAND_EYE_TSAI
    )

    # 5. 构建 4x4 变换矩阵
    T_cam2base = np.eye(4)
    T_cam2base[:3, :3], T_cam2base[:3, 3] = R_cam2base, t_cam2base.reshape(-1)

    return T_cam2base

5.4 提高标定精度的技巧

5.4.1 数据采集

  • 多样性: 覆盖工作空间的不同区域
  • 旋转充分: 包含绕三个轴的旋转运动
  • 数量充足: 建议 20 组以上数据
  • 避免共面: 标定板法向量不要始终保持同一方向

5.4.2 图像处理

  • 分辨率: 使用较高清晰度图像(角点检测更精确)
  • 光照均匀: 避免强反光和阴影
  • 棋盘格完整: 确保整个棋盘格在视野内

5.4.3 相机标定

  • 相机内参需预先精确标定
  • 使用与手眼标定相同焦距和光圈设置
  • 标定后不要改变镜头焦距(如果是变焦镜头)

5.5 标定结果验证

def verify_calibration(T_cam2base, R_base2gripper_list, t_base2gripper_list,
                       R_target2cam_list, t_target2cam_list):
    """
    验证手眼标定结果的一致性
    计算所有变换到同一坐标系下的位置标准差
    """
    positions = []
    for R_b2g, t_b2g, R_t2c, t_t2c in zip(
        R_base2gripper_list, t_base2gripper_list,
        R_target2cam_list, t_target2cam_list):

        T_b2g = np.eye(4)
        T_b2g[:3, :3], T_b2g[:3, 3] = R_b2g, t_b2g

        T_t2c = np.eye(4)
        T_t2c[:3, :3], T_t2c[:3, 3] = R_t2c, t_t2c.flatten()

        # 变换到基座坐标系下的标定点位置
        T_result = T_b2g @ T_cam2base @ T_t2c
        positions.append(T_result[:3, 3])

    positions = np.array(positions)
    std_dev = np.std(positions, axis=0)
    print(f"位置标准差 (mm): {std_dev * 1000}")
    print(f"平均误差 (mm): {np.mean(std_dev) * 1000:.3f}")

5.6 总结

手眼标定是机器人视觉集成的核心环节,OpenCV 提供了完整的工具链:

步骤OpenCV 函数
角点检测findChessboardCorners
亚像素优化cornerSubPix
位姿估计solvePnP + Rodrigues
手眼求解calibrateHandEye
Logo

立足具身智能前沿赛道,致力于搭建全球化、开源化、全栈式技术交流与实践共创平台。

更多推荐