Skip to content

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 by opencv-python for 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 where braid looks for camera calibration files. As an end user, you should not change it — only pass --save-dir if you have a specific reason. If braid is 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

  1. Point the camera at a ChArUco board and move it around the frame.
  2. Frames are captured automatically when they add meaningful coverage — no clicking required.
  3. 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
  1. When all four bars reach 70% the status line changes to READY — press C to calibrate.
  2. Press c to run calibration.
  3. 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 to Basler-<serial>.yaml.bak first, so the previous calibration is never silently lost.
  4. Press q to 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_length to 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.