跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++AI算法

手眼标定:原理、方法及 C++ 代码实战

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

黑客帝国发布于 2026/2/8更新于 2026/10/26108 浏览
手眼标定:原理、方法及 C++ 代码实战

一、手眼标定简述

手眼标定的目的是建立机械臂与相机之间的空间关联,使相机成为机械臂的'眼睛',最终实现精准定位。

在使用相机前,首先需要进行相机标定(获取内参和畸变系数),完成后再进行后续的手眼标定处理。

1. 坐标系

坐标系描述
base机械臂基坐标系
cam相机坐标系
end机械臂法兰坐标系(Tool Center Point)
obj物体坐标系(通常与法兰相对静止)

2. 分类

手眼标定主要分为两种模式:

  • 眼在手上 (Eye-in-Hand):相机安装在机械臂末端。
  • 眼在手外 (Eye-to-Hand):相机固定在工作区域外。

![图片]

3. 原理

以 眼在手外 为例进行分析。已知条件如下:

  1. $T^{base}_{end}$:基座到末端的位姿,可通过示教器读取。
  2. $T^{end}_{obj}$:末端到物体的位姿,两者相对静止,可视为恒等式桥梁。
  3. $T^{cam}_{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 $$

其中:

  • $A = (T^{end_2}{base_2})^{-1} \cdot T^{end_1}{base_1}$ (机械臂运动矩阵)
  • $B = T^{cam_2}{obj_2} \cdot (T^{cam_1}{obj_1})^{-1}$ (相机观测变化矩阵)
  • $X = T^{base}_{cam}$ (待求的手眼标定矩阵)

所有手眼标定算法的核心均为求解该方程,常用算法包括 Tsai-Lenz 法等。

同理,眼在手上 是将 $T^{obj}_{base}$ 作为中间桥梁,因为基座和物体在特定配置下相对静止。

二、手眼标定方法汇总

根据相机类型,分为 2D 和 3D 两类。

1. 2D 相机手眼标定

本质是世界坐标系到像素坐标系的映射(仿射变换)。通常假设 Z 轴固定,机械臂在同一水平面运作。

最常用方法是九点标定法(或多点标定)。

2. 3D 相机手眼标定

3D 相机拍摄数据为点云或深度图,可通过 ICP 点云匹配或 OpenCV 的 solvePnP 求解变换矩阵。

三、代码实战

1. 2D 相机手眼标定——九点标定法

① 操作流程

  1. 制作含有圆点的标定板置于相机正下方(点数越多精度越高,示例使用 3x5=15 个点)。
  2. 相机固定,拍摄标定板照片。
  3. 使用圆查找算法识别标定板中圆点的像素坐标 $(x_{img}, y_{img})$。
  4. 机械臂保持同一水平面,依次触碰标定板圆心,记录机械臂位姿 $(x_{robot}, y_{robot})$。

*注意:9 点标定要求机械臂在同一水平面(Z 值固定),无法直接获取姿态信息。

② 具体实现

找圆点函数 findCirclesGrid

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(3, 1, CV_64F);
    src.at<double>(0, 0) = imagePoint.x;
    src.at<double>(1, 0) = imagePoint.y;
    src.at<double>(2, 0) = 1.0;
    cv::Mat dst = transformMatrix * src;
    Point2D robotPoint;
    robotPoint.x = dst.at<double>(0, 0);
    robotPoint.y = dst.at<double>(1, 0);
    return robotPoint;
}

int main() {
    int w = 3; int h = 5;
    cv::TermCriteria criteria(cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 30, 0.001);
    std::vector<Point2D> imagePoints;

    // 读取图像
    cv::Mat image = cv::imread("calibration_board.jpg");
    if (image.empty()) {
        std::cerr << "Could not open or find the image" << std::endl;
        return -1;
    }

    cv::Mat gray;
    cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);

    // 设置斑点检测器
    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(gray, cv::Size(w, h), corners, cv::CALIB_CB_SYMMETRIC_GRID, blobDetector);

    if (found) {
        imagePoints.resize(corners.size());
        for (size_t i = 0; i < corners.size(); ++i) {
            imagePoints[i].x = corners[i].x;
            imagePoints[i].y = corners[i].y;
        }
        // 亚像素优化
        cv::cornerSubPix(gray, corners, cv::Size(5, 5), cv::Size(-1, -1), criteria);
    } else {
        std::cerr << "Could not find the circle grid" << std::endl;
        return -1;
    }

    // 机器人坐标系中的点坐标(示例数据)
    std::vector<Point2D> robotPoints = {
        {5, 5}, {10, 5}, {15, 5}, {5, 10}, {10, 10}, {15, 10},
        {5, 15}, {10, 15}, {15, 15}, {5, 20}, {10, 20}, {15, 20},
        {5, 25}, {10, 25}, {15, 25}
    };

    // 计算仿射变换矩阵
    cv::Mat affineMatrix = calculateAffineTransform(imagePoints, robotPoints, w * h);
    std::cout << "Affine Transformation Matrix:" << std::endl << affineMatrix << std::endl;

    // 测试变换
    Point2D testImagePoint = {590.203f, 885.33f};
    Point2D testRobotPoint = transformPoint(affineMatrix, testImagePoint);
    std::cout << "Image Point: (" << testImagePoint.x << ", " << testImagePoint.y << ") -> Robot Point: (" 
              << testRobotPoint.x << ", " << testRobotPoint.y << ")" << std::endl;

    return 0;
}

运行效果将输出仿射变换矩阵及转换后的坐标。

2. 3D 相机手眼标定——OpenCV

2D 标定仅能获取 XY 位置,3D 标定可求解机械臂姿态(XYZWPR)。需要多组拍摄数据构建 AX=XB 方程组。

OpenCV 提供核心函数 solvePnP 和 cv::calibrateHandEye。

① 操作流程

  1. 准备工作:准备已知尺寸的棋盘格标定板,提前完成相机内参标定。
  2. 数据采集:
    • 固定标定板,移动机械臂至不同位姿(至少 10 组,建议 20 组以上)。
    • 每个位姿下记录机械臂末端位姿(XYZWPR)并拍摄图像。
  3. 数据预处理:
    • 将 XYZWPR 转换为旋转矩阵和平移向量。
    • 使用 solvePnP 计算相机到标定板的位姿。
  4. 手眼标定计算:调用 calibrateHandEye 求解变换矩阵。

② 具体实现

准备标定板参数

使用 OpenCV 自带棋盘格数据,定义单格尺寸和内角点数量。

获取相机内参

需预先完成相机标定,获取 camera_matrix 和 dist_coeffs。

采集机械臂和相机数据
  • 收集机械臂姿态数据(xyzwpr),转换为 RT 矩阵。
  • 收集相机拍摄的标定板图像,检测角点并计算位姿。
手眼标定核心

调用 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, -sin(P), 0, cos(P));
    cv::Mat Rx = (cv::Mat_<double>(3, 3) << 1, 0, 0, 0, cos(R), -sin(R), 0, sin(R), cos(R));
    
    R_base2grip = Rz * Ry * Rx;
}

int main() {
    // 1. 准备标定板参数
    const float square_size = 0.025f;
    const cv::Size board_size(9, 6);
    std::vector<cv::Point3f> object_points;
    for (int i = 0; i < board_size.height; ++i) {
        for (int j = 0; j < board_size.width; ++j) {
            object_points.emplace_back(j * square_size, i * square_size, 0);
        }
    }

    // 2. 读取相机内参和畸变系数
    cv::Mat camera_matrix, dist_coeffs;
    camera_matrix = (cv::Mat_<double>(3, 3) << 542.35, 0, 328.32, 0, 541.61, 246.95, 0, 0, 1);
    dist_coeffs = (cv::Mat_<double>(5, 1) << -0.28, 0.10, -0.0005, 0.0013, -0.0237);

    // 3. 采集数据
    std::vector<std::vector<double>> xyzwpr_data = {
        {100.0, 200.0, 300.0, 45.0, 30.0, 15.0},
        {150.0, 250.0, 350.0, 60.0, 45.0, 30.0},
        {200.0, 300.0, 400.0, 75.0, 60.0, 45.0},
        {250.0, 350.0, 450.0, 90.0, 75.0, 60.0},
        {300.0, 400.0, 500.0, 105.0, 90.0, 75.0},
        {350.0, 450.0, 550.0, 120.0, 105.0, 90.0},
        {400.0, 500.0, 600.0, 135.0, 120.0, 105.0},
        {450.0, 550.0, 650.0, 150.0, 135.0, 120.0},
        {500.0, 600.0, 700.0, 165.0, 150.0, 135.0}
    };

    std::vector<cv::Mat> R_base2gripper, t_base2gripper;
    std::vector<cv::Mat> R_target2cam, t_target2cam;

    for (int i = 0; i < 9; ++i) {
        // 获取机械臂位姿
        cv::Mat R_base2grip = cv::Mat::eye(3, 3, CV_64F);
        cv::Mat t_base2grip = (cv::Mat_<double>(3, 1) << 0.1 * i, 0, 0);
        convertXYZWPRToMat(xyzwpr_data[i][0], xyzwpr_data[i][1], xyzwpr_data[i][2], 
                           xyzwpr_data[i][3], xyzwpr_data[i][4], xyzwpr_data[i][5], 
                           R_base2grip, t_base2grip);
        R_base2gripper.push_back(R_base2grip.clone());
        t_base2gripper.push_back(t_base2grip.clone());

        // 模拟检测棋盘格位姿(实际需从图像读取)
        // 此处省略图像读取逻辑,直接使用 solvePnP 模拟结果
        cv::Mat rvec, tvec;
        cv::solvePnP(object_points, std::vector<cv::Point2f>(), camera_matrix, dist_coeffs, rvec, tvec);
        cv::Mat R_target2cam_i;
        cv::Rodrigues(rvec, R_target2cam_i);
        R_target2cam.push_back(R_target2cam_i);
        t_target2cam.push_back(tvec);
    }

    // 4. 手眼标定
    cv::Mat X_rot, X_trans;
    cv::calibrateHandEye(R_base2gripper, t_base2gripper, R_target2cam, t_target2cam, 
                         X_rot, X_trans, cv::CALIB_HAND_EYE_TSAI);

    // 5. 输出结果
    cv::Mat X = cv::Mat::eye(4, 4, CV_64F);
    X_rot.copyTo(X(cv::Rect(0, 0, 3, 3)));
    X_trans.copyTo(X(cv::Rect(3, 0, 1, 3)));
    std::cout << "Camera to Base Transform Matrix X =\n" << X << std::endl;

    return 0;
}

运行后将输出相机到基座的变换矩阵 X,即 AX=XB 的解。

目录

  1. 一、手眼标定简述
  2. 1. 坐标系
  3. 2. 分类
  4. 3. 原理
  5. 二、手眼标定方法汇总
  6. 1. 2D 相机手眼标定
  7. 2. 3D 相机手眼标定
  8. 三、代码实战
  9. 1. 2D 相机手眼标定——九点标定法
  10. ① 操作流程
  11. ② 具体实现
  12. 找圆点函数 findCirclesGrid
  13. 斑点检测器参数设置
  14. ③ 完整代码
  15. 2. 3D 相机手眼标定——OpenCV
  16. ① 操作流程
  17. ② 具体实现
  18. 准备标定板参数
  19. 获取相机内参
  20. 采集机械臂和相机数据
  21. 手眼标定核心
  22. ③ 完整代码

更多推荐文章

查看全部
  • Python 系统学习路线与核心开发技术详解
  • openJiuwen 企业级 Agent 平台架构设计与私有化部署实战
  • Python 数据分析:无监督学习、文本及图像处理
  • TarsosDSP Java 实时音频处理指南
  • 宇树 Unitree 机器人 ROS 2 Humble 环境部署指南 (Go2/B2/H1)
  • Python 爬虫基础入门与实战详解
  • Node.js 与 OpenClaw 一键安装配置指南
  • 开源 AI 编程工具选型对比:OpenCode 与 GitHub Copilot
  • 使用 Java 和 Python 发送 Webhook 消息至飞书机器人
  • 无需 API:OpenCode 本地模型部署与配置实战
  • 大模型 API 注册与调用实战:OpenAI、文心一言与通义千问
  • AI 视觉人体姿态关键点实时跟踪 Python 代码
  • Stable Diffusion 写真生成完整教程
  • 大模型驱动地图实战:前端直连与完整 MCP 架构对比
  • OpenClaw 对接飞书:配置多个机器人实现群聊
  • ZeroClaw 本地 AI 管理面板:Gateway + LM Studio + Reflex
  • Kimi-VL-A3B-Thinking 部署与 vLLM 性能调优实战
  • 基于 LLaMA-Factory 的大模型 LoRA 微调实战指南
  • 基于 CLIProxyAPI 和 New API 搭建 AI 模型统一代理平台
  • 算法实战:一维与二维前缀和模板详解

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online