机械臂视觉抓取(二):机械臂手眼标定
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)⋅gTc⋅cTt(1)=bTg(2)⋅gTc⋅cTt(2)
其中,bTg^bT_gbTg可以通过示教器读取,cTt^cT_tcTt可以通过opencv封装的库来计算,这样就转换成了一个普通的空间坐标转换问题,只要列的方程个数足够就可求解出相机和夹爪的关系。
3、夹爪坐标系和基底坐标系的关系
以夹爪坐标系和基底坐标系为例说明一下空间坐标转换的基本方式,其它几组坐标转换也是类似的关系,这里的夹爪到基底坐标系的关系是可以直接从机械臂读取的。夹爪坐标系gripper和机械臂基座坐标系base之间的关系求解如下所示:

若空间中某一点P在基底坐标系与夹爪坐标系下分别表示为PbP_bPb和PgP_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θ0−sinθ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_xRx,RyR_yRy,RzR_zRz可以由示教器中的A,B,C获取,总之示教器中通常提供的6个自由度包含3个转角、3个平移量的数据就是夹爪在基座坐标系下的位姿,这个数值是已知的。值得注意的是,不同厂家的机械臂对于位姿信息的定义可能会有区别,甚至有的机械臂是Z-X-Y的机械臂,所以需要搞清楚机械臂上6个位姿信息到底是什么。
记夹爪(gripper)到机器人底座(base)的旋转矩阵是bRg^bR_gbRg,即bRg=RzRyRx^bR_g=R_zR_yR_xbRg=RzRyRx, Pb=bRgPg+btgP_b=^bR_gP_g + ^bt_gPb=bRgPg+btg
若空间中某一点P在基底坐标系与夹爪坐标系下的齐次坐标分别是(Xb,Yb,Zb,1)(X_b,Y_b,Z_b, 1)(Xb,Yb,Zb,1)与(Xg,Yg,Zg,1)(X_g,Y_g,Z_g, 1)(Xg,Yg,Zg,1),那么写成其次坐标与矩阵的形式,如下:
[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)⋅bTc⋅cTt(1)=gTb(2)⋅bTc⋅cTt(2)
眼在手外和眼在手上两种模式在解算难度上是相同的,只是使用的场景不同。
眼在手外适用场景:
- 全局定位与抓取: 例如传送带上的工件抓取、托盘码垛/拆垛。相机俯视整个料箱或传送带,识别物体位置后告诉机械臂去哪里拿。
- 环境监测与防碰撞: 监控工作区域内是否有人员闯入,或者工件是否放置到位,侧重于安全与状态监测。
- 大尺寸工件的粗定位: 如大型板材的抓取,只需要知道工件的大概位置和角度,不需要微米级的末端精度。
- 多机器人协同: 一个固定相机同时观测多台机械臂的工作区域,协调它们之间的作业,避免干涉。
眼在手上适用场景:
- 精密装配与插拔: 例如电子元件的插装、轴承压入。当机械臂接近目标时,相机可以近距离对孔位或销钉进行精确对准。
- 小物体抓取与分拣: 在料盘或传送带上抓取尺寸较小的零件,相机可以移动到物体正上方获取高分辨率图像,避免远处拍摄像素不足的问题。
- 曲面加工与跟踪: 如打磨、焊接或涂胶,相机可以实时观察工具与工件接触点的状态,动态调整轨迹。
- 大范围检测: 需要检测大型工件(如汽车车身)时,机械臂可以带动相机移动到不同位置拍照,合成全景图像,避免安装多个固定相机。
两种方式选型:
| 考量维度 | 眼在手上 (Eye-in-Hand) | 眼在手外 (Eye-to-Hand) |
|---|---|---|
| 核心任务 | 精密作业、微调、小物体识别 | 粗定位、抓取、全局监测、防撞 |
| 视野 (FOV) | 局部、可变、可放大 | 全局、固定、范围大 |
| 精度 | 极高(近距离作业时) | 中等或较低(取决于架设高度) |
| 遮挡问题 | 基本无遮挡(相机可调整角度) | 可能存在机械臂遮挡工件的风险 |
| 动态模糊 | 易受机械臂振动影响 | 无运动模糊,成像稳定 |
| 标定关系 | 标定相机与末端的关系 | 标定相机与机器人基座的关系 |
| 典型行业 | 3C电子、精密打磨、医疗手术 | 物流分拣、码垛、机床上下料 |
5、眼在手外实战相关
相机固定不动,机械臂自动抓取相机视场范围内的某个产品。
5.1标定流程
步骤 1:数据采集
需要采集 N 组(建议 ≥15 组,我采集24组) 数据,每组包含:
- 机械臂末端位姿(位置 + 姿态)
- 相机拍摄的棋盘格图像
数据采集时让机械臂带动标定板(或相机)运动到不同位置,覆盖工作空间的不同区域,包含足够的旋转和平移变化。
步骤 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: 最大迭代次数 300.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=0imagePoints: 检测到的角点像素坐标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:
| 算法 | 常量 | 特点 |
|---|---|---|
| TSAI | cv2.CALIB_HAND_EYE_TSAI | 经典两阶段法,速度快 |
| SHIU | cv2.CALIB_HAND_EYE_SHIU | 封闭解,精度较高 |
| DANIILIDIS | cv2.CALIB_HAND_EYE_DANIILIDIS | 对偶四元数法 |
| PARK | cv2.CALIB_HAND_EYE_PARK | 非线性优化 |
| HORAUD | cv2.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 |
更多推荐
所有评论(0)