How calibrationd Calibrates Device Pose and Camera Extrinsics for Accurate Driving Assistance

The calibrationd daemon continuously estimates the device’s roll‑pitch‑yaw (RPY) pose and wide‑angle camera extrinsics by fusing high‑confidence camera odometry with a kinematic model, smoothing estimates through a block‑based moving average, and publishing a liveCalibration message that corrects perception pipelines across the openpilot stack.

The calibrationd process is essential to openpilot’s accuracy, ensuring the vehicle‑mounted device understands its orientation relative to the car and the camera’s physical offset. Located in selfdrive/locationd/calibrationd.py, this dedicated daemon ingests raw sensor data, validates motion conditions, and produces the geometric transforms consumed by locationd, modeld, and the UI to maintain precise lane‑keeping and path planning.

Data Inputs for calibrationd

The daemon subscribes to three primary data sources to build its state:

  • cameraOdometry — Provides raw vehicle‑relative translation (trans), rotation (rot), and their standard deviations (transStd, rotStd). It also carries wideFromDeviceEuler for wide‑camera mounting offsets and roadTransformTrans for height estimates.
  • carState — Supplies vEgo (vehicle speed) used to gate calibration updates to high‑speed, straight‑line motion where odometry is most reliable.
  • liveCalibration (cached) — On startup, the daemon loads previous rpyCalib, validBlocks, wideFromDeviceEuler, and height from the CalibrationParams key to resume calibration across reboots.

The Calibration Pipeline: From Raw Odometry to Validated Pose

Each incoming odometry sample triggers handle_cam_odom (lines 81‑100 in calibrationd.py), which executes a multi‑stage validation and fusion pipeline.

Quality Gating with Straight-and-Fast Logic

Before accepting a sample, calibrationd verifies the vehicle is moving fast and straight to minimize integration error. The checks include:

  • Speed thresholds: v_ego > MIN_SPEED_FILTER and trans[0] > MIN_SPEED_FILTER ensure forward motion dominates.
  • Yaw rate limit: |rot[2]| < MAX_YAW_RATE_FILTER filters out turning maneuvers.
  • Uncertainty bounds: Angle standard deviation must stay below MAX_VEL_ANGLE_STD and height standard deviation below MAX_HEIGHT_STD.

If any check fails, the sample is discarded immediately.

Converting Translation to Orientation Observations

For valid samples, the daemon interprets the 3‑D translation vector as a small‑angle rotation to derive observed pitch and yaw:

observed_rpy = np.array([0,
                        -np.arctan2(trans[2], trans[0]),   # pitch

                        np.arctan2(trans[1], trans[0])])   # yaw

This calculation (lines 102‑104) yields the instantaneous orientation of the camera relative to the vehicle frame based purely on the current odometry drift.

Fusing Observations with Rotation Matrices

Rather than averaging Euler angles directly, the daemon composes rotations in matrix form to avoid gimbal lock and preserve geometric consistency:

new_rpy = euler_from_rot(
            rot_from_euler(self.get_smooth_rpy())
            .dot(rot_from_euler(observed_rpy))
        )

This operation (line 105) applies the observed rotation in the device frame, respecting the current smoothed estimate while integrating fresh data. The result is sanity‑clipped against hard limits (PITCH_LIMITS, YAW_LIMITS, and RPY_INIT) to reject NaNs or impossible values (lines 106‑108).

Block-Based Smoothing and Persistence

Validated observations are blended into a per‑block circular buffer using moving_avg_with_linear_decay (lines 118‑122), which weights recent data more heavily while preserving older samples for stability.

  • Block structure: Each block contains BLOCK_SIZE = 100 samples. When self.idx wraps to 0, the block index advances and valid_blocks increments (lines 124‑128).
  • Calibration readiness: The status transitions from uncalibrated to calibrated only after accumulating INPUTS_NEEDED = 5 valid blocks.
  • Spread monitoring: update_status (lines 138‑147) tracks the max‑min spread of RPY values. Excessive spread triggers an automatic reset to recalibrating (lines 162‑166).
  • Persistence: Every few cycles, the daemon serializes parameters to the Params store under the key "CalibrationParams" (line 70), enabling seamless recovery after power cycles.

Publishing the liveCalibration Message

The get_msg function (lines 233‑247) constructs a Cap’n Proto message published on the liveCalibration channel at approximately 4 Hz (every 5th camera odometry frame). The message contains:

  • validBlocks — Count of filled 100‑sample blocks.
  • calStatus — Enum state (uncalibrated, calibrating, calibrated, invalid, or recalibrating).
  • calPerc — Percentage progress toward the required block count.
  • rpyCalib — Smoothed roll‑pitch‑yaw (roll is fixed to zero per design).
  • rpyCalibSpread — Current pitch/yaw spread values.
  • wideFromDeviceEuler — Extrinsic RPY of the wide‑angle camera relative to the device.
  • height — Calibrated camera height above the ground in meters.

Downstream Consumption in openpilot

Downstream modules consume the liveCalibration message to align their coordinate frames:

  • locationd (line 55 in selfdrive/locationd/locationd.py) reads rpyCalib to compute device_from_calib = rot_from_euler(calib), rotating incoming cameraOdometry into the vehicle frame.
  • modeld (line 348 in selfdrive/modeld/modeld.py) uses the calibrated RPY to transform neural network predictions into the correct camera pose.
  • UI monitors calStatus to display progress bars and alert the driver when recalibration is required after physical device movement.

Practical Code Examples

Reading Persisted Calibration Parameters

from cereal import messaging
from openpilot.common.params import Params

params = Params()
cal_bytes = params.get("CalibrationParams")
if cal_bytes:
    calib_msg = messaging.log_from_bytes(cal_bytes, "log.Event").liveCalibration
    rpy = calib_msg.rpyCalib          # [roll, pitch, yaw] in radians

    height = calib_msg.height[0]      # meters above road

    wide_euler = calib_msg.wideFromDeviceEuler
    print(f"RPY: {rpy}, Height: {height:.2f} m")

Applying Calibration in a Perception Pipeline

import numpy as np
from openpilot.common.transformations.orientation import rot_from_euler

# live_calibration_msg obtained from liveCalibration subscription

device_R = rot_from_euler(np.array(live_calibration_msg.rpyCalib))

# Rotate raw camera translation into vehicle NED frame

trans_vehicle = device_R @ np.array(camera_odometry_msg.trans)

Summary

  • calibrationd fuses cameraOdometry with vehicle speed data to estimate device pose and camera extrinsics in real time.
  • Quality gating restricts updates to straight‑and‑fast motion, ensuring high‑confidence observations.
  • Rotation matrix fusion in handle_cam_odom composes incremental orientation updates while avoiding gimbal lock.
  • Block‑based smoothing with BLOCK_SIZE = 100 and INPUTS_NEEDED = 5 provides stable, drift‑resistant averages.
  • Persistence to CalibrationParams allows recovery across reboots, while liveCalibration messages synchronize the entire openpilot stack.

Frequently Asked Questions

How often does calibrationd update its estimates?

The daemon processes every incoming cameraOdometry message but publishes the liveCalibration result at roughly 4 Hz, corresponding to every 5th odometry frame. This rate balances real‑time responsiveness with network overhead.

What triggers a recalibration event?

If update_status detects that the spread (max‑min) of accumulated RPY values exceeds safe thresholds, or if the device is physically moved while powered off (detected via persistent parameter mismatch), the state transitions to recalibrating and the block buffer resets.

Why does calibrationd require straight‑line driving?

Curved paths introduce centripetal acceleration and yaw‑rate errors that corrupt the small‑angle assumption used to derive observed_rpy from the translation vector. The MAX_YAW_RATE_FILTER and minimum speed checks ensure the vehicle motion approximates a straight line, making the odometry‑to‑pose conversion mathematically valid.

Where is the calibration data stored between drives?

The daemon serializes the current rpyCalib, validBlocks, wideFromDeviceEuler, and height to the Params database under the key "CalibrationParams" approximately every few blocks. On startup, calibrationd attempts to load these values, allowing the system to resume calibration without requiring a full 500‑sample collection cycle after every reboot.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →