当前位置: 首页 > article >正文

(二)手眼标定——概述+原理+常用方法汇总+代码实战(C++)

一、手眼标定简述

手眼标定的目的:让机械臂和相机关联,相机充当机械臂的”眼睛“,最终实现指哪打哪

相机的使用前提首先需要进行相机标定,可以参考博文:(一)相机标定——四大坐标系的介绍、对于转换、畸变原理以及OpenCV完整代码实战(C++版)

当通过相机内参和畸变纠正之后再进行后续的处理

1,坐标系

坐标系描述
base机械臂的基坐标系
cam相机坐标系
end机械臂的法兰坐标系,也称为tcl
obj物体坐标系,一般情况下法兰会有夹爪夹取物体,end和obj是相对静止状态

2,分类

手眼标定分为眼在手上眼在手外两种
在这里插入图片描述
在这里插入图片描述

3,原理

眼在手外为例进行分析

首先,我们先列举出有用的已知条件
Ⅰ. b a s e base base下的 e n d end end是已知的,可以通过示教器读取, T e n d b a s e T^{base}_{end} Tendbase
Ⅱ. e n d end end o b j obj obj是一个整体,相对静止关系,是个恒等式可作为桥梁 T o b j e n d T^{end}_{obj} Tobjend
Ⅲ. 相机可以拍摄 o b j obj obj,故 T o b j c a m T^{cam}_{obj} Tobjcam是已知的

为了形成闭合回路,各个坐标系之间都可以相互转换,需要求解 T c a m b a s e T^{base}_{cam} Tcambase,也就是让机械臂知道相机,或者相机知道机械臂

T b a s e 1 e n d 1 ⋅ T c a m 1 b a s e 1 ⋅ T o b j 1 c a m 1 = T b a s e 2 e n d 2 ⋅ T c a m 2 b a s e 2 ⋅ T o b j 2 c a m 2 T^{end_1}_{base_1} · T^{base_1}_{cam_1} · T^{cam_1}_{obj_1}= T^{end_2}_{base_2} · T^{base_2}_{cam_2} · T^{cam_2}_{obj_2} Tbase1end1Tcam1base1Tobj1cam1=Tbase2end2Tcam2base2Tobj2cam2

( T b a s e 2 e n d 2 ) − 1 ⋅ T b a s e 1 e n d 1 ⋅ T c a m 1 b a s e 1 = T c a m 2 b a s e 2 ⋅ T o b j 2 c a m 2 ⋅ T c a m 1 o b j 1 (T^{end_2}_{base_2})^{-1} · T^{end_1}_{base_1} · T^{base_1}_{cam_1}= T^{base_2}_{cam_2} · T^{cam_2}_{obj_2} · T^{obj_1}_{cam_1} (Tbase2end2)1Tbase1end1Tcam1base1=Tcam2base2Tobj2cam2Tcam1obj1

[ ( T b a s e 2 e n d 2 ) − 1 ⋅ T b a s e 1 e n d 1 ] ⋅ T c a m 1 b a s e 1 = T c a m 2 b a s e 2 ⋅ [ ( T c a m 2 o b j 2 ) − 1 ⋅ T c a m 1 o b j 1 ] [(T^{end_2}_{base_2})^{-1}·T^{end_1}_{base_1}] · T^{base_1}_{cam_1}= T^{base_2}_{cam_2} · [(T^{obj_2}_{cam_2})^{-1}·T^{obj_1}_{cam_1}] [(Tbase2end2)1Tbase1end1]Tcam1base1=Tcam2base2[(Tcam2obj2)1Tcam1obj1]

A X = X B A X = X B AX=XB
其中A和B是已知的,目标是求解X
所有的手眼标定都是为了求解AX=XB,只不过求解的方法和思路不同,比较出名的是Tsai-Lenz算法

同理眼在手上是将 T b a s e o b j T^{obj}_{base} Tbaseobj作为中间桥梁,因为 b a s e base base o b j obj obj相对静止关系

二、手眼标定方法汇总

因为相机分为两类,2D相机和3D相机,故手眼标定方法又可以进行细分2D和3D类别

1,2D相机手眼标定

2D相机进行手眼标定的本质是真实的世界坐标系到相机的像素坐标系的映射,本质就是仿射变换
比如:一张图片上的某个像素坐标通过变换矩阵进行映射到实际的世界坐标系下,前期是不考虑Z轴,机械臂的运作均在同一个水平面
最常用的方法是九点标定法

2,3D相机手眼标定

3D相机拍摄的数据为点云,故可以通过ICP进行点云匹配求解得到变换矩阵
也可以通过OpenCV的solvePnP求解变换矩阵

三、代码实战

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

①操作流程

Ⅰ. 制作一张含有9个圆点的标定板于相机正下方(多少个点都行,9个点只是为了提高精度而已,我们用3*5=15个点为例进行演示)
Ⅱ. 相机固定,拍一张标定板照片
Ⅲ. 通过圆查找相关算法,依次识别出标定板中的9个圆点的像素坐标 ( x i m g i , y i m g i ) (x_{img_i},y_{img_i}) (ximgi,yimgi)
Ⅳ. 机械臂保持同一水平面,依次去触碰标定板的圆心,并依次记录这9个机械臂位姿 ( x r o b o t i , y r o b o t i ) (x_{robot_i},y_{robot_i}) (xroboti,yroboti)

9点标定必须要求机械臂所在同一水平面(Z是固定值),无法得到机械臂的姿态

这里我是用的是15个点进行手眼标定测试,以下面图片为例进行实战演示
在这里插入图片描述

②具体实现

Ⅰ. 找圆点函数——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输入:检测圆点标定板的类型对称圆点标定板cv::CALIB_CB_SYMMETRIC_GRID、非对称圆点标定板cv::CALIB_CB_ASYMMETRIC_GRID、还有另一个不常用
blobDetector输入:斑点检测器有很多参数可以调节cv::Ptr<cv::SimpleBlobDetector>

举例:

int w = 3;
int h = 5;// 读取图像
cv::Mat image = cv::imread("beyondyanyu.jpg");// 设置 SimpleBlobDetector 参数
cv::SimpleBlobDetector::Params params;
params.maxArea = std::numeric_limits<float>::max();
params.minArea = 2;
params.minDistBetweenBlobs = 1;// 创建 SimpleBlobDetector 对象
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_ASYMMETRIC_GRID, blobDetector);
Ⅱ. 斑点检测器参数设置——cv::SimpleBlobDetector

findCirclesGrid函数的核心在于斑点检测器参数的设置,也就是cv::SimpleBlobDetector::Params params;,源码是这样的:

struct CV_EXPORTS_W_SIMPLE Params
{CV_WRAP Params();CV_PROP_RW float thresholdStep;CV_PROP_RW float minThreshold;CV_PROP_RW float maxThreshold;CV_PROP_RW size_t minRepeatability;CV_PROP_RW float minDistBetweenBlobs;CV_PROP_RW bool filterByColor;CV_PROP_RW uchar blobColor;CV_PROP_RW bool filterByArea;CV_PROP_RW float minArea, maxArea;CV_PROP_RW bool filterByCircularity;CV_PROP_RW float minCircularity, maxCircularity;CV_PROP_RW bool filterByInertia;CV_PROP_RW float minInertiaRatio, maxInertiaRatio;CV_PROP_RW bool filterByConvexity;CV_PROP_RW float minConvexity, maxConvexity;CV_PROP_RW bool collectContours;void read( const FileNode& fn );void write( FileStorage& fs ) const;
};
参数描述
thresholdStep二值化的阈值步长
minThreshold二值化的最小阈值
maxThreshold二值化的最大阈值
minRepeatability重复的最小次数,属于斑点且斑点数量大于该值时才被认为是特征点
minDistBetweenBlobs最小的斑点距离,不同斑点之间距离大于该值才被认为是不同斑点
filterByColorbool,需要设置为true才生效,通过斑点颜色进行限制
blobColor只提取斑点的颜色,0为黑色,255为白色
filterByAreabool,需要设置为true才生效,通过斑点的面积进行限制
minArea斑点最小面积
maxArea斑点最大面积
filterByCircularitybool,需要设置为true才生效,通过斑点的圆度进行限制
minCircularity斑点的最小圆度
maxCircularity斑点的最大圆度
filterByInertiabool,需要设置为true才生效,通过斑点的惯性率进行限制
minInertiaRatio斑点的最小惯性率
maxInertiaRatio斑点的最大惯性率
filterByConvexitybool,需要设置为true才生效,通过斑点的凸度进行限制
minConvexity斑点的最小凸度
maxConvexity斑点的最大凸度
collectContoursbool,需要设置为true才生效,是否收集斑点的轮廓

举例:

// 设置 SimpleBlobDetector 参数
cv::SimpleBlobDetector::Params params;params.maxArea = std::numeric_limits<float>::max();
params.minArea = 2;
params.minDistBetweenBlobs = 1;
params.filterByColor = true;
params.blobColor = 255;
Ⅲ. 需要改动的地方

①你的图片宽高实际多少个圆点

int w = 3;
int h = 5;

②你的圆点标定板的路径

cv::Mat image = cv::imread("35_1.jpg");

③你的圆点标定板是否是对称的
对称:cv::CALIB_CB_SYMMETRIC_GRID
非对称:cv::CALIB_CB_ASYMMETRIC_GRID

bool found = cv::findCirclesGrid(gray, cv::Size(w, h), corners, cv::CALIB_CB_SYMMETRIC_GRID, blobDetector);

④你的机械臂依次指向对应的圆点坐标时机械臂的位置,因为我这边标定板是3*5的对称圆点标定板,故需要得到机械臂的15个位置

    // 机器人坐标系中的点的坐标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}};

③. 完整代码

#include <opencv2/opencv.hpp>
#include <iostream>// 定义一个结构体来存储点的坐标
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;}cv::Mat transformMatrix = cv::estimateAffine2D(src, dst);return transformMatrix;
}// 将图像坐标点通过仿射变换矩阵转换为机器人坐标点
// 机械臂坐标 = 仿射变换矩阵 * 图像坐标
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("35_1.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);// 设置 SimpleBlobDetector 参数cv::SimpleBlobDetector::Params params;params.maxArea = std::numeric_limits<float>::max();params.minArea = 2;params.minDistBetweenBlobs = 1;// 创建 SimpleBlobDetector 对象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 (int i = 0; i < corners.size(); i++){std::cout << "corner: " << corners[i] << std::endl;imagePoints.at(i).x = corners[i].x;imagePoints.at(i).y = corners[i].y;}std::cout << corners.size() << std::endl;// 优化角点位置cv::cornerSubPix(gray, corners, cv::Size(w, h), cv::Size(-1, -1), criteria);// 绘制角点cv::drawChessboardCorners(image, cv::Size(w, h), cv::Mat(corners), found);cv::namedWindow("findCorners", cv::WINDOW_NORMAL);cv::imshow("findCorners", image);cv::waitKey(0);cv::destroyAllWindows();}else {std::cerr << "Could not find the circle grid" << std::endl;}for (auto point : imagePoints){std::cout << "x: " << point.x << " y: " << point.y << std::endl;}// 机器人坐标系中的点的坐标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;// 测试一个图像坐标点的变换std::cout << "Testing a testImagePoint transformation to robot point..." << std::endl;//Point2D testImagePoint = { 250, 250 };Point2D testImagePoint1 = { 590.203, 885.33 };Point2D testRobotPoint1 = transformPoint(affineMatrix, testImagePoint1);std::cout << "Image Point1 (" << testImagePoint1.x << ", " << testImagePoint1.y << ") transforms to Robot2 Point ("<< testRobotPoint1.x << ", " << testRobotPoint1.y << ")" << std::endl;/*//变换矩阵affineMatrix会存在不可逆情况Point2D testRobotPoint2 = { 10, 10 };Point2D testImagePoint2 = transformPoint(affineMatrix.inv(), testRobotPoint2);std::cout << "Robot Point2 (" << testRobotPoint2.x << ", " << testRobotPoint2.y << ") transforms to Image2 Point ("<< testImagePoint2.x << ", " << testImagePoint2.y << ")" << std::endl;*/return 0;
}

④. 运行效果

在这里插入图片描述
在这里插入图片描述
得到仿射变换矩阵 R R R
R = [ 0.02821525305435079 − 5.038386495271226 e − 08 − 1.652816243402001 3.693734340049677 e − 08 0.02822203196237374 0.01414373627149304 ] R= \begin{bmatrix} 0.02821525305435079&-5.038386495271226e{-08}&-1.652816243402001\\ 3.693734340049677e{-08}&0.02822203196237374&0.01414373627149304\\ \end{bmatrix} R=[0.028215253054350793.693734340049677e085.038386495271226e080.028222031962373741.6528162434020010.01414373627149304]
图像中的点 ( 590.203 , 885.33 ) (590.203, 885.33) (590.203,885.33),也就是最后一个点,对应我们输入的机械臂的最后一个位置,可以映射到机械臂位置为 ( 15 , 25 ) (15, 25) (15,25),可以看到最终实际的求解效果为 ( 14.9999 , 25 ) (14.9999, 25) (14.9999,25),还可以

{ P o i n t r o b o t = R ⋅ P o i n t i m g P o i n t i m g = R − 1 ⋅ P o i n t r o b o t \begin{cases} Point_{robot} = R · Point_{img}\\ Point_{img} = R^{-1} · Point_{robot} \end{cases} {Pointrobot=RPointimgPointimg=R1Pointrobot

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

2D相机手眼标定常用九点标定法,其只能得到X和Y的位置关系,无法的机械臂姿态信息,毕竟2D相机和3D相机的成本有很大差距

2D相机手眼标定只需要拍照一张照片+机械臂的9个点位即可,但只能得到X和Y信息,Z轴信息是固定的
而3D相机的手眼标定可以求解机械臂的姿态,但需要多组拍摄标定板进行组成AX=XB多组方程求解

OpenCV提供了求解方法,核心函数是solvePnPcv::calibrateHandEye

①操作流程

Ⅰ.准备工作

1.1 标定板——使用棋盘格(如 9x6 内角点),尺寸需已知(单位:米或毫米
1.2 相机内参——提前完成相机标定,获取 camera_matrix 和 dist_coeffs
具体代码之前写的博文有,可参考:(一)相机标定——四大坐标系的介绍、对应转换、畸变原理以及OpenCV完整代码实战(C++版)

Ⅱ.数据采集

2.1 固定标定板——将标定板固定在工作区域内,确保机械臂运动时相机始终可观测到它
2.2 移动机械臂——控制机械臂移动至 不同位姿(需包含旋转和平移),每个位姿下:

  • 记录机械臂末端的 XYZWPR(基座坐标系下)
  • 拍摄标定板图像。

2.3 数据量——至少 10组 数据,位姿需多样化(建议20组以上)

Ⅲ.数据预处理

3.1 机械臂位姿转换——将 XYZWPR 转换为 旋转矩阵R_base2gripper和 平移向量t_base2gripper
3.2 标定板检测——对每张图像检测棋盘格角点,使用 solvePnP计算相机到标定板的位姿R_target2camt_target2cam

Ⅳ.眼标定计算

调用OpenCV的calibrateHandEye 函数,输入机械臂和相机的位姿数据,求解变换矩阵X

②具体实现

Ⅰ.准备标定板参数,这里使用opencv自带的棋盘格

路径为opencv\sources\samples\data
在这里插入图片描述

Ⅱ.获取相机的内参以及畸变系数

具体代码之前写的博文有,可参考:(一)相机标定——四大坐标系的介绍、对应转换、畸变原理以及OpenCV完整代码实战(C++版)
在这里插入图片描述
求解得到相机的内参矩阵camera_matrix和透镜畸变系数dist_coeffs

Ⅲ.采集机械臂和相机的数据

①收集机械臂姿态数据(xyzwpr)
机械臂夹取标定板在相机视野下运动9个位姿,相机拍摄9组数据,当然数据越多,切姿态越丰富标定效果越好
机械臂的位姿通过示教器读取并转换为矩阵形式
例如fanuc示教器上的读数是:xyzwpr,w表示Rx,p表示Ry,r表示Rz
Fanuc 机器人示教器上的 XYZWPR是基于固定轴的位姿表示,其中:
X, Y, Z:末端执行器在基座坐标系中的平移分量(单位:毫米或米)。
W, P, R:分别表示绕 X、Y、Z 轴 的旋转角度(单位:度),即 Roll-Pitch-Yaw 顺序(但按 Z-Y-X 轴顺序 组合旋转)
假设我们以及移动了9组位置并进行记录机械臂位置存放到std::vector<std::vector<double>> xyzwpr_data
函数convertXYZWPRToMat化将xyzwpr转化为RT矩阵

②收集相机拍摄机械臂所夹持的棋盘格图像
为例方便演示,这里我采用的是opencv自带的棋盘格图像数据
路径为opencv\sources\samples\data

Ⅳ.检测棋盘格位姿

之前第一篇博文以及介绍,这里就不再赘述,涉及的方法都是一样的
(一)相机标定——四大坐标系的介绍、对应转换、畸变原理以及OpenCV完整代码实战(C++版)
findChessboardCorners找标定板的角点
cornerSubPix亚像素优化角点
solvePnP计算相机到标定板的位姿

Ⅴ.手眼标定

手眼标定核心函数calibrateHandEye,输入机械臂和相机的位姿数据,输出旋转X_rot矩阵和平移X_trans向量,然后通过copyTo拼接成变换矩阵(4x4)即可
calibrateHandEye 参数:
R_base2gripper, t_base2gripper:机械臂末端在基座坐标系下的位姿。
R_target2cam,t_target2cam:标定板在相机坐标系下的位姿。
X_rot, X_trans:输出的相机到基座的旋转矩阵和平移向量。
flags:标定算法选择(推荐 CALIB_HAND_EYE_TSAI

③完整代码

需要修改的地方:

Ⅰ.实际你使用的棋盘格单个尺寸和内角点数量

const float square_size = 0.025f; // 棋盘格单格尺寸(m)
const cv::Size board_size(9, 6); // 棋盘格内角点数量

Ⅱ.相机内参和畸变系数

cv::Mat camera_matrix, dist_coeffs;
可以参考博文:(一)相机标定——四大坐标系的介绍、对应转换、畸变原理以及OpenCV完整代码实战(C++版)

Ⅲ.多组从示教器读取的机械臂位姿

std::vector<std::vector<double>> xyzwpr_data

Ⅳ.机械臂运动时,相机所拍摄的标定板图片路径

std::string image_path = "D:/opencv_4.7/opencv/sources/samples/data/right0" + std::to_string(i+1) + ".jpg";

#include <opencv2/opencv.hpp>
#include <opencv2/calib3d.hpp>
#include <vector>
#include <cmath>
#include <Windows.h>
#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;// 绕Z轴的旋转矩阵cv::Mat Rz = (cv::Mat_<double>(3, 3) <<cos(W), -sin(W), 0,sin(W), cos(W), 0,0, 0, 1);// 绕Y轴的旋转矩阵cv::Mat Ry = (cv::Mat_<double>(3, 3) <<cos(P), 0, sin(P),0, 1, 0,-sin(P), 0, cos(P));// 绕X轴的旋转矩阵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; // 棋盘格单格尺寸(m)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.3547430629291, 0, 328.3241889680722,0, 541.614996071113, 246.9472922233208,0, 0, 1);dist_coeffs = (cv::Mat_<double>(5, 1) << -0.2805430825289494, 0.1043237314547243, -0.0005582136242986065, 0.001303557702627711, -0.02372163114377487);// --------------- 3. 采集数据 ---------------// 示例数据:每组数据是一个包含6个元素的 std::vector<double>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) {// ------------------- 3.1 获取机械臂位姿 -------------------// 假设从Fanuc机器人读取基座到末端的变换矩阵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].at(0), xyzwpr_data[i].at(1), xyzwpr_data[i].at(2), xyzwpr_data[i].at(3),xyzwpr_data[i].at(4), xyzwpr_data[i].at(5), R_base2grip, t_base2grip);R_base2gripper.push_back(R_base2grip.clone());t_base2gripper.push_back(t_base2grip.clone());// ------------------- 3.2 检测棋盘格位姿 -------------------// 假设从相机捕获图像std::string image_path = "D:/opencv_4.7/opencv/sources/samples/data/right0" + std::to_string(i+1) + ".jpg";cv::Mat image = cv::imread(image_path,-1);std::cout<<image.size()<<std::endl;std::vector<cv::Point2f> corners;bool found = cv::findChessboardCorners(image, board_size, corners);if (found) {// 亚像素优化角点cv::cornerSubPix(image, corners, cv::Size(11, 11), cv::Size(-1, -1),cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 30, 0.1));// 计算相机到标定板的位姿cv::Mat rvec, tvec;cv::solvePnP(object_points, corners, 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 << "相机到基座的变换矩阵 X = \n" << X << std::endl;return 0;
}

④运行效果

在这里插入图片描述
得到相机到基座的变换矩阵X,也就是AX = XB中的解

相关文章:

(二)手眼标定——概述+原理+常用方法汇总+代码实战(C++)

一、手眼标定简述 手眼标定的目的&#xff1a;让机械臂和相机关联&#xff0c;相机充当机械臂的”眼睛“&#xff0c;最终实现指哪打哪 相机的使用前提首先需要进行相机标定&#xff0c;可以参考博文&#xff1a;&#xff08;一&#xff09;相机标定——四大坐标系的介绍、对…...

3D点云的深度学习网络分类(按照作用分类)

1. 3D目标检测&#xff08;Object Detection&#xff09; 用于在点云中识别和定位目标&#xff0c;输出3D边界框&#xff08;Bounding Box&#xff09;。 &#x1f539; 方法类别&#xff1a; 单阶段&#xff08;Single-stage&#xff09;&#xff1a;直接预测3D目标位置&am…...

【Linux网络-NAT、代理服务、内网穿透】

一、NAT技术 1.NAT技术背景 之前我们讨论了&#xff0c;IPV4协议中&#xff0c;IP地址数量不充足的问题 NAT技术当前解决IP地址不够用的主要手段&#xff0c;是路由器的一个重要功能 NAT&#xff08;网络地址转换&#xff0c;Network Address Translation&#xff09;是一种…...

Windows 和 Linux 操作系统架构对比以及交叉编译

操作系统与架构兼容性详解 1. 可执行文件格式&#xff1a;PE vs ELF Windows: PE (Portable Executable) 格式 详细解释&#xff1a; PE 格式是 Windows 下的可执行文件标准 包含多个区段&#xff08;Sections&#xff09;&#xff0c;如代码段、数据段、资源段 文件头包含…...

heapq库的使用——python代码

Python中heapq库的基础使用方法和示例代码&#xff0c;包含详细注释说明&#xff1a; 1. 基本功能 heapq 实现的是最小堆&#xff08;父节点值 ≤ 子节点值&#xff09;&#xff0c;核心操作包括&#xff1a; 插入元素&#xff1a;heappush(heap, item)弹出最小值&#xff1a…...

新手村:逻辑回归-理解02:逻辑回归中的伯努利分布

新手村&#xff1a;逻辑回归-理解02&#xff1a;逻辑回归中的伯努利分布 伯努利分布在逻辑回归中的潜在含义及其与后续推导的因果关系 1. 伯努利分布作为逻辑回归的理论基础 ⭐️ 逻辑回归的核心目标是: 建模二分类问题中 目标变量 y y y 的概率分布。 伯努利分布&#xff08…...

golang Error的一些坑

golang Error的一些坑 golang error的设计可能是被人吐槽最多的golang设计了。 最经典的err!nil只影响代码风格设计&#xff0c;而有一些坑会导致我们的程序发生一些与我们预期不符的问题&#xff0c;开发过程中需要注意。 ​​ errors.Is​判断error是否Wrap不符合预期 ​…...

【干货,实战经验】nginx缓存问题

文章目录 案例背景出现的问题:定位到问题解决方式修改配置修改后的nginx配置 案例背景 有2个服务器A 和B&#xff0c;A是一个动态ip经常变公网ip&#xff0c;B是一个云服务器&#xff0c;公网ip固定. 于是我通过ddns &#xff0c;找了个域名C&#xff0c;动态解析A服务器上的公…...

分布式理论:CAPBASE理论

1 CAP理论 1.1 简介 CAP也就是Consistency&#xff08;一致性&#xff09;、Availability&#xff08;可用性&#xff09;、Partition Tolenrance&#xff08;分区容错性&#xff09;这三个单词首字母组合。 在理论计算机科学中&#xff0c;CAP定理&#xff08;CAP theorem&…...

大数据学习(86)-Zookeeper去中心化调度

&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一…...

uniapp再次封装uni-nav-bar导航栏组件

<!-- components/custom-nav-bar/custom-nav-bar.vue --> <template><view class"custom-nav" :style"{ backgroundColor: bgColor }"><!-- 状态栏占位 --><view class"status-bar" :style"{ height: statusBar…...

ngx_http_index_t

定义在 src\http\modules\ngx_http_index_module.c typedef struct {ngx_str_t name;ngx_array_t *lengths;ngx_array_t *values; } ngx_http_index_t; 该结构体用于 存储和解析 index 指令中单个索引文件的信息 &#xff0c;支持静态…...

深入解析Flink Kafka Connector的分布式流数据采集架构与底层实现

目录 1. Flink Kafka连接器的分布式流采集架构 1.1 架构组成 1.2 分布式流模型 2. 数据分区分配策略 3. 为什么重写序列化和偏移量管理 3.1 与Flink分布式架构集成 3.2 与Flink检查点机制集成同时承接多级并行架构 3.3 OffsetsInitializer与细粒度偏移量控制 3.4 与Fl…...

vcd波形转仿真激励

我们使用vivado的ila抓取波形后&#xff0c;常常希望用该波形作为激励参与仿真。稍微复杂的项目中手动输入的工作量巨大&#xff0c;几乎是不可能采取的方式。我的方法是保存ila波形为vcd格式文件&#xff0c;用python解析vcd文件&#xff0c;转换成仿真激励的代码。 python代码…...

【STM32】知识点介绍二:GPIO引脚介绍

文章目录 一、概述二、GPIO的工作模式三、寄存器编程 一、概述 GPIO&#xff08;英语&#xff1a;General-purpose input/output&#xff09;,即通用I/O(输入/输出)端口&#xff0c;是STM32可控制的引脚。STM32芯片的GPIO引脚与外部设备连接起来&#xff0c;可实现与外部通讯、…...

【AI】NLP

不定期更新&#xff0c;建议关注收藏点赞。 目录 transformer大语言模型Google Gemma疫情网民情绪识别 整体框架 baseline构建 模型调参、模型优化、其他模型 数据trick、指标优化、magic feature 数据增强、伪标签、迁移学习 模型融合sklearn中TFIDF参数详解 频率阈值可以去掉…...

Go 代理爬虫

现在注册&#xff0c;还送15美金注册奖励金 --- 亮数据-网络IP代理及全网数据一站式服务商 使用代理服务器&#xff0c;通过 Colly、Goquery、Selenium 进行网络爬虫的基础示例程序 本仓库包含两个分支&#xff1a; basic 分支包含供 Go Proxy Servers 这篇文章改动的基础代码…...

【NLP 43、大模型技术发展】

目录 一、ELMo 2018 训练目标 二、GPT-1 2018 训练目标 三、BERT 2018 训练目标 四、Ernie —— baidu 2019 五、Ernie —— Tsinghua 2019 六、GPT-2 2019 七、UNILM 2019 八、Transformer - XL & XLNet 2019 1.模型结构 Ⅰ、循环机制 Recurrence Mechanism Ⅱ、相对位置…...

在普通用户下修改root用户密码

1 从普通用户切换到root用户 sudo -s 再输入密码。 2 输入passwd ,会提醒你输入当前用户密码&#xff0c;验证后会提醒你输入root用户密码。 3 切换到root用户&#xff0c;使用修改过的密码登陆。 4 成功进入root用户。...

【每日算法】Day 6-1:哈希表从入门到实战——高频算法题(C++实现)

摘要 &#xff1a;掌握高频数据结构&#xff01;今日深入解析哈希表的核心原理与设计实现&#xff0c;结合冲突解决策略与大厂高频真题&#xff0c;彻底掌握O(1)时间复杂度的数据访问技术。 一、哈希表核心思想 哈希表&#xff08;Hash Table&#xff09; 是一种基于键值对的…...

go命令使用

查看配置信息 go env配置go国内源 export GO111MODULEon export GOPROXYhttps://goproxy.cn测试 go install github.com/jesseduffield/lazydockerlatesthttps://github.com/jesseduffield/lazydocker...

深入 SVG:矢量图形、滤镜与动态交互开发指南

1.SVG 详细介绍 SVG&#xff08;Scalable Vector Graphics&#xff09; 是一种基于 XML 的矢量图形格式&#xff0c;用于描述二维图形。 1. 命名空间 (Namespace) 命名空间 URI&#xff1a;http://www.w3.org/2000/svg 用途&#xff1a;在 XML 或 XHTML 中区分不同标记语言的…...

SPPAS安装及问题汇总

SPPAS下载地址 文件找不到&#xff0c;可能是MAC的自动化操作问题&#xff0c;解决方案有二&#xff1a; 方案一&#xff1a; 直接查看SPPAS中的readme&#xff0c;运行sppas.command 方案二&#xff1a; 在自动化脚本中添加 export PATH/usr/local/bin:$PATH...

LINUX基础 [三] - 进程创建

目录 前言 进程创建的初次了解&#xff08;创建进程的原理&#xff09; 什么是fork函数&#xff1f; 初识fork函数 写时拷贝 fork函数存在的意义 fork调用失败的原因 进程终止 运行完毕结果不正确 main函数返回 库函数函数exit 系统调用接口_exit 进程异常终止 进…...

【day1】数据结构刷题 链表

一 反转链表 206. 反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&#xff1a;[2,1]…...

鼠标在客户区内按下左键和双击右键

书籍&#xff1a;《Visual C 2017从入门到精通》的2.6鼠标 环境&#xff1a;visual studio 2022 内容&#xff1a;【例2.44】鼠标在客户区内按下左键和双击右键 1.创建一个单文档程序 一个简单的单文档程序-CSDN博客https://blog.csdn.net/qq_20725221/article/details/1463…...

c++ map和vector模板类

在这一章中C语法之模板函数和模板类-CSDN博客 我们学习了怎样写模板函数和模板类&#xff0c;接下来我们来学习系统给我们写好的两个模板类:map和vector。 我相信有了上文的基础&#xff0c;能帮助我们更好的理解这些模板类。 map和vector 是C STL(标准模板库) 中的一部分&a…...

hn航空app hnairSign unidbg 整合Springboot

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 逆向分析 学习unidbg补环境。先弄一个…...

Arm Linux ceres库编译

由于工作需要&#xff0c;需在国产化系统上编译ceres库&#xff0c;手上有一块树莓派&#xff0c;就在树莓派上面进行测试编译ceres库&#xff0c;总体来说比较顺利。只出现了一点小问题 参考链接&#xff1a; Ceres中文教程-安装 Ceres官方网站&#xff08;英文&#xff09; …...

c++中的四种cast转换

文章目录 前言一、dynamic_cast二、static_cast三、const_cast四、reinterpret_cast总结 前言 C继承并扩展C语言的传统类型转换方式&#xff0c;提供了功能更加强大的转型机制&#xff08;检查与风险&#xff09; 转换类型典型用途安全性static_cast相关类型转换&#xff08;…...