机械臂 手眼标定 手眼矩阵 eye-in-hand 原理、实践及代码

2023-11-05

1.手眼标定

所谓手眼系统,就是人眼睛看到一个东西的时候要让手去抓取,就需要大脑知道眼睛和手的坐标关系。而相机知道的是像素坐标,机械手是空间坐标系,所以手眼标定就是得到像素坐标系和空间机械手坐标系的坐标转化关系。

目前工业上通常使用两种方法进行机械臂的手眼协作。第一种是:手在眼外(eye-to-hand),即摄像机与机械臂分离,应用场景为:机械臂的工作位置固定,工作平面固定,摄像机置于合理的位置以识别机械臂要工作的平面,例如:货物码垛、货物搬运等。第二种是:手在眼上(eye-in-hand),即摄像机布置在机械臂末端,应用场景为:机械臂移动式的工作,例如:果园采摘、货物运转等。总的来说,采用手在眼上可以适应更多的应用场景,所以本项目的摄像机搭载方案为眼在手上。

(1)原理

在推导过程中,我们会用到四个坐标系,分别是:

  • 基础坐标系(用base表示)
  • 机械臂末端坐标系(用gripper表示)
  • 相机坐标系(用cam表示)
  • 标定物坐标系(用target表示)

下图为坐标系分布的示意图:
在这里插入图片描述

坐标系之间的转换关系说明:

  • t a r g e t b a s e T _{target}^{base}T targetbaseT :目标坐标系到基础坐标系的变换矩阵
  • t a r g e t c a m T _{target}^{cam}T targetcamT :目标坐标系到相机坐标系的变换矩阵
  • c a m g r i p p e r T _{cam}^{gripper}T camgripperT :相机坐标系到机械臂末端坐标系的变换矩阵
  • g r i p p e r b a s e T _{gripper}^{base}T gripperbaseT :机械臂末端坐标系到基础坐标系的变换矩阵

根据线性代数中矩阵连乘的性质可知:

t a r g e t b a s e T _{target}^{base}T targetbaseT = t a r g e t c a m T _{target}^{cam}T targetcamT * c a m g r i p p e r T _{cam}^{gripper}T camgripperT * g r i p p e r b a s e T _{gripper}^{base}T gripperbaseT

在此项目中我们采用张正友博士的张氏标定法,第一步:我们将标定板置于固定位置不动。第二步:移动机械臂使相机可以清晰拍到整个标定板,记录此时机械臂位姿,并拍摄照片,第三步:重复第二步十次以上,保存数据备用。

这样的图片
类似这样的多张图片

根据上述标定步骤可知,在标定过程中,标定板与基础坐标系的位姿关系不变,即 t a r g e t b a s e T _{target}^{base}T targetbaseT 保持不变。同理,在标定过程中相机与机械臂末端的位姿关系保持不变,即 c a m g r i p p e r T _{cam}^{gripper}T camgripperT 保持不变。

故可对 ① 进行变换:

t a r g e t b a s e T 1 _{target}^{base}T_1 targetbaseT1 = t a r g e t c a m T 1 _{target}^{cam}T_1 targetcamT1 * c a m g r i p p e r T _{cam}^{gripper}T camgripperT * g r i p p e r b a s e T 1 _{gripper}^{base}T_1 gripperbaseT1

t a r g e t b a s e T 2 _{target}^{base}T_2 targetbaseT2 = t a r g e t c a m T 2 _{target}^{cam}T_2 targetcamT2 * c a m g r i p p e r T _{cam}^{gripper}T camgripperT * g r i p p e r b a s e T 2 _{gripper}^{base}T_2 gripperbaseT2

由于 t a r g e t b a s e T 1 = t a r g e t b a s e T 2 _{target}^{base}T_1 = _{target}^{base}T_2 targetbaseT1=targetbaseT2 故可得到下式:

t a r g e t c a m T 1 _{target}^{cam}T_1 targetcamT1 * c a m g r i p p e r T _{cam}^{gripper}T camgripperT * g r i p p e r b a s e T 1 _{gripper}^{base}T_1 gripperbaseT1 = t a r g e t c a m T 2 _{target}^{cam}T_2 targetcamT2 * c a m g r i p p e r T _{cam}^{gripper}T camgripperT * g r i p p e r b a s e T 2 _{gripper}^{base}T_2 gripperbaseT2

可得: t a r g e t c a m T 2 − 1 _{target}^{cam}T_2^{-1} targetcamT21 * t a r g e t c a m T 1 _{target}^{cam}T_1 targetcamT1 * c a m g r i p p e r T _{cam}^{gripper}T camgripperT = c a m g r i p p e r T _{cam}^{gripper}T camgripperT * g r i p p e r b a s e T 2 _{gripper}^{base}T_2 gripperbaseT2 * g r i p p e r b a s e T 1 − 1 _{gripper}^{base}T_1^{-1} gripperbaseT11

即:AX = XB

A:不同位姿下目标物坐标系到相机坐标系的转换

B:不同位姿下机械臂末端坐标系到基础坐标系的转换

接下来利用先前拍照获得的多组数据即可求出 c a m g r i p p e r T _{cam}^{gripper}T camgripperT,再将 c a m g r i p p e r T _{cam}^{gripper}T camgripperT 带回 ① 式即可获得每张照片对应的 t a r g e t b a s e T _{target}^{base}T targetbaseT 。至此,手眼标定的工作完成。

(2)机械臂末端坐标系到基础坐标系的变换矩阵的获取

使用D-H Model对机械臂进行建模,对各个旋转轴建立坐标系:

Z轴:穿过旋转轴中心

X轴:前一个关节的Z轴和本关节Z轴的公垂线

Y轴:根据右手定则确定Y轴

根据D-H model可得机械臂的每个连杆都可以用四个运动学参数来描述,其中两个参数用于描述连杆本身,另外两个参数用于描述连杆之间的连接关系。这四个参数分别为:

a i − 1 a_{i-1} ai1:连杆长度,用来描述关节轴 i-1 和关节轴 i 之间公垂线的长度。

α i − 1 \alpha_{i-1} αi1:连杆转角,用来描述两关节轴相对位置的第二个参数。

d i d_i di:连杆偏距,用来描述沿两个相邻连杆公共轴线方向的距离。

θ i \theta_i θi:关节角,用来描述两相邻连杆绕公共轴线旋转的夹角。

连杆参数的获取:

a i = a_i= ai= 沿 X i X_i Xi轴,从 Z i Z_i Zi移动到 Z i + 1 Z_{i+1} Zi+1的距离;

α i = \alpha_i= αi= X i X_i Xi轴,从 Z i Z_i Zi旋转到 Z i + 1 Z_{i+1} Zi+1的角度;

d i = d_i= di= 沿 Z i Z_i Zi轴,从 X i − 1 X_{i-1} Xi1移动到 X i X_i Xi的距离;

θ i = \theta_i= θi= Z i Z_i Zi轴,从 X i − 1 X_{i-1} Xi1旋转到 X i X_i Xi的角度;

由以上条件结合实际机械臂情况可得该机械臂的D-H model表:

ai-1 αi-1 di θi
1 0 0 0 θ \theta θ1-160°
2 0 90° 37 mm 180°- θ 2 \theta_2 θ2
3 96 mm 0 0 θ 3 \theta_3 θ3-180°
4 96 mm 0 0 θ 4 \theta_4 θ4-180°
5 0 90° 55 mm 0

已知D-H model参数表,再根据连杆转换矩阵的一般表达式:

i i − 1 T = [ c o s θ i − s i n θ i 0 a i − 1 s i n θ i ∗ c o s α i − 1 c o s θ i ∗ c o s α i − 1 − s i n α i − 1 − s i n α i − 1 ∗ d i s i n θ i ∗ s i n α i − 1 c o s θ i ∗ s i n α i − 1 c o s α i − 1 c o s α i − 1 ∗ d i 0 0 0 1 ] _{i}^{i-1}T = \begin{bmatrix} cos\theta_i & -sin\theta_i &0 &a_{i-1} \\ sin\theta_i*cos\alpha_{i-1} & cos\theta_i*cos\alpha_{i-1} & -sin\alpha_{i-1} & -sin\alpha_{i-1}*d_i \\ sin\theta_i*sin\alpha_{i-1} & cos\theta_i*sin\alpha_{i-1} & cos\alpha_{i-1} & cos\alpha_{i-1}*d_i \\ 0 & 0 & 0 & 1 \end{bmatrix} ii1T=cosθisinθicosαi1sinθisinαi10sinθicosθicosαi1cosθisinαi100sinαi1cosαi10ai1sinαi1dicosαi1di1

已知DH参数表,利用DH参数表,以及每次拍摄时舵机旋转的角度得到每次拍摄时的机械臂末端坐标系到基础坐标系的变换矩阵:

1 0 T = [ c o s ( θ 1 − 160 ° ) − s i n ( θ 1 − 160 ° ) 0 0 s i n ( θ 1 − 160 ° ) c o s ( θ 1 − 160 ° ) 0 0 0 0 1 0 0 0 0 1 ] _1^0T = \begin{bmatrix} cos(\theta_1-160°) & -sin(\theta_1-160°) &0 &0 \\ sin(\theta_1-160°) & cos(\theta_1-160°) & 0 & 0 \\ 0 & 0& 1& 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} 10T=cos(θ1160°)sin(θ1160°)00sin(θ1160°)cos(θ1160°)0000100001

2 1 T = [ c o s ( 180 ° − θ 2 ) − s i n ( 180 ° − θ 2 ) 0 0 0 0 − 1 − 37 s i n ( 180 ° − θ 2 ) c o s ( 180 ° − θ 2 ) 0 0 0 0 0 1 ] _2^1T = \begin{bmatrix} cos(180°-\theta_2) & -sin(180°-\theta_2) &0 &0 \\ 0 & 0& -1& -37 \\ sin(180°-\theta_2) & cos(180°-\theta_2) & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} 21T=cos(180°θ2)0sin(180°θ2)0sin(180°θ2)0cos(180°θ2)0010003701

3 2 T = [ c o s ( θ 3 − 180 ° ) − s i n ( θ 3 − 180 ° ) 0 96 s i n ( θ 3 − 180 ° ) c o s ( θ 3 − 180 ° ) 0 0 0 0 1 0 0 0 0 1 ] _3^2T = \begin{bmatrix} cos(\theta_3-180°) & -sin(\theta_3-180°) &0 &96 \\ sin(\theta_3-180°) & cos(\theta_3-180°) & 0 & 0 \\ 0 & 0& 1& 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} 32T=cos(θ3180°)sin(θ3180°)00sin(θ3180°)cos(θ3180°)00001096001

4 3 T = [ c o s ( θ 4 − 180 ° ) − s i n ( θ 4 − 180 ° ) 0 96 s i n ( θ 4 − 180 ° ) c o s ( θ 4 − 180 ° ) 0 0 0 0 1 0 0 0 0 1 ] _4^3T = \begin{bmatrix} cos(\theta_4-180°) & -sin(\theta_4-180°) &0 &96 \\ sin(\theta_4-180°) & cos(\theta_4-180°) & 0 & 0 \\ 0 & 0& 1& 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} 43T=cos(θ4180°)sin(θ4180°)00sin(θ4180°)cos(θ4180°)00001096001

5 4 T = [ 1 0 0 0 0 0 − 1 − 55 0 1 0 0 0 0 0 1 ] _5^4T = \begin{bmatrix} 1 & 0 &0 &0 \\ 0 & 0& -1& -55 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} 54T=10000010010005501

(3)目标坐标系到相机坐标系的变换矩阵的获取

我们将使用opencv-python的方法获得目标坐标系到相机坐标系得变换矩阵,但同时我们需要解决一些相机带来的问题。

在实际应用上,镜头并非理想的透视成像,带有不同程度的畸变。理论上镜头的畸变包括径向畸变和切向畸变,切向畸变影响较小,通常只考虑径向畸变。

径向畸变:径向畸变主要由镜头径向曲率产生(光线在远离透镜中心的地方比靠近中心的地方更加弯曲)。导致真实成像点向内或向外偏离理想成像点。其中畸变像点相对于理想像点沿径向向外偏移,远离中心的,称为枕形畸变;径向畸点相对于理想点沿径向向中心靠拢,称为桶状畸变。

在这里插入图片描述

用数学公式表示:

x ~ K[R|t]X

其中,x为相机中的像素坐标;X为相机坐标系下的三维坐标;K为内参矩阵,包含了焦距和光心坐标;[R|t]为外参矩阵,R和t分别为世界坐标系到相机坐标系的旋和平移。然后我们利用opencv-python中的cameraCalibration方法,将棋盘格上的真实世界坐标与我们假设出来的坐标对应,就可以得到该相机在该焦距下的内参矩阵、外参矩阵,以及相机的畸变系数。

(4)实现

在熟悉原理后,实现上面两个步骤即可得到多组机械臂末端到基础坐标系的变换矩阵以及多组目标坐标系到相机坐标系得变换矩阵。接下来使用python-opencv中的calibrateHandeye()
参数描述如下:

R_gripper2base,t_gripper2base是机械臂抓手相对于机器人基坐标系的旋转矩阵与平移向量,需要通过机器人运动控制器获得相关参数转换计算得到;

R_target2cam, t_target2cam 是标定板相对于双目相机的齐次矩阵,在进行相机标定时可以求取得到;

R_cam2gripper ,t_cam2gripper是求解的手眼矩阵分解得到的旋转矩阵与平移矩阵;

将上述得到的矩阵带入该方法即可得到该机械臂的手眼矩阵。

2.抓取

相机参数和手眼参数标定完成后,就可以进行基于视觉的抓取了。其步骤为:

  • 首先将机械臂末端移动到拍照点(拍照高度需要已知,也就是物体到相机光心的距离),并保证相机垂直向下。
  • 相机拍摄桌面上的目标,识别其中彩色方块的位置。
  • 根据相机内参及拍照高度,将彩色方块中心点的像素坐标转换为相机坐标系下的坐标。
  • 根据手眼矩阵,将相机坐标系下的坐标转换为机械臂坐标系下的坐标。
  • 控制机械臂末端移动到机械臂坐标系下的坐标,抓取方块。

根据上述手眼标定原理中的矩阵关系我们可以得到:

[ X c a m e r a Y c a m e r a Z c a m e r a ] = \begin{bmatrix}X_{camera} & Y_{camera} & Z_{camera} \end{bmatrix}= [XcameraYcameraZcamera]= t a r g e t c a m e r a T ∗ [ X t a r g e t Y t a r g e t Z t a r g e t ] _{target}^{camera}T * \begin{bmatrix}X_{target} & Y_{target} & Z_{target}\end{bmatrix} targetcameraT[XtargetYtargetZtarget]

[ X g r i p p e r Y g r i p p e r Z g r i p p e r ] = \begin{bmatrix}X_{gripper} & Y_{gripper} & Z_{gripper} \end{bmatrix}= [XgripperYgripperZgripper]= c a m e r a g r i p p e r T ∗ [ X c a m e r a Y c a m e r a Z c a m e r a ] _{camera}^{gripper}T * \begin{bmatrix}X_{camera} & Y_{camera} & Z_{camera}\end{bmatrix} cameragripperT[XcameraYcameraZcamera]

[ u v 1 ] = C a m M a t r i x ∗ t a r g e t c a m e r a T ∗ [ X t a r g e t Y t a r g e t Z t a r g e t ] \begin{bmatrix} u&v&1 \end{bmatrix}=CamMatrix*_{target}^{camera}T*\begin{bmatrix}X_{target} & Y_{target} & Z_{target}\end{bmatrix} [uv1]=CamMatrixtargetcameraT[XtargetYtargetZtarget]

用以上的转换关系,可以计算出相机坐标系中的点、物体坐标系中的点、图像像素坐标系中的点在基础坐标系下的点坐标:

[ X b a s e Y b a s e Z b a s e ] = g r i p p e r b a s e T ∗ c a m e r a g r i p p e r T ∗ [ X c a m e r a Y c a m e r a Z c a m e r a ] \begin{bmatrix}X_{base} & Y_{base} & Z_{base} \end{bmatrix}=_{gripper}^{base}T*_{camera}^{gripper}T * \begin{bmatrix}X_{camera} & Y_{camera} & Z_{camera}\end{bmatrix} [XbaseYbaseZbase]=gripperbaseTcameragripperT[XcameraYcameraZcamera]

[ X b a s e Y b a s e Z b a s e ] = g r i p p e r b a s e T ∗ c a m e r a g r i p p e r T ∗ t a r g e t c a m e r a T ∗ [ X t a r g e t Y t a r g e t Z t a r g e t ] \begin{bmatrix}X_{base} & Y_{base} & Z_{base} \end{bmatrix}=_{gripper}^{base}T*_{camera}^{gripper}T*_{target}^{camera}T * \begin{bmatrix}X_{target} & Y_{target} & Z_{target}\end{bmatrix} [XbaseYbaseZbase]=gripperbaseTcameragripperTtargetcameraT[XtargetYtargetZtarget]

[ X b a s e Y b a s e Z b a s e ] = g r i p p e r b a s e T ∗ c a m e r a g r i p p e r T ∗ C a m M a t r i x − 1 ∗ [ u v 1 ] \begin{bmatrix}X_{base} & Y_{base} & Z_{base} \end{bmatrix}=_{gripper}^{base}T*_{camera}^{gripper}T*CamMatrix^{-1}*\begin{bmatrix} u&v&1 \end{bmatrix} [XbaseYbaseZbase]=gripperbaseTcameragripperTCamMatrix1[uv1]

经上述变换即可得到物体在基础坐标系下的坐标。接下来凭借该坐标对机械臂进行运动学逆解,若能达到该点则判断多组解中,哪个解最省时省力,即采用该解,最后再将该物块放置至预定位置即可。

import cv2
import numpy as np
import glob
import math
import serial
import pigpio
import time

serialHandle = serial.Serial("/dev/ttyAMA0", 115200)  #初始化串口, 波特率为115200

command = {"MOVE_WRITE":1, "POS_READ":28, "LOAD_UNLOAD_WRITE": 31}

gripperPose = np.array([[ -47.46614798,-30.93409332,-106.33248484,-2.66670287,0.17899222,1.52106046],
                        [ -50.64555014,-25.69099839,-106.03448307,-2.60620767,0.31446276,1.49742558],
                        [-4.11761269e+01,-3.96028022e+01,-1.05198809e+02,2.69146962e+00,8.27020253e-02,-1.55768099e+00],
                        [ -35.24396468,-44.69683285,-105.73538777,2.62497237,0.25738079,-1.51919588],
                        [-1.35740504e+02,-5.24577701e+01,-1.42120520e+02,2.49769933e+00,1.39643072e-01,-1.79081589e+00],
                        [-1.40319905e+02,-3.85697620e+01,-1.42120520e+02,2.54783819e+00,1.42299609e-02,-1.82676476e+00],
                        [-1.40607129e+02,-3.85729704e+01,-1.41216360e+02,2.54206370e+00,1.41977097e-02,-1.83474341e+00],
                        [-144.89888667,-16.20343465,-141.21636039,-2.4742213,0.1799589,1.78577792],
                        [-2.87355186e+01,-3.54512169e+01,-1.17795839e+02,-2.94734829e+00,8.23268923e-02,.02983728e+00],
                        [ -23.97006612,-40.2412104,-119.7291674,2.85844706,0.17583165,-1.17350036],
                        [ -34.66609758,-31.49890098,-119.7291674,-2.83632764,0.24615135,1.16441951],
                        [ -33.0391174,-33.69010423,-118.9732689,-2.86150817,0.15196856,1.18527549],
                        [-5.87034928e+01,-3.25139467e+01,-1.52581512e+02,-2.84878652e+00,1.11431156e-01,1.24342019e+00],
                        [-1.20653741e+01,-3.61680661e+01,-1.65803499e+02,-3.04942726e+00,1.19279280e-01,6.71576060e-01],
                        [-7.21552695e+01,-4.10918877e+01,-2.20040184e+02,3.12903017e+00,8.74017266e-02,-2.16608054e-01],
                        [-2.71427089e+01,-3.14464549e+01,-2.10189887e+02,-3.10436556e+00,3.74570060e-01,1.46398163e-01],
                        [-1.81869149e+01,-3.72054666e+01,-2.10918478e+02,3.13801503e+00,1.75261644e-02,-1.38106174e-01],
                        [-1.17375606e+02,-2.47069077e+01,-1.87936493e+02,-2.92199001e+00,1.55180620e-01,1.04162328e+00]])

criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

objp = np.zeros((4 * 6, 3), np.float32)
objp[:, :2] = np.mgrid[0:6, 0:4].T.reshape(-1, 2)

objpoints = []  # 3d point in real world space
imgpoints = []  # 2d points in image plane

images = glob.glob('image/*.jpg')

# 命令发送
def servoWriteCmd(id, cmd, par1=None, par2=None):
    buf = bytearray(b'\x55\x55')
    try:
        len = 3  # 若命令是没有参数的话数据长度就是3
        buf1 = bytearray(b'')

        ## 对参数进行处理
        if par1 is not None:
            len += 2  # 数据长度加2
            par1 = 0xffff & par1
            buf1.extend([(0xff & par1), (0xff & (par1 >> 8))])  # 分低8位 高8位 放入缓存
        if par2 is not None:
            len += 2
            par2 = 0xffff & par2
            buf1.extend([(0xff & par2), (0xff & (par2 >> 8))])  # 分低8位 高8位 放入缓存

        buf.extend([(0xff & id), (0xff & len), (0xff & cmd)])  # 追加 id, 数据长度, 命令
        buf.extend(buf1)  # 追加参数

        ##计算校验和
        sum = 0x00
        for b in buf:  # 求和
            sum += b
        sum = sum - 0x55 - 0x55  # 去掉命令开头的两个 0x55
        sum = ~sum  # 取反
        buf.append(0xff & sum)  # 取低8位追加进缓存

        serialHandle.write(buf)  # 发送

    except Exception as e:
        print(e)


# 读取位置
def readPosition(id):
    serialHandle.flushInput()  # 清空接收缓存
    servoWriteCmd(id, command["POS_READ"])  # 发送读取位置命令
    time.sleep(0.00034)  # 小延时,等命令发送完毕。不知道是否能进行这么精确的延时的,但是修改这个值的确实会产生影响。
    # 实验测试调到这个值的时候效果最好
    time.sleep(0.005)  # 稍作延时,等待接收完毕
    count = serialHandle.inWaiting()  # 获取接收缓存中的字节数
    pos = None
    if count != 0:  # 如果接收到的数据不空
        recv_data = serialHandle.read(count)  # 读取接收到的数据
        if count == 8:  # 如果接收到的数据是8个字节(符合位置读取命令的返回数据的长度)
            if recv_data[0] == 0x55 and recv_data[1] == 0x55 and recv_data[4] == 0x1C:
                # 第一第二个字节等于0x55, 第5个字节是0x1C 就是 28 就是 位置读取命令的命令号
                pos = 0xffff & (recv_data[5] | (0xff00 & (recv_data[6] << 8)))  # 将接收到的字节数据拼接成完整的位置数据
                # 上面这小段代码我们简化了操作没有进行和校验,只要帧头和命令对了就认为数据无误

    return pos  # 返回读取到的位置

def init():
    servoWriteCmd(1, 1, 500, 1000)
    servoWriteCmd(2, 1, 750, 1000)
    servoWriteCmd(3, 1, 700, 1000)
    servoWriteCmd(4, 1, 700, 1000)
    servoWriteCmd(5, 1, 500, 1000)
    servoWriteCmd(6, 1, 500, 1000)

def getPosition(frame):
    img = cv2.resize(frame, (720, 480))
    copy = img.copy()

    lower = np.array([0, 96, 92])
    upper = np.array([24, 255, 255])
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, lower, upper)
    result = cv2.bitwise_and(img, img, mask=mask)
    canny = cv2.Canny(result, 80, 180)
    contours, hierarchy = cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    size = copy.shape

    maxPixels = 0
    maxContour = 0
    print(size)
    for i in range(len(contours)):
        single_rect = np.zeros((size[0], size[1]))
        fill_image = cv2.fillConvexPoly(single_rect, contours[i], 255)
        pixels = cv2.countNonZero(fill_image)
        peri = cv2.arcLength(contours[i], True)
        vertices = cv2.approxPolyDP(contours[i], peri*0.02, True)
        corners = len(vertices)
        if pixels > maxPixels and pixels > 500:
            maxPixels = pixels
            maxContour = i

    peri = cv2.arcLength(contours[maxContour], True)
    vertices = cv2.approxPolyDP(contours[maxContour], peri*0.02, True)
    corners = len(vertices)
    x, y, w, h = cv2.boundingRect(vertices)
    cv2.rectangle(copy, (x, y), (x + w, y + h), (0, 255, 0), 4)
    cv2.putText(copy, 'rectangle', (x, y-5), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 1)
    cv2.imshow('result', result)
    cv2.imshow('canny', canny)
    cv2.imshow('img', copy)
    # cv2.waitKey(0)
    position = [x+w/2, y+h/2]
    if maxPixels == 0:
        return None
    return position

def gripper2base(a1, a2, a3, a4):
    t1 = np.array([[math.cos(math.radians(a1 * 0.32 - 160)), -math.sin(math.radians(a1 * 0.32 - 160)), 0, 0],
                  [math.sin(math.radians(a1 * 0.32 - 160)), math.cos(math.radians(a1 * 0.32 - 160)), 0, 0],
                  [0, 0, 1, 0],
                  [0, 0, 0, 1]])

    t2 = np.array([[math.cos(math.radians(180 - a2 * 0.36)), -math.sin(math.radians(180 - a2 * 0.36)), 0, 0],
                  [0, 0, -1, -37],
                  [math.sin(math.radians(180 - a2 * 0.36)), math.cos(math.radians(180 - a2 * 0.36)), 0, 0],
                  [0, 0, 0, 1]])

    t3 = np.array([[math.cos(math.radians(a3 * 0.36 - 180)), -math.sin(math.radians(a3 * 0.36 - 180)), 0, 96],
                  [math.sin(math.radians(a3 * 0.36 - 180)), math.cos(math.radians(a3 * 0.36 - 180)), 0, 0],
                  [0, 0, 1, 0],
                  [0, 0, 0, 1]])
    t4 = np.array([[math.cos(math.radians(a4 * 0.36 - 180)), -math.sin(math.radians(a4 * 0.36 - 180)), 0, 96],
                  [math.sin(math.radians(a4 * 0.36 - 180)), math.cos(math.radians(a4 * 0.36 - 180)), 0, 0],
                  [0, 0, 1, 0],
                  [0, 0, 0, 1]])

    t5 = np.array([[1, 0, 0, 0],
                  [0, 0, -1, -55],
                  [0, 1, 0, 0],
                  [0, 0, 0, 1]])

    t = t1 @ t2 @ t3 @ t4 @ t5
    return t

def getBasePosition(position):
    pos3 = readPosition(3)  # 获取3号舵机的位置
    pos4 = readPosition(4)  # 获取4号舵机的位置
    pos5 = readPosition(5)  # 获取5号舵机的位置
    pos6 = readPosition(6)  # 获取6号舵机的位置
    H = np.zeros(shape=(4, 4), dtype=float)
    H = gripper2base(pos6, pos5, pos4, pos3)
    mtx, R = getEyeHand()
    mtx = mtx.T
    base = np.array([0, 0, 0])
    base = H * R * mtx * [position[0], position[1], 1]
    return base

def grasp(basePosition):
    # 进行运动学逆解,得到多个解,从多个解中找到最适合的解
    # 机械臂各个舵机旋转到指定角度
    # 机械臂末端到达指定位置,并略微调整位置
    # 夹取,抬升物体
    # 移动到稳定点

def place():
    # 第一步:云台转到朝向目标方向
    # 第二步:移到接近点
    # 第三步:移到目标点
    # 第四步:抬升
    # 第五步:放置
    # 第六步:移到稳定点

def getEyeHand():
    R_gripper2base = []
    t_gripper2base = []
    R_target2camera = []
    t_target2camera = []

    for frame in images:
        img = cv2.imread(frame)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

        ret, corners = cv2.findChessboardCorners(gray, (6, 4), None)
        if ret == True:
            objpoints.append(objp)

            corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)  # 在初步提取的角点信息上进一步提取亚像素信息,降低相机标定偏差
            imgpoints.append(corners)

            img = cv2.drawChessboardCorners(img, (6, 4), corners2, ret)

            cv2.imshow("img", img)
            cv2.waitKey(0)

    # mtx:相机内参矩阵
    # dist:畸变系数
    # rvecs:旋转向量
    # tvecs:平移向量
    ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
    # 求外参,对于每张图片外参矩阵都是不一样的,外参矩阵随着刚体位置的变换而变换

    # retval, rvec, tvec  = cv2.solvePnP(np.float32(objpoints), np.float32(imgpoints), mtx, dist)
    a = np.array([0, 0, 0, 1])
    for i in range(len(rvecs)):
        # 先利用solvePnP得到各个R_target2cam和T_target2cam
        # 利用gripper2base得到RT_gripper2base,然后将其分为R_gripper2base和T_gripper2base
        # 最后得到R,T = cv2.calibrateHandEye(R_gripper2base,T_gripper2base,R_target2cam,T_target2cam)
        # RT = np.column_stack((R,T))
        # RT = np.row_stack((RT, np.array([0, 0, 0, 1])))
        # 即可得到手眼标定最后的结果
        R_matrix, _ = cv2.Rodrigues(rvecs[i])
        R_target2camera.append(R_matrix)
        t_target2camera.append(tvecs[i])
        # Rt_matrix = np.concatenate((R_matrix, tvecs[i]), axis=1)
        # Rt_matrix = np.row_stack((Rt_matrix, a))
        # print(Rt_matrix)


    for i in range(len(gripperPose)):
        R_vector = gripperPose[i, 3:6]
        R_matrix, _ = cv2.Rodrigues(R_vector)
        R_gripper2base.append(R_matrix)
        t_gripper2base.append(gripperPose[i, 0:3].T)


    print("内参=", mtx)
    print("畸变系数=", dist)
    print("旋转向量=", rvecs)
    print("平移向量=", tvecs)

    newImage = cv2.imread('image/0.jpg')
    h, w = newImage.shape[:2]
    newCameraMtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
    dst = cv2.undistort(newImage, mtx, dist, None, newCameraMtx)
    x, y, w, h = roi
    dst = dst[y:y + h, x:x + w]

    # 求解重投影误差,越趋近于0越好
    mean_error = 0
    for i in range(len(objpoints)):
        imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
        error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
        mean_error += error
    print("total error: ", mean_error / len(objpoints))

    cv2.imshow('dst', dst)
    cv2.waitKey(0)

    # calibrateHandEye的结果是gripper2camera
    print("基坐标系的旋转矩阵与平移向量求解完毕————————————————")
    print("进入手眼标定函数————————————————————————————————")

    R, T = cv2.calibrateHandEye(R_gripper2base, t_gripper2base, R_target2camera, t_target2camera)#手眼标定

    print("手眼矩阵分解得到的旋转矩阵")
    print(R)
    print("\n")


    print("手眼矩阵分解得到的平移矩阵")
    print(T)

    RT=np.column_stack((R,T))
    RT = np.row_stack((RT, np.array([0, 0, 0, 1])))
    print("\n")
    print('相机相对于末端的变换矩阵为:')
    print(RT)
    return mtx, RT

if __name__=='__main__':
    init()
    cap = cv2.VideoCapture(0)
    while True:
        ret, frame = cap.read()
        if ret:
            position = getPosition(frame)
            if position is not None:
                basePosition = getBasePosition(position)
                print("basePosition", basePosition)
                grasp(basePosition)
                time.sleep(2)
                place()
                time.sleep(2)
            else:
                continue


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

机械臂 手眼标定 手眼矩阵 eye-in-hand 原理、实践及代码 的相关文章

  • bitblt 在 Windows 10 版本 1703 上失败 (15063.138)

    使用 Visual Studio 2017 vc141 以下代码应该从前游戏窗口获取屏幕截图 但现在它返回黑色和空白图像 唯一的游戏问题 尝试过 OpenGL 和 Vulkan ogl 返回黑色 vulkan 返回白色 在升级到 Windo
  • 相机标定(OpenCV 2.3)-如何使用畸变参数?

    我有一组带有一些附加标记的刚体图像 我在这些标记之一中定义了一个原点坐标系 我想获得该坐标系与在相机原点定义的坐标系之间的旋转和平移 我尝试了一段时间 POSIT 以下this http goo gl cUYYt 但从未获得可接受的结果 直
  • 如何设置K-means openCV c++的初始中心

    我正在尝试使用 OpenCv 和 Kmeans 对图像进行分割 我刚刚实现的代码如下 include opencv2 objdetect objdetect hpp include opencv2 highgui highgui hpp i
  • 如何计算图像中的 RGB 或 HSV 通道组合?

    我使用 python opencv 加载形状为 30 100 3 的图像 现在想要按颜色计算所有颜色的频率 我不是指单个通道 而是指通道组合 含义 3 个频道列表 例如 255 0 0 表示红色 255 255 0 表示黄色 100 100
  • 在 QtCreator 中将 OpenCV 2.3 与 Qt 结合使用

    随着 OpenCV 2 3 版本终于发布 我想在我的系统上编译并安装这个最新版本 由于我经常使用 Qt 和 QtCreator 我当然希望能够在我的 Qt 项目中使用它 我已经尝试了几种方法几个小时 但总是出现错误 第一次尝试 使用WITH
  • 曲线/路径骨架二值图像处理

    我正在尝试开发一个可以处理图像骨架的路径 曲线的代码 我想要一个来自两点之间骨架的点向量 该代码在添加一些点后结束 我没有找到解决方案 include opencv2 highgui highgui hpp include opencv2
  • OpenCV 仅围绕大轮廓绘制矩形?

    第一次发帖 希望我以正确的方式放置代码 我正在尝试检测和计算视频中的车辆 因此 如果您查看下面的代码 我会在阈值处理和膨胀后找到图像的轮廓 然后我使用 drawContours 和矩形在检测到的轮廓周围绘制一个框 我试图在 drawCont
  • 多视图几何

    我从相距一定距离的两台相同品牌的相机捕获了两张图像 捕获了相同的场景 我想计算两个相机之间的现实世界旋转和平移 为了实现这一点 我首先提取了两张图像的 SIFT 特征并进行匹配 我现在有基本矩阵也单应性矩阵 然而无法进一步进行 有很多混乱
  • opencv 2.3.* 读取不工作

    我无法让 imread 工作 与这个人有同样的问题 OpenCV imwrite 2 2 在 Windows 7 上导致异常 并显示消息 OpenCV 错误 未指定错误 无法找到指定扩展名的编写器 https stackoverflow c
  • 如何在 OpenCV 中从 YUV 文件读取帧?

    如何在 OpenCV 中从 YUV 文件读取帧 我编写了一个非常简单的 python 代码来从二进制文件读取 YUV NV21 流 import cv2 import numpy as np class VideoCaptureYUV de
  • 检查图像中是否有太薄的区域

    我正在尝试验证雕刻机的黑白图像 更多的是剪贴画图像 不是照片 我需要考虑的主要事情之一是区域的大小 或线条的宽度 因为机器无法处理太细的线条 所以我需要找到比给定阈值更细的区域 以此图为例 竖琴的琴弦可能太细而无法雕刻 我正在阅读有关 Ma
  • 如何使用 python、openCV 计算图像中的行数

    我想数纸张 所以我正在考虑使用线条检测 我尝试过一些方法 例如Canny HoughLines and FLD 但我只得到处理过的照片 我不知道如何计算 有一些小线段就是我们想要的线 我用过len lines or len contours
  • 如何去除给定图像中的噪声,使 ocr 输出完美?

    我已经对这个孟加拉文本图像进行了大津阈值处理 并使用 tesseract 进行 OCR 但输出非常糟糕 我应该应用什么预处理来消除噪音 我也想校正图像 因为它有轻微的倾斜 我的代码如下 import tesserocr from PIL i
  • uri 警告中缺少端口:使用 Python OpenCV cv2.VideoCapture() 打开文件时出错

    当我尝试流式传输 ipcam 时 出现了如下所示的错误 tcp 000000000048c640 uri 中缺少端口 警告 打开文件时出错 build opencv modules videoio src cap ffmpeg impl h
  • 同时从多个流中捕获、最佳方法以及如何减少 CPU 使用率

    我目前正在编写一个应用程序 该应用程序将捕获大量 RTSP 流 在我的例子中为 12 个 并将其显示在 QT 小部件上 当我超过大约 6 7 个流时 问题就会出现 CPU 使用率激增并且出现明显的卡顿 我认为它不是 QT 绘制函数的原因是因
  • Opencv Mat内存管理

    内存管理对于图像类至关重要 在opencv中 图像类是cv Mat 它有一个微妙的内存管理方案 假设我已经有了自己的图像类SelfImage class SelfImage public int width int height unsig
  • 将 4 通道图像转换为 3 通道图像

    我正在使用 OpenCV 2 4 6 我正在尝试将 4 通道 RGB IplImage 转换为 4 通道 HSV 图像 下面是我的代码 给出错误 OpenCV 错误 未知函数断言失败 我认为 cvCvtColor 支持 3 通道图像 有没有
  • 如何使用 opencv.omnidir 模块对鱼眼图像进行去扭曲

    我正在尝试使用全向模块 http docs opencv org trunk db dd2 namespacecv 1 1omnidir html用于对鱼眼图像进行扭曲处理Python 我正在尝试适应这一点C 教程 http docs op
  • 是否可以在 PyScript 中使用 OpenCV 模块?

    我想使用 opencv 模块 但无法导入 OpenCV 那么我该如何解决这个问题呢 顺便说一句 Pyodide 支持 OpenCV 示例代码 https i stack imgur com ahwex jpg 尚不支持 OpenCV 此时O
  • 如何在 cv2.VideoWriter 中使用 FPS 参数?

    好的 所以我正在制作视频 我想确切地知道如何使用 FPS 参数 它是一个浮点数 所以我假设这是我想要的每帧之间的间隔 你能给个例子吗 我只想知道视频会如何随着 FPS 参数值的变化而变化 因为我制作的视频现在太快了 谢谢 确实只是这样 fr

随机推荐

  • Sql Server查询语句

    文章目录 Sql Server查询语句 基础查询 条件查询 模糊查询 Sql Server查询语句 对于Sql Server创建的表中的数据进行查询 可以进行 基础查询 条件查询 模糊查询 基础查询 基础查询语句为 select from
  • 央行数字货币研究所、中国信通院合作立项2项区块链国际标准

    国际电信联盟第十六研究组 简称ITU T SG16 于2021年4月19日至2021年4月30日召开全体会议 来自中国 美国 德国 巴西 西班牙 俄罗斯 瑞士 加拿大 英国 韩国 日本等国家和世界卫生组织等国际组织的百余名代表参加了此次为期
  • spring三级缓存总结

    前言 其实说到spring的三级缓存 也是经常被提到 自己也看过对应的源码 但是 总觉得自己还是没有真正的理解 为什么这样说呢 因为每次看到三级缓存相关的技术问题 自己心里感觉还是迷糊的 不知道为什么要有三级缓存 一级缓存不行吗 二级缓存是
  • unity3d学习笔记-动画(2.控制动画与Animator Controller)

    一 探索Animator Controller动画控制器 每当为选定的游戏对象创建第一个动画剪辑时 就会自动创建动画器组件 Animator负责分配动画 但是 它不控制实际的动画剪辑 这个任务落到了动画控制器身上 它也是用第一个动画剪辑自动
  • $.ajax如何在response中获取请求头

    ajax如何在response中获取请求头 下载文件时需要获取后端带在responseHeader中的文件名称 ajax type get url http xxxx currentuser contentType application
  • Qt操作excel的三方库Qtxlsx在Windows下使用注意事项

    Qt操作excel的三方库Qtxlsx在Windows下使用注意事项 文章目录 Qt操作excel的三方库Qtxlsx在Windows下使用注意事项 1 Qt Xlsx简介 2 编译及添加模块 2 1 下载及编译 2 2 拷贝相关文件集成到
  • WebGL 杂记

    WebGL 2D RotationHow to rotate in 2Dhttps webglfundamentals org webgl lessons webgl 2d rotation html 假设您有一个矩形 并且想要旋转它 在开
  • java 删除文件路径下的指定文件

    起因 租赁项目过期或未按指定机器被套用 检测到违反规定 需要删除数据库并删除重要文件 本文主要解决问题 删除文件夹下的指定文件 解决方案 import java io File public class TestMain public st
  • 水星迷你无线路由器ap模式 下要不要启用 dhcp服务器,水星(Mercury)Mini无线路由器AP模式设置...

    本文介绍了水星 MERCURY Mini系列无线路由器AP模式的设置方法 水星迷你路由器实现了即插即用 非常适合出差或者在旅行途中入住酒店时使用 直接把酒店房间里面的网线插在水星迷你无线路由器上 就可以正常使用了 下面以水星MW150RM迷
  • 编程 打油诗_CNC操机10年老员工献给大家的一首心酸打油诗

    操机苦 倒班累 操机倒班活受罪 半夜起 早晨睡 好比参加革命游击队 整日在思 彻夜不能寐 工作不顺奖金全作废 煎熬一夜 下班打瞌睡 吃个早餐凌晨还得排队 洗漱列队 厕所抢位 回到宿舍心神疲惫 请假难请 扣钱双倍 每天都在请假排队 数控操机何
  • Android之使用本地缓存数据

    前言 在通常做项目的时候 需要存储数据 会使用GreenDAO数据库 bmob后端云 或者其他方法 以及本篇文章所讲解的本地缓存 也就是通过SharedPreferences 来进行缓存 第一部分 1 那么首先呢需要创建一个缓存数据的类Ca
  • python编程中的注意事项

    虚拟环境 win下的虚拟环境创建 virtualenv name python 3 7 9 conda创建虚拟环境 conda create prefix home coData venv python 3 8 conda环境恢复 起因 c
  • 我的128创作纪念日

    机缘 写CSDN博客的时候 应该纯属一个巧合 还记得当初是和一个班上的同学一起记录学习笔记 最初是在博客园的平台上记录笔记 可以在以后复习时使用 后来我的同学开始推荐使用CSDN平台 于是我们两就转战CSDN啦 不知不觉 从3月份到现在也有
  • PyQt中的多线程QThread示例

    PyQt中的多线程 一 PyQt中的多线程 二 创建线程 2 1 设计ui界面 2 2 设计工作线程 2 3 主程序设计 三 运行结果示例 一 PyQt中的多线程 传统的图形用户界面应用程序都只有一个执行线程 并且一次只执行一个操作 如果用
  • Vscode:常用插件 & 快捷键 & 配置等

    一 常用插件 1 git相关的 1 在代码中显示git提交日志 包含Git账号 commit信息 给Vscode添加Git功能 GitLens Git supercharged 2 显示图形化的瀑布时间线Git记录 可基于某个分支创建新分支
  • w10计算机怎么恢复出厂设置路由器,技术编辑为你解决win10系统打不开192.168.1.1设置界面的还原步骤...

    很多人都懂一些简单的电脑系统问题的解决方案 但是win10系统打不开192 168 1 1设置界面的情况 想必大家都遇到过win10系统打不开192 168 1 1设置界面的情况吧 那么应该怎么处理win10系统打不开192 168 1 1
  • 【SQL】5 SQL SELECT DISTINCT 语句

    SELECT DISTINCT 语句用于返回唯一不同的值 SQL SELECT DISTINCT 语句 在表中 一个列可能会包含多个重复值 有时您也许希望仅仅列出不同 distinct 的值 DISTINCT 关键词用于返回唯一不同的值 S
  • Vue 中如何实现监测数组变化

    vue中响应式数据的原理是通过Object defineProperty控制getter和setter 并利用观察者模式完成的响应式设计 数组考虑性能原因没有用defineProperty对数组的每一项进行拦截 而是选择重写数组api方法
  • git提交忽略文件名称大小写问题解决

    在项目开发中 关于文件名称大小写的修改 项目可能会默认忽略 对于一开始就新建的文件名问题不大 1 git在提交代码时 会忽略文件名称大小写 导致本地代码与远程代码不一致 此时可利用终端指令来检查下 git config get core i
  • 机械臂 手眼标定 手眼矩阵 eye-in-hand 原理、实践及代码

    1 手眼标定 所谓手眼系统 就是人眼睛看到一个东西的时候要让手去抓取 就需要大脑知道眼睛和手的坐标关系 而相机知道的是像素坐标 机械手是空间坐标系 所以手眼标定就是得到像素坐标系和空间机械手坐标系的坐标转化关系 目前工业上通常使用两种方法进