denied.systems

field guide

Quaternion kinematics for the error-state Kalman filter

A working guide to Joan Sola’s quaternion kinematics for the error-state Kalman filter - the reference for fusing IMU data with external corrections in an ESKF/VIO estimator.

updated 2026-07-06 state-estimation sensor-fusion vio

The foundational reference for doing orientation estimation with quaternions inside a Kalman filter. Joan Solà’s 2017 paper (arXiv:1711.02508) is the definitive treatment: it derives every formula from first principles, resolves the convention wars, and provides a complete ESKF recipe for fusing IMU data with external corrections (GPS, vision, etc.). If you’re implementing an IMU-driven state estimator that uses quaternions, this is the paper to have open.

Why quaternions for rotation

Rotation matrices (3×33\times3, 9 parameters) over-parameterize orientation. Euler angles (3 parameters) gimbal-lock. Quaternions are the sweet spot: 4 numbers, no singularities, and the algebra handles composition cleanly. A unit quaternion q=[qw,qx,qy,qz]Tq = [q_w, q_x, q_y, q_z]^T with q=1\|q\| = 1 encodes a rotation of angle φ\varphi around axis 𝐮\mathbf{u}:

q=[cos(φ/2)𝐮sin(φ/2)] q = \begin{bmatrix} \cos(\varphi/2) \\ \mathbf{u} \sin(\varphi/2) \end{bmatrix}

The half-angle is not a bug — it’s a feature of the double cover of SO(3), which is what makes the quaternion algebra work. qq and q-q represent the same rotation.

The rotation action on a vector 𝐱3\mathbf{x} \in \mathbb{R}^3 is the double product:

𝐱=q𝐱q* \mathbf{x}' = q \otimes \mathbf{x} \otimes q^*

Where q*=[qw,𝐪v]Tq^* = [q_w, -\mathbf{q}_v]^T is the conjugate (equal to the inverse for unit quaternions). This is equivalent to 𝐱=R𝐱\mathbf{x}' = R\mathbf{x} with RR given by the quaternion-to-rotation-matrix formula.

Core quaternion algebra

The paper uses Hamilton convention: i2=j2=k2=ijk=1i^2 = j^2 = k^2 = ijk = -1, with ij=k=jiij = k = -ji. This is the right-handed convention. Other conventions exist (JPL uses left-handed, some swap real/vector order) — Section 3 of the paper disambiguates all four binary choices.

Quaternion product (\otimes): using scalar-vector notation q=qw+𝐪vq = q_w + \mathbf{q}_v:

pq=[pwqw𝐩v𝐪vpw𝐪v+qw𝐩v+𝐩v×𝐪v] p \otimes q = \begin{bmatrix} p_w q_w - \mathbf{p}_v \cdot \mathbf{q}_v \\ p_w \mathbf{q}_v + q_w \mathbf{p}_v + \mathbf{p}_v \times \mathbf{q}_v \end{bmatrix}

The cross product in the vector part makes \otimes non-commutative (rotation composition is order-dependent). Product is bilinear and can be expressed as matrix-vector products using left- and right- product matrices [q]L[q]_L and [q]R[q]_R, which will appear throughout the ESKF derivations.

Exponential and logarithmic maps connect the quaternion to the rotation vector (axis-angle) representation:

Quaternion-to-rotation-matrix:

R{q}=(qw2𝐪vT𝐪v)I+2𝐪v𝐪vT+2qw[𝐪v]× R\{q\} = (q_w^2 - \mathbf{q}_v^T\mathbf{q}_v)I + 2\mathbf{q}_v\mathbf{q}_v^T + 2q_w[\mathbf{q}_v]_\times

Where [𝐚]×[\mathbf{a}]_\times is the skew-symmetric cross-product matrix.

The four derivative types on SO(3)

Section 4 defines four Jacobian types depending on whether the function domain/codomain is vector space or SO(3). The ones that matter for ESKF:

Jr(𝛉)=I1cos𝛉𝛉2[𝛉]×+𝛉sin𝛉𝛉3[𝛉]×2 J_r(\boldsymbol{\theta}) = I - \frac{1 - \cos\|\boldsymbol{\theta}\|}{\|\boldsymbol{\theta}\|^2}[\boldsymbol{\theta}]_\times + \frac{\|\boldsymbol{\theta}\| - \sin\|\boldsymbol{\theta}\|}{\|\boldsymbol{\theta}\|^3}[\boldsymbol{\theta}]_\times^2

Time integration of rotation rates

Given gyroscope readings 𝛚\boldsymbol{\omega}, the quaternion kinematics are:

q̇=12q𝛚 \dot{q} = \frac{1}{2} q \otimes \boldsymbol{\omega}

Zeroth-order integrator (constant 𝛚\boldsymbol{\omega} over Δt\Delta t):

qn+1=qnq{𝛚Δt} q_{n+1} = q_n \otimes q\{\boldsymbol{\omega}\Delta t\}

This is what most implementations use. It preserves unit norm by construction (product of two unit quaternions).

First-order integrator (accounts for 𝛚̇\dot{\boldsymbol{\omega}} via midpoint samples):

qn+1qnq{𝛚Δt}+Δt224[0𝛚n×𝛚n+1] q_{n+1} \approx q_n \otimes q\{\boldsymbol{\omega}\Delta t\} + \frac{\Delta t^2}{24} \begin{bmatrix} 0 \\ \boldsymbol{\omega}_n \times \boldsymbol{\omega}_{n+1} \end{bmatrix}

The correction term is second-order small (106𝛚2\sim 10^{-6} \|\boldsymbol{\omega}\|^2 at 100 Hz) and only matters when the rotation axis changes significantly between samples. For constant rotation axis, the zeroth-order integrator is exact: qn+1=qnq{𝐮Δθ}q_{n+1} = q_n \otimes q\{\mathbf{u}\Delta\theta\} where Δθ=ω(t)dt\Delta\theta = \int \omega(t)dt.

The ESKF architecture

The error-state Kalman filter decomposes the state into three layers:

Concept Meaning Size
True state xtx_t The actual physical state (unknown) 18: p,v,q,ab,ωb,gp, v, q, a_b, \omega_b, g
Nominal state xx Large-signal integration of IMU, no noise model Same 18
Error state δx\delta x Small-signal difference: xt=xδxx_t = x \oplus \delta x 18: δp,δv,δθ,δab,δωb,δg\delta p, \delta v, \delta\theta, \delta a_b, \delta\omega_b, \delta g

The composition \oplus is linear addition for vectors (pt=p+δpp_t = p + \delta p), quaternion product for orientation (qt=qδqq_t = q \otimes \delta q, with δq=eδθ/2\delta q = e^{\delta\theta/2}), and addition for biases.

Why this decomposition works

  1. Minimal orientation error: δθ3\delta\theta \in \mathbb{R}^3 (3 parameters, no redundancy), avoiding singular covariance matrices from over-parameterized constraints.
  2. Always near origin: The error state is reset to zero after every correction → linearization always valid.
  3. Second-order products negligible: δθ\|\delta\theta\| is small → Jacobians simplify enormously.
  4. Slow error dynamics: Large-signal motion lives in the nominal state. Error state only changes slowly → corrections can run at lower rate than predictions.

Nominal-state kinematics (continuous time)

These are the noise-free integration equations you’d run at IMU rate (e.g., 200 Hz):

ṗ=vv̇=R(amab)+gq̇=12q(ωmωb)ȧb=0ω̇b=0ġ=0 \begin{aligned} \dot{p} &= v \\ \dot{v} &= R(a_m - a_b) + g \\ \dot{q} &= \frac{1}{2} q \otimes (\omega_m - \omega_b) \\ \dot{a}_b &= 0 \\ \dot{\omega}_b &= 0 \\ \dot{g} &= 0 \end{aligned}

Where am,ωma_m, \omega_m are IMU readings, ab,ωba_b, \omega_b are estimated biases, and R=R{q}R = R\{q\}. The gravity vector gg is also estimated — this decouples initial orientation uncertainty from gravity uncertainty (Lupton & Sukkarieh, 2009).

Discrete-time integration (Euler form):

pp+vΔt+12(R(amab)+g)Δt2vv+(R(amab)+g)Δtqqq{(ωmωb)Δt}abab,ωbωb,gg \begin{aligned} p &\leftarrow p + v\Delta t + \frac{1}{2}(R(a_m - a_b) + g)\Delta t^2 \\ v &\leftarrow v + (R(a_m - a_b) + g)\Delta t \\ q &\leftarrow q \otimes q\{(\omega_m - \omega_b)\Delta t\} \\ a_b &\leftarrow a_b,\quad \omega_b \leftarrow \omega_b,\quad g \leftarrow g \end{aligned}

Error-state kinematics — the three key equations

1. Continuous-time error dynamics

The linearized error state evolves as:

δṗ=δvδv̇=R[amab]×δθRδab+δgRanδθ̇=[ωmωb]×δθδωbωnδȧb=aw,δω̇b=ωw,δġ=0 \begin{aligned} \delta\dot{p} &= \delta v \\ \delta\dot{v} &= -R[a_m - a_b]_\times \delta\theta - R\delta a_b + \delta g - R a_n \\ \delta\dot{\theta} &= -[\omega_m - \omega_b]_\times \delta\theta - \delta\omega_b - \omega_n \\ \delta\dot{a}_b &= a_w,\quad \delta\dot{\omega}_b = \omega_w,\quad \delta\dot{g} = 0 \end{aligned}

The velocity error equation is the critical one — it couples orientation errors δθ\delta\theta into position drift. The term R[amab]×δθ-R[a_m - a_b]_\times \delta\theta says: an orientation error δθ\delta\theta causes the acceleration vector to be projected slightly wrong, and the resulting position error grows quadratically with time.

2. Discrete-time error Jacobian FxF_x

The prediction step propagates the error covariance via δx̂Fxδx̂\delta\hat{x} \leftarrow F_x \delta\hat{x} and PFxPFxT+FiQiFiTP \leftarrow F_x P F_x^T + F_i Q_i F_i^T:

Fx=[IIΔt00000IR[amab]×ΔtRΔt0IΔt00RT{(ωmωb)Δt}0IΔt0000I000000I000000I] F_x = \begin{bmatrix} I & I\Delta t & 0 & 0 & 0 & 0 \\ 0 & I & -R[a_m - a_b]_\times \Delta t & -R\Delta t & 0 & I\Delta t \\ 0 & 0 & R^T\{(\omega_m - \omega_b)\Delta t\} & 0 & -I\Delta t & 0 \\ 0 & 0 & 0 & I & 0 & 0 \\ 0 & 0 & 0 & 0 & I & 0 \\ 0 & 0 & 0 & 0 & 0 & I \end{bmatrix}

Notable: the orientation error block uses RT{(ωmωb)Δt}R^T\{(\omega_m - \omega_b)\Delta t\} (a rotation matrix, not a quaternion) — this is the discrete-time equivalent of exp([ω]×Δt)\exp(-[\omega]_\times \Delta t).

Since δx̂\delta\hat{x} is initialized to zero and the prediction equation (268) always returns zero, you skip the mean prediction in code. But the covariance prediction (269) is essential — FiQiFiTF_i Q_i F_i^T injects process noise and makes PP grow continuously.

3. The correction step and injection

When external measurements arrive (GPS position, visual odometry, etc.):

Kalman update: K=PHT(HPHT+V)1δx̂K(yh(xt))P(IKH)P \begin{aligned} K &= P H^T (H P H^T + V)^{-1} \\ \delta\hat{x} &\leftarrow K(y - h(x_t)) \\ P &\leftarrow (I - KH)P \end{aligned}

Observation Jacobian chain rule: H=hδx=hxtxtδx=HxXδx H = \frac{\partial h}{\partial \delta x} = \frac{\partial h}{\partial x_t} \cdot \frac{\partial x_t}{\partial \delta x} = H_x \cdot X_{\delta x}

HxH_x is sensor-specific. XδxX_{\delta x} is the ESKF-specific mapping from error state to true state, which is identity except for the quaternion block:

Qδθ=(qδq)δθ=12[qxqyqzqwqzqyqzqwqxqyqxqw] Q_{\delta\theta} = \frac{\partial(q \otimes \delta q)}{\partial \delta\theta} = \frac{1}{2} \begin{bmatrix} -q_x & -q_y & -q_z \\ q_w & -q_z & q_y \\ q_z & q_w & -q_x \\ -q_y & q_x & q_w \end{bmatrix}

Error injection into nominal state: pp+δp̂vv+δv̂qqq{δθ̂}abab+δâbωbωb+δω̂bgg+δĝ \begin{aligned} p &\leftarrow p + \delta\hat{p} \\ v &\leftarrow v + \delta\hat{v} \\ q &\leftarrow q \otimes q\{\delta\hat{\theta}\} \\ a_b &\leftarrow a_b + \delta\hat{a}_b \\ \omega_b &\leftarrow \omega_b + \delta\hat{\omega}_b \\ g &\leftarrow g + \delta\hat{g} \end{aligned}

ESKF reset: After injection, δx̂0\delta\hat{x} \leftarrow 0 and the covariance is transformed:

PGPGT P \leftarrow G P G^T

Where GG is identity except for the orientation block: δθ+δθ=I12[δθ̂]×\frac{\partial \delta\theta^+}{\partial \delta\theta} = I - \frac{1}{2}[\delta\hat{\theta}]_\times. In practice, most implementations neglect this reset Jacobian (G=IG = I) since δθ̂\|\delta\hat{\theta}\| is tiny. The full expression is provided for high-precision odometry.

Global vs. local angular errors (Section 7)

The classical ESKF defines the orientation error locally: δθ\delta\theta is expressed relative to the nominal orientation qq. An alternative is to define it globally: δθ\delta\theta is the rotation vector from the nominal to true orientation, expressed in a fixed inertial frame. Li & Mourikis (2012) showed the global formulation has better observability properties. Section 7 derives the full ESKF for global angular errors — the kinematics change but the architecture is identical.

Practical implementation notes

Where the ESKF fits relative to the complementary filter

The complementary-filter is a fixed-gain fusion: one α\alpha parameter, no covariance, no bias estimation, no position. It gives you roll/pitch at $$200 Hz from a 6-DOF IMU. The ESKF is the full probabilistic version: it estimates position, velocity, orientation, and IMU biases, maintains a full 18×1818\times18 covariance, and fuses external corrections optimally via the Kalman gain. For a drone that needs drift-free navigation in GPS-denied environments, the ESKF is the target state estimator — the complementary filter is a debugging scaffold on the way there.

See also: rotating-coordinate-frames for the frame convention (NED) that feeds the RR matrices, euler-body-rate-cross-coupling for the gyroscopic terms that the ESKF’s dynamics must account for in aggressive flight, accel-roll-pitch-derivation for the accelerometer angle formulas used in the simpler complementary filter.