2. 相机标定进阶
任务定义
在激光焊接视觉定位系统中,相机固定安装于焊接工作区上方(眼在手外构型),需要建立相机坐标系与机器人基坐标系之间的变换关系,使视觉系统检测到的焊缝位置能够转换为机器人可执行的运动指令。本案例完成眼在手外手眼标定的完整实现:采集标定板在机器人不同位姿下的图像与对应机器人位姿,求解相机到机器人基座的变换矩阵,并输出验证误差。
手眼标定原理(眼在手外)
眼在手外构型下,标定板固定在机器人末端,相机固定于外部。对第 i 次观测,标定板在相机坐标系下的位姿为 Pci(由图像求解),机器人末端在基坐标系下的位姿为 Pbi(由机器人控制器读取)。标定板在机器人基坐标系下的位姿 Pobj 固定不变,因此满足:
Pobj = X · Pbi · Pci-1
其中 X 为待求的相机到机器人基座的变换矩阵。对任意两次观测 i、j,消去 Pobj 可得经典手眼方程:
Aij · X = X · Bij
其中 Aij = Pbj · (Pbi)-1(机器人运动),Bij = Pcj · (Pci)-1(相机观测运动)。本案例采用 Tsai-Lenz 两步法求解:先由旋转方程 Rot(A)·Rot(X) = Rot(X)·Rot(B) 求旋转,再代入平移方程求平移。
代码实现
以下 Python 代码实现完整的眼在手外标定流程。核心函数 hand_eye_calibration 接收两组位姿列表(机器人末端位姿 robot_poses 与标定板在相机下的位姿 cam_poses),返回变换矩阵 X(相机→机器人基座)。代码包含 Tsai-Lenz 求解、平移求解与验证误差计算。
import numpy as np
from scipy.linalg import logm, expm
def pose_to_matrix(pose):
"""将 [x, y, z, rx, ry, rz] (mm, rad) 转为 4x4 齐次矩阵"""
x, y, z, rx, ry, rz = pose
# 简化:使用欧拉角 ZYX 顺序构造旋转矩阵
cx, sx = np.cos(rx), np.sin(rx)
cy, sy = np.cos(ry), np.sin(ry)
cz, sz = np.cos(rz), np.sin(rz)
Rx = np.array([[1,0,0],[0,cx,-sx],[0,sx,cx]])
Ry = np.array([[cy,0,sy],[0,1,0],[-sy,0,cy]])
Rz = np.array([[cz,-sz,0],[sz,cz,0],[0,0,1]])
R = Rz @ Ry @ Rx
T = np.eye(4)
T[:3,:3] = R
T[:3,3] = [x, y, z]
return T
def matrix_to_pose(T):
"""将 4x4 矩阵转为 [x, y, z, rx, ry, rz]"""
R = T[:3,:3]
x, y, z = T[:3,3]
sy = np.clip(-R[2,0], -1.0, 1.0)
ry = np.arcsin(sy)
if np.abs(np.cos(ry)) > 1e-6:
rx = np.arctan2(R[2,1], R[2,2])
rz = np.arctan2(R[1,0], R[0,0])
else:
rx = np.arctan2(-R[1,2], R[1,1])
rz = 0.0
return [x, y, z, rx, ry, rz]
def hand_eye_calibration(robot_poses, cam_poses):
"""
眼在手外手眼标定 (Tsai-Lenz)
robot_poses: 机器人末端在基座下的位姿列表 (N x 6)
cam_poses: 标定板在相机下的位姿列表 (N x 6)
返回: X (4x4), 相机坐标系 -> 机器人基座坐标系
"""
assert len(robot_poses) == len(cam_poses) and len(robot_poses) >= 3, "至少需要3组位姿"
N = len(robot_poses)
T_be = [pose_to_matrix(p) for p in robot_poses] # 末端->基座
T_co = [pose_to_matrix(p) for p in cam_poses] # 标定板->相机
# ---- 步骤1: 构造旋转方程并求解 ----
P = [] # 旋转方程系数矩阵
Q = [] # 旋转方程右侧向量
for i in range(N-1):
for j in range(i+1, N):
A = np.linalg.inv(T_be[i]) @ T_be[j] # A_ij
B = np.linalg.inv(T_co[i]) @ T_co[j] # B_ij
Ra, Rb = A[:3,:3], B[:3,:3]
# 旋转轴-角度表示
la = logm(Ra); lb = logm(Rb)
pa = np.array([la[2,1], la[0,2], la[1,0]]) # 旋转向量
pb = np.array([lb[2,1], lb[0,2], lb[1,0]])
# 叉积构造线性方程: (pa + pb) x rx = pb - pa 的变形
# 使用 Kronecker 积形式: (Ra ⊗ I - I ⊗ Rb^T) vec(Rx) = 0
K = np.kron(Ra, np.eye(3)) - np.kron(np.eye(3), Rb.T)
P.append(K[:9,:]) # 取前9行(实际9个方程中选独立行)
Q.append(np.zeros(9))
P = np.vstack(P)
# 对 P 做 SVD 求零空间
_, _, Vt = np.linalg.svd(P)
Rx_vec = Vt[-1, :] # 最小奇异值对应向量
Rx = Rx_vec.reshape(3,3)
# 投影到 SO(3)
U, _, Vt2 = np.linalg.svd(Rx)
Rx = U @ Vt2
if np.linalg.det(Rx) < 0:
U[:, -1] *= -1
Rx = U @ Vt2
# ---- 步骤2: 求解平移 ----
C = []
D = []
for i in range(N-1):
for j in range(i+1, N):
A = np.linalg.inv(T_be[i]) @ T_be[j]
B = np.linalg.inv(T_co[i]) @ T_co[j]
Ra, ta = A[:3,:3], A[:3,3]
Rb, tb = B[:3,:3], B[:3,3]
# (Ra - I) * tx = Rx * tb - ta
C.append(Ra - np.eye(3))
D.append(Rx @ tb - ta)
C = np.vstack(C)
D = np.hstack(D)
tx, _, _, _ = np.linalg.lstsq(C, D, rcond=None)
X = np.eye(4)
X[:3,:3] = Rx
X[:3,3] = tx
return X
def compute_error(X, robot_poses, cam_poses):
"""计算标定验证误差: 标定板在基座下位置的重投影误差 (mm)"""
errors = []
for rp, cp in zip(robot_poses, cam_poses):
T_be = pose_to_matrix(rp)
T_co = pose_to_matrix(cp)
T_obj = X @ T_be @ np.linalg.inv(T_co) # 标定板在基座下位姿
errors.append(T_obj[:3,3])
errors = np.array(errors)
# 计算所有位置到均值的距离标准差
mean_pos = errors.mean(axis=0)
dists = np.linalg.norm(errors - mean_pos, axis=1)
return dists.mean(), dists.max()
# ============ 示例数据(模拟采集8个位姿) ============
# 真实 X: 相机在基座前方 500mm, 高 300mm, 无旋转
X_true = np.eye(4)
X_true[:3,3] = [500, 0, 300]
# 生成模拟观测数据
np.random.seed(42)
robot_poses = []
cam_poses = []
for k in range(8):
# 机器人末端在基座下的位姿(绕基座Z轴旋转 + 平移变化)
ang = k * np.pi / 4
x = 200 * np.cos(ang)
y = 200 * np.sin(ang)
z = 100 + 20 * np.sin(k)
rp = [x, y, z, 0, 0, ang]
# 标定板在末端下的固定偏移
T_eo = pose_to_matrix([50, 0, 0, 0, 0, 0])
T_be = pose_to_matrix(rp)
T_co = np.linalg.inv(X_true) @ T_be @ T_eo # 标定板在相机下
cp = matrix_to_pose(T_co)
robot_poses.append(rp)
cam_poses.append(cp)
# 执行标定
X_est = hand_eye_calibration(robot_poses, cam_poses)
mean_err, max_err = compute_error(X_est, robot_poses, cam_poses)
print("标定结果 X (相机->机器人基座):")
print(np.round(X_est, 4))
print(f"\n验证误差: 平均 {mean_err:.4f} mm, 最大 {max_err:.4f} mm")
print("预期输出: 平均误差接近 0.0000 mm (无噪声模拟数据)")
运行与验证
依赖安装:
pip install numpy scipy
运行方式:将上述代码保存为 hand_eye_calib.py,执行 python hand_eye_calib.py。
验证判定标准:
- 程序正常输出 4×4 变换矩阵
X_est,其平移部分应接近[500, 0, 300],旋转部分接近单位阵; - 输出的平均验证误差应接近 0(模拟数据无噪声时理论为 0,实际受浮点精度影响通常 < 1e-6 mm);
- 将
X_est与真实X_true逐元素比较,最大偏差应小于 1e-3。
实际应用说明:在真实系统中,robot_poses 从机器人控制器读取(单位:mm 与 rad),cam_poses 通过拍摄标定板图像并利用相机内参求解(如 OpenCV 的 solvePnP 或 calibrateCamera)。采集时需保证机器人运动包含充分的旋转变化(至少绕两个不同轴旋转),否则旋转方程可能退化导致求解不稳定。本案例代码直接使用模拟数据验证算法正确性,真实数据采集与预处理需结合相机内参标定结果另行接入。