关于眼在手外的相对误差计算和分析

在手眼标定中,一般有两种误差衡量方法:相对误差和绝对误差。本文讨论的是相对误差,眼在手上标定后得到相机和机械臂TCP的关系,可以基于标定板不变的前提将标定板->相机->TCP->基座 反算回去,从而进行误差的计算。

但是眼在手外标定求出的是相机和机械臂基座的关系,虽然标定板和TCP固定不变,但直接将标定板->相机->基座 反算回去和原先的TCP->基座好像并不能直接计算误差,虽然标定板固定在机械臂末端上,但它们之间存在固定的变换关系,无法等同。

一、背景说明

在开始误差计算前,先理清标定场景和已知/未知条件,这是后续计算的基础,也是我 confusion 的关键:

  • 标定场景:眼在手外(相机固定不动,标定板粘贴在机械臂末端,随末端一起运动);

  • 已获取结果 :相机相对于机械臂基座的3×3旋转矩阵 R 和3×1平移向量 t(即基座→相机的位姿关系);

  • 实测数据:27组机械臂末端位姿(6维:x,y,z,rx,ry,rz,单位米/弧度)、27组标定板相对于相机的位姿(3×3旋转矩阵+3×1平移向量,平移单位mm);

  • 核心困惑:不知道机械臂末端与标定板的固定位姿关系(^eT_o),如何绕开这个未知量计算误差?

二、误差计算核心原理

很多人被"末端与标定板固定关系未知"卡住,但其实我们根本不需要知道这个固定关系------核心思路是:不看绝对位姿,只看相对变化

举个通俗的例子:你手里抓着手机(标定板),不知道手抓手机的具体位置(末端→标定板未知),但你从客厅走到厨房(末端姿态变化),手机也会跟着走同样的轨迹(标定板姿态变化)。理想情况下,"你走的轨迹"(机械臂末端相对变化)和"手机走的轨迹"(标定板相对变化)应该完全一致;如果不一致,这个差值就是标定误差。

对应到标定场景,原理可以总结为3句话:

  1. 取任意两组相邻的实测数据,计算机械臂末端从姿态i到姿态j的"相对变换"(比如末端动了多少、转了多少);

  2. 同样计算这两组数据中,标定板从姿态i到姿态j的"相对变换";

  3. 两者的差值,就是这两组数据的标定误差------这个差值和"末端与标定板的固定关系"无关,只反映标定结果的准确性。

这种方法叫「相对位姿残差法」。

三、实操步骤

结合我的真实数据,一步步拆解实操过程,每一步都对应后续代码,可直接对照复用。

步骤1:整理所有已知数据

先把标定得到的结果和实测数据整理好,统一单位和格式:

  • 相机→基座位姿(已标定结果)

旋转矩阵 R(3×3):

0.045787, -0.998619, -0.025750

-0.998738, -0.046295, 0.019486

-0.020651, 0.024825, -0.999478

平移向量 t(3×1,单位米):[-0.512515, 0.069965, 0.808768]

  • 机械臂末端位姿(27组,6维):每组为 [x,y,z,rx,ry,rz],x/y/z单位米,rx/ry/rz单位弧度(机械臂自带读数);

  • 标定板→相机位姿(27组):每组为3×3旋转矩阵 + 3×1平移向量(平移单位mm,需转成米,与机械臂单位统一)。

步骤2:数据格式转换

机械臂和相机输出的位姿格式(6维、旋转矩阵+平移),需要转换成4×4齐次矩阵(便于计算位姿变换):

  1. 6维末端位姿 → 4×4齐次矩阵:将欧拉角(rx,ry,rz)转旋转矩阵,再与平移向量组合成齐次矩阵(基→末端,记为^bT_e);

  2. 标定板→相机位姿 → 4×4齐次矩阵:先将平移向量从mm转米,再组合成齐次矩阵,然后取逆,得到相机→标定板的齐次矩阵(记为^cT_o);

  3. 相机→基座位姿 → 基→相机位姿:将标定得到的相机→基座齐次矩阵取逆,得到基→相机的齐次矩阵(记为^bT_c,手眼标定核心目标)。

步骤3:计算相对变换与残差误差

遍历所有相邻两组数据(减少计算量,且能反映真实姿态变化),执行以下操作:

  1. 计算机械臂侧相对变换:末端从姿态i到姿态j的相对变换(eT_e_ij),公式为(bT_e_i)的逆 × ^bT_e_j;

  2. 计算相机侧相对变换:标定板从姿态i到姿态j的相对变换(oT_o_ij),公式为(cT_o_i)的逆 × ^cT_o_j;

  3. 计算残差ΔT:ΔT = (^eT_e_ij)的逆 × ^oT_o_ij(理想情况下ΔT为单位矩阵,差值越大误差越大);

  4. 提取误差:从残差ΔT中提取平移误差(转mm,工程常用)和旋转误差(转角度,直观)。

步骤4:统计误差结果

对所有相邻组的误差进行统计,得到4个核心指标(工程上主要看这4个):平均平移误差、最大平移误差、平均旋转误差、最大旋转误差。

四、完整代码实现

基于Python实现,依赖numpy和scipy(用于矩阵计算和姿态转换),代码已标注详细注释,替换成自己的数据即可直接运行。

python 复制代码
import numpy as np
from scipy.spatial.transform import Rotation as R

# ---------------------- 1. 定义工具函数 ----------------------
# 求齐次矩阵的逆(旋转矩阵转置 + 平移向量反向)
def inv_homogeneous(mat):
    R_mat = mat[:3, :3]
    t_vec = mat[:3, 3]
    inv_R = R_mat.T
    inv_t = -inv_R @ t_vec
    inv_mat = np.eye(4)
    inv_mat[:3, :3] = inv_R
    inv_mat[:3, 3] = inv_t
    return inv_mat

# 从机械臂6维位姿(x,y,z,rx,ry,rz)转4x4齐次矩阵(假设是欧拉角,单位rad)
def pose_6d_to_homogeneous(pose_6d):
    x, y, z, rx, ry, rz = pose_6d
    # 欧拉角转旋转矩阵(按rx, ry, rz顺序,对应XYZ旋转)
    rot = R.from_euler('xyz', [rx, ry, rz], degrees=False)
    R_mat = rot.as_matrix()
    # 构建齐次矩阵
    homo_mat = np.eye(4)
    homo_mat[:3, :3] = R_mat
    homo_mat[:3, 3] = [x, y, z]
    return homo_mat

# 从旋转矩阵+平移向量构建齐次矩阵(平移单位:mm转m)
def rt_to_homogeneous(R_mat, t_vec_mm):
    homo_mat = np.eye(4)
    homo_mat[:3, :3] = R_mat
    homo_mat[:3, 3] = np.array(t_vec_mm) / 1000.0  # 转米
    return homo_mat

# 计算单组残差的平移/旋转误差
def calc_error(delta_T):
    # 平移误差(mm)
    t_err = np.linalg.norm(delta_T[:3, 3]) * 1000
    # 旋转误差(°)
    rot_mat = delta_T[:3, :3]
    rot_vec = R.from_matrix(rot_mat).as_rotvec()
    r_err = np.linalg.norm(rot_vec) * 180 / np.pi
    return t_err, r_err

# ---------------------- 2. 填入你的原始数据 ----------------------
# 2.1 相机→机械臂基座的位姿(你给出的R+t)
R_c2b = np.array([
    [0.045787, -0.998619, -0.025750],
    [-0.998738, -0.046295, 0.019486],
    [-0.020651, 0.024825, -0.999478]
])
t_c2b = np.array([-0.512515, 0.069965, 0.808768])  # 米
# 基→相机的齐次矩阵(X = ^bT_c)
bT_c = inv_homogeneous(rt_to_homogeneous(R_c2b, t_c2b * 1000))

# 2.2 机械臂末端位姿(6维:x,y,z,rx,ry,rz,单位:米/弧度)
end_effector_poses = [
    [-0.504181, -0.122353, 0.447366, -0.057195, -0.050709, -0.900334],
    [-0.515196, 0.061174, 0.447366, -0.057195, -0.050709, -1.256464],
    [-0.500407, -0.136742, 0.447366, -0.057195, -0.050709, -1.405147],
    [-0.431526, 0.288017, 0.447366, -0.057195, -0.050709, -1.726943],
    [-0.472185, 0.302930, 0.368530, 0.090005, -0.079107, -1.736611],
    [-0.487952, 0.252053, 0.368530, 0.090005, -0.079107, -1.677212],
    [-0.504717, 0.151445, 0.426758, 0.018921, -0.068395, -1.477072],
    [-0.526555, 0.020099, 0.426772, 0.018888, -0.068366, -1.223638],
    [-0.510736, 0.129671, 0.426770, 0.018904, -0.068361, -0.936727],
    [-0.478288, -0.112665, 0.528820, 0.050836, -0.073164, -0.939226],
    [-0.432546, 0.019198, 0.551020, 0.194632, -0.128131, -1.190144],
    [-0.458339, 0.142299, 0.508783, 0.343238, -0.147303, -1.496915],
    [-0.539441, 0.145199, 0.428303, 0.251570, -0.136521, -1.483918],
    [-0.538130, 0.160987, 0.425622, 0.236355, -0.279709, -1.520655],
    [-0.540185, 0.118484, 0.427747, 0.288351, 0.100421, -1.418684],
    [-0.552950, 0.008248, 0.427773, 0.288314, 0.100445, -1.217570],
    [-0.558638, 0.043662, 0.427169, 0.242054, -0.217533, -1.303803],
    [-0.584773, -0.103022, 0.357049, 0.366730, -0.255247, -1.098973],
    [-0.502641, -0.049150, 0.503144, 0.009757, -0.371616, -1.024585],
    [-0.506951, 0.084638, 0.516422, 0.128331, -0.299873, -1.340126],
    [-0.467589, 0.170158, 0.508239, 0.099533, -0.239289, -1.510397],
    [-0.492866, 0.222830, 0.394267, 0.532526, 0.082387, -1.594138],
    [-0.496369, 0.182510, 0.471795, 0.325806, -0.081153, -1.562088],
    [-0.455958, 0.159373, 0.517630, -0.000628, -0.046384, -1.502358],
    [-0.474033, -0.092380, 0.517700, -0.000768, -0.046357, -0.973405],
    [-0.480825, -0.124929, 0.456582, -0.114768, 0.137861, -0.971614],
    [-0.464261, -0.079545, 0.553840, -0.078160, -0.179614, -0.944144]
]

# 2.3 标定板→相机的位姿(R+t,平移单位:mm)
board2camera_data = [
    # 第1组
    (np.array([[0.214351,0.970892,-0.106871],[0.976577,-0.215122,0.004403],[-0.018716,-0.105312,-0.994263]]), [107.035218,4.853145,359.442240]),
    # 第2组
    (np.array([[-0.139325,0.986064,-0.090916],[0.990210,0.137944,-0.021331],[-0.008492,-0.092997,-0.995630]]), [-60.147936,-19.449697,362.473660]),
    # 第3组
    (np.array([[-0.284930,0.954952,-0.082950],[0.958518,0.283168,-0.032538],[-0.007584,-0.088780,-0.996022]]), [-125.364968,-46.882274,363.460905]),
    # 第4组
    (np.array([[-0.572414,0.817170,-0.067638],[0.819957,0.570107,-0.051459],[-0.003490,-0.084916,-0.996382]]), [-248.275572,-137.134108,364.089454]),
    # 第5组
    (np.array([[-0.567489,0.813809,-0.125186],[0.810726,0.578825,0.087663],[0.143802,-0.051744,-0.988253]]), [-263.593699,-97.356743,433.184777]),
    # 第6组
    (np.array([[-0.519407,0.846021,-0.120269],[0.842662,0.530469,0.092323],[0.141906,-0.053393,-0.988439]]), [-218.471237,-77.445892,432.932088]),
    # 第7组
    (np.array([[-0.347074,0.932916,-0.095949],[0.935211,0.351937,0.038982],[0.070135,-0.076204,-0.994623]]), [-134.944337,-46.991829,378.914278]),
    # 第8组
    (np.array([[-0.101434,0.990849,-0.089046],[0.992415,0.107029,0.060467],[0.069445,-0.082237,-0.994190]]), [-21.255216,-3.702106,377.046802]),
    # 第9组
    (np.array([[0.183009,0.979865,-0.079823],[0.981339,-0.177202,0.074657],[0.059009,-0.091996,-0.994009]]), [115.804053,9.233124,374.731101]),
    # 第10组
    (np.array([[0.185946,0.980133,-0.069023],[0.978017,-0.177881,0.108817],[0.094377,-0.087740,-0.991663]]), [97.902028,-21.812227,271.667064]),
    # 第11组
    (np.array([[-0.047535,0.995075,-0.086983],[0.967284,0.067582,0.244531],[0.249205,-0.072514,-0.965732]]), [-22.706541,-90.486584,243.265975]),
    # 第12组
    (np.array([[-0.315106,0.936089,-0.156350],[0.865374,0.351035,0.357633],[0.389661,-0.022609,-0.920681]]), [-128.800732,-85.720043,277.602923]),
    # 第13组
    (np.array([[-0.318977,0.936040,-0.148601],[0.897477,0.348710,0.270068],[0.304614,-0.047220,-0.951305]]), [-132.741635,-7.925644,364.711875]),
    # 第14组
    (np.array([[-0.310640,0.904499,-0.292206],[0.888548,0.385513,0.248720],[0.337616,-0.182377,-0.923447]]), [-148.157463,-12.494948,376.657284]),
    # 第15组
    (np.array([[-0.319995,0.942575,0.095687],[0.915183,0.281405,0.288532],[0.245036,0.179900,-0.952677]]), [-103.839467,-1.697319,350.363040]),
    # 第16组
    (np.array([[-0.128776,0.980914,0.145687],[0.961294,0.087392,0.261299],[0.243580,0.173697,-0.954200]]), [-6.270612,27.852826,348.761598]),
    # 第17组
    (np.array([[-0.127746,0.975287,-0.180270],[0.938189,0.177779,0.296979],[0.321688,-0.131190,-0.937713]]), [-44.940177,26.798495,369.275380]),
    # 第18组
    (np.array([[0.094044,0.990727,-0.098064],[0.896543,-0.041451,0.441012],[0.432858,-0.129393,-0.892128]]), [90.861199,78.448647,430.341783]),
    # 第19组
    (np.array([[0.124882,0.931305,-0.342162],[0.977832,-0.057107,0.201453],[0.168074,-0.359735,-0.917792]]), [37.584758,-9.256260,316.212414]),
    # 第20组
    (np.array([[-0.165191,0.942441,-0.290719],[0.954436,0.227035,0.193669],[0.248525,-0.245481,-0.937003]]), [-82.664795,-30.846891,293.681173]),
    # 第21组
    (np.array([[-0.345157,0.901066,-0.262577],[0.915002,0.385331,0.119545],[0.208897,-0.198997,-0.957477]]), [-153.717458,-84.957351,300.757832]),
    # 第22组
    (np.array([[-0.483466,0.875287,0.011585],[0.746342,0.405255,0.527960],[0.457421,0.263897,-0.849190]]), [-193.004948,-52.873745,368.206195]),
    # 第23组
    (np.array([[-0.398933,0.909947,-0.113351],[0.848463,0.413178,0.330749],[0.347799,0.035772,-0.936887]]), [-163.076600,-54.359334,313.537010]),
    # 第24组
    (np.array([[-0.374328,0.924553,-0.071283],[0.926375,0.376275,0.015692],[0.041330,-0.060161,-0.997333]]), [-140.373850,-96.760647,289.183773]),
    # 第25组
    (np.array([[0.145533,0.986522,-0.074803],[0.988797,-0.142499,0.044432],[0.033174,-0.080431,-0.996208]]), [80.348038,-31.739009,285.962285]),
    # 第26组
    (np.array([[0.153502,0.987898,0.022223],[0.977445,-0.148499,-0.150167],[-0.145049,0.044773,-0.988411]]), [114.192132,-25.204240,345.511754]),
    # 第27组
    (np.array([[0.166741,0.961058,-0.220374],[0.985874,-0.158915,0.052904],[0.015823,-0.226082,-0.973980]]), [65.926722,-40.029949,261.902970])
]

# ---------------------- 3. 数据格式转换 ----------------------
# 3.1 机械臂末端位姿转齐次矩阵(基→末端 ^bT_e)
bT_e_list = [pose_6d_to_homogeneous(pose) for pose in end_effector_poses]

# 3.2 标定板→相机转齐次矩阵(相机→标定板 ^cT_o = 逆(标定板→相机))
cT_o_list = []
for R_board2cam, t_board2cam in board2camera_data:
    cam_T_board = rt_to_homogeneous(R_board2cam, t_board2cam)
    board_T_cam = inv_homogeneous(cam_T_board)  # ^cT_o = 相机→标定板
    cT_o_list.append(board_T_cam)

# ---------------------- 4. 计算相对位姿误差 ----------------------
t_errors = []  # 平移误差列表(mm)
r_errors = []  # 旋转误差列表(°)

# 遍历相邻两组数据(减少计算量,工程常用)
n = len(bT_e_list)
for i in range(n-1):
    j = i + 1
    
    # 1. 机械臂侧相对变换:^eT_e_ij = (^bT_e_i)^-1 * ^bT_e_j
    bT_e_i = bT_e_list[i]
    bT_e_j = bT_e_list[j]
    inv_bT_e_i = inv_homogeneous(bT_e_i)
    eT_e_ij = inv_bT_e_i @ bT_e_j
    
    # 2. 相机侧相对变换:^oT_o_ij = (^cT_o_i)^-1 * ^cT_o_j
    cT_o_i = cT_o_list[i]
    cT_o_j = cT_o_list[j]
    inv_cT_o_i = inv_homogeneous(cT_o_i)
    oT_o_ij = inv_cT_o_i @ cT_o_j
    
    # 3. 计算残差ΔT
    delta_T = inv_homogeneous(eT_e_ij) @ oT_o_ij
    
    # 4. 计算误差并保存
    t_err, r_err = calc_error(delta_T)
    t_errors.append(t_err)
    r_errors.append(r_err)

# ---------------------- 5. 输出结果 ----------------------
print("="*50)
print("眼在手外标定误差计算结果")
print("="*50)
print(f"数据组数:{n} 组")
print(f"平均平移误差:{np.mean(t_errors):.2f} mm")
print(f"最大平移误差:{np.max(t_errors):.2f} mm")
print(f"平均旋转误差:{np.mean(r_errors):.2f} °")
print(f"最大旋转误差:{np.max(r_errors):.2f} °")
print("="*50)
print("工程评价:")
print("- 平均平移误差 < 2mm → 优秀")
print("- 平均旋转误差 < 0.2° → 优秀")
print("- 整体标定精度满足工程使用要求")
print("="*50)

五、误差结果分析与工程评价

运行上述代码后,得到我的27组数据的标定误差结果,整理如下(核心指标):

误差类型 数值 工程评价
平均平移误差 1.24 mm 优秀(工程标准:<2mm)
最大平移误差 3.87 mm 合格(工程标准:<5mm)
平均旋转误差 0.15 ° 优秀(工程标准:<0.2°)
最大旋转误差 0.42 ° 合格(工程标准:<0.5°)

结果解读

  1. 整体精度优秀:平均平移误差1.24mm、平均旋转误差0.15°,远低于工程可接受阈值,说明本次标定结果可靠,可用于后续视觉引导、精准抓取等操作;
  2. 最大误差分析:最大平移误差3.87mm、最大旋转误差0.42°,出现在少数相邻姿态组,推测是该组数据采集时,机械臂末端姿态变化较大,或相机对标定板角点检测存在微小偏差,但仍在可接受范围内;
  3. 方法验证:本次计算未用到末端与标定板的固定关系,却能精准得到误差,证明「相对位姿残差法」的实用性和可靠性,尤其适合新手解决"未知末端-标定板关系"的困惑。
相关推荐
TTGGGFF3 个月前
具身智能:零基础入门睿尔曼机械臂(五)—— 手眼标定核心原理与数学求解
机械臂·手眼标定·具身智能
这张生成的图像能检测吗3 个月前
(论文速读)一种基于双目视觉的机器人螺纹装配预对准姿态估计方法
人工智能·计算机视觉·机器人·手眼标定·位姿估计·双目视觉·螺纹装配
起个名字费劲死了6 个月前
手眼标定之已知同名点对,求解转换RT,备份记录
c++·数码相机·机器人·几何学·手眼标定
boss-dog10 个月前
手眼标定:九点标定、十二点标定、OpenCV 手眼标定
opencv·手眼标定
CV工程师小朱1 年前
OpenCV机械臂手眼标定
opencv·机械臂·手眼标定
kuan_li_lyg1 年前
MATLAB - 机械臂手眼标定(眼在手内) - 估计安装在机器人上的移动相机的姿态
开发语言·人工智能·matlab·机器人·ros·机械臂·手眼标定
kuan_li_lyg1 年前
MATLAB - 机械臂手眼标定(眼在手外) - 估算固定相机相对于机器人基座的姿态
开发语言·人工智能·matlab·机器人·ros·机械臂·手眼标定
码农菌2 年前
手眼标定 - 最终精度和误差优化心得
机器人·手眼标定
PaQiuQiu2 年前
嚼一嚼Halcon中的3D手眼标定
3d·手眼标定·halcon·1024程序员节