Skip to content

Read and write Kalibr camchain YAML

Load a Kalibr camchain YAML into a DS-MSP camera model, and write a DS-MSP model back out as Kalibr YAML.

Move calibrations in and out of the Kalibr ecosystem without retyping intrinsics.

Prerequisites - DS-MSP installed (pip install ds-msp). - A Kalibr camchain*.yaml file to read, or a DS-MSP model to write out. The write recipe below needs no files at all. - Run from the repo root if you follow along with the read recipes below — they read the TUM-VI dataset fetched by scripts/download_datasets.sh tumvi at datasets/tumvi/…. - This is a format I/O recipe — it moves parameters between files and model objects. It does not run a calibration.

Read a camchain in one call

load_kalibr(path, cam="cam0") reads one camera's stanza and returns the matching DS-MSP model. The function picks the model class from the YAML's camera_model / distortion_model fields, so you get back the right type automatically:

from ds_msp.io.kalibr import load_kalibr

path = "datasets/tumvi/dataset-room1_512_16/dso/camchain.yaml"
model = load_kalibr(path, cam="cam0")

print(type(model).__name__)   # -> KannalaBrandtModel
print(model.name)             # -> 'kb'

This needs the TUM-VI dataset on disk, so it stays here as a verified excerpt rather than a docs_src/ file — see docs_src/README.md for why. examples/09_monocular_vo_tumvi.py makes the same call against real footage.

The TUM-VI camchain declares camera_model: pinhole with distortion_model: equidistant — Kalibr's name for the Kannala-Brandt fisheye model. So load_kalibr returns a KannalaBrandtModel. The file picks the class, not you.

Tip

cam="cam0" selects which camera in the chain to load. A stereo camchain has cam0, cam1, … — pass cam="cam1" for the second camera. If the requested name is absent, the loader falls back to the first cam* key it finds.

Get the resolution too

load_kalibr returns only the model. When you also need the sensor size — and you do, to write the file back out — call load_kalibr_with_resolution. It returns (model, (width, height)):

from ds_msp.io.kalibr import load_kalibr_with_resolution

path = "datasets/tumvi/dataset-room1_512_16/dso/camchain.yaml"
model, (width, height) = load_kalibr_with_resolution(path, cam="cam0")

print((width, height))                 # -> (512, 512)
print(round(model.K[0, 0], 4))         # fx -> 190.9785
print(round(model.K[1, 1], 4))         # fy -> 190.9733
print(round(model.K[0, 2], 4))         # cx -> 254.9317
print(model.distortion.round(6).tolist())
# -> [0.003482, 0.000715, -0.002053, 0.000203]   (k1, k2, k3, k4)

Same external-data caveat as above: see examples/02_double_sphere_tumvi.py for the runnable call.

model.K is the 3×3 intrinsic matrix. model.distortion is the (4,) Kannala-Brandt coefficient vector [k1, k2, k3, k4] — the four distortion_coeffs from the YAML, in order.

Which models the Kalibr I/O supports

DS-MSP maps five model families to and from the Kalibr camchain format:

  • On read — the camera_model / distortion_model pair in the YAML decides the DS-MSP class.
  • On save — the model's type decides the fields written.
DS-MSP model Kalibr camera_model distortion_model intrinsics order distortion_coeffs
DoubleSphereModel ds none [xi, alpha, fx, fy, cx, cy] []
EUCMModel eucm none [alpha, beta, fx, fy, cx, cy] []
KannalaBrandtModel pinhole equidistant [fx, fy, cx, cy] [k1, k2, k3, k4]
RadTanModel pinhole radtan [fx, fy, cx, cy] [k1, k2, p1, p2]
UCMModel omni none [xi_mei, fx, fy, cx, cy] []

Two mappings need care

  • UCM xi — Kalibr's omni model stores the Mei mirror parameter xi_mei = alpha / (1 - alpha), not DS-MSP's unified alpha. The I/O converts between them automatically on both read and write.
  • RadTan k3 — Kalibr's radtan has only four coefficients (k1, k2, p1, p2). A non-zero k3 cannot be stored, so it is dropped on export with a warning. Keep k3 = 0 if you need an exact RadTan round-trip.

Note

A plain pinhole with distortion_model: none loads as a RadTanModel with zero distortion. An omni model carrying distortion raises NotImplementedError — the combination is not representable.

One recipe, five families

This page shows two families end to end — a Kannala-Brandt read and a Double Sphere write — as representative of all five:

  • load_kalibr reads whatever class the file declares.
  • save_kalibr emits whatever class you hand it.

To write a EUCM, UCM, or RadTan file, pass that model to the same save_kalibr call below; it places the fields from the row above.

The same module also reads/writes one DS-MSP-only extension format, DSPlusModel (ds_plus) — omitted from the table above since it isn't a real Kalibr camera type, but supported the same way if your camchain declares one.

Write a model to Kalibr YAML

save_kalibr(model, path, width, height, cam="cam0") writes one model as a single-camera camchain. The function reads the model's type and emits the matching camera_model / distortion_model / intrinsics fields from the table above:

"""Write a DS-MSP model out as a Kalibr camchain YAML.

``save_kalibr`` reads the model's type and emits the matching ``camera_model`` /
``distortion_model`` / ``intrinsics`` fields (see ``ds_msp/io/kalibr.py``'s module
docstring for the full per-model table). Uses the bundled Double Sphere sample
calibration (``DoubleSphereModel.sample()``) so this runs with no external file.
"""
import os
import tempfile

from ds_msp.io.kalibr import save_kalibr
from ds_msp.models.double_sphere import DoubleSphereModel


def main() -> None:
    ds = DoubleSphereModel.sample()  # bundled DS calibration, no file needed
    out = os.path.join(tempfile.gettempdir(), "camchain.yaml")

    save_kalibr(ds, out, width=1920, height=1080, cam="cam0")
    print(open(out).read())


if __name__ == "__main__":
    main()
$ python -m docs_src.how_to.read_write_kalibr.write_kalibr_yaml
cam0:
  camera_model: ds
  intrinsics: [0.183, 0.809, 711.57, 711.24, 949.18, 518.81]
  distortion_model: none
  distortion_coeffs: []
  resolution: [1920, 1080]

The intrinsics list leads with xi, alpha (0.183, 0.809) — the Double Sphere ordering Kalibr expects — then fx, fy, cx, cy (711.57, 711.24, 949.18, 518.81).

save_kalibr placed every field in the right slot, so you never format the YAML by hand.

Confirm the round-trip

A save followed by a load must return the same model with the same parameters. Run this check to verify interop before you ship a file downstream:

"""Confirm a save-then-load round-trip through the Kalibr YAML format is exact.

Writes the bundled Double Sphere sample calibration out, reads it back, and
compares ``params`` -- the flat parameter vector every DS-MSP model exposes.
No external file is needed: everything round-trips through a temp file.
"""
import os
import tempfile

import numpy as np

from ds_msp.io.kalibr import load_kalibr, save_kalibr
from ds_msp.models.double_sphere import DoubleSphereModel


def main() -> None:
    ds = DoubleSphereModel.sample()
    out = os.path.join(tempfile.gettempdir(), "rt.yaml")

    save_kalibr(ds, out, width=1920, height=1080, cam="cam0")
    back = load_kalibr(out, cam="cam0")

    print(type(back) is type(ds))                          # same DS-MSP class
    print(np.allclose(back.params, ds.params, atol=1e-9))   # same parameters
    print(np.max(np.abs(back.params - ds.params)))          # exact max diff


if __name__ == "__main__":
    main()
$ python -m docs_src.how_to.read_write_kalibr.roundtrip_kalibr_yaml
True
True
0.0

The reloaded model is the same class with identical params — max difference 0.0 here, exact to machine precision.

The same exact round-trip holds for EUCM, KB, UCM, and RadTan with k3 = 0; RadTan with k3 ≠ 0 loses only k3, per the note above.

Note

model.params is each model's flat parameter vector — for Double Sphere, [fx, fy, cx, cy, xi, alpha]. Comparing params is the quickest way to assert two models are equal.

Read stereo extrinsics

A multi-camera camchain stores each camera's pose relative to the previous one in T_cn_cnm1, a 4×4 transform. Call load_kalibr_extrinsics(path, cam="cam1") to read that matrix:

import numpy as np
from ds_msp.io.kalibr import load_kalibr_extrinsics

path = "datasets/tumvi/dataset-room1_512_16/dso/camchain.yaml"
T_cam1_cam0 = load_kalibr_extrinsics(path, cam="cam1")   # 4x4, cam0 -> cam1
print(T_cam1_cam0.shape)                                 # -> (4, 4)
print(round(float(np.linalg.norm(T_cam1_cam0[:3, 3])), 5))  # baseline -> 0.10109 m

Same external-data caveat as the read recipes above: see examples/06_stereo_extrinsics_tumvi.py for the runnable call.

T_cn_cnm1 maps points from the previous camera's frame into this one, so cam1's T_cn_cnm1 is T_cam1_cam0. The TUM-VI stereo baseline read here is 0.10109 m (~10 cm).

Warning

Only cam1 and later cameras carry T_cn_cnm1. cam0 is the chain origin and has no pose relative to a previous camera, so calling load_kalibr_extrinsics(path, cam="cam0") raises KeyError: "'cam0' has no T_cn_cnm1 in ...". Pass cam="cam1" (or higher) to read a transform; for cam0 the relative pose is the identity by definition.

Real-world usage

examples/09_monocular_vo_tumvi.py loads the same TUM-VI camchain with load_kalibr to drive monocular visual odometry on real data — the recipe above is exactly the front end of that pipeline.

Confirm it works for a second camera

A multi-camera camchain has one block per camera. Change cam="cam0" to cam="cam1" in the read recipe and reload from the same TUM-VI camchain — same KannalaBrandtModel class, different fx/distortion (it's the other physical lens): fx rounds to 190.4424.

Next steps


Recap

  • load_kalibr(path, cam) reads a camchain stanza into the DS-MSP model the file declares.
  • load_kalibr_with_resolution adds (width, height).
  • save_kalibr(model, path, width, height, cam) writes a model back out with the right Kalibr field ordering.
  • Five model families are supported (DS, EUCM, KB, RadTan, UCM); omni uses the Mei xi_mei parameter; RadTan k3 cannot be stored.
  • Round-trips are exact to machine precision (0.0 max param diff).

Source: ds_msp/io/kalibr.py.