amino  1.0-beta2
Lightweight Robot Utility Library
Transformation

\[ \newcommand{\normtwo}[1]{\left| #1 \right|} \newcommand{\MAT}[1]{\boldsymbol{#1}} \newcommand{\unitvec}[1]{\boldsymbol{\hat{#1}}} \newcommand{\ielt}[0]{\unitvec{\imath}} \newcommand{\jelt}[0]{\unitvec{\jmath}} \newcommand{\kelt}[0]{\unitvec{k}} \newcommand{\quat}[1]{\mathcal{#1}} \newcommand{\sequat}[0]{\quat{h}} \newcommand{\setranssym}[0]{v} \newcommand{\setrans}[0]{\vec{\setranssym}} \newcommand{\qmul}[0]{\otimes} \newcommand{\dotprod}{\boldsymbol{\cdot}} \newcommand{\tf}[3]{{^{#2}{#1}_{#3}}} \newcommand{\mytf}[3]{{^{#2}\!}{#1}_{#3}} \newcommand{\tfmat}[3]{{^{#2}\MAT{#1}_{#3}}} \newcommand{\tfquat}[3]{{^{#2}\quat{#1}_{#3}}} \newcommand{\qmul}[0]{\otimes} \newcommand{\dualelt}[0]{\boldsymbol{\varepsilon}} \newcommand{\pttf}[2]{{^{#2}\!}{#1}} \newcommand{\qconj}[1]{{#1}^*} \newcommand\overcmt[2]{\overbrace{{#1}}^{#2}} \newcommand\undercmt[2]{\underbrace{{#1}}_{#2}} \newcommand{\dualmark}[1]{\tilde{#1}} \require{cancel} \]

This tutorial covers the combination of rotation and translation in three dimensions.

Euclidean Transformation

Motion of a rigid body in 3D space consists of both rotation (angular motion) and translation (linear motion). We call the combination of a rotation and translation a Euclidean transformation because Euclidean distances between points on the rigid body are preserved under motion.

Robot Local Frames

Euclidean transformations are also called rigid transformations (since bodies are rigid) or Euclidean isometry ("equal measure").

Local Coordinate Frames

It is often convenient to define the points on a rigid body in terms of a local coordinate frame affixed to the body. Then, these local coordinates of points on the body remain constant regardless as to how the body rotates and translates.

Robot Local Frames

The global coordinates of all points on the body following a Euclidean transformation are fully defined by the rotation and translation (linear displacement) of the local frame:

Local Coordinate Frames

Since we have to keep track of many different frames, it is helpful to adopt a notation convention that indicates which frames a particular variable relates to. A useful convention we will use is to indicate parent frame with a leading superscript and a child frame with a trailing subscript:

\[ \mytf{X}{\rm parent}{\rm child} \; . \]

For example, in the figure above, \(\tf{x}{0}{1}\) is the x-translation from parent frame 0 to child frame 1.

Transforming a Point

The Euclidean transformation lets us map points between coordinate frames. For example, we may commonly want to find the global coordinates of some point, which is possible using the local coordinates of the point and the Euclidean transformation of the local coordinate frame.

In the figure below, we start with the local coordinates of point \(p\) in frame b. To obtain the coordinates of the point in frame a, we first rotate and then we translate the point:

To transform a point from frame b to frame a, we first rotate the point and then add the translation.

\[ \mytf{p}{a}{} = \overcmt{ (\mytf{\sequat}{a}{b}) \qmul (\mytf{p}{b}{}) \qmul {(\mytf{\sequat}{a}{b})}^* }{\text{rotate}} \ + \overcmt{\mytf{\setrans}{a}{b}}{\text{translate}} \]

Transforming a Point

Chaining Transforms

Sometimes we need to chain a sequence of transformations. This need occurs when dealing with robot manipulators, where we have a series of links, each with its own local coordinate frame:

Robot Local Frames

Generally, we chain transformations with a multiplication, either matrix or quaternion as discussed below. Our notation convention for parent and child frames helps us keep track of the appropriate products. To chain the transorm from a-to-b \(\tf{T}{a}{b}\) and from b-to-c \(\tf{T}{b}{c}\), we multiply:

\[ \tf{T}{a}{b}\,\tf{T}{b}{c} = \tf{T}{a}{c} \]

Transforming a Point

Representations

Transformation Matrices

We may combine a rotation matrix and a translation vector to produce a transformation matrix. To represent point transformation and transform chaining as matrix-vector and matrix-matrix products, we must augment the points and matrices. We augment points with an additional one,

\[ \begin{bmatrix} p_x \\ p_y \\ p_z \end{bmatrix} \leadsto \begin{bmatrix} p_x \\ p_y \\ p_z \\ 1 \end{bmatrix} \; . \]

Then, we construct the transformation matrix as follows,

\[ \tfmat{T}{a}{b} = \begin{bmatrix} \tfmat{R}{a}{b} & \tfmat{v}{a}{b} \\ 0_{1 \times 3} & 1 \end{bmatrix} = \begin{bmatrix} r_{11} & r_{12} & r_{13} & v_x \\ r_{21} & r_{22} & r_{23} & v_y \\ r_{31} & r_{32} & r_{33} & v_z \\ 0 & 0 & 0 & 1 \end{bmatrix} \; . \]

Transforming a point corresponds to a matrix-vector product:

\[ \tfmat{p}{a}{} = \tfmat{T}{a}{b} \, \tfmat{p}{b}{} \quad\leadsto\quad \begin{bmatrix} \tf{p}{a}{x} \\ \tf{p}{a}{y} \\ \tf{p}{a}{z} \\ 1 \end{bmatrix} \begin{bmatrix} \tfmat{R}{a}{b} & \tfmat{v}{a}{b} \\ 0_{1 \times 3} & 1 \end{bmatrix} \begin{bmatrix} \tf{p}{b}{x} \\ \tf{p}{b}{y} \\ \tf{p}{b}{z} \\ 1 \end{bmatrix} \]

Chaining transformations corresponds to a matrix-matrix product:

\[ \tfmat{T}{a}{c} = \tfmat{T}{a}{b} \, \tfmat{T}{b}{c} \]

Since the bottom row of the transformation matrix is constant, we can save memory and computation time by omitting this row from our storage and computation. Thus, we store the transformation matrix in memory using only the rotation matrix and translation vector, requiring a total of 12 numbers:

\[ \begin{bmatrix} r_{11} & r_{12} & r_{13} & v_x \\ r_{21} & r_{22} & r_{23} & v_y \\ r_{31} & r_{32} & r_{33} & v_z \\ 0 & 0 & 0 & 1 \end{bmatrix} \leadsto \begin{array}{|c|c|} \hline r_{11} & r_{21} & r_{31} & r_{12} & r_{22} & r_{32} & r_{13} & r_{23} & r_{33} & v_x & v_y & v_z \\ \hline \end{array} \; . \]

Dual Number Quaternions

Dual Number Quaternions

Dual quaternions a compact, computationally efficient, and analytically convenient representation for Euclidean transformations. Dual quaternions are an extension of the ordinary quaternions that is capable of representing both rotation and translation. A dual quaternion is a quaternion of dual numbers. Dual numbers are constructed using the dual element \(\dualelt\), where:

\[ \dualelt^2 = 0 \quad{\rm and}\quad \dualelt \neq 0 \]

Multiplication of dual numbers cancels the product of the dual parts:

\[ (a_r + a_d \dualelt) * (b_r + b_d \dualelt) \quad = \quad a_r b_r + a_r b_d \dualelt + b_r a_d \dualelt + \cancelto{0}{a_d b_d \dualelt^2} \quad = \quad a_r b_r + (a_r b_d + b_r a_d) \dualelt \]

Dual numbers yield interesting properties. In particular, the Taylor series for any dual number, evaluated at the real part, consists of only two terms; all higher order terms contain and \(\dualelt^2\) and cancel to zero. Consequently, we can evaluate any dual function, such as sine, cosine, exponential, and logarithm, in terms of the real function and its derivative.

Combining the quaternion units and the dual element yields eight factors in the dual quaternion,

Dual Quaternion Coefficients

We construct the dual quaternion for a rotation \(\quat{h}\) and translation \(\vec{v}\) as follows:

\[ \quat{S} = \quat{h} + \quat{d}\dualelt = \quat{h} + \frac{1}{2} \vec{v} \qmul \quat{h} \dualelt \; . \]

Chaining dual quaternions corresponds to multiplication. Note that the product of the two dual parts cancels as \(\dualelt^2 = 0\) and that the product of the real parts is a chaining of the rotations.

\[ \tfquat{S}{a}{c} = \tfquat{S}{a}{b} \qmul \tfquat{S}{b}{c} = \left( \tfquat{r}{a}{b} + \tfquat{d}{a}{b}\dualelt \right) \qmul \left( \tfquat{r}{b}{c} + \tfquat{d}{b}{c}\dualelt \right) = \tfquat{r}{a}{b} \qmul \tfquat{r}{b}{c} + \left( \tfquat{r}{a}{b} \qmul \tfquat{d}{b}{c} + \tfquat{d}{a}{b} \qmul \tfquat{r}{b}{c} \right) \dualelt \]

To represent the dual quaternion in-memory, we need eight numbers, which we store as separate oridinary quaternions for the real-part and dual-part,

\[ \left( r_x \ielt + r_y \jelt + r_z \kelt + r_w \right) + \left( d_x \ielt + d_y \jelt + d_z \kelt + d_w \right) \dualelt \quad\leadsto\quad \begin{array}{|c|c|} \hline r_x & r_y & r_z & r_w & d_x & d_y & d_z & d_w \\ \hline \end{array} \; . \]

double dual_quaternion_as_array[8];
struct duqu {
struct quat real;
struct quat dual;
};

Implicit Dual Quaternions

Since we can construct the dual quaternion for a Euclidean transformation from an ordinary quaternion and translation vector, we take this quaternion and vector as the implicit representation of a dual quaternion. Such a quaternion-vector representation is more compact, requiring only 7 elements, compared to the eight elements of the explicit dual quaternion. Moreover, common operations such as chaining transformations, are more efficient using the implicit representation. We retain the view of this quaternion-vector as a dual quaternion for analysis, e.g., when considering derivatives and velocities, where chaining as multiplication and defined logarithm and exponential maps simplify some operations.

To represent the implicit dual quaternion in-memory, we need a total of 7 numbers for the ordinary (rotation) quaternion and translation vector:

\[ \left\lgroup \left( h_x \ielt + h_y \jelt + h_z \kelt + h_w \right),\ \left( v_x, v_y, v_z \right) \right\rgroup \quad\leadsto\quad \begin{array}{|c|c|} \hline h_x & h_y & h_z & h_w & v_x & v_y & v_z \\ \hline \end{array} \; . \]

double implicit_dual_quaternion_as_array[7];
struct qutr {
struct quat rotation;
double translation[3];
};

We can chain the transforms from a to b and from b to c, giving a single transform from a to consider. Consider transforming a point in c first to b and then to a.

Transforming a Point

\[ \mytf{p}{a}{} = \left( \mytf{\quat{h}}{a}{b}\right) \qmul \overcmt{ \left( \left( \mytf{\quat{h}}{b}{c}) \qmul (\mytf{p}{c}{} \right) \qmul \qconj{\left(\mytf{\quat{h}}{b}{c}\right)} + \mytf{v}{b}{c} \right) }{\mytf{p}{b}{}} \qmul \qconj{\left(\mytf{\quat{h}}{a}{b}\right)} + \mytf{v}{a}{b} \]

\[ \pttf{p}{a} = \undercmt{ \left(\mytf{\quat{h}}{a}{b}\right) \qmul \left( \mytf{\quat{h}}{b}{c}\right) }{\mytf{\quat{h}}{a}{c}} \qmul \left(\mytf{p}{c}{} \right) \qmul \qconj{ \undercmt{ \left(\mytf{\quat{h}}{a}{b} \qmul \mytf{\quat{h}}{b}{c}\right) }{\mytf{\quat{h}}{a}{c}} } + \undercmt{ \left(\mytf{\quat{h}}{a}{b}\right) \qmul \mytf{v}{b}{c} \qmul \qconj{\left(\mytf{\quat{h}}{a}{b}\right)} + \mytf{v}{a}{b} }{ \mytf{v}{a}{c} } \]

Example Code

#!/usr/bin/env python3
from math import pi
from amino import Vec3, YAngle, ZAngle, TfMat, DualQuat, QuatTrans
def h1(name):
"""Print a level 1 heading"""
print("")
print("{:^16}".format(name))
print("{:=^16}".format(''))
def h2(name):
"""Print a level 2 heading"""
print("")
print("{:^16}".format(name))
print("{:-^16}".format(''))
def check_equiv_tf(a, b):
eps = 1e-6
rel = (a * ~b)
assert rel.translation.nrm2() < eps
assert rel.rotation.ln().nrm2() < eps
def check_equiv_vec(a, b):
eps = 1e-6
assert a.ssd(b) < eps
h1("Construction")
# parent frame: 0
# child frame: 1
rot_0_1 = ZAngle(pi / 4)
trans_0_1 = Vec3([1, 2, 3])
tf_0_1 = (rot_0_1, trans_0_1)
h2("Dual Quaternion")
S_0_1 = DualQuat(tf_0_1)
print(S_0_1)
h2("Tf Matrix")
T_0_1 = TfMat(tf_0_1)
print(T_0_1)
h2("Quaternion-Translation")
E_0_1 = QuatTrans(tf_0_1)
print(E_0_1)
h1("Conversions")
h2("Tf Matrix")
T_S = TfMat(S_0_1)
T_E = TfMat(E_0_1)
check_equiv_tf(T_0_1, T_S)
check_equiv_tf(T_0_1, T_E)
print(T_S)
print(T_E)
h2("Dual Quaternion")
S_T = DualQuat(T_0_1)
S_E = DualQuat(E_0_1)
check_equiv_tf(S_0_1, S_T)
check_equiv_tf(S_0_1, S_E)
print(S_T)
print(S_E)
h2("Quaternion-Translation")
E_T = QuatTrans(T_0_1)
E_S = QuatTrans(S_0_1)
check_equiv_tf(E_0_1, E_S)
check_equiv_tf(E_0_1, E_T)
print(E_T)
print(E_S)
h1("Transform")
h2("Initial Point")
# point a in frame 1
p_1_a = Vec3([3, 5, 7])
print(p_1_a)
h2("Transformed Point")
# transform to frame 0
p_0_a_T = T_0_1.transform(p_1_a)
p_0_a_E = E_0_1.transform(p_1_a)
p_0_a_S = S_0_1.transform(p_1_a)
check_equiv_vec(p_0_a_T, p_0_a_E)
check_equiv_vec(p_0_a_S, p_0_a_E)
print(p_0_a_T)
print(p_0_a_E)
print(p_0_a_S)
h1("Inverse Transform")
Ti = ~T_0_1
Si = ~S_0_1
Ei = ~E_0_1
# inverse transform back to frame 1
# same as original p_1_a
p_1_a_T = Ti.transform(p_0_a_T)
p_1_a_S = Si.transform(p_0_a_S)
p_1_a_E = Ei.transform(p_0_a_S)
check_equiv_vec(p_1_a, p_1_a_T)
check_equiv_vec(p_1_a, p_1_a_S)
check_equiv_vec(p_1_a, p_1_a_E)
print(p_1_a_T)
print(p_1_a_S)
print(p_1_a_E)
h1("Chaining Transforms")
rot_1_2 = YAngle(pi / 2)
trans_1_2 = Vec3([2, 4, 8])
tf_1_2 = (rot_1_2, trans_1_2)
S_1_2 = DualQuat(tf_1_2)
T_1_2 = TfMat(tf_1_2)
E_1_2 = QuatTrans(tf_1_2)
h2("Initial Point")
# point b in frame 2
p_2_b = Vec3([2, 1, 0])
print(p_2_b)
h2("Successive Transforms")
# Transform twice
p_1_b_T = T_1_2.transform(p_2_b)
p_0_b_T = T_0_1.transform(p_1_b_T)
p_1_b_E = E_1_2.transform(p_2_b)
p_0_b_E = E_0_1.transform(p_1_b_E)
p_1_b_S = S_1_2.transform(p_2_b)
p_0_b_S = S_0_1.transform(p_1_b_S)
check_equiv_vec(p_1_b_T, p_1_b_E)
check_equiv_vec(p_1_b_S, p_1_b_E)
print(p_0_b_E)
print(p_0_b_T)
print(p_0_b_S)
h2("Chained Transforms")
S_0_2 = S_0_1 * S_1_2
T_0_2 = T_0_1 * T_1_2
E_0_2 = E_0_1 * E_1_2
print(S_0_2)
print(T_0_2)
print(E_0_2)
h2("Transformation via chained transform")
# Use chained transform
p_0_b_T_chain = T_0_2.transform(p_2_b)
p_0_b_E_chain = E_0_2.transform(p_2_b)
p_0_b_S_chain = S_0_2.transform(p_2_b)
check_equiv_vec(p_0_b_E_chain, p_0_b_T_chain)
check_equiv_vec(p_0_b_E_chain, p_0_b_S_chain)
check_equiv_vec(p_0_b_T_chain, p_0_b_T)
check_equiv_vec(p_0_b_E_chain, p_0_b_E)
check_equiv_vec(p_0_b_S_chain, p_0_b_S)
print(p_0_b_E_chain)
print(p_0_b_T_chain)
print(p_0_b_S_chain)

See Also

Python

C

References