手眼标定:原理、方法及 C++ 代码实战
手眼标定用于建立机械臂与相机的空间关系,实现精准定位。文章介绍了手眼标定的基本原理,包括坐标系定义、AX=XB 方程推导以及眼在手上和眼在手外的区别。内容涵盖 2D 相机九点标定法和 3D 相机 PnP 标定法,提供了基于 OpenCV 的 C++ 完整代码实现,包含圆点检测、仿射变换计算及手眼标定矩阵求解过程,适用于机器人视觉系统集成开发。

手眼标定用于建立机械臂与相机的空间关系,实现精准定位。文章介绍了手眼标定的基本原理,包括坐标系定义、AX=XB 方程推导以及眼在手上和眼在手外的区别。内容涵盖 2D 相机九点标定法和 3D 相机 PnP 标定法,提供了基于 OpenCV 的 C++ 完整代码实现,包含圆点检测、仿射变换计算及手眼标定矩阵求解过程,适用于机器人视觉系统集成开发。

手眼标定的目的是建立机械臂与相机之间的空间关联,使相机成为机械臂的'眼睛',最终实现精准定位。
在使用相机前,首先需要进行相机标定(获取内参和畸变系数),完成后再进行后续的手眼标定处理。
| 坐标系 | 描述 |
|---|---|
| base | 机械臂基坐标系 |
| cam | 相机坐标系 |
| end | 机械臂法兰坐标系(Tool Center Point) |
| obj | 物体坐标系(通常与法兰相对静止) |
手眼标定主要分为两种模式:
![图片]
以 眼在手外 为例进行分析。已知条件如下:
目标是求解 $T^{base}_{cam}$(相机相对于基座的位姿)。为了形成闭合回路,各坐标系间需满足变换关系:
$$ T^{end_1}{base_1} \cdot T^{base_1}{cam_1} \cdot T^{cam_1}{obj_1} = T^{end_2}{base_2} \cdot T^{base_2}{cam_2} \cdot T^{cam_2}{obj_2} $$
整理后得到经典的 AX=XB 形式:
$$ A \cdot X = X \cdot B $$
其中:
所有手眼标定算法的核心均为求解该方程,常用算法包括 Tsai-Lenz 法等。
同理,眼在手上 是将 $T^{obj}_{base}$ 作为中间桥梁,因为基座和物体在特定配置下相对静止。
根据相机类型,分为 2D 和 3D 两类。
本质是世界坐标系到像素坐标系的映射(仿射变换)。通常假设 Z 轴固定,机械臂在同一水平面运作。
最常用方法是九点标定法(或多点标定)。
3D 相机拍摄数据为点云或深度图,可通过 ICP 点云匹配或 OpenCV 的 solvePnP 求解变换矩阵。
*注意:9 点标定要求机械臂在同一水平面(Z 值固定),无法直接获取姿态信息。
OpenCV 提供 findCirclesGrid 函数检测圆点。
CV_EXPORTS_W bool findCirclesGrid(
InputArray image,
Size patternSize,
OutputArray centers,
int flags = CALIB_CB_SYMMETRIC_GRID,
const Ptr<FeatureDetector>& blobDetector = SimpleBlobDetector::create()
);
参数说明:
image: 输入图像 (cv::Mat)。patternSize: 图像宽高方向的圆点数量 (cv::Size(w, h))。centers: 输出检测到的圆心坐标 (std::vector<cv::Point2f>)。flags: 标定板类型(对称 CALIB_CB_SYMMETRIC_GRID 或非对称 CALIB_CB_ASYMMETRIC_GRID)。blobDetector: 斑点检测器对象。示例代码:
int w = 3; int h = 5;
cv::Mat image = cv::imread("calibration_board.jpg");
cv::SimpleBlobDetector::Params params;
params.maxArea = std::numeric_limits<float>::max();
params.minArea = 2;
params.minDistBetweenBlobs = 1;
cv::Ptr<cv::SimpleBlobDetector> blobDetector = cv::SimpleBlobDetector::create(params);
std::vector<cv::Point2f> corners;
bool found = cv::findCirclesGrid(image, cv::Size(w, h), corners, cv::CALIB_CB_ASYMMETRIC_GRID, blobDetector);
核心在于 cv::SimpleBlobDetector::Params 的配置,关键参数包括:
thresholdStep, minThreshold, maxThreshold: 二值化阈值范围。minRepeatability: 特征点最小重复次数。filterByColor, filterByArea, filterByCircularity: 过滤条件(颜色、面积、圆度)。#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
struct Point2D {
float x;
float y;
};
// 计算仿射变换矩阵
cv::Mat calculateAffineTransform(const std::vector<Point2D>& imagePoints,
const std::vector<Point2D>& robotPoints,
int points_num) {
cv::Mat src(points_num, 2, CV_64F);
cv::Mat dst(points_num, 2, CV_64F);
for (int i = 0; i < points_num; ++i) {
src.at<double>(i, 0) = imagePoints[i].x;
src.at<double>(i, 1) = imagePoints[i].y;
dst.at<double>(i, 0) = robotPoints[i].x;
dst.at<double>(i, 1) = robotPoints[i].y;
}
return cv::estimateAffine2D(src, dst);
}
// 将图像坐标转换为机器人坐标
Point2D transformPoint(const cv::Mat& transformMatrix, const Point2D& imagePoint) {
cv::Mat ;
src.<>(, ) = imagePoint.x;
src.<>(, ) = imagePoint.y;
src.<>(, ) = ;
cv::Mat dst = transformMatrix * src;
Point2D robotPoint;
robotPoint.x = dst.<>(, );
robotPoint.y = dst.<>(, );
robotPoint;
}
{
w = ; h = ;
;
std::vector<Point2D> imagePoints;
cv::Mat image = cv::();
(image.()) {
std::cerr << << std::endl;
;
}
cv::Mat gray;
cv::(image, gray, cv::COLOR_BGR2GRAY);
cv::SimpleBlobDetector::Params params;
params.maxArea = std::numeric_limits<>::();
params.minArea = ;
params.minDistBetweenBlobs = ;
cv::Ptr<cv::SimpleBlobDetector> blobDetector = cv::SimpleBlobDetector::(params);
std::vector<cv::Point2f> corners;
found = cv::(gray, cv::(w, h), corners, cv::CALIB_CB_SYMMETRIC_GRID, blobDetector);
(found) {
imagePoints.(corners.());
( i = ; i < corners.(); ++i) {
imagePoints[i].x = corners[i].x;
imagePoints[i].y = corners[i].y;
}
cv::(gray, corners, cv::(, ), cv::(, ), criteria);
} {
std::cerr << << std::endl;
;
}
std::vector<Point2D> robotPoints = {
{, }, {, }, {, }, {, }, {, }, {, },
{, }, {, }, {, }, {, }, {, }, {, },
{, }, {, }, {, }
};
cv::Mat affineMatrix = (imagePoints, robotPoints, w * h);
std::cout << << std::endl << affineMatrix << std::endl;
Point2D testImagePoint = {, };
Point2D testRobotPoint = (affineMatrix, testImagePoint);
std::cout << << testImagePoint.x << << testImagePoint.y <<
<< testRobotPoint.x << << testRobotPoint.y << << std::endl;
;
}
运行效果将输出仿射变换矩阵及转换后的坐标。
2D 标定仅能获取 XY 位置,3D 标定可求解机械臂姿态(XYZWPR)。需要多组拍摄数据构建 AX=XB 方程组。
OpenCV 提供核心函数 solvePnP 和 cv::calibrateHandEye。
solvePnP 计算相机到标定板的位姿。calibrateHandEye 求解变换矩阵。使用 OpenCV 自带棋盘格数据,定义单格尺寸和内角点数量。
需预先完成相机标定,获取 camera_matrix 和 dist_coeffs。
调用 cv::calibrateHandEye,输入机械臂位姿和相机位姿,输出旋转和平移向量。
#include <opencv2/opencv.hpp>
#include <opencv2/calib3d.hpp>
#include <vector>
#include <cmath>
#include <iostream>
// 将 Fanuc 风格的 XYZWPR 转换为旋转矩阵 R 和平移向量 t
void convertXYZWPRToMat(double X, double Y, double Z, double W, double P, double R,
cv::Mat& R_base2grip, cv::Mat& t_base2grip) {
t_base2grip = (cv::Mat_<double>(3, 1) << X, Y, Z);
W *= CV_PI / 180.0; P *= CV_PI / 180.0; R *= CV_PI / 180.0;
cv::Mat Rz = (cv::Mat_<double>(3, 3) << cos(W), -sin(W), 0, sin(W), cos(W), 0, 0, 0, 1);
cv::Mat Ry = (cv::Mat_<double>(3, 3) << cos(P), 0, sin(P), 0, 1, 0, -(P), , (P));
cv::Mat Rx = (cv::<>(, ) << , , , , (R), -(R), , (R), (R));
R_base2grip = Rz * Ry * Rx;
}
{
square_size = ;
;
std::vector<cv::Point3f> object_points;
( i = ; i < board_size.height; ++i) {
( j = ; j < board_size.width; ++j) {
object_points.(j * square_size, i * square_size, );
}
}
cv::Mat camera_matrix, dist_coeffs;
camera_matrix = (cv::<>(, ) << , , , , , , , , );
dist_coeffs = (cv::<>(, ) << , , , , );
std::vector<std::vector<>> xyzwpr_data = {
{, , , , , },
{, , , , , },
{, , , , , },
{, , , , , },
{, , , , , },
{, , , , , },
{, , , , , },
{, , , , , },
{, , , , , }
};
std::vector<cv::Mat> R_base2gripper, t_base2gripper;
std::vector<cv::Mat> R_target2cam, t_target2cam;
( i = ; i < ; ++i) {
cv::Mat R_base2grip = cv::Mat::(, , CV_64F);
cv::Mat t_base2grip = (cv::<>(, ) << * i, , );
(xyzwpr_data[i][], xyzwpr_data[i][], xyzwpr_data[i][],
xyzwpr_data[i][], xyzwpr_data[i][], xyzwpr_data[i][],
R_base2grip, t_base2grip);
R_base2gripper.(R_base2grip.());
t_base2gripper.(t_base2grip.());
cv::Mat rvec, tvec;
cv::(object_points, std::<cv::Point2f>(), camera_matrix, dist_coeffs, rvec, tvec);
cv::Mat R_target2cam_i;
cv::(rvec, R_target2cam_i);
R_target2cam.(R_target2cam_i);
t_target2cam.(tvec);
}
cv::Mat X_rot, X_trans;
cv::(R_base2gripper, t_base2gripper, R_target2cam, t_target2cam,
X_rot, X_trans, cv::CALIB_HAND_EYE_TSAI);
cv::Mat X = cv::Mat::(, , CV_64F);
X_rot.((cv::(, , , )));
X_trans.((cv::(, , , )));
std::cout << << X << std::endl;
;
}
运行后将输出相机到基座的变换矩阵 X,即 AX=XB 的解。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online