(inverse)-kinematics

Thanks! Yes, I’m using Bluetooth + Python. I made a lot of hardware and software modifications. I will make more posts about it soon.

Michal, that’s some awesome work. I see that you’ve implemented body rotations as well as lateral translations with your fixed feet code. Excellent.

How did you choose the location for the feet to maximise the range of motion of Hexy’s body?

Hardware and software changes? Could you elaborate? I presume that the software changes were exclusively in the Python code as I can see the legs are moving through a series of fixed points, rather than a smooth motion through a path? What hardware changes were needed to get to this state?

Finally, can we have a go? Source code? :smiley:

Here is how to calculate inverse kinematics for Hexy. Inverse kinematics calculates required servo angles from desired body and leg position/orientation.
For the calculations, I’m using numpy library

import numpy as np

You start with defining your body:

Hlength = 26
Tlength = 49
Flength = 55
bodyRadius = 100.0
bodyAngle = 50.0/180*np.pi
bodyAngles = [np.pi/2 - bodyAngle, np.pi/2, np.pi/2 + bodyAngle,
              3*np.pi/2 - bodyAngle, 3*np.pi/2, 3*np.pi/2 + bodyAngle]

d = bodyRadius  
body = np.array([[d*np.cos(bodyAngles[0]),d*np.sin(bodyAngles[0]),0],
                         [d*np.cos(bodyAngles[1]),d*np.sin(bodyAngles[1]),0],
                         [d*np.cos(bodyAngles[2]),d*np.sin(bodyAngles[2]),0],
                         [d*np.cos(bodyAngles[3]),d*np.sin(bodyAngles[3]),0],
                         [d*np.cos(bodyAngles[4]),d*np.sin(bodyAngles[4]),0],
                         [d*np.cos(bodyAngles[5]),d*np.sin(bodyAngles[5]),0]])

Then you perform rotation and translation of the body to desired new coordinates. For that you need rotation matrix for X,Y,Z rotations. You can do the rotations separately or combine them in one big matrix as I did:
Remark: It DOES mater in which order you perform rotational transformation (matrix multiplication). For info see Wiki: http://en.wikipedia.org/wiki/Rotation_matrix

def RotXYZ(point,a):
        Rxyz = [[np.cos(a[2])*np.cos(a[1]), np.cos(a[2])*np.sin(a[1])*np.sin(a[0])-np.sin(a[2])*np.cos(a[0]), np.cos(a[2])*np.sin(a[1])*np.cos(a[0])+np.sin(a[2])*np.sin(a[0])],
                [np.sin(a[2])*np.cos(a[1]), np.sin(a[2])*np.sin(a[1])*np.sin(a[0])+np.cos(a[2])*np.cos(a[0]), np.sin(a[2])*np.sin(a[1])*np.cos(a[0])-np.cos(a[2])*np.sin(a[0])],
                [-np.sin(a[1]), np.cos(a[1])*np.sin(a[0]), np.cos(a[1])*np.cos(a[0])]]
        return np.dot(Rxyz,point)

And you perform Rotation and Translation of the body:

Tr = [0, 0, 75]
Rot = [-0.2*np.pi/6, 0.2*np.pi/6, -0.1*np.pi/2]

b = Tr + RotXYZ(body.T, Rot).T   #  ".T" means transpose - due to my definition of X,Y,Z coordinates in columns instead of rows

Now I make a small trick to obtain an unique solution for hip angles. I need to create a second set of body points which are just slightly rotated in Rz at first.

def RotZ(pos,angle):
        Rz = [[np.cos(angle),-np.sin(angle),0],
                [np.sin(angle),np.cos(angle),0],
                [0,0,1]]
        return np.dot(Rz,pos)

b2 = RotZ(body.T, 0.001).T
b2 = Tr + RotXYZ(b2.T, Rot).T

Remark: For clarity, the figure shows the Rz rotation 100x amplified

Now you define/obtain your desired/previous foot position:

f = [-50, -150, 0]

Next step is to calculate projection P of the foot point F to the plane defined by the body B. For that you need to calculate normal vector of the B-plane. For details see Wiki: http://en.wikipedia.org/wiki/Plane_(geometry) and then project the point to the plane, see e.g. http://stackoverflow.com/questions/8942950/how-do-i-find-the-orthogonal-projection-of-a-point-onto-a-plane

v0 = b[1] - b[0]  # 2 vectors from any 3 points defining the plane B
v1 = b[2] - b[0]
normal = np.cross(v0, v1)
normal /= np.linalg.norm(normal)
p = f - np.dot(normal, f - b[4])*normal

Now you can calculate hip angle. For that you utilize the second set of body points from which we make a vector: B-B*. This vector is perpendicular to the hip servo zero angle. The second vector is from the projected foot point to to the body point: P-B. Now we calculate angle between those two vectors (see Wiki: http://en.wikipedia.org/wiki/Dot_product):
Remark: You might ask why I didn’t use the center body point instead of this cumbersome B*. You must realize that solution for an angle between two vectors is always in range of 0 to 180 degrees. For zero hip servo angle the solution would be exactly 180 deg and any other angle would always be smaller than 180 deg. However, you would not knot to which side to turn, because the sign is always positive. In my calculation, the solution gives 90 degree when hip servo angle is 0 and if you offset these 90 degrees you get the needed servo angle.

h0 = p - b[4]
h1 = b1[4] - b[4]
Hangle = (np.arccos(np.dot(h0,h1)/np.linalg.norm(h0)/np.linalg.norm(h1))) - np.pi/2 

Next step is to calculate knee servo position from know hip length (Hlength) and normalized vector h0
= distance Hlength from B[4] in direction h0

k = b[4] + Hlength*h0/np.linalg.norm(h0)

Now you need to calculate two lengths of K-F and B-F:

kf = np.linalg.norm(k - f)
bf = np.linalg.norm(b[4] - f)

The ankle and knee angles are then calculated using cosine law (see wiki: http://en.wikipedia.org/wiki/Cosine_law):
cos(gamma) = (a^2+b^2-c^2)/(2ab).
Ankle angle by using thigh and foot lengths (Tlength, Flength) and K-F length.
And knee angle as combination of 2 angles. And of course you have to offset them to obtain servo angles.

Aangle = np.pi - np.arccos((Tlength**2 + Flength**2 - kf**2)/(2*Tlength*Flength))
Kangle = np.pi - np.arccos((Hlength**2 + kf**2 - bf**2)/(2*Hlength*kf)) + np.arccos((Tlength**2 + kf**2 - Flength**2)/(2*Tlength*kf))

And so you get the solution:

Michal. Wow. That’s such an informative post!

I look forward to digesting this information in some detail this weekend. Love the graphs. Thanks!

Here is complete class I made for controlling the robot by inverse kimenatics. You can append this at the end of robot.py

EDIT:

  • speed optimization (about 3x faster now), but still uses numpy for several operations. Due to speed reasons.
  • uses binary serial communication (see here).
import numpy as np

#################################################################################################
# Inverse-kinematic robot Hexy
# by Michal G., 01-07-2013
# - speed optimization (about 3x faster now)
#  - it uses binary serial communication for data transfer, see http://forum.arcbotics.com/viewtopic.php?f=21&t=481#p2979
#################################################################################################
class kinematicRobot():
    def __init__(self, con, Dict):
        self.con = con
        self.Dict = self.reverseDict(Dict)
        self.bodyRadius = 100
        self.hip = 26
        self.thigh = 49
        self.foot = 55
        self.bodyAngle = 50.0/180*np.pi
        self.bodyAngles = [np.pi/2 - self.bodyAngle, np.pi/2, np.pi/2 + self.bodyAngle,
                          3*np.pi/2 - self.bodyAngle, 3*np.pi/2, 3*np.pi/2 + self.bodyAngle]

        self.CycleStep = 0  # counter for moving legs sychnronization 
        self.MovingLegs = 3 # how many legs are moving at once
        self.FootPos = np.zeros((6,3))  # X,Y,Z position of all feet
        self.footDist = 160 # nominal distance of foot from body center. Empirical value
        for i in range(6):  # reset feet position
            self.FootPos[i] = [self.footDist*np.cos(self.bodyAngles[i]),self.footDist*np.sin(self.bodyAngles[i]),0]

        d = self.bodyRadius
        self.body = np.array([[d*math.cos(self.bodyAngles[0]),d*math.sin(self.bodyAngles[0]),0],
                              [d*math.cos(self.bodyAngles[1]),d*math.sin(self.bodyAngles[1]),0],
                              [d*math.cos(self.bodyAngles[2]),d*math.sin(self.bodyAngles[2]),0],
                              [d*math.cos(self.bodyAngles[3]),d*math.sin(self.bodyAngles[3]),0],
                              [d*math.cos(self.bodyAngles[4]),d*math.sin(self.bodyAngles[4]),0],
                              [d*math.cos(self.bodyAngles[5]),d*math.sin(self.bodyAngles[5]),0]]).T
        self.body2 = self.RotZ(self.body,0.001)

        if legsMirrored:
            self.sign = [1,1,1,-1,-1,-1]
        else:
            self.sign = [1,1,1,1,1,1]

    # internal - reverse servo dictionary
    def reverseDict(self,Dict):
        outDict = [] 
        for i in range(9):    # left side from front to back
            outDict.append(Dict[i][1])

        for i in range(3):     # right side from back to front 
            outDict.append(Dict[15-3*i][1])
            outDict.append(Dict[16-3*i][1])
            outDict.append(Dict[17-3*i][1])
            
        outDict.append(Dict[18][1])  # head   
        return outDict

    # internal - Rotation X,Y,Z
    def RotXYZ(self,pos,a):
        Rxyz = [[math.cos(a[2])*math.cos(a[1]), math.cos(a[2])*math.sin(a[1])*math.sin(a[0])-math.sin(a[2])*math.cos(a[0]), math.cos(a[2])*math.sin(a[1])*math.cos(a[0])+math.sin(a[2])*math.sin(a[0])],
                [math.sin(a[2])*math.cos(a[1]), math.sin(a[2])*math.sin(a[1])*math.sin(a[0])+math.cos(a[2])*math.cos(a[0]), math.sin(a[2])*math.sin(a[1])*math.cos(a[0])-math.cos(a[2])*math.sin(a[0])],
                [-math.sin(a[1])              , math.cos(a[1])*math.sin(a[0])                                             , math.cos(a[1])*math.cos(a[0])]]
        return np.dot(Rxyz,pos)

    # internal - Rotation Z
    def RotZ(self,pos,angle):
        Rz = [[math.cos(angle),-math.sin(angle),0],
              [math.sin(angle),math.cos(angle),0],
              [0,0,1]]
        return np.dot(Rz,pos) 

    # internal - build body and perform translation
    def buildBody(self, pos = [0,0,0], angle = [0,0,0]):
        b  = self.RotXYZ(self.body,angle).T + pos 
        b2 = self.RotXYZ(self.body2,angle).T + pos
        return b, b2

    # internal - shift all legs in X,Y,Rz
    def setFeetPosInc(self, (x, y, Rz) = (0,0,0)):   
        self.FootPos = self.RotZ(self.FootPos.T, Rz*np.pi/180).T + [x, y, 0]

    # set number of legs moving at once (1 to 3)
    def setMovingLegs(self, iMovingLegs = 2):
        if iMovingLegs > 0 and iMovingLegs < 4:
            self.CycleStep = 0
            self.MovingLegs = iMovingLegs

    # initialization
    def initRobot(self, z = 75):
        # get body on the ground (z = 35 mm)
        b = np.array([self.bodyRadius, 35])
        k = np.array([self.bodyRadius + self.hip, 35])
        f = np.array([self.footDist, 0])
        kf = np.linalg.norm(k - f)
        bf = np.linalg.norm(b - f)
        ankleAngle = np.arccos((self.thigh**2 + self.foot**2 - kf**2)/(2*self.thigh*self.foot))
        kneeAngle = np.arccos((self.hip**2 + kf**2 - bf**2)/(2*self.hip*kf)) + np.arccos((self.thigh**2 + kf**2 - self.foot**2)/(2*self.thigh*kf))
        for i in range(6):
            self.con.servos[self.Dict[i*3+1]].setPos(deg = int(-kneeAngle/np.pi*180 + 180)*self.sign[i], move = False)
            self.con.servos[self.Dict[i*3+2]].setPos(deg = int(ankleAngle/np.pi*180 - 180 + ankleOffset)*self.sign[i], move = False)
        self.con.sendBinary()

        time.sleep(0.5)
        # reset hip position
        for i in range(6):
            self.con.servos[self.Dict[i*3+0]].setPos(deg = 0)
        self.con.sendBinary()    

        time.sleep(0.3) 
        # get body to nominal height z
        b = np.array([self.bodyRadius, z])
        k = np.array([self.bodyRadius + self.hip, z])
        kf = np.linalg.norm(k - f)
        bf = np.linalg.norm(b - f)
        ankleAngle = np.arccos((self.thigh**2 + self.foot**2 - kf**2)/(2*self.thigh*self.foot))
        kneeAngle = np.arccos((self.hip**2 + kf**2 - bf**2)/(2*self.hip*kf)) + np.arccos((self.thigh**2 + kf**2 - self.foot**2)/(2*self.thigh*kf))
        for i in range(6):
            self.con.servos[self.Dict[i*3+1]].setPos(deg = int(-kneeAngle/np.pi*180 + 180)*self.sign[i], move = False)
            self.con.servos[self.Dict[i*3+2]].setPos(deg = int(ankleAngle/np.pi*180 - 180 + ankleOffset)*self.sign[i], move = False)
        self.con.sendBinary()    

    # update robot position - the main calculation loop
    def updateRobot(self, pos = (0,0,75), angle = (0,0,0), move = False, dPos = (0,0,0), send = True):
        """
        The idea is that during move the body stays still, but the legs move in opposite direction
        The body position and orientation is given by absolute pos and angle.
        If move is enabled, the legs are shifted by dPos = (dx,dy,dRz) each time
        """
        angle = map(lambda x: math.radians(x), angle) # angles from deg to rad

        # build body with desired position and orientation
        b, b2 = self.buildBody(pos, angle) 

        # if move, shift all feet position by dPos = (dx,dy,dRz) increment
        if move:
            self.setFeetPosInc(dPos)           

        # calculate body normal vector
        v0 = b[1] - b[0]
        v1 = b[2] - b[0]
        normal = np.cross(v0, v1)
        normal /= math.sqrt(np.dot(normal,normal))

        doMove = False

        """
        1 Cycle step = one leg move up or down
        Cycle steps = 12/MovingLegs
        if MovingLegs = 2
            Cycle:  Leg:     Vertical move:
            0       0 and 3      Up
            1       0 and 3      Down
            2       1 and 4      Up
            3       1 and 4      Down
            4       2 and 5      Up
            5       2 and 5      Down
            0       0 and 3      Up
            ...
        """
        # if legs movement enabled or some legs in the air, update legs position
        if move or (self.CycleStep % 2 == 1): 
            d = self.footDist 
            i = self.CycleStep/2

            for j in range(self.MovingLegs):  # reset the position of the legs which should move this cycle (1 to 3 legs)
                self.FootPos[i + j*6/self.MovingLegs] = [d*math.cos(self.bodyAngles[i + j*6/self.MovingLegs]),d*math.sin(self.bodyAngles[i + j*6/self.MovingLegs]),0]

            self.CycleStep += 1
            self.CycleStep %= (12/self.MovingLegs)

            if (self.CycleStep % 2 == 1):  # go up with the legs
                doMove = True

        # calculate inverse kinematics for each leg
        
        for i in range(6):
            f = self.FootPos[i]
            f_b = f - b[i]
            p = f - np.dot(normal, f_b)*normal  # x = (q - (n.(q-p))*n , q = out of plane, p = plane point, n = normal
            h0 = p - b[i]
            h1 = b2[i] - b[i]
            h0norm = math.sqrt(np.dot(h0,h0))
            h1norm = math.sqrt(np.dot(h1,h1))
            
            hipAngle = math.acos(np.dot(h0,h1)/h0norm/h1norm)

            k = b[i] + self.hip*h0/h0norm
            
            k_f = k-f
            kfnorm = math.sqrt(np.dot(k_f,k_f))  # norm
            
            x = (self.thigh**2 + self.foot**2 - kfnorm**2)/(2*self.thigh*self.foot)
            if -1 <= x <= 1:
                ankleAngle = math.acos(x)
            else:
                ankleAngle = np.nan

            bfnorm = math.sqrt(np.dot(f_b,f_b))  # norm

            x = (self.hip**2 + kfnorm**2 - bfnorm**2)/(2*self.hip*kfnorm)
            y = (self.thigh**2 + kfnorm**2 - self.foot**2)/(2*self.thigh*kfnorm)
            if -1 <= x <= 1 and -1 <= y <= 1:
                kneeAngle = math.acos(x) + math.acos(y)
            else:
                kneeAngle = np.nan
                
            # move proper legs up
            if doMove and (self.CycleStep / 2 == i % (6/self.MovingLegs)):
                kneeAngle += np.pi/5.0
                ankleAngle -= np.pi/5.0

            # send updated angles to servos only if solution exists
            if np.isnan((hipAngle, kneeAngle, ankleAngle)).any():
                print "No solution for leg %d" % i
            else:
                self.con.servos[self.Dict[i*3+0]].setPos(deg = int(-hipAngle/np.pi*180 + 90), move = False)
                self.con.servos[self.Dict[i*3+1]].setPos(deg = int(-kneeAngle/np.pi*180 + 180)*self.sign[i], move = False)
                self.con.servos[self.Dict[i*3+2]].setPos(deg = int(ankleAngle/np.pi*180 - 180 + ankleOffset)*self.sign[i], move = False)

        if send:
            self.con.sendBinary()

How to use it
In GUI.py, in init of class App you create a new variables

self.Dict = [   # Setup names and servo assignment
            ("LF hip", 7),
            ("LF knee", 6),
            ("LF ankle", 5),
            ("LM hip", 11),
            ("LM knee", 10),
            ("LM ankle", 9),
            ("LB hip", 15),
            ("LB knee", 14),
            ("LB ankle", 13),
            ("RF hip", 24),
            ("RF knee", 25),
            ("RF ankle", 26),
            ("RM hip", 20),
            ("RM knee", 21),
            ("RM ankle", 22),
            ("RB hip", 16),
            ("RB knee", 17),
            ("RB ankle", 18),
            ("Head",31)
        ]

self.Robot = kinematicRobot(con, self.Dict)   # robot instance

self.pos = (0,0,75)  # (x,y,z) position of body [mm]
self.angle = (0,0,0) # (Rx,Ry,Rz) orientation of body [deg]
self.dPos  = (0,0,0) # (dx,dy,dRz) displacement of legs per one cycle
self.move = False  # no move now
self.once = False  # no position update now

Then when you want to initialize the position of the robot you call

then in pool you call updateRobot.

def poll(self):
      ...
      if self.once or self.move:  
                self.Robot.updateRobot(self.pos, self.angle, self.move, self.dPos)
                if self.move == False:
                    self.once = False
     ...
     self.master.after(1000/FPS, self.poll) 

For FPS I recommend FPS= 20-30 as an optimal value between Hexy movement speed and reaction times of the servos. With binary communication and speed improvements you can achieve as much as about 50 FPS, but this might cause overload of Servotor32 serial buffer and stop response from the robot. I also recommend to use Bluetooth which by my experience causes less disturbance of the active servos.

If you only want to change body position/angle (with static legs) set

self.pos = (30, 0, 75)  # example, lean forward
self.once = True

If you want to do the complete move (moving legs) set

self.dPos = (5, 0, 0)  # example, move forward 5mm per cycle
self.move = True

To stop the movement, update the position once more such that all legs get to ground for sure:

self.move = False
self.once = True

You can also change the number of simultaneously moving legs (between 1 and 3, default is 2). Try it, it’s fun :smiley:. See the video

self.Robot.setMovingLegs(iMovingLegs = 2)

http://youtu.be/9sOlLHbRjdI

Thant’s it. Enjoy! :sunglasses:

Anyone up to connecting a ps3 controller or something for this?

I’m using Logitech Wireless Gamepad F710.
and pygame.joystick library to interface it in PoCoMo/GUI

It has 5 analog axes, 10 buttons and 1 hat which is more than enough for controlling hexy.

Can you pls post a complete zip or rar file with all the modified files you are using (i meed the modefied pomoco)

That would help a lot.

Be patient. There are several reasons why I didn’t do that yet:

  1. My version of PoCoMo is adjusted to the hardware modifications I made. So it wouldn’t work with your Hexy without modifications.
  2. I’m bad in making comments inside code. Hence it would backfire on me with many questions like “What does this function does?”, “My Hexy behaves strangely when I use your code, what am I doing wrong?
  3. Where’s the fun if I give you all the solution immediately? I post here a lot of hints and even complete classes. If you don’t want to use your brain cells, buy a radio-controlled car instead.

I first want to walk you through my modifications in the thread I’m writing. Once I will describe all major changes I will share the complete code.

Thx Michal. Im sure im going to love all your changes and upgrades :slight_smile:

Michael,

Would you mind sharing the changes with me? I’d love to have them folded into mainline PoMoCo :wink:

I just got samples of metal gears that do 180 degrees reasonably without jamming, and at a good price. It looks like we’ll be offering a metal gear Hexy within a month or so (probably $300 vs $250 for plastic), and a discount bulk upgrade pack for people who’ve already purchased a Hexy.

Michal,

Great example on the IK code to demonstrate the different sorts of walking gaits that are available with a hexapod. I’m very impressed. How long has this taken you, out of interest?

Thanks,
Rob

@roboalchemist - will do, soon, probably this weekend.

@rpcook - thanks! The basic IK code took me about 2 days + about 14 days of polishing it (API, GUI, Joystick control etc).

16 days to get to this level of polish!? I really hope you are a student with little else to do with your time, because if you fit in this much awesomeness around a full-time job I’m going to have to come up with a new word to describe this level of productivity!

Well, I made the virtual (kinematic) robot and multi-point offset calibration before I started IK project, so technically I’m busy with Hexy for a bit more than month. I’m not a student for some time :slight_smile:, but I use my brain a lot at work which helps me staying sharp.

Great explanation of the inverse kinematics Michael. Very helpful.

I have been trying to implement inverse kinematics on the Servotor32 for Hexy for a couple of weeks now.
Main trouble I think I am having now is nonlinearity of servos and math functions taking too long to compute on the microcontroller.

Has anyone else got IK working on Hexy? Or is anyone else working on IK for the Servotor32?

When you say working on IK, is there a specific problem to solve?

klims,

servo nonlinearity is definitely an issue for accurate IK, but even one constant offset value per servo should be enough initially.

I agree that the math functions can be pretty demanding, but did you try to optimize the IK code? For example if you are trying to implement my IK code, than I would initially skip computation of body orientation - just keep it horizontal. Then you can store body points as a precomputed array. This already saves you about half of the computation time.

I’m working on something like what Michael has put together for controlling hexy using IK to calculate leg positions, but working entirely on the Servotor32. My aim is to connect a ps3 controller to hexy directly and control it as Michael does.

I’ve done the interesting task of solving the IK and doing the math, now I need to implement it properly which I would much rather grab someone else’s solution if it’s available.

Michael: I haven’t copied your IK code, I have tried to implement my own based around some IK/Computer vision tutorials.
Yep that’s right, I am taking the precomputed array approach but I think some of my point transformation functions are overly complex for what they need to do on hexy.

I have found http://blog.oscarliang.net/arduino-hexapod-robot/ tutorial very useful, and am considering just copying the code and making the necessary changes for Hexy.

klims, PS3 controlled Hexy is an awesome goal, good luck with it.

I’d schemed this as one of my projects with Hexy but I’ve not had enough time to have a go yet. Few problems that I foresaw were:
[ul]
[]I don’t know what the communication protocol is from the PS3 controller (apart from Bluetooth), but I’d be very surprised if it’s a low baud rate serial interface, which I think would preclude direct communication between the controller and the (current) Bluetooth module.[/]
[]Given that the stock build of software in the Servotor32 makes the onboard memory about half full. Implementation of full-robot IK code might be either too large to fit onboard, or the processor speed would limit the speed of the IK code such that Hexy’s speed would be limited.[/]
[]A way that I saw to get around these would have been to use an intermediate device (I’d schemed an Android device), which has enough resources to form two Bluetooth links (one to PS3 controller, one to Hexy) and do the difficult IK maths. Someone’s already posted a port of PoMoCo to Android, which might serve as a decent starting point.[/]
[/ul]
Of course, these are just my foreboding, ominous thoughts, and they might all be solvable. Just thought that other peeps might benefit from sharing these ideas.

Would really like to see a project like this work, as demonstrating Hexy at schools etc, the PoMoCo interface is a bit of an accessibility block. Having a games controller operate the robot will really boost the engagement of people with Hexy. Good luck to you!