Notes

quaternion

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

Basics about complex number C\mathbb C

i2=1z=a+bi=[abba]z1z2=z2z1z=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 θ=arccosba2+b2,r=z=a2+b2\theta = \arccos \frac{b}{\sqrt{a^2+b^2}}, r=||z||=\sqrt{a^2+b^2}:

z=[abba]=a2+b2[aa2+b2ba2+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ϕ0sinϕ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+(1cosθ)(uv)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,dR)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:

q1q2q2q1q_1q_2 \neq q_2q_1

image-20231120102716151

The matrix formulation of multiplication:

q1=a+bi+cj+dk,q2=e+fi+gj+hkq1q2=[abcdbadccdabdcba][efgh]q2q1=[abcdbadccdabdcba][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=[aevu,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=[vu,v×u]v = [0, \mathbf v], u=[0, \mathbf u] \\ vu=[-\mathbf v\cdot\mathbf u, \mathbf v\times\mathbf u]

Inverse

qq1=q1q=1qq^{-1} = q^{-1}q = 1

Conjugate

q=[s,u]q=[s,u](q)=qqq=qq=q2=[s2+uu,0]q=qq1q2=(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:

q1=qq2=[ss2+uu,us2+uu]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:

q1=1q(q1)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=qvq1v'=qvq^*=qvq^{-1}

To understand it, we still need to decompose it:

v=q(v+v)q=qvq+qvq=qqv+qqv=v+qqvv'=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:

θ=2arccosau=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! q1q2q2q1q_1q_2 \ne q_2q_1.

Double cover

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

(q)v(q)=qvqq=[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θ2veuθ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.