Skip to article frontmatterSkip to article content

License: CC-BY-NC-SA 4.0

Author: Murilo M. Marinho (murilo.marinho@manchester.ac.uk)

Pre-requisites for the learner

The user of this notebook is expected to have prior knowledge in

  • All the content and pre-requisites of lesson 1.

I found an issue

Thank you! Please report it at https://github.com/MarinhoLab/OpenExecutableBooksRobotics/issues

Latex Macros

\providecommand{\myvec}[1]{{\mathbf{\boldsymbol{{#1}}}}} \providecommand{\mymatrix}[1]{{\mathbf{\boldsymbol{{#1}}}}}

\providecommand{\myvec}[1]{{\mathbf{\boldsymbol{{#1}}}}}
\providecommand{\mymatrix}[1]{{\mathbf{\boldsymbol{{#1}}}}}

Installing pre-requisites

%%capture
%pip install numpy
%pip install numpy --break-system-packages

Imports

import numpy as np
from math import pi, sin, cos

Rigid bodies

As briefly defined in the past lecture, if the relative transformation between all points of a given object remain the same regardless of motion, it is a rigid body.

In other words, it has no flexibility and the motion of the entire body can be prescribed by its position and orientation with respect to a given reference frame.

Reference frames

Positions/translations and orientations/rotations of objects are always defined with respect to a reference frame. Reference frames can be attached to rigid bodies or at fixed locations in space. The neutral reference frame has the following notation

F.\mathcal{F}.

Unless otherwise stated, a given position/orientation/pose is given with respect to that frame. When other frames are needed we usually rely on notations such as

F,F\mathcal{F}',\mathcal{F}''

when frames are sequential or

Fa,Fb\mathcal{F}_a,\mathcal{F}_b

when relationships are more complex.

2D Position/translation

Positions/translations in 2D can be uniquely defined as any

pR2.\myvec{p} \in \mathbb{R}^2.

Hence, if we would like to define

p=[xy]=[12],\myvec{p} = \left[\begin{array}{ccc} x \\ y \end{array}\right] =\left[\begin{array}{ccc} 1 \\ 2 \end{array}\right],

we can do so with

It is common for column and row vectors to not be distinguishable in Python with numpy. That is in general convenient but can cause problems when the dimension is important, so always pay close attention to what each function expects as input or outputs.
p = np.array([1.0, 2.0])

print(f"p={p}")
p=[1. 2.]

Composition of translations

Sequential translations, such as

R2pi=[xiyi],\mathbb{R}^2 \ni \myvec{p}_i = \left[\begin{array}{ccc} x_i \\ y_i \end{array}\right],

with

iNi \in \mathbb{N}

can be composed with sequential additions

p=p0+p1+p2+p3.\myvec{p} = \myvec{p}_{0} + \myvec{p}_{1} + \myvec{p}_{2} + \myvec{p}_{3}.
p0 = np.array([1.0, 2.0])
p1 = np.array([2.0, 3.0])
p2 = np.array([3.0, 4.0])
p3 = np.array([4.0, 5.0])
p = p0 + p1 + p2 + p3

print(f"p={p}")
p=[10. 14.]

Inverse translation

The inverse translation can be obtained by subtractions and the element of no translation is the zero vector, that is, if we unwind all rotations we’re back to the origin of the reference frame

p=pp0p1p2p3=[00].\myvec{p}' = \myvec{p} - \myvec{p}_{0} - \myvec{p}_{1} - \myvec{p}_{2} - \myvec{p}_{3} = \left[\begin{array}{ccc} 0 \\ 0 \end{array}\right].
p0 = np.array([1.0, 2.0])
p1 = np.array([2.0, 3.0])
p2 = np.array([3.0, 4.0])
p3 = np.array([4.0, 5.0])
p = p0 + p1 + p2 + p3

p_ = p - p0 - p1 - p2 - p3

print(f"p_={p_}")
p_=[0. 0.]

2D orientation/rotation

Orientations/rotations in 2D can be defined in many different ways. In this tutorial, we will address the special orthogonal group for two dimensions, i.e., SO(2). As you have learned in theory, the rotations are defined, in this representation, as a matrix

RR2×2,\mymatrix{R} \in \mathbb{R}^{2 \times 2},

where the identity rotation means no rotation, hence, for any frame Fa\mathcal{F}_a,

Raa=I2.\mymatrix{R}^{a}_{a} = \mymatrix{I}_2.

An element of SO(2) is simply a matrix with the correct properties, therefore we can define one directly in numpy. For

R(θ)=[cosθsinθsinθcosθ],R(\theta) = \left[\begin{array}{ccc} \cos{\theta} & -\sin{\theta} \\ \sin{\theta} & \cos{\theta} \end{array}\right],

when θ=π2\theta = \frac{\pi}{2} we have

θ = pi/2
R = np.array([[cos(θ),-sin(θ)],
              [sin(θ), cos(θ)]])

print(f"R={R}")
R=[[ 6.123234e-17 -1.000000e+00]
 [ 1.000000e+00  6.123234e-17]]

2D poses (combined translation/orientation)

2D poses can be represented using elements of SE(2). As you have learned in theory, translations and rotations can be combined into a single HR3×3\mymatrix{H}\in\mathbb{R}^{3 \times 3} with the following structure

H(x,y,θ)=[cosθsinθxsinθcosθy001],\mymatrix{H}(x,y,\theta) = \left[\begin{array}{ccc} \cos{\theta} & -\sin{\theta} & x \\ \sin{\theta} & \cos{\theta} & y \\ 0 & 0 & 1 \end{array}\right],

that for θ=π2\theta = \frac{\pi}{2}, x=0.1x = 0.1, and y=0.2y = 0.2, we have

θ = pi/2
x = 0.1
y = 0.2
H = np.array([[cos(θ),-sin(θ), x],
              [sin(θ), cos(θ), y],
              [0,      0,      1]])


print(f"H = {H}")
H = [[ 6.123234e-17 -1.000000e+00  1.000000e-01]
 [ 1.000000e+00  6.123234e-17  2.000000e-01]
 [ 0.000000e+00  0.000000e+00  1.000000e+00]]

3D Position/translation

The 3D position/translations are a trivial extention of the 2D ones with one extra dimension, being defined in R3\mathbb{R}^3.

R3pi=[xiyizi].\mathbb{R}^3 \ni \myvec{p}_i = \left[\begin{array}{ccc} x_i \\ y_i \\ z_i \end{array}\right].

This is straightforward and there’s nothing surprising in terms of properties, so we will move on to SO(3).

3D orientation/rotation

You have seen the general formulation of rotational matrices in 3D. For rotations about the basis vectors, we have

R(z,θ)=[cosθsinθ0sinθcosθ0001],\mymatrix{R}(z,\theta) = \left[\begin{array}{ccc} \cos{\theta} & -\sin{\theta} & 0 \\ \sin{\theta} & \cos{\theta} & 0 \\ 0 & 0 & 1 \end{array}\right],
R(y,θ)=[cosθ0sinθ010sinθ0cosθ],\mymatrix{R}(y,\theta) = \left[\begin{array}{ccc} \cos{\theta} & 0 & \sin{\theta} \\ 0 & 1 & 0 \\ -\sin{\theta} & 0 & \cos{\theta} \end{array}\right],
R(x,θ)=[1000cosθsinθ0sinθcosθ].\mymatrix{R}(x,\theta) = \left[\begin{array}{ccc} 1 & 0 & 0 \\ 0 & \cos{\theta} & -\sin{\theta} \\ 0 & \sin{\theta} & \cos{\theta} \end{array}\right].
Rz = np.array([[cos(θ),-sin(θ), 0],
               [sin(θ), cos(θ), 0],
               [0,      0,      1]])

Ry = np.array([[ cos(θ), 0, sin(θ)],
               [      0, 1, 0],
               [-sin(θ), 0, cos(θ)]])

Rx = np.array([[1,      0,  0],
               [0, cos(θ), -sin(θ)],
               [0, sin(θ),  cos(θ)]])

# A rotation about z
print(f"Rz={Rz}")

# A rotation about y
print(f"Ry={Ry}")

# A rotation about x
print(f"Rx={Rx}")
Rz=[[ 6.123234e-17 -1.000000e+00  0.000000e+00]
 [ 1.000000e+00  6.123234e-17  0.000000e+00]
 [ 0.000000e+00  0.000000e+00  1.000000e+00]]
Ry=[[ 6.123234e-17  0.000000e+00  1.000000e+00]
 [ 0.000000e+00  1.000000e+00  0.000000e+00]
 [-1.000000e+00  0.000000e+00  6.123234e-17]]
Rx=[[ 1.000000e+00  0.000000e+00  0.000000e+00]
 [ 0.000000e+00  6.123234e-17 -1.000000e+00]
 [ 0.000000e+00  1.000000e+00  6.123234e-17]]

Compositions of rotations in SO(3)

Compositions of SO(3) follow the same rules as SO(2), where sequential frame transformations as right multiplications

Rc=RaRbaRcb,\mymatrix{R}_c = \mymatrix{R}_a\mymatrix{R}^a_b\mymatrix{R}^b_c,

for instance

θa = pi/4
Ra = np.array([[cos(θa),-sin(θa), 0],
               [sin(θa), cos(θa), 0],
               [0,      0,        1]])

θa_b = -pi/2
Ra_b = np.array([[ cos(θa_b), 0, sin(θa_b)],
                 [         0, 1, 0],
                 [-sin(θa_b), 0, cos(θa_b)]])

θb_c = pi/8
Rb_c = np.array([[1,         0,  0],
                 [0, cos(θb_c), -sin(θb_c)],
                 [0, sin(θb_c),  cos(θb_c)]])

Rc = Ra @ Ra_b @ Rb_c
print(f"Rc={Rc}")
Rc=[[ 4.32978028e-17 -9.23879533e-01 -3.82683432e-01]
 [ 4.32978028e-17  3.82683432e-01 -9.23879533e-01]
 [ 1.00000000e+00  2.34326020e-17  5.65713056e-17]]

Inverse rotations in SO(3)

Inverse operations in SO(3) are analogous to the inversions in SO(2), which are simply matrix transpositions.

Ra_ = Rc @ Rb_c.T @ Ra_b.T 

if np.allclose(Ra, Ra_):
    print('The results are pretty much the same!')
else:
    print('The results are too different.')
The results are pretty much the same!

3D poses

3D poses are represented in SE(3) with matrices HR4×4\mymatrix{H}\in\mathbb{R}^{4 \times 4} with the following structure

H(t,R)=[Rt01].\mymatrix{H}(\myvec{t},\mymatrix{R}) = \left[\begin{array}{ccc} \mymatrix{R} & \myvec{t} \\ \myvec{0} & 1 \end{array}\right].

Sequential transformations

We can also perform transformations using sequential multiplications, as for SO(3). Note also that we can verify the lack of commutativity on these transformations.

# An SE(3) translation
x = 1
y = 2
z = 3
θza = pi/4
Ha =  np.array([[cos(θza),-sin(θza), 0, 1],
                [sin(θza), cos(θza), 0, 2],
                [0,        0,        1, 3],
                [0,        0,        0, 1]])

# An SE(3) rotation
θxb = pi/4
Hb = np.array([[1, 0,        0,       0],
               [0, cos(θxb), -sin(θxb), 0],
               [0, sin(θxb),  cos(θxb), 0],
               [0, 0,        0,       1]])

# Ha then Hb
Hc = Ha @ Hb 

# Hb then Ha
Hc_ = Hb @ Ha 

if np.isclose(Hc,Hc_).all():
    print('The results are close!')
else:
    print('The results are far, therefore the operation is not commutative')
The results are far, therefore the operation is not commutative

Inverse transformations

As you have learned in theory, the inverse transformation can be found as

H1(t,R)=[RTRTt01],\mymatrix{H^{-1}}(\myvec{t},\mymatrix{R}) = \left[\begin{array}{ccc} \mymatrix{R}^T & -\mymatrix{R}^T\myvec{t} \\ \myvec{0} & 1 \end{array}\right],

so it is important to remember to never invert the matrix with regular inversion algorithms. Using the matricial properties leads to a simpler and faster inversion. The only “trick” is to obtain the submatrix corresponding to the rotation matrix.

# Extract Rc from Hc
Rc = Hc[0:3,0:3] # The 3x3 Rotation matrix
tc = Hc[0:3,3].reshape((3,1)) # The 3x1 translation vector at the forth column. Reshape it into a column vector.

A =  Rc.T
B = -Rc.T@tc
C = np.array([0,0,0])
D = np.array([1])

# This is how you can use `np.block` to build a matrix like so
# | A B |
# | C D |
Hc_inv = np.block([[A, B], 
                   [C, D]])

print(f"Rc_inv={Hc_inv}")

print(f"Rc @ Rc_inv = {Hc @ Hc_inv}")
Rc_inv=[[ 0.70710678  0.70710678  0.         -2.12132034]
 [-0.5         0.5         0.70710678 -2.62132034]
 [ 0.5        -0.5         0.70710678 -1.62132034]
 [ 0.          0.          0.          1.        ]]

Rc @ Rc_inv = [[ 1.00000000e+00  0.00000000e+00 -2.29934717e-17  2.22044605e-16]
 [ 0.00000000e+00  1.00000000e+00  0.00000000e+00  0.00000000e+00]
 [-2.29934717e-17  0.00000000e+00  1.00000000e+00 -4.44089210e-16]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  1.00000000e+00]]