Skip to content

quaternion

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

Basics about complex number C

i2=1z=a+bi=[abba]z1z2=z2z1||z||=a2+b2=zz¯

2D Rotation

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

v=[cosθsinθsinθcosθ]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:

z=[abba]=a2+b2[aa2+b2ba2+b2ba2+b2aa2+b2]=r[cosθsinθsinθcosθ]=r(cosθ+isinθ)=reiθ

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

3D Rotation

3D rotation can be represented by three Euler angles (θ,ϕ,γ), 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]

Another representation is axis-angle: rotation θ degree along axis u=(x,y,z)T, where ||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)

Basics about Quaternion H

q=a+bi+cj+dk=[a,b,c,d]T(a,b,c,dR)||q||=a2+b2+c2+d2

where

i2=j2=k2=ijk=1

by left-multiplying i or right-multiplying k to ijk, we have:

ij=k,jk=i

further left-multiplying i or right-multiplying j to ij and similar to jk, we have:

kj=i,ik=j,ji=k

lastly right-multiplying i to ji, we have:

ki=j

which leads to an important difference with Complex number:

q1q2q2q1

image-20231120102716151

The matrix formulation of multiplication:

q1=a+bi+cj+dk,q2=e+fi+gj+hkq1q2=[abcdbadccdabdcba][efgh]q2q1=[abcdbadccdabdcba][efgh]

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]

Pure quaternion

the real part equals 0.

v=[0,v],u=[0,u]vu=[vu,v×u]

Inverse

qq1=q1q=1

Conjugate

q=[s,u]q=[s,u](q)=qqq=qq=||q||2=[s2+uu,0]||q||=||q||q1q2=(q2q1)

And we get a method to calculate the inverse:

q1=q||q||2=[ss2+uu,us2+uu]

which also indicates:

||q1||=1||q||(q1)1=q

Quaternion for 3D Rotation

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

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

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

To rotate v for θ degree along axis u=(x,y,z)T, where ||u||=1,

define

v=[0,v],q=[cosθ2,sinθ2u]

note that \(||q||=1\), we have:

v=qvq=qvq1

To understand it, we still need to decompose it: $$ v'=q(v_{||}+v_{\perp})q^* = qv_{||}q^+qv_{\perp}q^=qq^*v_{||}+qqv_\perp = v_{||}+qqv_\perp $$ where $$ qq = [\cos\theta, \sin\theta \mathbf u] $$ (rotate θ2 twice equals rotate θ)

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

θ=2arccosau=bsinθ

Matrix form

Very complicated form...

image-20231120114151607

Composition of rotation

Just do it sequentially,

v=q2(q1vq1)q2=(q2q1)v(q2q1)

First apply q1 then apply q2 leads to equal rotation of q2q1.

Note that the order matters! q1q2q2q1.

Double cover

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

(q)v(q)=qvqq=[cosθ2,sinθ2u]=[cos(πθ2),sin(πθ2)(u)]

image-20231120113854817

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

Euler power form

euθ=cosθ+usinθv=euθ2veuθ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:]