经过一周的研究,对六自由度机械臂运动进行了研究,利用高中几何知识进行了运动控制策略的设计,无偿贡献出源码,可以为入门的小伙伴提供一定的借鉴。

1、机械臂物理参数的介绍

        买了一个六轴机械臂,作为研究对象,如果是其它机械臂,可以根据机械臂参数对代码进行修改。机械臂参数如下图所示:

python控制机械臂运动 python控制机械臂6轴_算法

1.1  部件与尺寸

        机械臂如下部件组成,描述如下表:

序号

名称

描述

在python中的定义

1

骨长1

机械臂原点至第1弯头中心,距离170.48mm

类名:Mini_Arm,属性名:骨长1

2

骨长2

第1弯头至第4弯头中心,距离138.35mm

类名:Mini_Arm,属性名:骨长2

3

骨长3

第4弯头至第5弯头中心,距离100mm

类名:Mini_Arm,属性名:骨长3

4

骨长4

第5弯头至第7弯头中心,距离85mm

类名:Mini_Arm,属性名:骨长4

5

小骨

第(1至2)、(3至4)、(5至6)弯头距离的统称,距离80mm

类名:Mini_Arm,属性名:小骨

6

末骨

J5转轴中心至机械臂末端的距离,62.40mm

类名:Mini_Arm,属性名:末骨

7

J1

第1个旋转电机角度,范围[-165°,165°]

类名:Mini_Arm,属性名:J[0]

8

J2

第2个旋转电机角度,范围[-90°,90°]

类名:Mini_Arm,属性名:J[1]

9

J3

第3个旋转电机角度,范围[-120°,120°]

类名:Mini_Arm,属性名:J[2]

10

J4

第4个旋转电机角度,范围[-120°,120°]

类名:Mini_Arm,属性名:J[3]

11

J5

第5个旋转电机角度,范围[-180°,180°]

类名:Mini_Arm,属性名:J[4]

12

J6

第6个旋转电机角度,范围[-180°,180°]

类名:Mini_Arm,属性名:J[5]

1.2坐标系与工作空间

        机械臂坐标系为数学直角正交坐标系,每个电机绕着旋转轴顺时针、逆时针旋转(正负已经标明),方向如图1所示,机械臂通过6个电机的旋转运动,实现了一系列复杂的动作。

        注意,由于机械结构和设计的原因,有的电机不能进行-180°至180°的全向转动,会有一个转动的范围(见上表),这部分没有在python中实现。

2、机械臂运动控制原理

        机械臂运动控制要解决的核心问题是,知道物体的位置(中心点坐标)和主方向(物体的朝向),程序自动计算出机械臂的各关节角度位置,将夹具准确送到抓取物体的部位,并对准抓取位置。

2.1机械臂运动的几何简化

        总体思路是将机械臂的三维复杂运动简化为两个平面的运动,即“俯视平面”,和“垂直平面(本文叫R平面)”的组合运动。如下两个图:

python控制机械臂运动 python控制机械臂6轴_机械臂_02

       

python控制机械臂运动 python控制机械臂6轴_机器人_03

 2.2机械臂抓手即后面末关节的处理

        普通的机械臂的控制以上就可以,由于我手头这个大象机械臂比较特殊,有很多90度弯头,而且抓手安装后,他的落点与上面简化模型就出现了偏差,因此,还需要根据这个特点,根据实际的物体坐标和主方向,计算出上节的理想抓手坐标点(x,y),然后就能求出全部关节的角度了。

python控制机械臂运动 python控制机械臂6轴_机械臂_04

3、python源码

以下是源代码,可以直接运行,

按键盘"O"键,进入自动鼠标点击右侧图机械臂工作半径范围的地方,自动计算各关节的旋转坐标。

再按键盘"O"键,退出鼠标点击计算模式,改用监盘手动操作模式,按“[”或 “]”,是J1关节旋转,按“w"是扩大作业半径,按"s"是缩小作业半径。

运行效果如下:

python控制机械臂运动 python控制机械臂6轴_python控制机械臂运动_05

# -*- coding: utf-8 -*-
"""
Created on Sat Jul 31 11:29:37 2021

@author: JAMES费
"""

import matplotlib.animation as animation
import matplotlib.pyplot as plt
import sys
import logging
import time
import numpy as np
import math
import cmath
import cv2
logging.basicConfig(level=logging.DEBUG)


class Mini_Arm:
    """
    定义机械臂的长度,单位mm
    工作空间
    坐标计算
    """
    exval={
            "com":"COM21",
            "baud":115200,
            "port":6666,
            "host":"localhost"
            }    
    #以下定义机械臂的物理参数,对照手册配图
    骨长1=170.46
    骨长2=136.35
    骨长3=100
    骨长4=75
    小骨长=80
    末骨长=62.4
    抓手长=60
    J=[0,0,0,0,0,0] #表示关节角度,J1,J2,J3,J4,J5,J6
    抓手坐标=(0,0)
    工作半径范围=(50,骨长2+骨长3-5)
    R=0
    mycobot=None
    当前坐标=[0,0,0,0,0,0]
    当前角度=[0,0,0,0,0]
    winevent={
            "doubleclick":0,
            "keymod":0
            }
    winH=900
    winW=1600
    pz2=(int(winW*3/4),int(winH/3))
    p0=(None,None)
    thi=0
    pJ5=(None,None)
    
    
    def __init__(self, **kwargs):
        
        """
        初始化类:
                串口:com="COM21"
                波特率:baud=115200
                web端口:port=6666
                主机地址:host="localhost"
        """
        self.mathods=dir(self) 
        
        for k,v in kwargs.items():
            if k in self.exval.keys():                
                self.exval[k]=v
        
        #self.mycobot = MyCobot(self.exval["com"])
        cv2.namedWindow('mini_arm')
        cv2.setMouseCallback('mini_arm',self.mousecallback)
        #print(self.exval["com"])
    

    #监听点击像素坐标
    def mousecallback(self,event,x,y,flags,param):
         if event==cv2.EVENT_LBUTTONDBLCLK:             
             if self.winevent["keymod"]==1:
                 self.根据窗口点击计算目标点(x,y)
                 print("点击坐标点",(x,y))
                 
                 

    def 根据窗口点击计算目标点(self,x,y):
        #转化为机械臂坐标
        (px,py)=self.pz2
       
        #r=np.sqrt((x-px)**2+(py-y)**2) 
        (x0,y0)=(x-px,py-y)
        self.thi=30
        res,J1,J5,R=self.计算J1_5_R(x0,y0,self.thi) 
        if res!=True:
            print("out of workspace")
            self.p0=(None,None)
        else:   
            self.p0=(x0,y0)            
            self.J[0],self.J[4],self.R=J1,J5,R
            print("计算结果J1,J5,R:",[self.J[0],self.J[4],self.R])
            print("目标点坐标",self.p0)

        #计算其它坐标
        
        return 1
    
    
    def 计算J1和R(self):
        """
        #已知抓手坐标,求J1单位/°,R距离/mm
        """        
        try:
             (x,y)=self.抓手坐标
             J1=np.arctan(y/x)/np.pi*180
             self.R=x/np.cos(J1)
             self.J[0]=J1
             return 1
        except Exception as e:
             logging.error("计算J1异常:",e)    
    

    def 计算抓手坐标(self,J1,R): 
        """
        #已知J1单位/°,R距离/mm,求抓手坐标
        """
        try:
             (x,y)=(0,0)
             x=R*np.cos(J1/180*np.pi)
             y=R*np.sin(J1/180*np.pi)
             return (round(x, 2),round(y, 2))
        except Exception as e:
             logging.error("计算J1异常:",e)   
        
    
    def 计算J1(self,x,y):  
        """
        #已知抓手坐标,求J1单位/°,R距离/mm
        """
        try:
             R=np.sqrt(x**2+y**2)             
             J1=math.degrees(cmath.polar(complex(x,y))[1])
             return round(R, 2),round(J1, 2)
        except Exception as e:
             logging.error("计算J1异常:",e)  


    def 判断象限(self,x,y):
        """
        根据直角坐标(x,y),判断在第几象限
        返回值
        1:第一象限
        2:第二象限
        3:第三象限
        4:第四象限
        10:在x正半轴
        -10:在x负半轴
        20:在y正半轴
        -20:在y负半轴
        0:在坐标原点
        """
        if x>0 and y>0:
            return 1
        elif x<0 and y>0:
            return 2
        elif x<0 and y<0:
            return 3
        elif x>0 and y<0:
            return 4
        elif x>0 and y==0:
            return 10
        elif x<0 and y==0:
            return -10
        elif x==0 and y>0:
            return 20
        elif x==0 and y<0:
            return -20
        else:
            return 0
            
    def 计算J5坐标(self,x0,y0,thi):
        """
        已知物体的坐标,主方向,计算J1、J6、R
        x0,y0为物体中心点坐标
        thi为物体主方向与机械臂x轴的逆时针夹角°
        """
        g6=self.末骨长
        g0=self.小骨长
        try:        
            x5=x0+g6*np.cos(math.radians(thi))
            x5i=x0+g6*np.cos(math.radians(180+thi))
            y5=y0+g6*np.sin(math.radians(thi))
            y5i=y0+g6*np.sin(math.radians(180+thi))
            r=np.sqrt(x5**2+y5**2)
            ri=np.sqrt(x5i**2+y5i**2)  
            #print("x5,y5,x5i,y5i,r,ri",x5,y5,x5i,y5i,r,ri)
                        
            if r<=ri and g0<r:
                return (x5,y5)
            elif r<=ri and g0<ri and g0>r:
                return (x5i,y5i)
            elif r>ri and g0<ri:
                return (x5i,y5i)
            elif r>ri and g0<r and g0>ri:
                return (x5,y5)
            else:
                self.p0=(None,None)
                logging.error("cannot 计算 J5,woring 目标值")
                return False
                
        except Exception as e:
             logging.error("计算计算J5坐标异常:",e)        

    def 计算三点逆时针角度(self,p1,p0,p2):
        """
        计算向量P0-->p1至 P0-->p2的逆时针转角
        math.degrees(x)弧度转换为角度
        math.radians(x)角度转弧度
        cn = complex(3,4)
        cmath.polar(cn)  #返回长度和弧度
        cn1 = cmath.rect(2, cmath.pi)极坐标转直直角坐标
        cn1.real,cn1.imag#返回x,y
        """
 
        try:     
            (x0,y0)=p0
            (x1,y1)=p1
            (x2,y2)=p2 
            
            #向量P0-->p1的极坐标转角
            x=x1-x0
            y=y1-y0
            alpha1=math.degrees(cmath.polar(complex(x,y))[1])
            #向量P0-->p2的极坐标转角
            x=x2-x0
            y=y2-y0
            alpha2=math.degrees(cmath.polar(complex(x,y))[1])    
            
            alpha=alpha2-alpha1
            return round(alpha, 2)  
        except Exception as e:
             logging.error("计算计算J5坐标异常:",e)   

        

    
    def 计算J1_5_R(self,x0,y0,thi): 
        """
        已知物体的坐标,主方向,计算J1、J5、R
        x0,y0为物体中心点坐标
        thi为物体主方向与机械臂x轴的逆时针夹角°
        """
        g0=self.小骨长
        (minr,maxr)=self.工作半径范围
        try:
                            
            pJ5=self.计算J5坐标(x0,y0,thi)
            if pJ5!=0:
                               
                (x5,y5)=pJ5
                self.pJ5=pJ5
                #print("Pj5:",pJ5)
                cr=np.sqrt(x5**2+y5**2)
                i1=math.degrees(cmath.polar(complex(x5,y5))[1])                
                R=np.sqrt(cr**2-g0**2)
                i2=math.degrees(np.arcsin(g0/cr))
                J1=i1+i2
                p0=(x0,y0)#目标点中心坐标
                pm=self.计算抓手坐标(J1,R)#假想目标点中心坐标
                J5=self.计算三点逆时针角度(p0,pJ5,pm)
                if R<minr or R>maxr:
                    logging.error("超出工作范围")
                    return False,self.J[0],self.J[4],self.R
                else:
                    return True,round(J1, 2),round(J5, 2),round(R, 2)
            else:
                logging.error("计算J1_5_R异常")
                return False,self.J[0],self.J[4],self.R
        except Exception as e:
            logging.error("计算计算J1_5_R异常:",e)          
    
    def 计算J2_4(self):
        """
        已知
            g1=self.骨长1
            g2=self.骨长2
            g3=self.骨长3
            g4=self.骨长4
            g5=g4+self.抓手长  
            r=self.R
        求关节,J2、J3、J4
        """
        try:
            g1=self.骨长1
            g2=self.骨长2
            g3=self.骨长3
            g4=self.骨长4
            g5=g4+self.抓手长  
            r=self.R
            
            lr=np.sqrt(r**2+(g5-g1)**2)
            Ji=np.arcsin(r/lr)/np.pi*180;
            i2=np.arccos((lr**2+g2**2-g3**2)/(2*lr*g2))/np.pi*180
            i3=np.arccos((g3**2+g2**2-lr**2)/(2*g3*g2))/np.pi*180
            i4=np.arccos((lr**2+g3**2-g2**2)/(2*lr*g3))/np.pi*180
            J2=Ji-i2
            J3=180-i3
            J4=180-i4-Ji
            self.J[1]=J2
            self.J[2]=J3
            self.J[3]=J4    
            #print("计算结果",lr,Ji,i2,i3,i4,J2,J3,J4)
            return 1
        except Exception as e:
             logging.error("计算J2_4异常:",e)    
    def 计算各关节点坐标(self):
        """
        已知关节旋转角度,求在R平面的关节坐标
        """
        g1=self.骨长1
        g2=self.骨长2
        g3=self.骨长3
        g4=self.骨长4
        g5=g4+self.抓手长  
        r=self.R
        J2=self.J[1]
        
        try:
            
            x=[0,0,0,0,0] 
            y=[0,0,0,0,0]
            x[1]=0#关节J2的x坐标
            y[1]=g1#关节J2的y坐标
            x[2]=g2*np.cos((90+J2)/180*np.pi)
            y[2]=g1+g2*np.sin((90+J2)/180*np.pi)
            x[3]=-r
            y[3]=g5 
            x[4]=-r
            y[4]=0
            return (x,y)
        except Exception as e:
             logging.error("计算各点坐标异常:",e) 
             
    def ui(self,H,W):
            #text format
        org = (40, 80)
        fontFace = cv2.FONT_HERSHEY_COMPLEX
        fontScale = 0.5
        fontcolor = (0, 255, 0) # BGR
        thickness = 1 
        lineType = 4
        bottomLeftOrigin = 1
        
        while 1: 
            #背景颜色               高,宽
            self.winH=H
            self.winW=W
            background = np.zeros((H, W, 3), np.uint8) #生成一个空灰度图像        
            
            key = cv2.waitKey(1)
            if int(key) == ord('q'):
                cv2.destroyWindow('mini_arm')
                break
            if self.winevent["keymod"]==0:
                if int(key) == ord('['):
                    self.J[0]+=2
                    if self.J[0]>=160:
                        self.J[0]=160
                if int(key) == ord(']'):
                    self.J[0]-=2
                    if self.J[0]<=-160:
                        self.J[0]=-160
                if int(key) == ord('w'):
                    self.R+=2
                    if self.R>=self.工作半径范围[1]:
                        self.R=self.工作半径范围[1]
                if int(key) == ord('s'):
                    self.R-=2
                    if self.R<=self.工作半径范围[0]:
                        self.R=self.工作半径范围[0]
                if int(key)==ord('z'):
                    self.R=200
                    self.J[0]=0
            else:
                if int(key) == ord('['):
                    self.thi+=2
                    if self.thi>=180:
                        self.thi=180
                    res,J1,J5,R=self.计算J1_5_R(self.p0[0],self.p0[1],self.thi) 
                    if res:
                        self.J[0],self.J[4],self.R=J1,J5,R
      
                    
                if int(key) == ord(']'):
                    self.thi-=2
                    if self.thi<=-180:
                        self.thi=-180
                    res,J1,J5,R=self.计算J1_5_R(self.p0[0],self.p0[1],self.thi) 
                    if res:
                        self.J[0],self.J[4],self.R=J1,J5,R 

            
            if int(key)==ord('o'):
                if self.winevent["keymod"]==0:
                    self.winevent["keymod"]=1
                else:
                    self.winevent["keymod"]=0            
            #update(a,axes) 
            
            
            if self.winevent["keymod"]==1:
                text = "window selected mod press o to esc"
                (px,py)=self.pz2
                if self.p0 !=(None,None) and self.pJ5!=(None,None):
                    p=self.计算抓手坐标(self.J[0],self.R)
                    px0=px+int(p[0])
                    py0=py-int(p[1])  
                    cv2.circle(background,(int(self.p0[0]+px),int(py-self.p0[1])),5,(255,255,255),-1) 
                    cv2.circle(background,(int(self.pJ5[0]+px),int(py-self.pJ5[1])),5,(255,255,255),-1) 
                    cv2.line(background,(px0,py0),(int(self.pJ5[0]+px),int(py-self.pJ5[1])),(255,0,255),4,cv2.LINE_AA)
                    
                
            else:
                text = "keyboard manhand control mod " 
                tips="press [/] turn J1,w/s to front / back"
                cv2.putText(background, tips, (W-400, 30), fontFace, fontScale, fontcolor, thickness, lineType)       
            cv2.putText(background, text, (W-400, 10), fontFace, fontScale, fontcolor, thickness, lineType)        
                

            #######绘制左边边的坐标图
            pz1=(int(W*1.5/4),int(H/2))#左侧坐标的原点
            
            if self.计算J2_4():
                point=self.计算各关节点坐标()
                x=point[0]
                y=point[1]
                ##画关节点
                num=0
                bones=[]           
                for dx,dy,j in zip(x,y,self.J):
                    tx=pz1[0]+int(dx)
                    ty=pz1[1]-int(dy) 
                    bones.append((tx,ty))
                    cv2.circle(background,(tx,ty),5,(255,255,0),-1) 
                            #画标注
                    text = str((round(dx, 2),round(dy, 2)))+"J:"+str(round(j, 2))+"'"
                    #text = 'angles:little[ddd'+str(888)
                    cv2.putText(background, text, (tx+8, ty-8), fontFace, fontScale, fontcolor, thickness, lineType)
                    
                   
                ##画骨架
                for i in range(4):                
                    cv2.line(background,bones[i],bones[i+1],(255,0,255),4,cv2.LINE_AA)
                    
    
            
            #######绘制右边的坐标图
            
            pz2=(int(W*3/4),int(H/3))#旋转坐标的原点
            self.pz2=pz2
            #计算抓手坐标及转换成图像坐标        
            p=self.计算抓手坐标(self.J[0],self.R)
            px=pz2[0]+int(p[0])
            py=pz2[1]-int(p[1])       
            
            ###画半径
            cv2.circle(background,pz2,int(self.R),(0,0,255),2)  #画圆

            
            #画抓手坐标点
            cv2.circle(background,(px,py),5,(0,255,255),-1)  #画圆

            #画标注
            text = str(p)+"J1:"+str(self.J[0])
            #text = 'angles:little[ddd'+str(888)
            cv2.putText(background, text, (px+8, py-8), fontFace, fontScale, fontcolor, thickness, lineType)
            
            cv2.line(background,pz2,(px,py),(0,255,255),2,cv2.LINE_AA)
            
            #计算边界线
            p=self.计算抓手坐标(160,self.R)
            px=pz2[0]+int(p[0])
            py=pz2[1]-int(p[1]) 
            pyy=pz2[1]+int(p[1]) 
            cv2.line(background,pz2,(px,py),(0,0,255),4,cv2.LINE_AA)
            cv2.line(background,pz2,(px,pyy),(0,0,255),4,cv2.LINE_AA)
            
     
           
    
            cv2.imshow('mini_arm',background)
        

    
if __name__ == '__main__':
    a=Mini_Arm()    
    a.R=200
    a.J[0]=30
    print("正解抓手坐标为:",a.计算抓手坐标(a.J[0],a.R))
    
    x=173.20
    y=100
    print("正解抓手J1为:",a.计算J1(x,y))

    a.ui(900,1600)

以上是几何实现,那么如果采用更通用的数值逆解,应该如何实现呢,新开辟一个新专栏——机器人学基础——python算法_JAMES费的博客-CSDN博客,打算继续用python这个工具,把机械臂这件事情研究得更透彻一些。

可以继续移步相关文章:N轴机械臂的MDH正向建模,及python算法_JAMES费的博客-CSDN博客,

python控制机械臂运动 python控制机械臂6轴_python_06