手眼标定(眼在手上)代码及注意事项

前言

本人最近在做单目相机的手眼标定,相机的安装方式为固定在机械臂末端,为此最近翻阅了很多的资料和代码,很多资料已经将手眼标定的原理解释的非常清楚,因此本文就不再过多的阐述手眼标定的原理,如果希望熟悉原理的话可以参考这篇文章:手眼标定之基本原理

本文重点放在了手眼标定的代码以及使用代码的注意事项,虽然网上有很多相关代码,但是个人感觉解释不够清楚或者没有提示需要注意的地方,因此本人希望本篇文章帮助大家少走一些弯路,这也是本人第一次写文章,如果有什么不合适的地方,希望大家多多担待,当然大家有什么问题也可以给我留言,我会尽力解决各位的疑惑。

运行环境

程序的运行环境为Visual Studio+Opencv4.8.0,opencv版本在4以上较好;

程序源码

本人的代码主要参考自:手眼标定(一):Opencv4实现手眼标定及手眼系统测试

程序大体上与参考文章一样,但是有几处需要说明并进行修改。

注意事项:

(一)关于标定板相对于相机的位姿关系,由平移向量和旋转向量组成,关于平移向量和旋转向量获取的方式为Matlab的相机标定工具箱,关于该标定的工具箱的使用,可以参考:使用Matlab做相机标定(获取相机的内外参数矩阵)在标定完成后注意检查标定板的起点,一定要保证所有标定板的起点一致。

(二)关于机械臂末端相对于基座的位姿关系,本人所使用的是史陶比尔机械臂,可以直接从示教器上获取末端的位姿关系,前三个数据为末端坐标系相对于基坐标系的平移,后三个数据为末端坐标系相对于基坐标系的旋转,其旋转是以欧拉角的方式进行描述。在这里有一个容易忽略的细节,就是欧拉角是内旋还是外旋,关于内旋和外旋的区别可以参考:欧拉角细节/旋转顺序/内旋外旋简单来说内旋每次是绕着自身轴进行旋转,而外旋每次是绕着固定轴进行旋转。内旋是矩阵右乘,外旋是矩阵左乘,举个例子就是内旋X-Y-Z等价于外旋Z-Y-X。在欧拉角转旋转矩阵的函数中本人做了相应的修改,需要额外传入一个内旋还是外旋的参数。

此外,在欧拉角转旋转矩阵还需要注意的是单位问题,C++中的三角函数是以弧度制进行计算的,本人传入的是角度,需要进行转换,如果本身传入的就是弧度就不需要进行转换。

整理代码如下:

#include <iostream>
#include <string>
#include <opencv2/opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/calib3d.hpp>

//导入静态链接库
#if (defined _WIN32 || defined WINCE || defined __CYGWIN__)
#define OPENCV_VERSION "480"
#pragma comment(lib, "opencv_world" OPENCV_VERSION ".lib")
#endif

using namespace cv;
using namespace std;

//第二组
//标定板相对于相机的位姿,平移向量+旋转向量
Mat_<double> CalPose = (Mat_<double>(13, 6) << 
	-17.00157419, -81.58147217, 355.7542094, 0.029088994, -0.018921115, -0.05339676,
	-28.96389924, -51.10505073, 356.782175, -0.003821439, 0.036377063, -0.312840676,
	-1.336799619, -14.59851038, 343.103271, -0.064835906, -0.027856025, 0.11723227,
	0.599159361, -29.21464607, 317.5905327, 0.153990913, -0.164997269, 0.141422701,
	-30.57797266, -15.33702401, 307.928564, -0.051195625, -0.09995785, -0.300064261,
	-13.17969414, -32.4384572, 379.3117752, -0.08341498, 0.099063095, -0.154433415,
	-9.912571488, -51.70341331, 377.3018235, -0.119340607, -0.091310513, 0.334251393,
	43.93753639, -98.11986502, 351.5100989, 0.305854801, -0.036788466, 0.47603346,
	7.27764392, -115.6325578, 289.6246863, 0.191094099, -0.314029023, 0.219271655,
	-41.66080761, -165.070534, 372.1309944, 0.111627518, 0.321585867, 0.38798498,
	-96.27253589, -50.22770075, 457.0584011, -0.492185963, -0.002850737, -0.387823227,
	5.183372925, -89.01940034, 303.5791468, 0.374755954, -0.246685178, 0.351494064,
	-63.27585387, -12.24742338, 395.4984493, -0.374408602, -0.356181557, -0.12614873
	);

//机械臂末端相对于机器人基座的位姿变化,X,Y,Z,Rx,Ry,Rz
Mat_<double> ToolPose = (cv::Mat_<double>(13, 6) <<
	-99.76, 711.11, -311.65, -1.09, 178.64, 4.02,
	-89.47, 740.35, -311.65, 0.85, -178.23, 18.88,
	-110.38, 755.38, -328.44, 4.36, 178.29, -5.81,
	-130.99, 771.58, -355.7, -7.6, 169.59, -6.48,
	-98.51, 771.65, -363.99, 2.27, 173.69, 18.15,
	-82.1, 739.79, -291.04, 5.51, -175.02, 10.07,
	-93.76, 698.95, -304.06, 8.47, 175.66, -18.49,
	-116.96, 713.27, -291.63, -15.43, 173.6, -25.81,
	-129.88, 680.86, -376.39, -8.29, 160.56, -10.32,
	54.49, 671.77, -300.37, -10.33, -164.05, -23.05,
	-61.9, 596.03, -276.03, 27.64, 174.52, 22.48,
	-123.6, 731.8, -349.62, -18.21, 162.17, -16.75,
	-129.45, 696.37, -302.57, 21.49, 158.61, 4.54
	);



//R和T合并为4×4齐次矩阵
Mat R_T2RT(Mat& R, Mat& T)
{
	Mat RT;
	Mat_<double>R1 = (Mat_<double>(4, 3) << 
		R.at<double>(0, 0), R.at<double>(0, 1), R.at<double>(0, 2),
		R.at<double>(1, 0), R.at<double>(1, 1), R.at<double>(1, 2),
		R.at<double>(2, 0), R.at<double>(2, 1), R.at<double>(2, 2),
		0, 0, 0);
	Mat_<double>T1 = (Mat_<double>(4, 1) << T.at<double>(0, 0), T.at<double>(0, 1), T.at<double>(0, 2), 1);
	hconcat(R1, T1, RT);
	return RT;
}

//齐次矩阵分为R矩阵和T矩阵
void RT2R_T(Mat& RT, Mat& R, Mat& T)
{
	R = (Mat_<double>(3, 3) <<
		RT.at<double>(0, 0), RT.at<double>(0, 1), RT.at<double>(0, 2),
		RT.at<double>(1, 0), RT.at<double>(1, 1), RT.at<double>(1, 2),
		RT.at<double>(2, 0), RT.at<double>(2, 1), RT.at<double>(2, 2));
	T = (Mat_ <double>(3, 1) << RT.at<double>(0, 3), RT.at<double>(1, 3), RT.at<double>(2, 3));
}

//判断是否为旋转矩阵
bool isRotationMatrix(const cv::Mat& R)
{
	Mat tmp33 = R({ 0,0,3,3 });
	Mat shouldBeIdentity;
	shouldBeIdentity = tmp33.t() * tmp33;
	Mat I = Mat::eye(3, 3, shouldBeIdentity.type());
	return  norm(I, shouldBeIdentity) < 1e-6;
}

/** @brief 欧拉角 -> 3*3 的R
*	@param 	eulerAngle		角度值
*	@param 	seq				指定欧拉角xyz的排列顺序如:"xyz" "zyx"
*/
//注意自己带入的欧拉角是内旋还是外旋,在传入旋转顺序之前一定要确定旋转方式
//内旋:绕着自身轴进行旋转,如果旋转顺序为X-Y-Z,则旋转矩阵为Rx*Ry*Rz
//外旋:绕着固定轴进行旋转,如果旋转顺序为X-Y-Z,则旋转矩阵为Rz*Ry*Rz
Mat eulerAngleToRotatedMatrix(const Mat& eulerAngle, const string &rotationseq,const string& seq)
{
	CV_Assert(eulerAngle.rows == 1 & eulerAngle.cols == 3);
	eulerAngle /= (180 / CV_PI);//将角度转为弧度,C++的三角函数是以弧度进行计算的
	Matx13d m(eulerAngle);
	auto rx = m(0, 0), ry = m(0, 1), rz = m(0, 2);
	auto xs = sin(rx), xc = cos(rx);
	auto ys = sin(ry), yc = cos(ry);
	auto zs = sin(rz), zc = cos(rz);
	Mat rotX = (cv::Mat_<double>(3, 3) <<
		1, 0, 0,
		0, xc, -1*xs,
		0, xs, xc);
	Mat rotY = (cv::Mat_<double>(3, 3) <<
		yc, 0, ys,
		0, 1, 0,
		-1*ys, 0, yc);
	Mat rotZ = (cv::Mat_<double>(3, 3) <<
		zc, -1*zs, 0,
		zs, zc, 0,
		0, 0, 1);
	Mat rotMat;
	if (rotationseq == "wai")
	{
		//外旋左乘
		if (seq == "zyx")		rotMat = rotX * rotY * rotZ;
		else if (seq == "yzx")	rotMat = rotX * rotZ * rotY;
		else if (seq == "zxy")	rotMat = rotY * rotX * rotZ;
		else if (seq == "xzy")	rotMat = rotY * rotZ * rotX;
		else if (seq == "yxz")	rotMat = rotZ * rotX * rotY;
		else if (seq == "xyz")	rotMat = rotZ * rotY * rotX;
		else {
			error(Error::StsAssert, "Euler angle sequence string is wrong.",
				__FUNCTION__, __FILE__, __LINE__);
		}
		if (!isRotationMatrix(rotMat)) {
			error(Error::StsAssert, "Euler angle can not convert to rotated matrix",
				__FUNCTION__, __FILE__, __LINE__);
		}
	}
	else if (rotationseq == "nei")
	{
		//内旋右乘
		if (seq == "xyz")		rotMat = rotX * rotY * rotZ;
		else if (seq == "xzy")	rotMat = rotX * rotZ * rotY;
		else if (seq == "yxz")	rotMat = rotY * rotX * rotZ;
		else if (seq == "yzx")	rotMat = rotY * rotZ * rotX;
		else if (seq == "zxy")	rotMat = rotZ * rotX * rotY;
		else if (seq == "zyx")	rotMat = rotZ * rotY * rotX;
		else {
			error(Error::StsAssert, "Euler angle sequence string is wrong.",
				__FUNCTION__, __FILE__, __LINE__);
		}
		if (!isRotationMatrix(rotMat)) {
			error(Error::StsAssert, "Euler angle can not convert to rotated matrix",
				__FUNCTION__, __FILE__, __LINE__);
		}
	}
	return rotMat;
}

/** @brief 四元数转旋转矩阵
*	@note  数据类型double; 四元数定义 q = w + x*i + y*j + z*k
*	@param q 四元数输入{w,x,y,z}向量
*	@return 返回旋转矩阵3*3
*/
Mat quaternionToRotatedMatrix(const Vec4d& q)
{
	double w = q[0], x = q[1], y = q[2], z = q[3];
	double x2 = x * x, y2 = y * y, z2 = z * z;
	double xy = x * y, xz = x * z, yz = y * z;
	double wx = w * x, wy = w * y, wz = w * z;
	Matx33d res{
		1 - 2 * (y2 + z2),	2 * (xy - wz),		2 * (xz + wy),
		2 * (xy + wz),		1 - 2 * (x2 + z2),	2 * (yz - wx),
		2 * (xz - wy),		2 * (yz + wx),		1 - 2 * (x2 + y2),
	};
	return Mat(res);
}

/** @brief (平移向量 && (四元数||欧拉角||旋转向量)) -> 4*4 的Rt
*	@param 	m				1*6 || 1*10的矩阵  -> 6  {x,y,z, rx,ry,rz}   10 {x,y,z, qw,qx,qy,qz, rx,ry,rz}
*	@param 	useQuaternion	如果是1*10的矩阵,判断是否使用四元数计算旋转矩阵
*	@param 	seq				如果通过欧拉角计算旋转矩阵,需要指定欧拉角xyz的排列顺序如:"xyz" "zyx" 为空表示旋转向量
*/
Mat attitudeVectorToMatrix(Mat& m, bool useQuaternion, const string& rotationseq, const string& seq)
{
	CV_Assert(m.total() == 6 || m.total() == 10);
	if (m.cols == 1)
		m = m.t();
	Mat tmp = Mat::eye(4, 4, CV_64FC1);
	//如果使用四元数转换为旋转矩阵从m矩阵的第四个成员开始读取,读取四个数据
	if (useQuaternion)
	{
		Vec4d quaternionVec = m({ 3,0,4,1 });
		quaternionToRotatedMatrix(quaternionVec).copyTo(tmp({ 0,0,3,3 }));
	}
	else
	{
		Mat rotVec;
		if (m.total() == 6)
			rotVec = m({ 3,0,3,1 });	//6
		else
			rotVec = m({ 7,0,3,1 });	//10
		if (0 == seq.compare(""))
		{
			Rodrigues(rotVec, tmp({ 0,0,3,3 }));
		}
		else
			eulerAngleToRotatedMatrix(rotVec, rotationseq, seq).copyTo(tmp({ 0,0,3,3 }));
	}
	tmp({ 3, 0, 1, 3 }) = m({ 0, 0, 3, 1 }).t();
	return tmp;
}

int main()
{
	vector<Mat>R_gripper2base;
	vector<Mat>T_gripper2base;
	vector<Mat>R_target2cam;
	vector<Mat>T_target2cam;
	Mat R_cam2gripper = (Mat_<double>(3, 3));
	Mat T_cam2gripper = (Mat_<double>(3, 1));

	vector<Mat> images;
	size_t num_images = 13;//标定数据数量
	vector<Mat>vecHg;//保存末端到基座的位姿矩阵
	vector<Mat>vecHc;//保存标定板到相机的位姿矩阵
	Mat Hcg;//保存相机到末端的位姿矩阵
	Mat tempR, tempT;
	//标定板位姿转换
	for (size_t i = 0; i < num_images; i++)
	{
		Mat tmp = attitudeVectorToMatrix(CalPose.row(i), false, "","");
		vecHc.push_back(tmp);
		RT2R_T(tmp, tempR, tempT);
		R_target2cam.push_back(tempR);
		T_target2cam.push_back(tempT);
	}
	//机械臂位姿转换
	for (size_t i = 0; i < num_images; i++)
	{
		Mat tmp = attitudeVectorToMatrix(ToolPose.row(i), false, "nei", "xyz");
		vecHg.push_back(tmp);
		RT2R_T(tmp, tempR, tempT);
		R_gripper2base.push_back(tempR);
		T_gripper2base.push_back(tempT);
	}
	//手眼标定,CALIB_HAND_EYE_TSAI法为11ms,最快
	calibrateHandEye(R_gripper2base, T_gripper2base, R_target2cam, T_target2cam, R_cam2gripper, T_cam2gripper, CALIB_HAND_EYE_TSAI);
	Hcg = R_T2RT(R_cam2gripper, T_cam2gripper);//矩阵合并
	cout << "Hcg 矩阵为: " << endl;
	cout << Hcg << endl;
	cout << "是否为旋转矩阵:" << isRotationMatrix(Hcg) << endl << endl;//判断是否为旋转矩阵
	//Tool_In_Base*Hcg*Cal_In_Cam,用第一组和第二组进行对比验证
	cout << "第一组和第二组手眼数据验证:" << endl;
	cout << vecHg[0] * Hcg * vecHc[0] << endl << vecHg[1] * Hcg * vecHc[1] << endl << endl;//.inv()

	cout << "标定板在相机中的位姿:" << endl;
	cout << vecHc[1] << endl;
	cout << "手眼系统反演的位姿为:" << endl;
	//用手眼系统预测第一组数据中标定板相对相机的位姿,是否与vecHc[1]相同
	cout << Hcg.inv() * vecHg[1].inv() * vecHg[0] * Hcg * vecHc[0] << endl << endl;

	cout << "----手眼系统测试----" << endl;
	cout << "机械臂下标定板XYZ为:" << endl;
	for (int i = 0; i < vecHc.size(); ++i)
	{
		Mat cheesePos{ 0.0,0.0,0.0,1.0 };//4*1矩阵,单独求机械臂下,标定板的xyz
		Mat worldPos = vecHg[i] * Hcg * vecHc[i] * cheesePos;
		cout << i << ": " << worldPos.t() << endl;
	}
	getchar();
	return 0;
}

运行结果

程序中的数据是本人采集了13张标定板的图片并记录了拍摄每张图片时记录的位姿,标定板为12×9,大小为20mm的棋盘格。关于棋盘格的获取,大家可以去棋盘格生成进行生成然后打印在A4纸上,打印时注意选择按实际大小打印。

程序运行结果如下:

Hcg 矩阵为:
[-0.9990454704212959, -0.04238522090013363, 0.01056603425787106, -44.96410892580678;
 0.04231801734668055, -0.9990830155543935, -0.006504878060788962, 5.968794328297632;
 0.01083205606233982, -0.006051535341263867, 0.9999230197777607, -171.296159884876;
 0, 0, 0, 1]
是否为旋转矩阵:1

第一组和第二组手眼数据验证:
[0.9996535308298152, 0.02559364590422199, -0.006146835674782315, -68.75214158039236;
 0.02562052249176114, -0.9996623447059848, 0.004334213213756957, 790.3723085544281;
 -0.006033831844908959, -0.004490196684160321, -0.9999717151034848, -497.2474810245966;
 0, 0, 0, 1]
[0.9996323685494792, 0.02603527427932847, -0.007569163848032558, -68.35639943681868;
 0.02606313919440882, -0.9996537936695049, 0.003606324652617171, 790.44443930086;
 -0.007472651704120613, -0.00380227502521064, -0.9999648504728262, -497.1223300221939;
 0, 0, 0, 1]

标定板在相机中的位姿:
[0.9508123825271663, 0.3076246664810327, 0.03637138718068197, -28.96389924;
 -0.3077625337469542, 0.9514613344103248, -0.001884660122944243, -51.10505073;
 -0.03518573652303708, -0.00940179209286761, 0.9993365650524221, 356.782175;
 0, 0, 0, 1]
手眼系统反演的位姿为:
[0.9508935102902822, 0.3072346284095964, 0.03752885814732836, -29.31712610571623;
 -0.3073431642061819, 0.9515940669701087, -0.00298515041836284, -50.92399568965126;
 -0.03662938033269298, -0.008695677851983271, 0.9992910855617282, 356.9233391451787;
 0, 0, 0, 1]

----手眼系统测试----
机械臂下标定板XYZ为:
0: [-68.75214158039236, 790.3723085544281, -497.2474810245966, 1]
1: [-68.35639943681868, 790.44443930086, -497.1223300221939, 1]
2: [-67.93511404415696, 790.4488236881581, -496.9255297819951, 1]
3: [-68.16612119340775, 790.6114021116049, -496.8191849206758, 1]
4: [-68.02219614847647, 790.7649086921571, -497.1676079976034, 1]
5: [-68.09228690059392, 790.0085079250829, -497.2195653114088, 1]
6: [-68.15764972599764, 789.538410116062, -497.6606948748483, 1]
7: [-69.11488569076617, 790.0387198493054, -496.4647603265529, 1]
8: [-69.43043243828146, 790.3231655243442, -498.3967464742822, 1]
9: [-70.31049357421708, 788.6010496221037, -498.2754660641958, 1]
10: [-69.50329717739049, 789.085088951899, -498.9327080177022, 1]
11: [-69.13181339344797, 790.2106436923892, -497.3082000034407, 1]
12: [-68.26098173569075, 789.2537519940606, -498.2797994765572, 1]

结果分析

X,Y,Z值的误差均在2.5mm以内,满足实际使用需求。

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值