Skip to content

Convert a calibration between camera models

Translate an already-calibrated camera from one model to another without images and without recalibrating — e.g. take a Double Sphere calibration and hand a downstream tool an OpenCV-ready Kannala-Brandt model instead.

Prerequisites - DS-MSP installed (pip install ds-msp). - A source model you have already calibrated. This recipe uses the bundled sample calibration so it runs with no files; load your own at the end. - You want a different model's parameters, not new measurements. Conversion fits the target model to reproduce the source's geometry; it adds no new information.

Convert in one call

convert(source, target_class, width=..., height=...) returns the fitted target model and a quality report. Pass the source instance and the target class:

"""How-to: convert a calibration between camera models, in one call.

`convert(source, target_class, width=..., height=...)` fits `target_class` to
reproduce `source`'s geometry -- no images, no recalibration. Runs on the bundled
Double Sphere sample calibration (`DoubleSphereModel.sample()`, no file needed),
converting it to Kannala-Brandt (OpenCV `cv2.fisheye`'s model).
"""
from ds_msp import DoubleSphereModel, KannalaBrandtModel, convert


def main() -> None:
    ds = DoubleSphereModel.sample()        # the bundled DS calibration (no file needed)

    kb, report = convert(ds, KannalaBrandtModel, width=1920, height=1080)
    print(f"rms_px={report['rms_px']:.5f}")
    print(f"converged={report['converged']}")


if __name__ == "__main__":
    main()
$ python -m docs_src.how_to.convert_between_models.convert_in_one_call
rms_px=0.00021
converged=True

kb is a KannalaBrandtModel whose project/unproject reproduce the original Double Sphere camera.

report["rms_px"] is the RMS pixel disagreement between the two models, sampled across the image. Here it reads 0.00021 px, far below one pixel, so the conversion is effectively lossless.

Tip

You pass KannalaBrandtModel (the class), not an instance. The converter constructs and fits the target itself.

Read the quality report

The report tells you whether the conversion is faithful and over what field of view. Always check it. Some conversions are lossy, and the report is how you catch that — a conversion never fails silently.

"""How-to: read every field of convert()'s quality report.

Repeats the same Double Sphere -> Kannala-Brandt conversion as
`convert_in_one_call.py`, then reads the rest of the report. Always check it --
some conversions are lossy, and the report is how you catch that; a conversion
never fails silently.
"""
from ds_msp import DoubleSphereModel, KannalaBrandtModel, convert


def main() -> None:
    ds = DoubleSphereModel.sample()
    kb, report = convert(ds, KannalaBrandtModel, width=1920, height=1080)

    print(f"median_px={report['median_px']:.5f}")   # typical pixel error
    print(f"max_px={report['max_px']:.5f}")          # worst-case pixel error
    print(f"fov_covered_deg={report['fov_covered_deg']:.1f}")
    print(f"source_model={report['source_model']!r}")
    print(f"target_model={report['target_model']!r}")


if __name__ == "__main__":
    main()
$ python -m docs_src.how_to.convert_between_models.quality_report
median_px=0.00018
max_px=0.00099
fov_covered_deg=179.9
source_model='ds'
target_model='kb'
Field Meaning
rms_px RMS pixel disagreement between source and target across the image. Your headline accuracy number.
max_px Worst single-sample error — catches edge-of-image blow-ups.
median_px Median pixel error.
fov_covered_deg Full-angle FOV the fit covered. A narrow target shrinks this.
converged True if the nonlinear least-squares refine (trust-region reflective) converged.
source_model / target_model The two models' names.

A conversion is trustworthy when converged is True and rms_px is small relative to a pixel over the fov_covered_deg you care about.

Pick a target model

Any model in the library is a valid target. How well it reproduces the source depends on how expressive the target is. These numbers are from converting the bundled Double Sphere calibration (1920×1080):

Target model Class rms_px Verdict
Kannala-Brandt KannalaBrandtModel 0.00021 near-exact; OpenCV cv2.fisheye-ready
EUCM EUCMModel 0.014 near-exact
UCM UCMModel 0.334 lossy — UCM has 1 shape parameter (alpha) vs. DS/EUCM's 2
RadTan @ 120° RadTanModel 0.768 lossy — pinhole cannot hold a wide FOV

Supported models: UCM, EUCM, Kannala-Brandt, RadTan, OCamCalib, Double Sphere. All implement the same contract, so any can be a source or a target.

Note

OCamCalib (class OCamModel) is supported but left out of the table above — it converts well (rms_px ≈ 0.55) — the four rows shown are enough to make the expressiveness point.

"""How-to: convert to a different target model -- same call, new class.

Any model in the library is a valid `target_class`. This converts the same
bundled Double Sphere calibration to EUCM instead of Kannala-Brandt.
"""
from ds_msp import DoubleSphereModel, EUCMModel, convert


def main() -> None:
    ds = DoubleSphereModel.sample()

    eucm, report = convert(ds, EUCMModel, width=1920, height=1080)
    print(f"rms_px={report['rms_px']:.3f}")


if __name__ == "__main__":
    main()
$ python -m docs_src.how_to.convert_between_models.pick_target_model
rms_px=0.014

Restrict the FOV for narrow targets

Pinhole-style models (RadTan) cannot represent a >180° fisheye. Converting one without limiting the field of view lets rays the target can never reproduce drag down the fit. Pass max_fov_deg to fit and report only the representable region:

"""How-to: restrict the FOV before converting to a narrower target model.

Pinhole-style models (RadTan) cannot represent a >180 deg fisheye. Converting
one without limiting the field of view lets rays the target can never
reproduce drag down the fit. `max_fov_deg` fits and reports only the
representable cone instead.
"""
from ds_msp import DoubleSphereModel, RadTanModel, convert


def main() -> None:
    ds = DoubleSphereModel.sample()

    rt, report = convert(ds, RadTanModel, width=1920, height=1080, max_fov_deg=120.0)
    print(f"rms_px={report['rms_px']:.3f}")
    print(f"fov_covered_deg={report['fov_covered_deg']:.1f}")


if __name__ == "__main__":
    main()
$ python -m docs_src.how_to.convert_between_models.restrict_fov
rms_px=0.768
fov_covered_deg=119.9

max_fov_deg is the full angle. The report now reflects the 120° cone the pinhole model is meant to cover, instead of being dominated by unrepresentable peripheral rays.

Tip

fov_covered_deg reads 119.9, just under the 120.0 requested. The fit samples a discrete pixel grid, so the widest sampled ray lands a fraction of a degree inside the cap rather than exactly on it.

Warning

A low rms_px over a restricted FOV does not mean the conversion is lossless — it means it is faithful within that FOV. Check fov_covered_deg against the field of view your application actually uses.

Use the converted model everywhere

The converted model is a first-class camera model. Every service in DS-MSP depends only on the model contract, so pose estimation, undistortion, and the rest work on it unchanged. Converting is a one-line swap in your pipeline:

"""How-to: use a converted model with the rest of DS-MSP -- e.g. solve_pnp.

Every DS-MSP service depends only on the CameraModel contract, so a converted
model works everywhere the source model did. Converting is a one-line swap in
a pipeline.
"""
import numpy as np

from ds_msp import DoubleSphereModel, KannalaBrandtModel, convert, solve_pnp


def main() -> None:
    ds = DoubleSphereModel.sample()
    kb, _ = convert(ds, KannalaBrandtModel, width=1920, height=1080)   # DS -> OpenCV fisheye

    object_points = np.array([[0, 0, 0], [0.1, 0, 0],           # (N, 3) known 3D points, metres
                              [0, 0.1, 0], [0.1, 0.1, 0]], dtype=float)
    image_points = np.array([[610, 480], [720, 470],            # (N, 2) their pixels
                             [600, 590], [715, 580]], dtype=float)

    ok, rvec, tvec = solve_pnp(kb, object_points, image_points)   # same call as for the DS model
    print(f"ok={ok}")


if __name__ == "__main__":
    main()
$ python -m docs_src.how_to.convert_between_models.use_everywhere
ok=True

Because KB and RadTan use exactly OpenCV's distortion conventions, the converted model also plugs straight into OpenCV: kb.K and kb.distortion (shape (4,) for KB) go directly into cv2.fisheye.*.

Load your own calibration

The recipe above used DoubleSphereModel.sample() so it runs with no files. To convert a calibration you produced, load it with from_dict and pass your real image size.

Warning

This snippet needs your own JSON file to run. results/calibration_params.json is a placeholder path, not a bundled file — copy-pasting it as-is raises FileNotFoundError until you point it at a real one. That is also why it stays a hand-authored excerpt here rather than a docs_src/ file: docs_src/ files must run unconditionally with no external input (see docs_src/README.md), and there is no such thing as a bundled fixture for your camera.

import json
from ds_msp import DoubleSphereModel, KannalaBrandtModel, convert

# point this at your own file; the JSON needs keys: fx, fy, cx, cy, xi, alpha
with open("results/calibration_params.json") as f:
    ds = DoubleSphereModel.from_dict(json.load(f))   # dict with fx, fy, cx, cy, xi, alpha

# use your camera's real sensor width/height, not the 1920x1080 placeholder
kb, report = convert(ds, KannalaBrandtModel, width=1920, height=1080)
print(report["rms_px"])

from_dict expects the model's parameter names as keys (for Double Sphere: fx, fy, cx, cy, xi, alpha). Each model exposes a matching to_dict to save back. Pass your camera's actual width/height so the sampling and report cover the real sensor.

Next steps


Recap: convert(source, target_class, width, height) fits a target model to an existing calibration with no images, returns it plus a report, and the converted model works with every DS-MSP feature. Check report["rms_px"], converged, and fov_covered_deg; use max_fov_deg for narrow targets.

The sample→unproject→linear-seed→refine conversion design follows the public Fisheye-Calib-Adapter work (credited in the project README). Source: ds_msp/adapt/convert.py, ds_msp/adapt/evaluate.py.