Skip to content

S0 — Sensor & Calibration Models

The mathematical foundation upon which the entire pipeline is built. S0 is not an algorithmic step — it is the collection of physical and geometric measurement functions that map the 3D continuous world into the discrete pixels and inertial samples the filter consumes. Every residual, state propagation step, and EKF update Jacobian in the system is built on top of S0.

A bug in S0 (such as a coordinate system flip or a sign error in the Jacobian) does not fail loudly with an exception. Instead, it quietly poisons every downstream stage, manifesting as mysterious, untraceable quadratic drift or immediate filter divergence.


A camera model defines the mapping:

project(pc)z=[u,v]T\text{project}(\mathbf{p}_c) \rightarrow \mathbf{z} = [u, v]^T

where pc=[x,y,z]TR3\mathbf{p}_c = [x, y, z]^T \in \mathbb{R}^3 is a 3D point in the camera-frame coordinate system. Cortex supports both the standard Pinhole-RadTan model (for rectilinear lenses) and the Kannala-Brandt model (for wide-angle fisheye lenses).

A. Pinhole Projection (The Core Perspective)

Section titled “A. Pinhole Projection (The Core Perspective)”

The ideal pinhole camera projects the 3D camera-frame coordinate pc\mathbf{p}_c to the normalized image plane coordinates xn=[xn,yn]T\mathbf{x}_n = [x_n, y_n]^T:

xn=xz,yn=yzx_n = \cfrac{x}{z}, \qquad y_n = \cfrac{y}{z}

These are then mapped to pixel coordinates via focal lengths (fx,fyf_x, f_y) and principal points (cx,cyc_x, c_y):

u=fxxn+cx,v=fyyn+cyu = f_x x_n + c_x, \qquad v = f_y y_n + c_y
  • Focal Lengths (fx,fyf_x, f_y): Represent the distance between the lens optical center and the image sensor plane, expressed in pixel units. These scale the normalized coordinates to match the physical pixel spacing on the silicon wafer.
  • Principal Points (cx,cyc_x, c_y): Represent the exact pixel coordinate where the camera’s optical axis physically pierces the image sensor. Because the lens assembly is never perfectly centered on the silicon during manufacturing, the principal point is almost never exactly at the geometric center of the pixel grid.

B. Distortion Models (Modeling Lens Curvature)

Section titled “B. Distortion Models (Modeling Lens Curvature)”

Real lenses bend light, introducing distortion. Before mapping to pixels, the normalized coordinates xn\mathbf{x}_n are warped to distorted coordinates xd=[xd,yd]T\mathbf{x}_d = [x_d, y_d]^T.

1. Radial-Tangential (RadTan) Distortion (5-Lens / 4-Intrinsic Parameters)

Section titled “1. Radial-Tangential (RadTan) Distortion (5-Lens / 4-Intrinsic Parameters)”

The complete camera projection model combines 5 lens distortion parameters (k1,k2,k3,p1,p2k_1, k_2, k_3, p_1, p_2) and 4 intrinsic calibration parameters (fx,fy,cx,cyf_x, f_y, c_x, c_y):

  • Radial Distortion (k1,k2,k3k_1, k_2, k_3): Models barrel/pincushion lens symmetry. Glass lenses curve more aggressively near their outer borders. This pulls or pushes pixels along radial lines emerging from the optical center: r2=xn2+yn2r^2 = x_n^2 + y_n^2 radial_correction=1+k1r2+k2r4+k3r6\text{radial\_correction} = 1 + k_1 r^2 + k_2 r^4 + k_3 r^6
  • Tangential Distortion (p1,p2p_1, p_2): Models “decentering” or “thin prism” distortion. This is caused by individual lens elements being slightly tilted relative to each other or misaligned relative to the image sensor plane during assembly, creating a slight physical warp:
xd=xnradial_correction+2p1xnyn+p2(r2+2xn2)x_d = x_n \cdot \text{radial\_correction} + 2 p_1 x_n y_n + p_2 (r^2 + 2 x_n^2) yd=ynradial_correction+p1(r2+2yn2)+2p2xnyny_d = y_n \cdot \text{radial\_correction} + p_1 (r^2 + 2 y_n^2) + 2 p_2 x_n y_n

2. Kannala-Brandt (KB) Fisheye Distortion (4-Parameters)

Section titled “2. Kannala-Brandt (KB) Fisheye Distortion (4-Parameters)”

Wide-angle fisheye lenses cannot be modeled by simple rectilinear perspective because their field of view often exceeds 180180^\circ (light rays can literally bend around behind the camera origin).

Instead of assuming light travels in straight lines to a flat plane, the Kannala-Brandt model maps the angular direction of the incoming light ray (θ\theta) relative to the optical axis using an odd-power polynomial series:

r=xn2+yn2,θ=atan2(r,1)r = \sqrt{x_n^2 + y_n^2}, \qquad \theta = \text{atan2}(r, 1) d(θ)=θ+k1θ3+k2θ5+k3θ7+k4θ9d(\theta) = \theta + k_1 \theta^3 + k_2 \theta^5 + k_3 \theta^7 + k_4 \theta^9

The distorted coordinates are then mapped:

xd=d(θ)rxn,yd=d(θ)rynx_d = \cfrac{d(\theta)}{r} x_n, \qquad y_d = \cfrac{d(\theta)}{r} y_n
  • Optical Center Limit (r = 0): When r=0r = 0 (the principal ray), the angle θ=0\theta = 0, and the ratio d(θ)/rd(\theta) / r is mathematically defined as its limit: limr0d(θ)/r=1\lim_{r\to0} d(\theta)/r = 1. Thus, at the principal point, xd=xn=0x_d = x_n = 0 and yd=yn=0y_d = y_n = 0 with zero distortion, preventing any division-by-zero NaN values.

To perform the EKF Measurement Update (where we update our state estimate using the visual tracking error), we need to write the derivative of pixel coordinates z=[u,v]T\mathbf{z} = [u, v]^T w.r.t the 3D camera-frame point pc\mathbf{p}_c. This Jacobian, Hp\mathbf{H}_p, is computed via the chain rule:

Hp=zpc=zxd\ccdotxdxn\ccdotxnpc\mathbf{H}_p = \cfrac{\partial \mathbf{z}}{\partial \mathbf{p}_c} = \cfrac{\partial \mathbf{z}}{\partial \mathbf{x}_d} \ccdot \cfrac{\partial \mathbf{x}_d}{\partial \mathbf{x}_n} \ccdot \cfrac{\partial \mathbf{x}_n}{\partial \mathbf{p}_c}
  • Pinhole Quotient Rule: The normalized image coordinate is a quotient of coordinates: xn=x/zx_n = x/z. Under the standard quotient rule, its derivatives w.r.t the 3D coordinate vector pc=[x,y,z]T\mathbf{p}_c = [x, y, z]^T are given by:
    • $\partial x_n/\partial x = 1/z
    • $\partial x_n/\partial y = 0
    • $\partial x_n/\partial z = -x / z^2 This forms the linear perspective term:
xnpc=[1/z0x/z2 01/zy/z2]\cfrac{\partial \mathbf{x}_n}{\partial \mathbf{p}_c} = \begin{bmatrix} 1/z & 0 & -x/z^2 \ 0 & 1/z & -y/z^2 \end{bmatrix}

Cortex checks the rank of Hp\mathbf{H}_p (which must be strictly 2) and validates its analytic derivatives against numerical finite differences during unit testing down to machine precision (1015\approx10^{-15}).


An Inertial Measurement Unit (IMU) consists of a 3-axis gyroscope and a 3-axis accelerometer. The continuous-time physical models are:

ω~(t)=ω(t)+bg(t)+ng(t)\tilde{\boldsymbol{\omega}}(t) = \boldsymbol{\omega}(t) + \mathbf{b}_g(t) + \mathbf{n}_g(t) a~(t)=RWI(t)(aW(t)gW)+ba(t)+na(t)\tilde{\mathbf{a}}(t) = \mathbf{R}_{W}^{I}(t) \left( \mathbf{a}_W(t) - \mathbf{g}_W \right) + \mathbf{b}_a(t) + \mathbf{n}_a(t)

Where:

  • ω~,a~\tilde{\boldsymbol{\omega}}, \tilde{\mathbf{a}}: The raw gyro and accelerometer measurements.
  • ω,aW\boldsymbol{\omega}, \mathbf{a}_W: The true angular velocity and true world-frame acceleration of the IMU.
  • bg,ba\mathbf{b}_g, \mathbf{b}_a: The slow-varying sensor biases (modeled as random walks).
  • ng,na\mathbf{n}_g, \mathbf{n}_a: White Gaussian measurement noise.
  • gW\mathbf{g}_W: The global gravity vector.

Why an IMU Resting on a Table Reads +9.81 m/s2+9.81\text{ m/s}^2

Section titled “Why an IMU Resting on a Table Reads +9.81 m/s2+9.81\text{ m/s}^2+9.81 m/s2”

This is a common point of confusion for beginners. Accelerometers measure specific force (the reaction force relative to free-fall), not true acceleration. When an IMU is stationary on a flat table:

  • Its true world acceleration is aW=0\mathbf{a}_W = \mathbf{0}.
  • The world gravity is gW=[0,0,9.81]T\mathbf{g}_W = [0, 0, -9.81]^T.
  • Thus, the accelerometer reads:
a~=RWI(0[0,0,9.81]T)=RWI[0,0,9.81]T\tilde{\mathbf{a}} = \mathbf{R}_W^I (\mathbf{0} - [0, 0, -9.81]^T) = \mathbf{R}_W^I [0, 0, 9.81]^T
  • This registers as a +9.81 m/s2+9.81\text{ m/s}^2 upward acceleration. A VIO pipeline must subtract gravity during propagation to prevent the drone from quadratically shooting off into the sky in its state estimate.

Sensors do not live at the same physical point, nor do they capture data at the exact same instant. We must calibrate both spatial and temporal offsets.

The camera is offset from the IMU center by a translation pic\mathbf{p}_{ic} and a rotation Ric\mathbf{R}_{ic} (or TciSE(3)\mathbf{T}_{ci} \in \text{SE}(3)). A point pf\mathbf{p}_f in the world frame is mapped to the camera frame CC via the intermediate IMU frame II:

pimu=RWI(pfpW_imu)\mathbf{p}_{\text{imu}} = \mathbf{R}_{W}^{I} (\mathbf{p}_f - \mathbf{p}_{\text{W\_imu}}) pcam=RicT(pimupic)\mathbf{p}_{\text{cam}} = \mathbf{R}_{\text{ic}}^T (\mathbf{p}_{\text{imu}} - \mathbf{p}_{\text{ic}})

Under dynamic motion, omitting even a 1 cm1\text{ cm} offset introduces massive unmodeled centripetal and Euler accelerations into the camera’s visual track:

acam=aimu+ω×(ω×pic)+ω×pic\mathbf{a}_{\text{cam}} = \mathbf{a}_{\text{imu}} + \mathbf{\omega} \times (\mathbf{\omega} \times \mathbf{p}_{\text{ic}}) + \cdot{\mathbf{\omega}} \times \mathbf{p}_{\text{ic}}

B. Temporal Extrinsics (Time-Offset Calibration)

Section titled “B. Temporal Extrinsics (Time-Offset Calibration)”

Due to transmission latency, clock drift, or rolling shutters, there is a time delay tdt_d between the camera clock and the IMU clock:

tcamera=timutdt_{\text{camera}} = t_{\text{imu}} - t_d

If the drone is rotating at a moderate angular velocity of 100/s100^\circ\text{/s} (common in maneuvers), a tiny 5 millisecond5\text{ millisecond} unmodeled time offset (td=0.005t_d = 0.005) introduces a 0.50.5^\circ alignment error. This is equivalent to several pixels of visual tracking error, causing the EKF update to inject false corrections.


The contract defines the required behavior of the sensor and calibration mathematical block:

ElementContract Specification
Signatureproject(pc,ζ)(u,v)\text{project}(p_c, \zeta) \rightarrow (u,v)
Pre-conditionsFocal lengths fx,fy>0f_x, f_y > 0; distortion parameters are within stable convergence bounds; gravity vector orientation is frame-consistent.
Post-conditions & Invariants1. Round-Trip consistency: unproject(project(pc))pc/pc\text{unproject}(\text{project}(p_c)) \approx p_c / \|p_c\| down to 101510^{-15} machine precision for all depth z>0z > 0.
2. Jacobian rank: Projection Jacobian Hp\mathbf{H}_p is strictly rank-2 and matches numerical finite-differences.
3. Cheirality: Points behind the lens (z0z \le 0) are rejected, never projected.
4. Inertial Identity: A zero-noise, gravity-compensated stationary IMU integrates to exactly zero velocity and zero translation over time.

Cortex provides a generalized static release tool, s0_inspect, which can be invoked directly on actual EuRoC sensor streams to verify physical calibration.

Terminal window
# Execute the real-data sensor inspector on EuRoC V1_01_easy
s0_inspect --dataset /path/to/V1_01_easy/mav0 --out build/s0 --frame 0

This generates two critical diagnostic overlays:

Superimposes an ideal, distortion-free rectilinear grid (grey) against the actual lens-warped grid (green) calculated from the distortion coefficients. It draws lens displacement vectors and verifies the round-trip projection error across every node of the grid. This allows you to visually inspect and confirm that the lens model is perfectly inverting the camera’s physical barrel distortion.

RadTan Distortion Overlay showing the unwarped and warped grid overlays across the sensor frame RadTan Projection Jacobian sensitivity distribution over the image coordinate space

B. IMU Allan Deviation Curve (imu_allan.svg)

Section titled “B. IMU Allan Deviation Curve (imu_allan.svg)”

Computes the Allan Deviation of the gyroscope and accelerometer streams under static conditions. The Allan deviation curve (plotted on a log-log scale) allows you to extract:

  1. White Noise Density (N): Sourced from the slope of 0.5-0.5 at t=1 st = 1\text{ s}. This represents the continuous-time noise parameters used in the process noise covariance Q\mathbf{Q}.
  2. Bias Instability (B): Sourced from the flat bottom of the curve.
  3. Random Walk (K): Sourced from the slope of +0.5+0.5 at long integration times.

IMU Allan Deviation Curve for Gyros and Accelerometers showing noise densities N and bias random walk walk slope offsets

Running s0_inspect on the physical drone rig allows you to verify that your configured process noises exactly match the true physical noise profile of the sensor, protecting the filter from initial over-confidence.