Skip to the content
FixTheRobot Explore Robotics / UAV / Module 2 / M2-T10

UAV Systems Engineering · Module 2

M2-T10: Visual Odometry

Two images of a static scene constrain how the camera moved between them, up to an unknown scale: derive the geometry, run it, and see why it drifts.

  • Lesson 10 of 16
  • UAV Navigation and GNSS-Denied Operations
  • Established engineering
  • About 6 minutes to read

A camera is a cheap, light, information-rich sensor, and the geometry of how images change as a camera moves is exact. Visual odometry turns that geometry into a motion estimate, and the same geometry explains where it fails.

Learning objectives

After this lesson, you should be able to:

  • Describe the stages of a feature-based visual odometry pipeline.
  • State the epipolar constraint and the essential matrix, and recover RR and tt from it.
  • Explain why monocular translation has an unknown scale and how stereo or an IMU resolves it.
  • Name the main failure modes and the inlier statistics that reveal them.

Conceptual explanation

Visual odometry (VO) estimates the camera’s motion by tracking how image content moves. A feature-based pipeline detects distinctive points (corners), matches them between consecutive frames, rejects wrong matches with RANSAC, estimates the relative pose between the frames, and composes those relative poses into a trajectory.

Feature-based visual odometry pipelineImage stream to feature detection, then matching or tracking between frames, then outlier rejection with RANSAC, then relative pose estimation from epipolar geometry, then pose composition into a trajectory.ImageDetect featuresMatch / trackRANSAC outliersRelative pose R, tCompose pose monocular: translation known only up to scale
The standard pipeline. Each stage has a measurable output that tells you whether it is healthy.

Because each step estimates the motion relative to the previous frame, errors accumulate: VO is dead reckoning with a camera and drifts, typically as a small percentage of distance travelled when it works well.

Animation of the scale ambiguity of monocular visual odometry. A three dimensional scene and a camera translation are scaled together by a factor between 0.5 and 2, shown from above. The pixel positions of the scene points in the two images do not change at all, with the maximum change printed as zero pixels, so the two cameras cannot tell the large scene and long baseline from the small scene and short baseline.
Scaling the scene and the camera translation by the same factor gives identical images: two-view geometry fixes the direction of motion but not its length. The scale must come from outside (an IMU, a known object, stereo or a range sensor). Computed with a pinhole camera, f=500f=500 px. (Looping, about 6 seconds; drawn and computed for this course with illustrative values.)

Engineering intuition

For a camera that translates, nearby points move a lot in the image and distant points move little (parallax). For a camera that rotates, all points shift together regardless of depth. Parallax therefore carries the translation, and uniform image shifts carry the rotation. Without parallax (a distant scene, a pure rotation) the translation is unobservable, and VO degenerates.

A single camera sees only bearings, not distances, so it can tell you the direction of motion but not how far it went: doubling the scene and the motion gives identical images. That unknown scale is the central weakness of monocular VO and the main reason a UAV adds an IMU, a stereo pair or a rangefinder.

Mathematical formulation

For corresponding points xx, x′x' (homogeneous, in normalised camera coordinates, K−1K^{-1} applied to pixels), the two views satisfy the epipolar constraint:

Epipolar constraint and essential matrix
x′⊤E x=0,E=[t]×R\mathbf x'^{\top}\mathbf E\,\mathbf x = 0, \qquad \mathbf E = [\mathbf t]_{\times}\mathbf R
xx, x′x'
normalised image coordinates of the same 3-D point in the first and second camera.
RR, tt
rotation and translation taking points from the first camera frame to the second: X′=R X+tX'=R\,X+t.
[t]ₓ
skew-symmetric matrix of tt, so [t]ₓ v = t × v.
EE
essential matrix: rank 2 with two equal singular values and one zero.

The constraint is linear in the nine entries of EE, so five points suffice with the minimal solver (Nistér) and eight with the classical eight-point algorithm: stack the equations, take the singular vector of the smallest singular value, and enforce the rank-2 / equal-singular-values structure. Decompose with the SVD, E=U diag⁡(1, 1, 0)V⊤E=U\,\operatorname{diag} (1,\,1,\,0)V^{\top}:

Decomposition of E
R=UWV⊤  or  UW⊤V⊤,t=± u3,W=[0−10100001]\mathbf R = \mathbf U\mathbf W\mathbf V^{\top} \ \text{ or } \ \mathbf U\mathbf W^{\top}\mathbf V^{\top}, \qquad \mathbf t = \pm\,\mathbf u_3, \qquad \mathbf W = \begin{bmatrix}0 & -1 & 0\\ 1 & 0 & 0\\ 0 & 0 & 1\end{bmatrix}
This gives four (R, t) candidates: keep the one that places the triangulated points in front of both cameras (cheirality).

The translation is recovered only up to scale: EE is unchanged if tt is multiplied by a constant. Stereo (known baseline), an IMU (metric acceleration) or a known altitude fixes the scale.

RANSAC

Repeatedly sample a minimal set of matches, fit EE, count matches with small epipolar error (inliers), and keep the model with most inliers. The number of iterations needed is N=log⁡(1−p)/log⁡(1−(1−ε)s)N=\log (1-p)/\log (1-(1-\varepsilon )^{s}) for a desired success probability pp, outlier fraction ε\varepsilon and sample size ss: for s=5s=5, ε=0.5\varepsilon =0.5, p=0.99p=0.99 this is about 146.

Architecture and data flow

Camera (hardware-timestamped frames) → front-end (detect, track) → relative-pose solver → pose and covariance → fusion (lesson M3-T9). The front-end’s health metrics (feature count, inlier ratio, parallax) must be published with the pose so a monitor can lower confidence before the estimate fails (lesson M8-T12).

Real-world UAV applications

  • GNSS-denied navigation indoors, under canopy and in urban canyons.
  • Optical-flow-based velocity for position hold (a simplified VO).
  • Front-end of VIO and visual SLAM systems (ORB-SLAM3, VINS-Fusion, OpenVINS).
  • Landing on moving platforms and relative navigation near structures.

Implementation example

This runs as shown: it creates a synthetic scene and camera motion, estimates EE with the eight-point algorithm, decomposes it, and verifies that the rotation is recovered exactly and the translation direction matches (the length is lost).

import numpy as np
rng = np.random.default_rng(0)

def rodrigues(w):
    th = np.linalg.norm(w)
    if th < 1e-12: return np.eye(3)
    k = w / th; K = np.array([[0, -k[2], k[1]], [k[2], 0, -k[0]], [-k[1], k[0], 0]])
    return np.eye(3) + np.sin(th) * K + (1 - np.cos(th)) * K @ K

X = rng.uniform([-4, -3, 6], [4, 3, 14], size=(60, 3))      # 3-D points in front of camera 1
R_true = rodrigues(np.array([0.02, -0.05, 0.03]))
t_true = np.array([0.8, 0.1, 0.2])                         # metres: scale is arbitrary
X2 = X @ R_true.T + t_true
x1 = X[:, :2] / X[:, 2:3]; x2 = X2[:, :2] / X2[:, 2:3]       # normalised image coordinates
x1 += 1e-4 * rng.standard_normal(x1.shape); x2 += 1e-4 * rng.standard_normal(x2.shape)   # pixel noise at f ~ 700 px

def eight_point(a, b):
    A = np.array([[bx*ax, bx*ay, bx, by*ax, by*ay, by, ax, ay, 1] for (ax, ay), (bx, by) in zip(a, b)])
    E = np.linalg.svd(A)[2][-1].reshape(3, 3)
    U, _, Vt = np.linalg.svd(E)
    return U @ np.diag([1, 1, 0]) @ Vt                      # enforce the essential-matrix structure

E = eight_point(x1, x2)

def decompose(E):
    U, _, Vt = np.linalg.svd(E)
    if np.linalg.det(U) < 0: U = -U
    if np.linalg.det(Vt) < 0: Vt = -Vt
    W = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1.0]])
    return [(U @ W @ Vt, U[:, 2]), (U @ W @ Vt, -U[:, 2]), (U @ W.T @ Vt, U[:, 2]), (U @ W.T @ Vt, -U[:, 2])]

def in_front(R, t, a, b):
    count = 0
    for (ax, ay), (bx, by) in zip(a, b):                    # triangulate by least squares: depth along ray 1
        r1 = np.array([ax, ay, 1.0]); r2 = np.array([bx, by, 1.0])
        M = np.stack([R @ r1, -r2], axis=1)
        s, *_ = np.linalg.lstsq(M, -t, rcond=None)
        count += (s[0] > 0) and (s[1] > 0)
    return count

best = max(decompose(E), key=lambda c: in_front(c[0], c[1], x1, x2))
R_est, t_est = best
ang = np.degrees(np.arccos(np.clip((np.trace(R_est.T @ R_true) - 1) / 2, -1, 1)))
cos_t = t_est @ t_true / np.linalg.norm(t_true)
print(f"rotation error {ang:.4f} deg, translation direction cosine {cos_t:.5f}  (length lost: |t_est| = {np.linalg.norm(t_est):.2f}, true {np.linalg.norm(t_true):.2f})")
assert ang < 0.2 and cos_t > 0.995
Eight-point essential matrix, decomposition and cheirality check. Translation is recovered as a direction only.

Common engineering mistakes

Assuming the monocular translation has metric length

The scale is arbitrary; metric scale needs stereo, an IMU, an altitude or a known object.

Estimating E for a nearly planar or distant scene

Planar scenes give a degenerate EE: use a homography. A scene with no parallax gives no translation information.

Skipping RANSAC or using too few iterations

A few wrong matches can dominate a least-squares fit.

Not publishing front-end health

The downstream filter cannot lower its trust when tracking degrades.

Using a rolling-shutter camera with global-shutter geometry

Fast motion bends straight lines and biases rotation. Model it or use a global shutter.

Failure modes

Failure modes, symptoms and causes
Failure modeHow it shows upLikely cause
Low textureFeature count and inliers collapse; pose freezes or jumpsWhite walls, water, uniform grass, fog
Motion blur or exposure changeFeatures vanish during aggressive manoeuvres or sun transitionsExposure time too long, auto-exposure lag
Dynamic sceneInliers cluster on a moving object; estimate follows itCrowds, vehicles, foliage in wind
Pure rotation or no parallaxTranslation estimate is noisy or wrong while rotation is fineHovering rotation, distant scenes
Scale driftTrajectory shape is right but its size changes over timeMonocular scale is not constrained

Debugging approach

  1. Plot feature count, inlier ratio and median parallax over time: failures are visible there before they appear in the pose.
  2. Overlay tracked features on the image to see whether they sit on the static scene.
  3. Test on a dataset with ground truth first (TUM-VI, EuRoC) and measure drift as a percentage of distance with an evaluation tool such as evo.
  4. Check exposure and frame timestamps: dropped or irregular frames break constant-motion assumptions.

Practical exercises

  1. Compute the number of RANSAC iterations for the five-point solver with 70% outliers and 99.9% confidence.
  2. Add 20% random wrong matches to the code example and compare the eight-point fit with and without RANSAC.
  3. Show that scaling tt and all 3-D points by 2 leaves the images unchanged.
  4. Use the Visual Odometry Simulator to find the texture density at which tracking is lost at 3 m/s.

Interview questions

Write an answer in your own words, then reveal the model answer. Nothing is sent anywhere.

Question 1

Why can monocular VO not recover metric scale?

Model answer

Images are unchanged if the scene and translation are scaled together, so the epipolar constraint determines translation only up to scale. Scale needs extra information: stereo baseline, IMU accelerations, known altitude or object size.

Question 2

What is the essential matrix and what does it encode?

Model answer

E=[t]E=[t]ₓRR relates normalised image points in two views by x′⊤x'^{\top}Ex = 0. It encodes relative rotation and translation direction between two calibrated cameras.

Question 3

Why is RANSAC used?

Model answer

Matches contain outliers that destroy least-squares fits. RANSAC finds the model supported by the largest consistent set by sampling minimal subsets, tolerating a large fraction of wrong matches.

References and further reading

Primary papers, official documentation and standards. Each claim above that is specific to a tool or version points here.

What’s next

Mark this lesson

Saved in this browser and synced to your account when you are signed in.

Practice this lesson

M2-L2: Implement visual feature tracking

Detect corners, match them across frames and see tracking break.

Practice this lesson

M2-L3: Implement basic visual odometry

Estimate camera motion from matched features.

Practice this lesson

M2-L4: Compare monocular and stereo visual odometry

Show scale ambiguity in monocular VO against stereo.

Next lesson

M2-T11: Visual-Inertial Odometry

Explain why combining a camera and IMU resolves metric scale and gravity direction.