3D手眼标定全流程详解
3D手眼标定全流程详解及准备工作指南
手眼标定(Hand-Eye Calibration)是连接机器人与视觉系统的核心环节,通过确定相机与机器人基座/工具的空间变换关系,实现视觉数据到机器人坐标的精准转换。
一、硬件准备工作
二、环境搭建要求
-
照明条件
- 均匀漫射光源(避免反光/阴影)
- 光照强度 ≥ 1000 lux(推荐条形LED)
-
空间布局
#mermaid-svg-W8LK4ffIzHa6H63S {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-W8LK4ffIzHa6H63S .error-icon{fill:#552222;}#mermaid-svg-W8LK4ffIzHa6H63S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-W8LK4ffIzHa6H63S .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-W8LK4ffIzHa6H63S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-W8LK4ffIzHa6H63S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-W8LK4ffIzHa6H63S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-W8LK4ffIzHa6H63S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-W8LK4ffIzHa6H63S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-W8LK4ffIzHa6H63S .marker.cross{stroke:#333333;}#mermaid-svg-W8LK4ffIzHa6H63S svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-W8LK4ffIzHa6H63S .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-W8LK4ffIzHa6H63S .cluster-label text{fill:#333;}#mermaid-svg-W8LK4ffIzHa6H63S .cluster-label span{color:#333;}#mermaid-svg-W8LK4ffIzHa6H63S .label text,#mermaid-svg-W8LK4ffIzHa6H63S span{fill:#333;color:#333;}#mermaid-svg-W8LK4ffIzHa6H63S .node rect,#mermaid-svg-W8LK4ffIzHa6H63S .node circle,#mermaid-svg-W8LK4ffIzHa6H63S .node ellipse,#mermaid-svg-W8LK4ffIzHa6H63S .node polygon,#mermaid-svg-W8LK4ffIzHa6H63S .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-W8LK4ffIzHa6H63S .node .label{text-align:center;}#mermaid-svg-W8LK4ffIzHa6H63S .node.clickable{cursor:pointer;}#mermaid-svg-W8LK4ffIzHa6H63S .arrowheadPath{fill:#333333;}#mermaid-svg-W8LK4ffIzHa6H63S .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-W8LK4ffIzHa6H63S .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-W8LK4ffIzHa6H63S .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-W8LK4ffIzHa6H63S .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-W8LK4ffIzHa6H63S .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-W8LK4ffIzHa6H63S .cluster text{fill:#333;}#mermaid-svg-W8LK4ffIzHa6H63S .cluster span{color:#333;}#mermaid-svg-W8LK4ffIzHa6H63S div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-W8LK4ffIzHa6H63S :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}Eye-to-Hand配置固定固定在支架3D相机机器人末端标定板
#mermaid-svg-ocGbVlqs2tCkRWnC {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ocGbVlqs2tCkRWnC .error-icon{fill:#552222;}#mermaid-svg-ocGbVlqs2tCkRWnC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ocGbVlqs2tCkRWnC .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-ocGbVlqs2tCkRWnC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ocGbVlqs2tCkRWnC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ocGbVlqs2tCkRWnC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ocGbVlqs2tCkRWnC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ocGbVlqs2tCkRWnC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ocGbVlqs2tCkRWnC .marker.cross{stroke:#333333;}#mermaid-svg-ocGbVlqs2tCkRWnC svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ocGbVlqs2tCkRWnC .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ocGbVlqs2tCkRWnC .cluster-label text{fill:#333;}#mermaid-svg-ocGbVlqs2tCkRWnC .cluster-label span{color:#333;}#mermaid-svg-ocGbVlqs2tCkRWnC .label text,#mermaid-svg-ocGbVlqs2tCkRWnC span{fill:#333;color:#333;}#mermaid-svg-ocGbVlqs2tCkRWnC .node rect,#mermaid-svg-ocGbVlqs2tCkRWnC .node circle,#mermaid-svg-ocGbVlqs2tCkRWnC .node ellipse,#mermaid-svg-ocGbVlqs2tCkRWnC .node polygon,#mermaid-svg-ocGbVlqs2tCkRWnC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ocGbVlqs2tCkRWnC .node .label{text-align:center;}#mermaid-svg-ocGbVlqs2tCkRWnC .node.clickable{cursor:pointer;}#mermaid-svg-ocGbVlqs2tCkRWnC .arrowheadPath{fill:#333333;}#mermaid-svg-ocGbVlqs2tCkRWnC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ocGbVlqs2tCkRWnC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ocGbVlqs2tCkRWnC .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-ocGbVlqs2tCkRWnC .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-ocGbVlqs2tCkRWnC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ocGbVlqs2tCkRWnC .cluster text{fill:#333;}#mermaid-svg-ocGbVlqs2tCkRWnC .cluster span{color:#333;}#mermaid-svg-ocGbVlqs2tCkRWnC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ocGbVlqs2tCkRWnC :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}Eye-in-Hand配置固定固定3D相机机器人末端工作台标定板
三、四大坐标系定义与关联
四、手眼关系的数学本质
1. Eye-in-Hand(眼在手上)
2. Eye-to-Hand(眼在手外)
五、所得矩阵的具体应用方法
场景1:物体抓取(Eye-in-Hand)
场景2:视觉引导定位(Eye-to-Hand)
六、标定流程详述
步骤1:相机内参标定(需提前完成)
* 拍摄多角度标定板图像for Index := 1 to 15 by 1 grab_image (Image, AcqHandle) find_calib_object (Image, CalibDataID, Index, 0, [], [])endfor* 计算内参矩阵calibrate_cameras (CalibDataID, Error)get_calib_data (CalibDataID, \'camera\', 0, \'params\', CamParam)
步骤2:手眼数据采集
T_base_tool_i := [0.1,0.2,0.5,0,0,1.57]
rotate_tool_z(deg(30))
set_tool_pose(rx:deg(40))
sync_robot_vision()
步骤3:坐标系定义(关键!)
World Frame (W) : 机器人基座标系Tool Frame (T) : 机器人末端坐标系Camera Frame (C) : 相机光学中心Calib Frame (B) : 标定板坐标系手眼关系求解目标:Eye-in-Hand: T_tool_cam (W→T→C)Eye-to-Hand: T_base_cam (W→C)
步骤4:执行标定计算
* 创建标定模型create_hand_eye_calib_model (\'eye_in_hand\', CalibHandle)* 添加数据点for i := 1 to NumPoses by 1 * 获取机器人T_base_tool位姿 get_robot_pose(robotHandle, PoseBaseTool) * 获取标定板在相机系位姿 find_calib_object_pose(Image, CamParam, CalibObjDescr, PoseCamCalib) * 添加到位姿池 set_hand_eye_calib_data(CalibHandle, \'tool\', i, \'tool_pose\', PoseBaseTool) set_hand_eye_calib_data(CalibHandle, \'calib_obj\', i, \'obj_pose\', PoseCamCalib)endfor* 执行标定hand_eye_calibration(CalibHandle, \'nonlinear\', \'error\', Errors)get_hand_eye_calib_result(CalibHandle, \'camera_pose\', PoseToolCam)
步骤5:结果验证
* 坐标转换验证T_tool_workpiece := [0.3, -0.1, 0.5, 0, 0, 0]pose_compose(PoseToolCam, T_tool_workpiece, T_cam_workpiece)predict_shape_model_3d(T_cam_workpiece, Model3D, ScenePointCloud)* 投影误差计算dev_display(ScenePointCloud)calculate_3d_distance(ProjectedPoints, MeasuredPoints, AvgError)if (AvgError > 0.5) * 误差>0.5mm需重新标定endif
我将按照您要求的四种情况分别详细介绍手眼标定的原理、流程步骤、流程图、矩阵应用及代码实现。
下面我将为您详细解析基于3D模型和标定板的3D手眼标定原理、流程步骤、流程图及矩阵应用,并提供相应的OpenCV和Halcon代码实现。
七、基于3D模型的手眼标定
1. 眼在手上(Eye-in-Hand)基于3D模型
原理:
相机安装在机器人末端,与末端执行器一起移动。固定位置的3D已知几何模型作为标定目标。通过比较不同机器人位姿下相机观测到的模型位置,建立相机坐标系与机器人坐标系之间的转换关系。
详细流程步骤:
-
准备阶段:
- 固定3D模型在工作空间
- 测量/准备3D模型的几何数据和特征点
- 标定相机内参(焦距、畸变等)
-
数据采集:
- 将机器人移动到15-20个不同位姿
- 每个位姿下:
a. 记录机器人末端位姿(齐次变换矩阵)
b. 采集特征点图像
c. 保存机器人位姿数据
-
特征提取与匹配:
- 对每张图像提取特征点(角点、边缘等)
- 将图像特征点匹配到3D模型对应点
-
求解相机外参:
- 对每个位姿使用PnP算法计算模型坐标到相机坐标的变换
- 公式:s * [u v 1]ᵀ = K * [R | t] * [X Y Z 1]ᵀ
-
建立手眼方程:
- 对每个位姿i:T_base_cam_i = T_base_gripper_i * T_gripper_cam
- 对连续位姿变化:A * X = X * B
A = T_cam_i_cam_j = T_camj_inv * T_cami
B = T_gripper_i_gripper_j = T_gripperj_inv * T_gripperi
X = T_gripper_cam(待求手眼矩阵)
-
求解手眼矩阵:
- 收集多个位姿变化对
- 使用线性方法(如Tsai, Park)或非线性优化求解
-
验证与优化:
- 重投影误差分析
- 标定精度验证
- 优化(束调整)提高精度
流程图:
#mermaid-svg-O9i62fv9JbWdTm4i {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-O9i62fv9JbWdTm4i .error-icon{fill:#552222;}#mermaid-svg-O9i62fv9JbWdTm4i .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-O9i62fv9JbWdTm4i .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-O9i62fv9JbWdTm4i .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-O9i62fv9JbWdTm4i .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-O9i62fv9JbWdTm4i .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-O9i62fv9JbWdTm4i .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-O9i62fv9JbWdTm4i .marker{fill:#333333;stroke:#333333;}#mermaid-svg-O9i62fv9JbWdTm4i .marker.cross{stroke:#333333;}#mermaid-svg-O9i62fv9JbWdTm4i svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-O9i62fv9JbWdTm4i .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-O9i62fv9JbWdTm4i .cluster-label text{fill:#333;}#mermaid-svg-O9i62fv9JbWdTm4i .cluster-label span{color:#333;}#mermaid-svg-O9i62fv9JbWdTm4i .label text,#mermaid-svg-O9i62fv9JbWdTm4i span{fill:#333;color:#333;}#mermaid-svg-O9i62fv9JbWdTm4i .node rect,#mermaid-svg-O9i62fv9JbWdTm4i .node circle,#mermaid-svg-O9i62fv9JbWdTm4i .node ellipse,#mermaid-svg-O9i62fv9JbWdTm4i .node polygon,#mermaid-svg-O9i62fv9JbWdTm4i .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-O9i62fv9JbWdTm4i .node .label{text-align:center;}#mermaid-svg-O9i62fv9JbWdTm4i .node.clickable{cursor:pointer;}#mermaid-svg-O9i62fv9JbWdTm4i .arrowheadPath{fill:#333333;}#mermaid-svg-O9i62fv9JbWdTm4i .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-O9i62fv9JbWdTm4i .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-O9i62fv9JbWdTm4i .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-O9i62fv9JbWdTm4i .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-O9i62fv9JbWdTm4i .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-O9i62fv9JbWdTm4i .cluster text{fill:#333;}#mermaid-svg-O9i62fv9JbWdTm4i .cluster span{color:#333;}#mermaid-svg-O9i62fv9JbWdTm4i div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-O9i62fv9JbWdTm4i :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}是否开始固定3D模型移动机器人到不同位姿记录末端位姿 T_base_gripper_i采集模型图像提取图像特征点匹配3D模型点PnP求解 T_model_cam_i构建相对运动对求解 AX = XB得到 T_gripper_cam验证精度精度合格?结束
矩阵应用:
设:
T_base_gripper
:基座→末端的变换T_model_cam
:模型→相机的变换T_gripper_cam
:末端→相机的变换(待求)
完整变换链:T_base_model = T_base_gripper * T_gripper_cam * T_model_cam⁻¹
实际应用中主要用于:T_base_target = T_base_gripper * T_gripper_cam * T_cam_target
OpenCV代码(详细注释版):
void eyeInHand3DCalibration( const vector<Mat>& gripper_poses, // 机器人末端姿态,4x4齐次矩阵 const vector<vector<Point3f>>& model_points, // 每个位姿对应的3D模型点 const vector<vector<Point2f>>& image_points, // 对应的2D特征点 const Mat& camera_matrix, const Mat& dist_coeffs, Mat& T_gripper_cam, // 输出的手眼矩阵 vector<double>& reproj_errors) // 重投影误差{ vector<Mat> camera_poses; reproj_errors.clear(); // Step 1: 计算每个位姿下相机相对模型的位姿 for(size_t i = 0; i < image_points.size(); i++) { Mat rvec, tvec; // 使用迭代法提高精度 solvePnP(model_points[i], image_points[i], camera_matrix, dist_coeffs, rvec, tvec, false, SOLVEPNP_ITERATIVE); // 计算重投影误差 vector<Point2f> reprojected; projectPoints(model_points[i], rvec, tvec,camera_matrix, dist_coeffs,reprojected); double error = norm(image_points[i], reprojected, NORM_L2) / reprojected.size(); reproj_errors.push_back(error); // 转换为4x4变换矩阵 Mat R; Rodrigues(rvec, R); Mat pose = Mat::eye(4, 4, CV_64F); R.copyTo(pose(Rect(0, 0, 3, 3))); tvec.copyTo(pose(Rect(3, 0, 1, 3))); camera_poses.push_back(pose); } // Step 2: 构建相对运动对 vector<Mat> A_rel, B_rel; for(size_t i = 1; i < gripper_poses.size(); i++) { // 计算相机相对运动: A = P_i⁻¹ * P_{i-1} Mat A_i = camera_poses[i].inv() * camera_poses[i-1]; // 计算机械臂相对运动: B = G_i⁻¹ * G_{i-1} Mat B_i = gripper_poses[i].inv() * gripper_poses[i-1]; A_rel.push_back(A_i); B_rel.push_back(B_i); } // Step 3: 使用Tsai方法求解手眼矩阵 calibrateHandEye(A_rel, B_rel, T_gripper_cam, CALIB_HAND_EYE_TSAI); // Step 4: 验证手眼矩阵 double avg_error = 0; for(size_t i = 0; i < gripper_poses.size(); i++) { // 通过手眼矩阵计算相机位置 Mat T_base_cam_calc = gripper_poses[i] * T_gripper_cam; // 直接通过PnP得到的相机位置 Mat T_base_cam_meas = camera_poses[i]; // 计算位置误差 double pos_error = norm(T_base_cam_calc(Rect(3,0,1,3)) - T_base_cam_meas(Rect(3,0,1,3))); // 计算方向误差 Mat R_calc, R_meas; T_base_cam_calc(Rect(0,0,3,3)).copyTo(R_calc); T_base_cam_meas(Rect(0,0,3,3)).copyTo(R_meas); double rot_error = norm(R_calc - R_meas); avg_error += (pos_error + rot_error) / 2.0; } avg_error /= gripper_poses.size(); cout << \"Average calibration error: \" << avg_error << endl;}
Halcon代码:
* 创建手眼标定模型create_calib_data (\'hand_eye\', 1, 1, CalibDataID)* 设置相机参数(内参已标定)set_calib_data_cam_param (CalibDataID, 0, CamParam, \'area_scan_polynomial\')* 准备3D模型数据(假设已定义模型点)NumPoints := 12gen_object_model_3d_from_points ([X,Y,Z], ObjectModel3D)* 循环添加位姿数据for Index := 0 to NumPoses-1 by 1 * 获取机器人末端位姿(从机器人控制器) get_robot_pose (RobotPose, Index) * 获取当前图像和3D模型点对应的2D点 dev_display (Image) find_object_model_3d (ObjectModel3D, Image, CamParam, ObjInCamPose, MatchStartValue, MatchStopValue, MinSize, PointIndex, PointCoord) * 添加到位姿列表中 set_calib_data (CalibDataID, \'hand_eye\', \'add_observation\', [Index,0], [RobotPose, ObjInCamPose])endfor* 执行手眼标定calibrate_hand_eye (CalibDataID, Errors)* 获取标定结果(相机在末端坐标系中的位姿)get_calib_data (CalibDataID, \'hand_eye\', \'camera_in_hand_pose\', CameraInHandPose)
2. 眼在手外(Eye-to-Hand)基于3D模型
原理:
相机固定在工作环境中,观测安装在机器人末端上的3D模型。通过不同机器人位姿下模型在相机视野中的位置变化,求解相机相对于机器人基座的位姿。
详细流程步骤:
-
准备阶段:
- 在机器人末端安装3D标定模型
- 固定相机位置
- 标定相机内参
-
数据采集:
- 移动机器人到多个位姿
- 每个位姿下:
a. 记录机器人末端位姿
b. 采集末端模型图像
-
特征匹配:
- 检测图像特征
- 匹配到3D模型对应点
-
求解模型位姿:
- 使用PnP求解模型在相机坐标系中的位置
- 公式:s * p = K * [R|t] * P
-
建立手眼方程:
T_cam_base * T_base_gripper = T_cam_gripper = T_cam_model * T_model_gripper
T_cam_base * ΔT_base_gripper = ΔT_cam_model * T_cam_base
转换为:A * X = X * B
其中:A = T_cam_model_j⁻¹ * T_cam_model_i
B = T_base_gripper_j⁻¹ * T_base_gripper_i
X = T_cam_base
-
求解变换矩阵:
- 收集多个运动对
- 使用Shah或Daniilidis方法求解
-
验证与优化:
- 检查变换一致性
- 重投影误差计算
- 束调整优化位姿
流程图:
#mermaid-svg-O4tg0OjgvJRz8j3c {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-O4tg0OjgvJRz8j3c .error-icon{fill:#552222;}#mermaid-svg-O4tg0OjgvJRz8j3c .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-O4tg0OjgvJRz8j3c .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-O4tg0OjgvJRz8j3c .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-O4tg0OjgvJRz8j3c .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-O4tg0OjgvJRz8j3c .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-O4tg0OjgvJRz8j3c .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-O4tg0OjgvJRz8j3c .marker{fill:#333333;stroke:#333333;}#mermaid-svg-O4tg0OjgvJRz8j3c .marker.cross{stroke:#333333;}#mermaid-svg-O4tg0OjgvJRz8j3c svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-O4tg0OjgvJRz8j3c .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-O4tg0OjgvJRz8j3c .cluster-label text{fill:#333;}#mermaid-svg-O4tg0OjgvJRz8j3c .cluster-label span{color:#333;}#mermaid-svg-O4tg0OjgvJRz8j3c .label text,#mermaid-svg-O4tg0OjgvJRz8j3c span{fill:#333;color:#333;}#mermaid-svg-O4tg0OjgvJRz8j3c .node rect,#mermaid-svg-O4tg0OjgvJRz8j3c .node circle,#mermaid-svg-O4tg0OjgvJRz8j3c .node ellipse,#mermaid-svg-O4tg0OjgvJRz8j3c .node polygon,#mermaid-svg-O4tg0OjgvJRz8j3c .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-O4tg0OjgvJRz8j3c .node .label{text-align:center;}#mermaid-svg-O4tg0OjgvJRz8j3c .node.clickable{cursor:pointer;}#mermaid-svg-O4tg0OjgvJRz8j3c .arrowheadPath{fill:#333333;}#mermaid-svg-O4tg0OjgvJRz8j3c .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-O4tg0OjgvJRz8j3c .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-O4tg0OjgvJRz8j3c .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-O4tg0OjgvJRz8j3c .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-O4tg0OjgvJRz8j3c .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-O4tg0OjgvJRz8j3c .cluster text{fill:#333;}#mermaid-svg-O4tg0OjgvJRz8j3c .cluster span{color:#333;}#mermaid-svg-O4tg0OjgvJRz8j3c div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-O4tg0OjgvJRz8j3c :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}是否开始末端安装3D模型固定相机位置移动机器人到不同位姿记录末端位姿采集模型图像提取特征点匹配3D模型点PnP求解 T_cam_model构建相对运动对求解 AX = XB得到 T_cam_base验证精度精度合格?结束
矩阵应用:
设:
T_base_gripper
:基座→末端的变换T_cam_model
:相机→模型的变换T_cam_base
:相机→基座的变换(待求)
变换关系:T_base_model = T_cam_base⁻¹ * T_cam_model
实际应用:T_base_cam = (T_cam_base)⁻¹
OpenCV代码:
void eyeToHand3DCalibration( const vector<Mat>& gripper_poses, const vector<vector<Point3f>>& model_points, const vector<vector<Point2f>>& image_points, const Mat& camera_matrix, const Mat& dist_coeffs, Mat& T_cam_base) { vector<Mat> model_in_cam_poses; // 计算模型在相机坐标系中的位姿 for(size_t i = 0; i < image_points.size(); i++) { Mat rvec, tvec; solvePnP(model_points[i], image_points[i], camera_matrix, dist_coeffs, rvec, tvec); Mat R; Rodrigues(rvec, R); Mat pose = Mat::eye(4, 4, CV_64F); R.copyTo(pose(Rect(0, 0, 3, 3))); tvec.copyTo(pose(Rect(3, 0, 1, 3))); model_in_cam_poses.push_back(pose); } vector<Mat> A, B; // 考虑所有可能的运动对组合(需考虑运动幅度) vector<pair<int, int>> pairs = generateOptimalPairs(gripper_poses); for(auto& p : pairs) { int i = p.first, j = p.second; // 计算相机坐标系中模型的相对运动 Mat A_ij = model_in_cam_poses[j] * model_in_cam_poses[i].inv(); // 计算机器人基座坐标系中的相对运动 Mat B_ij = gripper_poses[j] * gripper_poses[i].inv(); A.push_back(A_ij); B.push_back(B_ij); } // 使用Daniilidis方法求解眼在手外标定 calibrateHandEye(A, B, T_cam_base, CALIB_HAND_EYE_DANIILIDIS); // 验证标定结果 double avg_error = 0; for(size_t i = 0; i < gripper_poses.size(); i++) { // 直接通过PnP得到的模型位姿 Mat T_cam_model = model_in_cam_poses[i]; // 通过标定得到的关系:T_cam_model = T_cam_base * T_base_gripper * T_gripper_model // 其中T_gripper_model是常数(模型在末端的位置) Mat T_cam_model_calc = T_cam_base * gripper_poses[i] * T_model_on_gripper; // 比较旋转矩阵 Mat R_calc, R_meas; T_cam_model_calc(Rect(0,0,3,3)).copyTo(R_calc); T_cam_model(Rect(0,0,3,3)).copyTo(R_meas); double error = rotationDifference(R_calc, R_meas); avg_error += error; } avg_error /= gripper_poses.size(); cout << \"Average alignment error: \" << avg_error << \" degrees\" << endl;}
Halcon代码:
* 眼在手外标定模型创建create_calib_data (\'hand_eye\', 1, 1, CalibDataID)* 设置相机参数set_calib_data_cam_param (CalibDataID, 0, CamParam, \'area_scan_telecentric_division\')* 准备模型点数据ModelPoints := [[x1,y1,z1], [x2,y2,z2], ... ]* 添加观测数据for Index := 0 to NumPoses-1 by 1 * 从机器人获取末端位姿 T_base_gripper := get_robot_pose(Index) * 采集图像并匹配模型 grab_image (Image, AcqHandle) find_surface_model (SurfaceModelID, Image, CamParam, ObjInCamPose, 0.7, ... \'num_matches\', 1, ... \'pose_refinement\', \'true\') * 添加观测:机器人位姿和模型在相机中的位姿 set_calib_data (CalibDataID, \'hand_eye\', \'add_observation\', [Index,0], [T_base_gripper, ObjInCamPose])endfor* 设置标定参数(眼在手外模式)set_calib_data (CalibDataID, \'hand_eye\', \'calibration_type\', \'eye_to_hand\')* 执行标定calibrate_hand_eye (CalibDataID, Errors)* 获取相机在基座坐标系中的位姿get_calib_data (CalibDataID, \'hand_eye\', \'camera_in_base_pose\', T_cam_base)
3D模型方法的优缺点:
优点:
- 无需物理标定板(适合复杂环境)
- 可以使用工件作为标定目标
- 算法鲁棒性较高(尤其在遮挡情况下)
缺点:
- 需要精确的3D模型数据
- 对环境光线变化敏感
- 计算复杂度较高
- 特征匹配置信度影响精度
八、基于标定板的手眼标定
1. 眼在手上(Eye-in-Hand)基于标定板
原理:
相机安装在机器人末端,观测固定位置的标定板。通过多个位姿下的标定板图像和对应机器人位姿,建立相机坐标系与机器人坐标系的关系。
详细流程步骤:
-
准备标定板:
- 固定标定板在视野内(如棋盘格、Charuco板)
- 精确测量标定板物理尺寸
-
数据采集:
- 移动机器人到多个姿势(5-30个)
- 每个姿势下:
a. 记录机器人末端位姿
b. 采集标定板图像
c. 确保标定板在视场范围内
-
角点检测:
- 自动检测标定板角点
- 计算亚像素级精确位置
- 关联对应3D物理点坐标
-
标定板位姿估计:
- 使用PnP算法计算每个位姿下标定板相对相机的位置
- 公式:s * [u v 1]ᵀ = K * [R|t] * [X Y 0]ᵀ(标定板在Z=0平面)
-
建立运动链:
T_base_board = T_base_gripper * T_gripper_cam * T_cam_board
通过位姿变换:ΔT_cam_board = T_gripper_cam⁻¹ * ΔT_base_gripper * T_gripper_cam
手眼方程:A * X = X * B
-
求解手眼矩阵:
- 收集多个运动对
- 使用Horaud或Andreff方法求解
-
验证与优化:
- 重投影误差计算
- 标定板位姿一致性检查
- 全局优化手眼矩阵
流程图:
#mermaid-svg-N6YwmsGWQMka9lCv {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-N6YwmsGWQMka9lCv .error-icon{fill:#552222;}#mermaid-svg-N6YwmsGWQMka9lCv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N6YwmsGWQMka9lCv .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-N6YwmsGWQMka9lCv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N6YwmsGWQMka9lCv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N6YwmsGWQMka9lCv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N6YwmsGWQMka9lCv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N6YwmsGWQMka9lCv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N6YwmsGWQMka9lCv .marker.cross{stroke:#333333;}#mermaid-svg-N6YwmsGWQMka9lCv svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N6YwmsGWQMka9lCv .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N6YwmsGWQMka9lCv .cluster-label text{fill:#333;}#mermaid-svg-N6YwmsGWQMka9lCv .cluster-label span{color:#333;}#mermaid-svg-N6YwmsGWQMka9lCv .label text,#mermaid-svg-N6YwmsGWQMka9lCv span{fill:#333;color:#333;}#mermaid-svg-N6YwmsGWQMka9lCv .node rect,#mermaid-svg-N6YwmsGWQMka9lCv .node circle,#mermaid-svg-N6YwmsGWQMka9lCv .node ellipse,#mermaid-svg-N6YwmsGWQMka9lCv .node polygon,#mermaid-svg-N6YwmsGWQMka9lCv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N6YwmsGWQMka9lCv .node .label{text-align:center;}#mermaid-svg-N6YwmsGWQMka9lCv .node.clickable{cursor:pointer;}#mermaid-svg-N6YwmsGWQMka9lCv .arrowheadPath{fill:#333333;}#mermaid-svg-N6YwmsGWQMka9lCv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N6YwmsGWQMka9lCv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N6YwmsGWQMka9lCv .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-N6YwmsGWQMka9lCv .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-N6YwmsGWQMka9lCv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N6YwmsGWQMka9lCv .cluster text{fill:#333;}#mermaid-svg-N6YwmsGWQMka9lCv .cluster span{color:#333;}#mermaid-svg-N6YwmsGWQMka9lCv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N6YwmsGWQMka9lCv :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}是否开始固定标定板移动机器人记录末端位姿采集标定板图像检测角点关联物理点PnP求解 T_board_cam构建相对运动求解手眼方程得到 T_gripper_cam验证精度OK?结束
矩阵应用:
设:
T_base_board
:基座→标定板的变换(常量)T_base_gripper
:基座→末端的变换T_board_cam
:标定板→相机的变换(每个位姿变化)
关系:T_board_cam = T_board_base * T_base_gripper * T_gripper_cam
因T_board_base
为常量,移项得:T_board_cam_j⁻¹ * T_board_cam_i = T_gripper_cam * (T_base_gripper_j⁻¹ * T_base_gripper_i) * T_gripper_cam⁻¹
OpenCV代码(棋盘格标定板):
void eyeInHandCheckerboardCalib( const vector<Mat>& gripperPoses, const vector<Mat>& images, const Size& boardSize, // 标定板角点数 float squareSize, // 标定板格子尺寸(单位:mm) Mat& cameraMatrix, Mat& distCoeffs, Mat& T_gripper_cam){ vector<vector<Point2f>> cornersList; vector<Point3f> objectPoints; // 生成3D物理坐标(标定板Z=0) for(int i=0; i<boardSize.height; i++) { for(int j=0; j<boardSize.width; j++) { objectPoints.push_back(Point3f(j*squareSize, i*squareSize, 0)); } } // 检测所有图像的角点 for(size_t i=0; i<images.size(); i++) { vector<Point2f> corners; bool found = findChessboardCorners(images[i], boardSize, corners); if(found) { // 亚像素精确化 Mat gray; cvtColor(images[i], gray, COLOR_BGR2GRAY); cornerSubPix(gray, corners, Size(11,11), Size(-1,-1), TermCriteria(TermCriteria::EPS+TermCriteria::COUNT, 30, 0.1)); cornersList.push_back(corners); } } // 计算每个位姿的相机外部参数 vector<Mat> T_board_cams; for(size_t i=0; i<cornersList.size(); i++) { Mat rvec, tvec; solvePnP(objectPoints, cornersList[i], cameraMatrix, distCoeffs, rvec, tvec); Mat R; Rodrigues(rvec, R); Mat T = Mat::eye(4,4,CV_64F); R.copyTo(T(Rect(0,0,3,3))); tvec.copyTo(T(Rect(3,0,1,3))); T_board_cams.push_back(T); } // 构建相对运动对 vector<Mat> A, B; for(size_t i=1; i<gripperPoses.size(); i++) { // 相机相对运动 Mat A_i = T_board_cams[i] * T_board_cams[i-1].inv(); // 机器人相对运动 Mat B_i = gripperPoses[i] * gripperPoses[i-1].inv(); A.push_back(A_i); B.push_back(B_i); } // 手眼标定(使用Horaud方法) calibrateHandEye(A, B, T_gripper_cam, CALIB_HAND_EYE_HORAUD);}
Halcon代码(Halcon棋盘格标定):
* 创建标定数据模型create_calib_data (\'hand_eye\', 1, 1, CalibDataID)* 设置相机参数(或进行内参标定)start_cam_calibration (CalibDataID, 0.03, 8, 8, 0.03, \'xyz_order\', \'z_vector\')...* 设置棋盘格标定板参数set_calib_data_calib_object (CalibDataID, 0, \'calplate_9x9_30mm.dat\')* 采集多个位姿数据for Index := 1 to NumPoses by 1 * 移动机器人到新位姿 move_robot_to_pose(Poses[Index]) * 采集图像 grab_image (Image, AcquisitionHandle) * 查找标定板 find_calib_object (Image, CalibDataID, 0, 0, Index, [], []) * 记录当前机器人位姿(基座坐标系) get_tcp_pose (RobotPose) set_calib_data (CalibDataID, \'hand_eye\', \'add_observation\', [Index-1,0], RobotPose)endfor* 执行手眼标定(眼在手上)calibrate_hand_eye (CalibDataID, Errors)* 获取结果get_calib_data (CalibDataID, \'hand_eye\', \'camera_in_hand_pose\', HCamInHand)
2. 眼在手外(Eye-to-Hand)基于标定板
原理:
相机固定在工作环境中,观测安装在机器人末端上的标定板。通过不同位姿下标定板在图像中的位置变化,求解相机相对于机器人基座的变换关系。
详细流程步骤:
-
安装标定板:
- 将标定板固定在机器人末端
- 确保标定板与末端刚性连接
-
相机固定:
- 将相机固定在立体视觉支架
- 覆盖机器人工作空间
-
数据采集:
- 移动机器人到多个位姿
- 每个位姿下:
a. 记录机器人末端位姿
b. 采集标定板图像
c. 确保标定板可见
-
角点检测与关联:
- 检测图像中的标定板角点
- 精确计算亚像素坐标
- 关联物理3D坐标(考虑标定板安装位置)
-
位姿估计:
- PnP求解标定板在相机坐标系中的位姿
- 理解为:T_cam_board = T_cam_base * T_base_gripper * T_gripper_board
-
建立方程:
ΔT_cam_board = T_cam_base * ΔT_base_gripper * T_cam_base⁻¹
转换为:A = X * B * X⁻¹
其中:
A = 标定板的相对运动(相机坐标系)
B = 机器人的相对运动(基座坐标系)
X = T_cam_base(待求) -
求解与优化:
- 收集多个运动对
- 使用Shah或Nakov方法求解
- 非线性优化提高精度
流程图:
#mermaid-svg-rEFlWRtVAasq1T2Q {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-rEFlWRtVAasq1T2Q .error-icon{fill:#552222;}#mermaid-svg-rEFlWRtVAasq1T2Q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rEFlWRtVAasq1T2Q .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-rEFlWRtVAasq1T2Q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rEFlWRtVAasq1T2Q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rEFlWRtVAasq1T2Q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rEFlWRtVAasq1T2Q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rEFlWRtVAasq1T2Q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rEFlWRtVAasq1T2Q .marker.cross{stroke:#333333;}#mermaid-svg-rEFlWRtVAasq1T2Q svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rEFlWRtVAasq1T2Q .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rEFlWRtVAasq1T2Q .cluster-label text{fill:#333;}#mermaid-svg-rEFlWRtVAasq1T2Q .cluster-label span{color:#333;}#mermaid-svg-rEFlWRtVAasq1T2Q .label text,#mermaid-svg-rEFlWRtVAasq1T2Q span{fill:#333;color:#333;}#mermaid-svg-rEFlWRtVAasq1T2Q .node rect,#mermaid-svg-rEFlWRtVAasq1T2Q .node circle,#mermaid-svg-rEFlWRtVAasq1T2Q .node ellipse,#mermaid-svg-rEFlWRtVAasq1T2Q .node polygon,#mermaid-svg-rEFlWRtVAasq1T2Q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rEFlWRtVAasq1T2Q .node .label{text-align:center;}#mermaid-svg-rEFlWRtVAasq1T2Q .node.clickable{cursor:pointer;}#mermaid-svg-rEFlWRtVAasq1T2Q .arrowheadPath{fill:#333333;}#mermaid-svg-rEFlWRtVAasq1T2Q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rEFlWRtVAasq1T2Q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rEFlWRtVAasq1T2Q .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-rEFlWRtVAasq1T2Q .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-rEFlWRtVAasq1T2Q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rEFlWRtVAasq1T2Q .cluster text{fill:#333;}#mermaid-svg-rEFlWRtVAasq1T2Q .cluster span{color:#333;}#mermaid-svg-rEFlWRtVAasq1T2Q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rEFlWRtVAasq1T2Q :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}是否开始末端安装标定板固定相机位置移动机器人记录末端位姿采集标定板图像检测角点关联物理坐标PnP求解 T_cam_board构建相对运动求解 XB = AX得到 T_cam_base验证精度OK?结束
矩阵应用:
设:
T_gripper_board
:末端→标定板的变换(常量)T_base_gripper
:基座→末端的变换T_cam_board
:相机→标定板的变换(每个位姿)
关系:T_cam_board = T_cam_base * T_base_gripper * T_gripper_board
移项:T_gripper_board = T_base_gripper⁻¹ * T_cam_base⁻¹ * T_cam_board
OpenCV代码(Charuco标定板):
void eyeToHandArucoCalibration( const vector<Mat>& gripperPoses, const Ptr<aruco::CharucoBoard>& board, const vector<Mat>& images, Mat& cameraMatrix, Mat& distCoeffs, Mat& T_cam_base){ Ptr<aruco::Dictionary> dictionary = board->dictionary; vector<Mat> boardInCamPoses; vector<double> timestamps; vector<vector<Point2f>> allCorners; vector<vector<int>> allIds; // 检测所有图像的标定板 for(size_t i=0; i<images.size(); i++) { vector<int> ids; vector<vector<Point2f>> corners, rejected; // 检测Aruco标记 aruco::detectMarkers(images[i], dictionary, corners, ids); if(ids.size() > 0) { // 精确定位角点 vector<Point2f> charucoCorners; vector<int> charucoIds; aruco::interpolateCornersCharuco(corners, ids, images[i], board, charucoCorners, charucoIds);// 使用角点估计位姿 Mat rvec, tvec; if(charucoIds.size() > 4) { solvePnP(board->chessboardCorners, charucoCorners, cameraMatrix, distCoeffs, rvec, tvec); // 转换为4x4齐次矩阵 Mat R; Rodrigues(rvec, R); Mat pose = Mat::eye(4,4,CV_64F); R.copyTo(pose(Rect(0,0,3,3))); tvec.copyTo(pose(Rect(3,0,1,3))); boardInCamPoses.push_back(pose); // 记录时间戳用于同步 timestamps.push_back(getCurrentTime()); } } } // 构建运动对 vector<Mat> A, B; for(size_t i=1; i<gripperPoses.size(); i++) { // 忽略时间差过大的数据对 if(fabs(timestamps[i] - timestamps[i-1]) > 0.1) continue; // 标定板在相机坐标系中的相对运动 Mat A_i = boardInCamPoses[i] * boardInCamPoses[i-1].inv(); // 机器人在基座坐标系的相对运动 Mat B_i = gripperPoses[i] * gripperPoses[i-1].inv(); // 使用带权重的运动对 double weight = motionQuality(B_i); // 基于运动幅度和质量 if(weight > 0.3) { A.push_back(A_i); B.push_back(B_i); } } // 标定求解 calibrateHandEye(A, B, T_cam_base, CALIB_HAND_EYE_SHAH);}
Halcon代码(Halcon标定板):
* 创建手眼标定模型create_calib_data (\'hand_eye\', 1, 1, CalibDataID)* 设置相机参数set_calib_data_cam_param (CalibDataID, 0, CamParam)* 设置标定板参数(安装于机器人末端)set_calib_data_calib_object (CalibDataID, 0, \'caltab_15x10.cpd\')* 采集多个位姿数据for Index := 1 to NumPoses by 1 * 移动机器人 move_robot(Poses[Index]) * 采集图像 grab_image (Image, AcqHandle) * 查找标定板 find_calib_object (Image, CalibDataID, 0, 0, Index, [], []) * 获取并记录机器人位姿(基座坐标系) get_robot_tcp (T_base_gripper) set_calib_data (CalibDataID, \'hand_eye\', \'add_observation\', [Index-1,0], T_base_gripper)endfor* 配置为眼在手外模式set_calib_data (CalibDataID, \'hand_eye\', \'general\', \'camera_in_base_pose[0]\', [0,0,0,0,0,0])* 执行标定calibrate_hand_eye (CalibDataID, Errors)* 获取标定结果get_calib_data (CalibDataID, \'hand_eye\', \'camera_in_base_pose\', HCamInBase)
标定板方法的优缺点:
优点:
- 算法成熟可靠
- 精度高(亚像素级角点检测)
- 鲁棒性好(不受光照影响)
- 开源库支持完善
缺点:
- 需要物理标定板
- 标定板需在视野内(有操作空间限制)
- 单一标定板分辨率和精度固定
九、3D模型与标定板方法的比较
十、手眼标定关键问题解决
1. 精度提升技巧
- 运动轨迹优化:
- 采用满秩运动(6自由度运动)
- 旋转和平移均在所需精度级别的10倍以上
理想运动轨迹: Position 1: Rx 0°, Ry 0°, Rz 0°, Tx 0, Ty 0, Tz 0 Position 2: Rx 30°, Ry 0°, Rz 0°, Tx 50mm, Ty 0, Tz 0 Position 3: Rx 0°, Ry 30°, Rz 0°, Tx 0, Ty 50mm, Tz 0 Position 4: Rx 0°, Ry 0°, Rz 30°, Tx 0, Ty 0, Tz 50mm Position 5: Rx -30°, Ry 0°, Rz 0°, Tx -50mm, Ty 0, Tz 0 ...
- 数据筛选策略:
// 筛选有效数据对vector<Mat> A_good, B_good;for(size_t i=0; i<A.size(); i++) { double rotation_angle = rotationNorm(B[i]); double translation_dist = translationNorm(B[i]); // 筛选标准 if(rotation_angle > 10.0 && // 最小旋转角度 translation_dist > 30.0 && // 最小移动距离 conditionNumber(A[i]) < 50) // 最小条件数 { A_good.push_back(A[i]); B_good.push_back(B[i]); }}
2. 鲁棒性增强
-
多算法融合投票:
Mat T1, T2, T3;calibrateHandEye(A, B, T1, CALIB_HAND_EYE_TSAI);calibrateHandEye(A, B, T2, CALIB_HAND_EYE_HORAUD);calibrateHandEye(A, B, T3, CALIB_HAND_EYE_PARK);// 计算一致性double diff12 = matDiff(T1, T2);double diff13 = matDiff(T1, T3);double diff23 = matDiff(T2, T3);// 选择最一致的解if(diff12 < diff13 && diff12 < diff23) { T_final = (T1 + T2) / 2.0;} else if(/* 其他条件 */) { // ...}
-
抗噪声优化:
# Halcon中的鲁棒性标定set_calib_data (CalibDataID, \'general\', \'sigma_rot\', 0.005)set_calib_data (CalibDataID, \'general\', \'sigma_trans\', 0.5)calibrate_hand_eye (CalibDataID, \'robust\', 0.95, \'residual_limit\', 0.1)
3. 实际应用建议
-
标定环境:
- 避免强光反射
- 减少环境运动干扰
- 固定所有部件对应振
-
运动规划原则:
角度覆盖:±30°绕每个轴平移范围:工作空间的50-80%运动路径:避免奇异位形,保持速度稳定
-
验证方法:
相对误差 = ||实测值 - 转换值||/实测值绝对误差 = ||T_base_gripper * X - T_base_cam||重投影误差 = Σ||原图像点 - 投影点||²
-
维护周期:
- 正常使用:3-6个月
- 撞击后立即检测
- 机械结构改变后重新标定
应用场景选择矩阵:
#mermaid-svg-Wv7z0be45DhBzrL1 {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Wv7z0be45DhBzrL1 .error-icon{fill:#552222;}#mermaid-svg-Wv7z0be45DhBzrL1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Wv7z0be45DhBzrL1 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-Wv7z0be45DhBzrL1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Wv7z0be45DhBzrL1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Wv7z0be45DhBzrL1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Wv7z0be45DhBzrL1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Wv7z0be45DhBzrL1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Wv7z0be45DhBzrL1 .marker.cross{stroke:#333333;}#mermaid-svg-Wv7z0be45DhBzrL1 svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Wv7z0be45DhBzrL1 .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Wv7z0be45DhBzrL1 .cluster-label text{fill:#333;}#mermaid-svg-Wv7z0be45DhBzrL1 .cluster-label span{color:#333;}#mermaid-svg-Wv7z0be45DhBzrL1 .label text,#mermaid-svg-Wv7z0be45DhBzrL1 span{fill:#333;color:#333;}#mermaid-svg-Wv7z0be45DhBzrL1 .node rect,#mermaid-svg-Wv7z0be45DhBzrL1 .node circle,#mermaid-svg-Wv7z0be45DhBzrL1 .node ellipse,#mermaid-svg-Wv7z0be45DhBzrL1 .node polygon,#mermaid-svg-Wv7z0be45DhBzrL1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Wv7z0be45DhBzrL1 .node .label{text-align:center;}#mermaid-svg-Wv7z0be45DhBzrL1 .node.clickable{cursor:pointer;}#mermaid-svg-Wv7z0be45DhBzrL1 .arrowheadPath{fill:#333333;}#mermaid-svg-Wv7z0be45DhBzrL1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Wv7z0be45DhBzrL1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Wv7z0be45DhBzrL1 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-Wv7z0be45DhBzrL1 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-Wv7z0be45DhBzrL1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Wv7z0be45DhBzrL1 .cluster text{fill:#333;}#mermaid-svg-Wv7z0be45DhBzrL1 .cluster span{color:#333;}#mermaid-svg-Wv7z0be45DhBzrL1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Wv7z0be45DhBzrL1 :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}标定板无法放置有标准平面移动相机固定相机移动相机固定相机应用需求环境限制3D模型方法标定板方法眼在手上/眼在手外眼在手上/眼在手外眼在手上+3D模型眼在手外+3D模型眼在手上+标定板眼在手外+标定板
性能指标对比:
发展趋势:
- 多模态融合标定:结合深度相机与RGB相机
- 在线自适应标定:机器人在工作过程中实时优化
- 无标定物方法:利用环境特征进行动态标定
- 深度学习方法:端到端的手眼标定网络
十一、关键处理对比
FindChessboardCorners()
+ SolvePnP
find_calib_object()
CalibrateHandEye()
calibrate_hand_eye()
pose_compose()
十二、误差分析与优化策略
黄金法则:当平均3D投影误差 > 预期精度×2时,必须重新标定(e.g.目标精度0.1mm → 误差>0.2mm时重做)
十三、典型应用场景设置
通过严谨执行上述步骤,可实现<0.1mm的定位精度(六轴机器人负载<5kg场景)。建议每3个月或生产批次变更后执行全流程再标定以保证系统稳定性。
以下是针对手眼标定中坐标系关系和矩阵应用的系统性解析,包含数学原理、应用方法和场景案例:
十四、关键技术的工程细节
1. 时间同步问题
- 问题:机器人位姿 ( T_{base}^{tool} ) 和相机采集存在延迟
- 方案:
- 使用硬件触发信号同步
- 添加机器人运动状态补偿(预测末端实际位置)
2. 误差传递分析
误差来源:
- 相机内参标定误差(约±0.2像素)
- 机器人重复定位误差(±0.1mm级)
- 标定板平面度误差(需<0.05mm/m)
误差累积公式:
( \\epsilon_{total} = \\sqrt{(\\epsilon_{cam}^2 + \\epsilon_{robot}^2 + \\epsilon_{calib}^2)} )
3. 动态补偿技术
当机械臂负载变化导致变形时:
# 根据负载重量动态调整标定矩阵def adjust_calibration(load_weight): k = load_weight * 0.001 # 假设每kg产生0.001mm的形变系数 T_adjusted = T_tool_cam.copy() T_adjusted[2,3] += k # Z方向补偿 return T_adjusted
十五、不同场景的配置建议
十六、验证与调试流程
-
重投影验证
- 机械臂运动到多个已知位置
- 比较标定板的理论投影坐标与实际检测坐标
# 计算重投影误差error = np.linalg.norm(actual_pixel - projected_pixel)
-
实物基准测试
- 在工作台上放置物理基准点
- 用机器人重复触碰基准点,统计实际偏差
-
热漂移测试
- 连续运行1小时后重新测量关键点
- 检查标定矩阵的稳定性
十七、调试建议
-
OpenCVSharp常见问题:
- 标定板角点检测失败 → 调整
FindChessboardCorners
的winSize
参数 - 解不稳定 → 确保机器人运动包含充分旋转(建议>30°)
- 标定板角点检测失败 → 调整
-
Halcon优化方向:
- 使用
set_calib_data_observ_points
添加额外观测点 - 启用
\'check_hand_eye_motion\'
参数验证运动充分性
- 使用
-
跨平台兼容处理:
// 坐标系方向修正(ROS↔工业机器人)Mat correction = new Mat(4,4,MatType.CV_64F, new double[]{ -1,0,0,0, 0,0,1,0, 0,1,0,0, 0,0,0,1});T_tool_cam = T_tool_cam * correction;
通过上述代码可实现毫米级精度的视觉引导,实际应用时建议:
- Eye-in-Hand配置:误差控制在±0.3mm内
- Eye-to-Hand配置:误差控制在±1.0mm内(受相机距离影响)
通过以上方法,可确保手眼标定结果在实际应用中的精确性和可靠性。对于Eye-to-Hand系统,只需将 ( T_{tool}^{cam} ) 替换为 ( T_{base}^{cam} ),其他计算逻辑完全一致。