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 Kalibrcamchain*.yamlfile 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 byscripts/download_datasets.sh tumviatdatasets/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_modelpair 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'somnimodel stores the Mei mirror parameterxi_mei = alpha / (1 - alpha), not DS-MSP's unifiedalpha. The I/O converts between them automatically on both read and write. - RadTan
k3— Kalibr'sradtanhas only four coefficients (k1, k2, p1, p2). A non-zerok3cannot be stored, so it is dropped on export with a warning. Keepk3 = 0if 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_kalibrreads whatever class the file declares.save_kalibremits 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()
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()
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¶
- Convert the loaded model to another model before exporting — e.g. read a Kalibr KB file and write back a Double Sphere or OpenCV-ready model: Convert a calibration between camera models.
- Calibrate from scratch, then export to Kalibr for the rest of your toolchain: Calibrate any camera model.
Recap
load_kalibr(path, cam)reads a camchain stanza into the DS-MSP model the file declares.load_kalibr_with_resolutionadds(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);
omniuses the Meixi_meiparameter; RadTank3cannot be stored. - Round-trips are exact to machine precision (
0.0max param diff).
Source: ds_msp/io/kalibr.py.