Notes

quaternion

Ref & Credits: https://github.com/Krasjet/quaternion

Basics about complex number C\mathbb C

i2=−1z=a+bi=[a−bba]z1z2=z2z1∣∣z∣∣=a2+b2=zzˉi^2 = -1 \\ z = a + bi = \begin{bmatrix}a &-b \\ b & a\end{bmatrix} \\ z_1z_2=z_2z_1 \\ ||z|| = \sqrt{a^2 + b^2} = \sqrt{z \bar z}

2D Rotation

2D rotation (counter-clockwise by θ\theta) can be represented by:

v′=[cos⁡θ−sin⁡θsin⁡θcos⁡θ]v\mathbf{v'} = \begin{bmatrix}\cos\theta &-\sin\theta \\ \sin\theta & \cos\theta\end{bmatrix} \mathbf{v}

A complex number can represents a 2D vector.

Multiply it with a complex number equals scaling and rotating in the 2D plane:

let θ=arccos⁡ba2+b2,r=∣∣z∣∣=a2+b2\theta = \arccos \frac{b}{\sqrt{a^2+b^2}}, r=||z||=\sqrt{a^2+b^2}:

z=[a−bba]=a2+b2[aa2+b2−ba2+b2ba2+b2aa2+b2]=r[cos⁡θ−sin⁡θsin⁡θcos⁡θ]=r(cos⁡θ+isin⁡θ)=reiθz = \begin{bmatrix}a &-b \\ b & a\end{bmatrix} = \sqrt{a^2+b^2} \begin{bmatrix} \frac {a} {\sqrt{a^2+b^2}} & \frac {-b} {\sqrt{a^2+b^2}} \\ \frac{b}{\sqrt{a^2+b^2}} & \frac{a} {\sqrt{a^2+b^2}}\end{bmatrix} = r \begin{bmatrix}\cos \theta & - \sin \theta \\ \sin \theta & \cos \theta\end{bmatrix} \\ = r(\cos\theta + i\sin\theta) \\ = re^{i\theta}

Therefore, we also have v′=zvv' = zv if the scaling factor r=∣∣z∣∣=1r=||z||=1.

3D Rotation

3D rotation can be represented by three Euler angles (θ,ϕ,γ)(\theta, \phi, \gamma), but it relies on the axes system and can lead to Gimbal Lock.

Rx(θ)=[10000cos⁡θ−sin⁡θ00sin⁡θcos⁡θ00001]Ry(ϕ)=[cos⁡ϕ0−sin⁡ϕ00100sin⁡ϕ0cos⁡ϕ00001]Rz(γ)=[cos⁡γ−sin⁡γ00sin⁡γcos⁡γ0000100001]\mathbf R_x(\theta) = \begin{bmatrix} 1&0&0&0\\ 0&\cos\theta & -\sin\theta &0 \\ 0&\sin\theta & \cos\theta &0 \\ 0&0&0 &1 \end{bmatrix} \\ \mathbf R_y(\phi) = \begin{bmatrix} \cos\phi &0 & -\sin\phi &0 \\ 0&1&0&0\\ \sin\phi &0 & \cos\phi &0 \\ 0&0&0 &1 \end{bmatrix} \\ \mathbf R_z(\gamma) = \begin{bmatrix} \cos\gamma & -\sin\gamma &0&0 \\ \sin\gamma & \cos\gamma &0&0 \\ 0&0&1&0 \\ 0&0&0&1\\ \end{bmatrix} \\

Another representation is axis-angle: rotation θ\theta degree along axis u=(x,y,z)T\textbf {u} = (x, y, z)^T, where ∣∣u∣∣=1||\mathbf u|| = 1.

(There are still only 3 Degree of Freedom)

image-20231120100645841

which leads to the Rodrigues' Rotation Formula:

v′=cos⁡θv+(1−cos⁡θ)(u⋅v)u+sin⁡θ(u×v)\mathbf v' = \cos\theta\mathbf v + (1 - \cos\theta)(\mathbf u \cdot \mathbf v)\mathbf u + \sin\theta(\mathbf u\times\mathbf v)

Basics about Quaternion H\mathbb H

q=a+bi+cj+dk=[a,b,c,d]T(a,b,c,d∈R)∣∣q∣∣=a2+b2+c2+d2q = a + bi + cj + dk = \begin{bmatrix}a,b,c,d\end{bmatrix}^T \quad (a, b, c, d \in \mathbb R) \\ ||q||=\sqrt{a^2 + b^2 + c^2 + d^2}

where

i2=j2=k2=ijk=−1i^2=j^2=k^2=ijk=-1

by left-multiplying ii or right-multiplying kk to ijkijk, we have:

ij=k,jk=iij=k, jk=i

further left-multiplying ii or right-multiplying jj to ijij and similar to jkjk, we have:

kj=−i,ik=−j,ji=−kkj=-i, ik=-j,ji=-k

lastly right-multiplying ii to jiji, we have:

ki=jki=j

which leads to an important difference with Complex number:

q1q2≠q2q1q_1q_2 \neq q_2q_1

image-20231120102716151

The matrix formulation of multiplication:

q1=a+bi+cj+dk,q2=e+fi+gj+hkq1q2=[a−b−c−dba−dccda−bd−cba][efgh]q2q1=[a−b−c−dbad−cc−dabdc−ba][efgh]q_1 = a + bi + cj + dk, q_2=e+fi+gj+hk \\ q_1q_2=\begin{bmatrix} a & -b & -c & -d \\ b & a & -d & c \\ c & d & a & -b \\ d & -c & b & a \end{bmatrix} \begin{bmatrix} e \\ f \\ g \\ h \end{bmatrix} \\ q_2q_1=\begin{bmatrix} a & -b & -c & -d \\ b & a & d & -c \\ c & -d & a & b \\ d & c & -b & a \end{bmatrix} \begin{bmatrix} e \\ f \\ g \\ h \end{bmatrix}

A more concise form can be represented by hybrid scalar-vector form (Grafman Product):

q1=[a,v],q2=[e,u]v=[b,c,d]T,u=[f,g,h]Tq1q2=[ae−v⋅u,au+ev+v×u]q_1 = [a, \mathbf v], q_2=[e, \mathbf{u}] \\ \mathbf v = [b, c, d]^T, \mathbf{u}=[f,g,h]^T \\ q_1q_2=[ae-\mathbf v \cdot \mathbf u,a\mathbf u+e\mathbf v+\mathbf v \times \mathbf u]

Pure quaternion

the real part equals 0.

v=[0,v],u=[0,u]vu=[−v⋅u,v×u]v = [0, \mathbf v], u=[0, \mathbf u] \\ vu=[-\mathbf v\cdot\mathbf u, \mathbf v\times\mathbf u]

Inverse

qq−1=q−1q=1qq^{-1} = q^{-1}q = 1

Conjugate

q=[s,u]→q∗=[s,−u](q∗)∗=qqq∗=q∗q=∣∣q∣∣2=[s2+u⋅u,0]∣∣q∣∣=∣∣q∗∣∣q1∗q2∗=(q2q1)∗q=[s, \mathbf u] \rightarrow q^* = [s, -\mathbf u] \\ (q^*)^* = q \\ qq^*=q^*q=||q||^2=[s^2 + \mathbf u \cdot \mathbf u, 0] \\ ||q||=||q^*|| \\ q_1^*q_2^*=(q_2q_1)^*

And we get a method to calculate the inverse:

q−1=q∗∣∣q∣∣2=[ss2+u⋅u,−us2+u⋅u]q^{-1} = \frac {q^*} {||q||^2} = [\frac s {s^2 + \mathbf u \cdot \mathbf u}, \frac {-\mathbf{u}} {s^2 + \mathbf u \cdot \mathbf u} ]\\

which also indicates:

∣∣q−1∣∣=1∣∣q∣∣(q−1)−1=q||q^{-1}|| = \frac 1 {||q||} \\ (q^{-1})^{-1} = q

Quaternion for 3D Rotation

A pure quaternion can represent a 3D vector: v=[0,v]v=[0, \mathbf v]

A unit quaternion can represent a 3D rotation: ∣∣q∣∣=1||q||=1

And we can rewrite the Rodrigues' Rotation Formula in a new form!

To rotate v\mathbf{v} for θ\theta degree along axis u=(x,y,z)T\textbf {u} = (x, y, z)^T, where ∣∣u∣∣=1||\mathbf u|| = 1,

define

v=[0,v],q=[cos⁡θ2,sin⁡θ2u]v = [0, \mathbf v], q=[\cos\frac\theta 2, \sin\frac\theta 2 \mathbf u] \\

note that ∣∣q∣∣=1||q||=1, we have:

v′=qvq∗=qvq−1v'=qvq^*=qvq^{-1}

To understand it, we still need to decompose it:

v′=q(v∣∣+v⊥)q∗=qv∣∣q∗+qv⊥q∗=qq∗v∣∣+qqv⊥=v∣∣+qqv⊥v'=q(v_{||}+v_{\perp})q^* = qv_{||}q^*+qv_{\perp}q^*=qq^*v_{||}+qqv_\perp = v_{||}+qqv_\perp

where

qq=[cos⁡θ,sin⁡θu]qq = [\cos\theta, \sin\theta \mathbf u]

(rotate θ2\frac \theta 2 twice equals rotate θ\theta)

Inversely, given a unit quaternion q=[a,b]q=[a, \mathbf b], we can get the rotation angle and axis by:

θ=2arccos⁡au=bsin⁡θ\theta = 2 \arccos a\\ \mathbf u = \frac {\mathbf {b}} {\sin\theta}

Matrix form

Very complicated form...

image-20231120114151607

Composition of rotation

Just do it sequentially,

v′=q2(q1vq1∗)q2∗=(q2q1)v(q2q1)∗v' = q_2(q_1vq_1^*)q_2^* = (q_2q_1)v(q_2q_1)^*

First apply q1q_1 then apply q2q_2 leads to equal rotation of q2q1q_2q_1.

Note that the order matters! q1q2≠q2q1q_1q_2 \ne q_2q_1.

Double cover

One 3D rotation can be represented with TWO quaternions: qq and −q-q.

(−q)v(−q)∗=qvq∗−q=[−cos⁡θ2,−sin⁡θ2u]=[cos⁡(π−θ2),sin⁡(π−θ2)(−u)](-q)v(-q)^*=qvq^* \\ -q = [-\cos\frac\theta 2, -\sin\frac\theta 2 \mathbf u] = [\cos(\pi - \frac\theta 2), \sin(\pi-\frac\theta 2) (-\mathbf u)]

image-20231120113854817

Notice that the matrix form is exactly the same for −q-q and qq, since all of the elements are multiplication of two coefficients!

Euler power form

euθ=cos⁡θ+usin⁡θv′=euθ2ve−uθ2e^{\mathbf u \theta}=\cos \theta + \mathbf u\sin\theta \\ v' = e^{\mathbf u \frac\theta 2} v e^{-\mathbf u \frac \theta 2}

Implementation

import torch

def norm(q):
    # q: (batch_size, 4)

    return torch.sqrt(torch.sum(q**2, dim=1, keepdim=True))

def normalize(q):
    # q: (batch_size, 4)

    return q / (norm(q) + 1e-20)

def conjugate(q):
    # q: (batch_size, 4)

    return torch.cat([q[:, 0:1], -q[:, 1:4]], dim=1)

def inverse(q):
    # q: (batch_size, 4)

    return conjugate(q) / (torch.sum(q**2, dim=1, keepdim=True) + 1e-20)


def from_vectors(a, b):
    # get the quaternion from two 3D vectors, such that b = qa.
    # a: (batch_size, 3)
    # b: (batch_size, 3)
    # note: a and b don't need to be unit vectors.

    q = torch.empty(a.shape[0], 4, device=a.device)
    q[:, 0] = torch.sqrt(torch.sum(a**2, dim=1)) * torch.sqrt(torch.sum(b**2, dim=1)) + torch.sum(a * b, dim=1)
    q[:, 1:] = torch.cross(a, b)
    q = normalize(q)

    return q

def from_axis_angle(axis, angle):
    # get the quaternion from axis-angle representation
    # axis: (batch_size, 3)
    # angle: (batch_size, 1), in radians

    q = torch.empty(axis.shape[0], 4, device=axis.device)
    q[:, 0] = torch.cos(angle / 2)
    q[:, 1:] = normalize(axis) * torch.sin(angle / 2)
    
    return q

def as_axis_angle(q):
    # get the axis-angle representation from quaternion
    # q: (batch_size, 4)

    q = normalize(q)
    angle = 2 * torch.acos(q[:, 0:1])
    axis = q[:, 1:] / torch.sin(angle / 2)

    return axis, angle

def from_matrix(R):
    # get the quaternion from rotation matrix
    # R: (batch_size, 3, 3)

    q = torch.empty(R.shape[0], 4, device=R.device)
    q[:, 0] = 0.5 * torch.sqrt(1 + R[:, 0, 0] + R[:, 1, 1] + R[:, 2, 2])
    q[:, 1] = (R[:, 2, 1] - R[:, 1, 2]) / (4 * q[:, 0])
    q[:, 2] = (R[:, 0, 2] - R[:, 2, 0]) / (4 * q[:, 0])
    q[:, 3] = (R[:, 1, 0] - R[:, 0, 1]) / (4 * q[:, 0])

    return q

def as_matrix(q):
    # get the rotation matrix from quaternion
    # q: (batch_size, 4)

    R = torch.empty(q.shape[0], 3, 3, device=q.device)
    R[:, 0, 0] = 1 - 2 * (q[:, 2]**2 + q[:, 3]**2)
    R[:, 0, 1] = 2 * (q[:, 1] * q[:, 2] - q[:, 0] * q[:, 3])
    R[:, 0, 2] = 2 * (q[:, 1] * q[:, 3] + q[:, 0] * q[:, 2])
    R[:, 1, 0] = 2 * (q[:, 1] * q[:, 2] + q[:, 0] * q[:, 3])
    R[:, 1, 1] = 1 - 2 * (q[:, 1]**2 + q[:, 3]**2)
    R[:, 1, 2] = 2 * (q[:, 2] * q[:, 3] - q[:, 0] * q[:, 1])
    R[:, 2, 0] = 2 * (q[:, 1] * q[:, 3] - q[:, 0] * q[:, 2])
    R[:, 2, 1] = 2 * (q[:, 2] * q[:, 3] + q[:, 0] * q[:, 1])
    R[:, 2, 2] = 1 - 2 * (q[:, 1]**2 + q[:, 2]**2)

    return R

def mul(q1, q2):
    # q1: (batch_size, 4)
    # q2: (batch_size, 4)
    # return: q1 * q2: (batch_size, 4)

    q = torch.empty_like(q1)
    q[:, 0] = q1[:, 0] * q2[:, 0] - q1[:, 1] * q2[:, 1] - q1[:, 2] * q2[:, 2] - q1[:, 3] * q2[:, 3]
    q[:, 1] = q1[:, 0] * q2[:, 1] + q1[:, 1] * q2[:, 0] + q1[:, 2] * q2[:, 3] - q1[:, 3] * q2[:, 2]
    q[:, 2] = q1[:, 0] * q2[:, 2] - q1[:, 1] * q2[:, 3] + q1[:, 2] * q2[:, 0] + q1[:, 3] * q2[:, 1]
    q[:, 3] = q1[:, 0] * q2[:, 3] + q1[:, 1] * q2[:, 2] - q1[:, 2] * q2[:, 1] + q1[:, 3] * q2[:, 0]

    return q

def apply(q, a):
    # q: (batch_size, 4)
    # a: (batch_size, 3)
    # return: q * a * q^{-1}: (batch_size, 3)

    q = normalize(q)
    q_inv = conjugate(q)

    return mul(mul(q, torch.cat([torch.zeros(q.shape[0], 1, device=q.device), a], dim=1)), q_inv)[:, 1:]
python

Type to search.