Wheel odometry: turn encoder counts into a moving pose

Convert wheel encoder increments into a differential-drive robot's position and heading. Replay measured counts, calculate exact arc updates, and see how calibration errors and wheel slip change the estimate.

By 12 min read

What you will learn

  • Convert signed wheel encoder counts into angles and wheel travel.
  • Update a planar pose from two wheel-distance increments.
  • Separate encoder measurements, calibration assumptions, and a physical reference.
  • Explain how wheel slip can change motion without changing encoder readings.
  • Recognize why returning to the start does not validate an entire estimated route.

Before you start

A wheel encoder can report a full revolution while the robot travels too little. Slip changes the relationship between wheel rotation and motion across the floor.

Wheel odometry estimates a robot's changing pose by accumulating wheel measurements. This lesson converts counts into a pose, checks one curved update by hand, and shows how the same encoder record can accompany different physical routes.

Begin with a pose and an encoder reading

Describe the axle midpoint with q = (x, y, θ). Position uses meters. Heading uses radians, measured counterclockwise from the positive x direction.

An encoder measures rotation. At each update, subtract the previous reading from the current reading to get a signed increment. Define positive counts on both wheels as forward rolling, even if their physical sensor wiring gives opposite signs.

Odometry needs an initial pose and an initial encoder baseline. Resetting a counter without resetting that baseline creates a false increment. Handling counter wraparound also belongs in the sensor-reading code.

Here both the simulated robot and its estimate start at (0, 0, 0). Odometry tracks motion relative to that chosen frame. The initial pose itself may be uncertain on a real robot.

Convert counts into wheel travel

Let N be the effective encoder counts per wheel revolution. For a signed count increment Δn:

Δφ = 2π Δn / N
Δs = r̂ Δφ

The angle Δφ uses radians. The assumed wheel radius r̂ uses meters. Their product gives an estimated distance in meters under the rolling assumption.

If the encoder sits on a motor shaft, account for gearing. Also account for the decoding mode, such as counting all four edges of a quadrature cycle. N must describe the counts your software actually reports per wheel revolution.

The experiment uses N = 1000 and physical wheel radius 0.1 m. With correct calibration, 1000 counts gives one revolution and 0.2π ≈ 0.628319 m of wheel travel. Negative counts give negative travel.

Find the center motion and heading change

Convert the left and right counts separately, giving Δs_L and Δs_R. Let b̂ be the assumed full distance between the wheels:

Δs = (Δs_L + Δs_R) / 2
Δθ = (Δs_R − Δs_L) / b̂

The sum determines forward travel; the difference determines turning. Equal wheel increments produce a straight update. Equal and opposite increments produce a turn in place.

These are the differential-drive kinematics equations applied to measured increments. The radius and track width describe different parts of the geometry. Using half the track width in this equation would double the estimated turn.

Integrate one encoder interval

Assume constant wheel rates during an interval. The resulting body motion follows a circular arc, with a straight line as its zero-turn limit. Define sinc(a) = sin(a)/a and sinc(0) = 1:

x′ = x + Δs sinc(Δθ/2) cos(θ + Δθ/2)
y′ = y + Δs sinc(Δθ/2) sin(θ + Δθ/2)
θ′ = θ + Δθ

The midpoint angle gives the chord direction. The sinc factor converts arc travel into chord length. At Δθ = 0, the formula becomes an ordinary straight translation.

This update is exact for the assumed constant motion within each interval. Total encoder increments alone cannot reveal changes in wheel-speed ratio inside that interval. More frequent readings help capture those changes.

The implementation uses a series near zero to evaluate sinc without division trouble. It keeps θ unwrapped, so a completed full turn reads 2π, even though its final orientation matches zero. Modern Robotics, Section 13.4 develops the same integration through a body twist and matrix exponential.

Calculate a turning update

Start at (0, 0, 0). Read 250 left counts and 500 right counts, with N = 1000, r̂ = 0.1 m, and b̂ = 0.4 m.

Δφ_L = π/2,   Δφ_R = π
Δs_L = π/20,   Δs_R = π/10
Δs = 3π/40 ≈ 0.235619 m
Δθ = π/8 ≈ 0.392699 rad

The arc radius is Δs/Δθ = 0.6 m. Starting with heading zero gives:

x′ = 0.6 sin(π/8) ≈ 0.229610 m
y′ = 0.6 [1 − cos(π/8)] ≈ 0.045672 m
θ′ = π/8 = 22.5°

Moving the full 0.235619 m along the old heading would give y = 0. That approximation misses the turn during this reading interval. The exact update includes it.

Replay a fixed encoder record

Wheel encoders and accumulated pose

Replay the same encoder record

Compare an encoder estimate with a simulated physical reference. Each of eight intervals lasts one second, with constant wheel rates.

0%
0%
0%

Physical geometry: wheel radius 0.1 m, track width 0.4 m. Encoders report 1000 signed counts per wheel revolution. Radius and track errors affect the estimate only. Travel loss reduces the physical right-wheel travel by the selected fraction, including in reverse; its encoder still reports the same rotation.

3 of 8

Physical reference and encoder estimate

Wheel odometry after 3 intervals: position error 0.000000 metersBoth axes use the same fixed meter scale. The solid blue route and circle show the simulated true pose. The dashed amber route and square show the encoder estimate. Lines from the markers show heading. Matching routes overlap. S marks the common start. The reference is known in this simulation and is unavailable from wheel encoders alone.000.50.5111.51.5x (m)y (m)S
Solid blue line and circle: simulated truth. Dashed amber line and square: encoder estimate. The plot traces the axle midpoint, with no robot footprint, obstacles, or controller. Square describes the intended no-slip sequence; imposed travel loss can change the actual route.
Completed intervals
3 of 8
Last left encoder increment (counts)
1000
Last right encoder increment (counts)
1000
Assumed wheel radius (m)
0.100000
Assumed track width (m)
0.400000
Estimated left increment (m)
0.628319
Estimated right increment (m)
0.628319
Estimated center increment (m)
0.628319
Estimated heading increment (rad)
0.000000
True x (m)
0.628319
True y (m)
0.628319
True heading (rad)
1.570796
Estimated x (m)
0.628319
Estimated y (m)
0.628319
Estimated heading (rad)
1.570796
Position error (m)
0.000000
Heading error (rad)
0.000000
Largest boundary position error (m)
0.000000
True center travel (m)
1.256637
Estimated center travel (m)
1.256637

Poses agree at this interval boundary. Position error: 0.000000 m. The largest position error at completed interval boundaries is 0.000000 m.

Increment readouts describe the last completed interval; step 0 shows zeros. Both headings stay unwrapped. Heading error is estimated heading minus true heading, including accumulated turns. The largest boundary error checks only completed interval endpoints, so it is not a continuous-time maximum.

Fixed encoder record, counts per one-second interval
IntervalLeft countsRight counts
110001000
2-500500
310001000
4-500500
510001000
6-500500
710001000
8-500500

This reference is a controlled simulation. A real robot needs an independent observation to measure odometry error. Constant travel loss is a teaching model, not a traction or tire-force simulation.

The initial view completes three intervals of a square sequence. The first interval moves both wheels forward by 1000 counts. The second uses −500 left counts and +500 right counts for a 90° turn. The third repeats the forward interval.

With zero calibration error and zero travel loss, both poses read (0.628319, 0.628319, 1.570796). Finish all eight intervals to return to the start with heading 6.283185 rad. Each ideal side measures 0.628319 m.

The controls replay a known log. Changing the sequence sets the completed interval count to three and preserves the error settings. Reset restores the original sequence and zero errors.

  • Eight arc intervals repeats the worked count pair eight times. With zero errors, it ends at (0, 1.2, π).
  • Forward, then reverse uses four equal forward increments followed by four equal reverse increments. It helps test whether endpoint agreement hides earlier errors.
  • Completed encoder intervals lets you inspect or rewind each update. The increment readouts show the last completed interval; step zero shows zeros.

The solid blue route uses the simulated physical geometry and travel loss. The dashed amber route uses the encoder record and assumed geometry. They overlap when their models agree.

Change the assumed geometry

The physical radius stays at 0.1 m and the physical track stays at 0.4 m. A +10% assumed-radius error makes the estimator use 0.11 m. It scales both inferred wheel distances, affecting both travel and turns.

A +10% assumed-track error makes the estimator use 0.44 m. For the same encoder pair, it predicts a smaller turn. Later forward updates then follow the wrong heading, creating a position error.

Set the track error to +10% and finish the square with other errors at zero. The physical robot returns to (0, 0). The estimate ends near (−0.146143, 0.227402) m, with heading 5.711987 rad and position error 0.270314 m.

Calibration estimates the effective geometry from measured motion. A measured travel distance helps check wheel scale; an independent turn measurement helps check the radius-to-track ratio. Use several maneuvers and an external reference to separate errors that produce similar encoder predictions.

Separate wheel rotation from ground travel

The travel-loss control imposes a simple physical model. With a loss fraction λ, the right wheel contributes only:

Δs_R,true = r Δφ_R (1 − λ)

The encoder still records Δφ_R. The estimate has no measurement of λ. Loss applies to the magnitude of right-wheel travel in both forward and reverse motion; this constant factor omits tire forces and changing traction.

Set travel loss to 20%, leave calibration errors at zero, and finish the square. The encoder estimate closes its square. The simulated robot ends near (0.088705, 0.829473) m, giving 0.834202 m of position error.

The same encoder counts are compatible with both physical outcomes. Encoders alone cannot identify this loss or supply the true pose. Exact integration removes one numerical approximation; it does not repair an incorrect motion model.

Skid steering gives another reason wheel rotation can mislead an estimate. A base with four fixed conventional wheels needs lateral slip to turn, and a simple effective-track model shows how its yaw can differ from a two-wheel calculation.

Measure error against an independent reference

The experiment knows its physical reference because it generates the motion. A real robot needs another observation, such as surveyed positions, a camera-based tracking system, or known landmarks. Each reference also has its own accuracy limits.

Position error here is the Euclidean distance between the two current positions. Heading error is estimated θ minus true θ, using the full unwrapped angles. The largest boundary error checks the completed interval endpoints only; it does not search for an error peak between readings.

Error need not increase on every step. Symmetry can cancel some errors on a return trip. With the forward/reverse sequence and +10% radius error, the final positions agree even though their midpoint positions differ by 0.125664 m.

WPILib's differential-drive odometry uses a gyro heading alongside encoder distances. That adds a heading measurement absent from this wheel-only experiment. Other observations can constrain position as well; Bayesian inference introduces how evidence changes an uncertain estimate.

Reproduce the updates in Python

This Python 3 example uses only the standard library. It converts counts, integrates the worked arc, and compares two models of the square sequence. Each tuple contains the signed left and right count increments for one interval.

from math import cos, sin, pi, hypot


def update(pose, counts, radius=0.1, track=0.4, right_loss=0.0):
    left, right = (n * 2 * pi * radius / 1000 for n in counts)
    right *= 1 - right_loss
    travel = (left + right) / 2
    turn = (right - left) / track
    half = turn / 2
    sinc = 1 - half**2 / 6 + half**4 / 120 if abs(half) < 1e-6 else sin(half) / half
    x, y, heading = pose
    return (x + travel * sinc * cos(heading + half),
            y + travel * sinc * sin(heading + half),
            heading + turn)


def replay(record, **geometry):
    pose = (0.0, 0.0, 0.0)
    for counts in record:
        pose = update(pose, counts, **geometry)
    return pose


def show(name, pose):
    values = [0.0 if abs(v) < 1e-10 else v for v in pose]
    print(f"{name}: x={values[0]:.6f} m; y={values[1]:.6f} m; "
          f"heading={values[2]:.6f} rad")


square = [(1000, 1000), (-500, 500)] * 4
show("One arc", replay([(250, 500)]))
truth = replay(square)
estimate = replay(square, track=0.44)
show("Square reference", truth)
show("Track +10% estimate", estimate)
print(f"Position error: {hypot(estimate[0]-truth[0], estimate[1]-truth[1]):.6f} m")
slipped = replay(square, right_loss=0.2)
show("Travel loss 20% reference", slipped)

Expected output:

One arc: x=0.229610 m; y=0.045672 m; heading=0.392699 rad
Square reference: x=0.000000 m; y=0.000000 m; heading=6.283185 rad
Track +10% estimate: x=-0.146143 m; y=0.227402 m; heading=5.711987 rad
Position error: 0.270314 m
Travel loss 20% reference: x=0.088705 m; y=0.829473 m; heading=4.398230 rad

The right_loss argument belongs to the simulated reference. An encoder-only estimator cannot fill it in from these counts. The example supplies that value to create a test case with a known physical difference.

Try it yourself

Exercise 1. Predict an underreported turn. Use the square's first turning interval: −500 left counts and +500 right counts. Keep the correct radius, but assume track width 0.44 m. What turn does the estimator report, and what heading error remains after four such turns?

Check the track-width calculation

The wheel increments are −π/10 and +π/10 meters. Their difference is π/5 m. Dividing by 0.44 m gives 1.427997 rad, about 81.818182°, for each turn.

Four turns give 5.711987 rad. The physical turn totals 2π, so the signed heading error is −0.571199 rad, about −32.727273°. Set track error to +10%, use step two to check the first turn, then finish the sequence.

Exercise 2. Test a misleading return. Choose forward/reverse motion with +10% radius error and all other errors zero. At step four, what is the position error? At step eight, do matching endpoints prove the scale calibration was correct?

Check the midpoint and final errors

Four forward increments move the reference by 0.4π ≈ 1.256637 m. The estimate reports 1.1 times that distance, 1.382301 m. Their difference is 0.125664 m.

The four reverse increments cancel both models' forward increments. Final position error is zero, while the largest boundary error remains 0.125664 m. Estimated total travel is 2.764602 m, compared with the reference's 2.513274 m. Closure alone did not check the distance scale.

Sources and further study

Continue with differential-drive kinematics to connect these measured increments to wheel commands. For a steered vehicle, Ackermann steering explains the geometry its motion model must respect.