Skip to main content

Calibration data

Every unit is calibrated individually at the factory — camera intrinsics, stereo extrinsics, and camera-to-IMU alignment. There is no calibration session on your side, and no target to shoot: recordings are metrically usable out of the box.

Every EGO1 recording carries that calibration inside the MCAP itself, republished at ~1 Hz on five topics. The values are constant for the life of a recording — read the first message of each topic and ignore the repeats (they exist so late-joining live subscribers also receive them).

This page states exactly what those values are, which of them our calibration actually measures and which are fixed defaults that are not calibrated at all, and how a SLAM / VIO pipeline should consume them.

Calibration topics

TopicSchemaContents
/ego/camera/0/intrinsicsfoxglove.CameraCalibrationCamera 0 intrinsics + distortion
/ego/camera/1/intrinsicsfoxglove.CameraCalibrationCamera 1 intrinsics + distortion
/ego/camera/1/extrinsicsfoxglove.FrameTransformPose of cam1 expressed in cam0
/ego/imu/0/extrinsicsfoxglove.FrameTransformPose of imu0 expressed in cam0
/ego/imu/0/infovisio_schema.v1.calibration.ImuCalibrationCam–IMU time offset + nominal IMU defaults

All spatial calibration is anchored to cam0: each FrameTransform gives the pose of that sensor's frame expressed in the cam0 frame (translation in meters, rotation quaternion in x, y, z, w order).

What our calibration measures (per unit)

EGO1 calibration is a camera and camera–IMU calibration (kalibr, AprilGrid target). It produces the following values, measured per device:

  • Camera intrinsicskannala_brandt (fisheye) model: focal lengths fx, fy, principal point cx, cy in K, and four distortion coefficients k1..k4 in D. R is identity and P is [K|0] — images are not rectified; apply the fisheye model yourself.
  • Stereo extrinsic (/ego/camera/1/extrinsics) — pose of cam1 in cam0. Expect a baseline of roughly 63 mm, nearly pure-X, with sub-degree relative rotation.
  • Camera–IMU extrinsic (/ego/imu/0/extrinsics) — pose of imu0 in cam0. The IMU is mounted rotated roughly 180° about Y relative to cam0; do not assume the axes are aligned with the camera.
  • Camera–IMU time offset (time_offset_to_cam0_s in /ego/imu/0/info) — interpret as t_imu = t_cam0 + offset. Typically a few tens of milliseconds; apply it when associating IMU samples with video frames.

The two variants are two different cameras

The kannala_brandt model applies to both variants — it is the right model for either lens, and there is no per-variant change in how you consume the intrinsics. What changes is the values, and by a lot: EGO1 covers 125° H × 75° V, while EGO1GS covers 157° H × 84° V. A field of view that much wider means a substantially shorter focal length and a different distortion curve.

Read fx, fy, cx, cy and k1..k4 out of every recording. Never hard-code one set across a dataset that mixes variants — the intrinsics from an EGO1 will not reproject an EGO1GS frame anywhere near correctly. The IMU also differs: /ego/imu/0/info and the extrinsics are per-unit, but the sample rate is a property of the variant (200 Hz on EGO1, ≈470 Hz on EGO1GS), so a pipeline with a hard-coded IMU period will be wrong for one of them. See EGO1/EGO1GS.

What is NOT calibrated

IMU intrinsics — scale, bias, misalignment — and the IMU noise parameters are not part of EGO1 calibration. No per-unit measurement of these is performed. The ImuCalibration schema has fields for them, but in every recording they hold fixed defaults, and the only calibrated quantity on /ego/imu/0/info is time_offset_to_cam0_s:

  • accel_scale_* = 1.0, gyro_scale_* = 1.0 (identity), bias fields = 0, misalignment fields empty — constants, identical on every unit. Do not read them as "this device was measured and found perfect"; they mean the values were never measured. The raw IMU stream relies on the sensor's factory trim for scale/misalignment (sub-percent error) and is not bias-corrected — see below for why that is the intended design.
  • Noise densities / random walks (accel_noise_density, gyro_noise_density, …) — datasheet-class figures for the sensor family, identical on every unit. Usable as starting values for an estimator's noise model, but they are not per-unit Allan-variance measurements.

The schema semantics are corrected = scale * (raw - bias); with the shipped identity values this is a no-op, so /ego/imu/0/raw is the sensor's direct output (axis-mapped into the body frame only).

What this means for your pipeline

Estimate IMU bias online — do not expect it in the file. Gyro and accelerometer bias change with every power-up and drift with temperature, so no stored constant can be correct at runtime. Every standard VIO / SLAM estimator (OpenVINS, VINS-Mono/Fusion, ORB-SLAM3, …) carries the biases in its state vector and estimates them continuously from raw measurements — feed them /ego/imu/0/raw and let them do so. Scale and misalignment, by contrast, are stable over the device's life and are covered by the sensor's factory trim; assuming identity (as the shipped defaults do) is the standard operating model.

Use the raw IMU stream for SLAM, not the quaternion. /ego/imu/0/quat is the on-device fused orientation — convenient for visualization, monitoring, and initialization, but it is orientation-only (no translation or scale information) and its error is filtered, not white, which violates the noise model tightly-coupled estimators assume. /ego/imu/0/raw is the stream that SLAM should consume.

Expected raw-IMU behavior (not defects)

Because the raw stream is genuinely raw, the following are normal and should not be reported as calibration or drift faults:

  • Gyro bias at rest. Up to roughly ±1 °/s of constant offset per axis, varying with each power-up and with temperature. Integrating raw gyro without bias handling drifts at exactly that rate (e.g. a 0.3 °/s bias accumulates ~3° over 10 s). This is a property of open-loop dead reckoning, not of the device — the on-device fusion and any correct SLAM estimator remove the bias online.
  • Accelerometer gravity reading. A stationary accelerometer reads the ~9.8 m/s² support force distributed across its three axes by the device's tilt — a single axis reads 9.8 · cos(tilt), so e.g. 9.57 m/s² on one axis just means that axis is ~12° off vertical. The health check is the vector norm sqrt(x² + y² + z²) over a stationary stretch: expect local gravity (9.78–9.83 m/s² depending on latitude) within ±0.3 m/s² of uncalibrated per-axis bias.

Reading the calibration in Python

from mcap.reader import make_reader
from mcap_protobuf.decoder import DecoderFactory

TOPICS = [
"/ego/camera/0/intrinsics", "/ego/camera/1/intrinsics",
"/ego/camera/1/extrinsics", "/ego/imu/0/extrinsics", "/ego/imu/0/info",
]

calibration = {}
with open("recording.mcap", "rb") as f:
reader = make_reader(f, decoder_factories=[DecoderFactory()])
for _, channel, _, message in reader.iter_decoded_messages(topics=TOPICS):
calibration.setdefault(channel.topic, message) # values repeat; first is enough
if len(calibration) == len(TOPICS):
break

info = calibration["/ego/imu/0/info"]
print("cam-imu time offset:", info.time_offset_to_cam0_s, "s")

Next