Basler ChArUco Calibrator
Intrinsic camera calibration for Basler cameras using a ChArUco board. Mirrors the UX of the ROS cameracalibrator tool: live detection overlay, automatic frame collection based on coverage, and user-triggered calibration. Saves results as a ROS-compatible YAML file.
Requirements
- uv (Python package manager)
- Python 3.14
- A Basler camera with Pylon drivers installed
- On Ubuntu/Debian:
sudo apt install libgtk2.0-dev(required byopencv-pythonfor GUI)
Installation
git clone <repo>
cd basler_charuco_calibrator
uv sync
Usage
uv run python -m basler_charuco_calibrator
With custom config paths, output directory, or distortion model:
uv run python -m basler_charuco_calibrator \
--board-config configs/board.yaml \
--camera-config configs/camera.yaml \
--save-dir /path/to/output \
--no-k3
| Flag | Default | Description |
|---|---|---|
--board-config |
configs/board.yaml |
ChArUco board parameters |
--camera-config |
configs/camera.yaml |
Basler camera acquisition settings |
--save-dir |
~/.config/strand-cam/camera_info |
Where the calibration YAML is written. Leave at the default — see note below |
--no-k3 |
off | Drop the k3 distortion coefficient from the saved distortion_coefficients.data (still computed internally and kept as a trailing comment in the file) |
If multiple Basler cameras are connected, you will be prompted to select one by number.
Important: The default save directory (
~/.config/strand-cam/camera_info) is wherebraidlooks for camera calibration files. As an end user, you should not change it — only pass--save-dirif you have a specific reason. Ifbraidis installed, this folder already exists; the app does not create it automatically. If it's missing, the app prints an error and exits immediately (before touching the camera), so a run never dies partway through a calibration session:
Save directory does not exist: /home/you/.config/strand-cam/camera_info
Create it (or pass --save-dir) and retry.
Calibration workflow
- Point the camera at a ChArUco board and move it around the frame.
- Frames are captured automatically when they add meaningful coverage — no clicking required.
- Watch the four progress bars in the overlay:
| Bar | What it tracks |
|---|---|
| X | Horizontal position of the board across the frame |
| Y | Vertical position of the board across the frame |
| Size | Apparent board size (distance from camera) |
| Skew | Board tilt/rotation angle |
- When all four bars reach 70% the status line changes to READY — press C to calibrate.
- Press
cto run calibration. - The YAML file is written automatically to the save directory as
Basler-<serial>.yaml. If a file with that name already exists, it's renamed toBasler-<serial>.yaml.bakfirst, so the previous calibration is never silently lost. - Press
qto quit.
Configuration
configs/board.yaml
squares_x: 9 # board columns
squares_y: 7 # board rows
square_length: 0.025 # checker square side length (meters)
marker_length: 0.018 # ArUco marker side length (meters)
dictionary: DICT_4X4_250 # ArUco dictionary
min_corners: 6 # minimum detected corners to accept a frame
coverage_threshold: 0.70 # fraction each metric must reach before calibration is enabled
min_capture_interval: 0.5 # seconds between automatic captures
The board parameters must match your physical board exactly. To print a new board, use the calib.io pattern generator — it produces printable ChArUco PDFs. Settings matching the default config:
| Generator setting | Value | board.yaml key |
|---|---|---|
| Target Type | ChArUco | — |
| Rows | 7 | squares_y |
| Columns | 9 | squares_x |
| Checker Width | 25 mm | square_length (in meters: 0.025) |
| Dictionary | DICT_4X4 | dictionary (DICT_4X4_250 — the 4×4 dictionaries share the same low marker ids) |
| Start Id | 0 | — |
Board Width/Height just need to fit your paper. Two gotchas when printing:
- Print at 100% scale — disable any "fit to page" scaling, then measure the checker side length on the printout and use the measured value for
square_length. Even small scaling errors degrade calibration accuracy. - Marker size is chosen by the generator — measure the ArUco marker side length on the printout and set
marker_lengthto match (meters).
configs/camera.yaml
framerate: 10.0
exposure_time_us: 5000 # used only when auto_exposure is "off"
gain: 0.0
pixel_format: Mono8 # e.g. Mono8, Mono12, BGR8; converted to BGR for OpenCV
# Auto exposure: off | once | continuous
# Gain is never touched by auto exposure — it stays at the manual `gain` value above.
auto_exposure: continuous
auto_exposure_target_brightness: 0.1 # target mean gray value, 0.0-1.0
Output format
Saved to <save-dir>/Basler-<serial>.yaml (default save dir: ~/.config/strand-cam/camera_info, the directory braid reads camera calibrations from). An existing file with that name is backed up to Basler-<serial>.yaml.bak before being overwritten.
The saved YAML matches the camera_calibration_parsers schema used by ROS:
image_width: 1920
image_height: 1200
camera_name: Basler-12345678
camera_matrix:
rows: 3
cols: 3
data: [fx, 0, cx, 0, fy, cy, 0, 0, 1]
distortion_model: plumb_bob
distortion_coefficients:
rows: 1
cols: 5 # 4 if run with --no-k3
data: [k1, k2, p1, p2, k3]
rectification_matrix:
rows: 3
cols: 3
data: [1, 0, 0, 0, 1, 0, 0, 0, 1]
projection_matrix:
rows: 3
cols: 4
data: [fx, 0, cx, 0, 0, fy, cy, 0, 0, 0, 1, 0]
Key bindings
| Key | Action |
|---|---|
c |
Trigger calibration (enabled once coverage threshold is met) |
f |
Force calibration immediately, regardless of coverage |
q |
Quit |
Utilities
scripts/convert_calibration.py reformats an existing calibration YAML so its numeric arrays use inline flow-style ([...]) instead of block-list style, matching the format this app writes:
uv run python scripts/convert_calibration.py input.yaml [output.yaml]
If output.yaml is omitted, the file is converted in place.
License
GPLv3. See LICENSE.