Camera calibration
Operator workflow for chessboard targets anddimos cameracalibrate (ROS-style CameraInfo YAML). The square size you pass to the CLI must match the board you actually print and measure.
Print and measure the chessboard
dimos cameracalibrate uses --cols and --rows as the number of inner corner points along each axis, the same convention as cv2.findChessboardCorners patternSize=(cols, rows).
A practical default is 8 by 6 inner corners on A4: enough intersections for a stable solve without making each square too small for a typical desk webcam.
- Generate a checkerboard. OpenCV documents pattern creation in Create calibration pattern. To draw your own board, download and run OpenCV’s gen_pattern.py from that OpenCV version (see the tutorial for dependencies). In that script,
--columnsand--rowscount checker squares along each axis. For 8 by 6 inner corners, use 9 columns and 7 rows of squares (inner corners are one less than square count in each direction):
--square_size so the pattern fits with margins; convert SVG to PDF in your viewer if needed.
- Print at nominal scale. Turn off “fit to page” or other scaling that would change the printed square size relative to the file.
-
Measure one printed square with calipers. Use the edge length of a single black or white square on the printed sheet, not the value from the generator unless you verified the print. Convert to meters for
--square-size-m(for example 24.85 mm becomes0.02485).
Capture practice
- Aim for 15-25 frames with the board fully in view and inner corners detected in each; keep a few spares so you can drop outliers.
- Cover the full image over the set: include poses where the board reaches toward the frame edges and corners, not only the center.
- Vary tilt and camera-to-board distance between frames so the solver sees diverse rigid poses.
- Lock exposure and use fixed white balance when the camera or capture app allows it, so brightness does not drift across the sequence.
- Avoid motion blur: mount or brace the camera, use enough light, and only save frames when the preview is sharp (same for stills saved to a folder).
./capture:
--square-size-m skews metric geometry even when reprojection error looks good.
Run dimos cameracalibrate
Run from the repo or any directory where uv run dimos resolves (same pattern as other dimos CLIs). Required flags are always --source, --cols, --rows, and --square-size-m. Folder mode also requires --images.
Webcam (interactive). Open a live preview on device --device-index. When inner corners are detected, the board is drawn on the preview; press Space to accept the current frame. The CLI collects --target-count accepted frames (default 20) then solves. Press q to quit early (that aborts unless enough frames were already accepted). The detector first tries --cols and --rows as inner-corner counts, then also accepts the common square-count form (for example a 12 by 8 square board is detected as 11 by 7 inner corners).
*.png, *.jpg, and *.jpeg in the directory, sorted by filename. Each image should show the full board with detectable inner corners.
--target-count (default 20). The publisher must emit sensor_msgs.Image; the calibration script normalizes to BGR via Image.to_opencv().
--topic is a single URI <proto>:<channel> from the pubsub registry (dimos.protocol.pubsub.registry). Pick the proto that matches how the publisher transports the image:
lcm:/color_image— standard typed LCM stream.jpeg_lcm:/color_image— Go2smartblueprint and other JPEG-LCM publishers.pshm:color_image— Go2basicblueprint (pickled shared memory).shm/jpeg_shm/plcmare also accepted for niche cases.
#<msg_type> URI suffix forwards a fully-qualified message name to the registry (e.g. lcm:/color_image#sensor_msgs.Image); when omitted, the calibration CLI passes sensor_msgs.Image for typed protos. Pickled / self-describing protos (plcm, pshm, …) ignore the type.
If the topic stays silent, --topic-timeout-sec (default 60) aborts the run instead of hanging the terminal. Increase it if your publisher is slow to start.
Distortion model: pinhole vs fisheye
--distortion-model selects the lens model the solver uses. Default is plumb_bob; pass --distortion-model fisheye for genuine wide-angle / fisheye lenses (e.g. the Go2 front camera).
plumb_bob—cv2.calibrateCamerawith the 5-coefficient radial-tangential model. Right for near-pinhole lenses (typical webcams, narrow-FOV USB cameras). YAML emitsdistortion_model: plumb_bob.fisheye—cv2.fisheye.calibratewith the 4-coefficient Kannala-Brandt model. YAML emitsdistortion_model: equidistant(the ROS-canonical name). Use this whenever the lens has noticeable barrel distortion or HFOV beyond roughly 100°.
K and D are nonsense. Plumb-bob fit to a fisheye lens typically produces inflated focal lengths and k coefficients far outside the usual [-0.5, 0.5] range (you’ll see numbers like k1 ≈ -1.6, k2 ≈ 4.7). Fisheye fit to a near-pinhole lens just over-parametrises and behaves similarly. When in doubt, look at the printed focal length vs the lens’s nominal FOV — fx ≈ width / (2 · tan(HFOV/2)) is a useful sanity check.
distortion_model; raw K works for either model, but anything that touches D (e.g. cv2.undistort vs cv2.fisheye.undistortImage) needs to pick the matching OpenCV function.
Output files are explicit. Pass --out ./camera_info.yaml to write the ROS CameraInfo YAML. Pass a preview PNG path immediately after it to write a corner-overlay preview, for example --out ./camera_info.yaml ./camera_info.preview.png. If you omit both output paths, the command still runs calibration and prints RMS, but does not write YAML or PNG files. A preview PNG path without --out is rejected.
Optional flags (shared across sources): --target-count (webcam/topic; default 20), --camera-name (default webcam), --no-display (no OpenCV window; for headless or automation), --debug (write detailed capture logs to the system temp directory).
On success the process prints the calibration RMS, the detected pattern, and any output paths you requested. Example:
Verify the YAML
You can sanity-check an existing CameraInfo YAML without rerunning calibration.-
Open the file and confirm
image_widthandimage_heightmatch the resolution your camera actually delivers in the stack (same width and height as your calibration images or as yourWebcamwidth and height settings). Wrong dimensions mean intrinsics are being applied to the wrong raster shape. -
Confirm
distortion_modelisplumb_bob. dimosload_camera_inforeads this key (notcamera_model); other values are only valid if every consumer in your pipeline agrees on the same model. -
frame_id: the YAML written bydimos cameracalibratedoes not embedframe_id. When you build aCameraInfofrom the file (for exampleload_camera_info(path, frame_id=...)), theframe_idyou pass must match theImage.header.frame_idused for that camera in your graph. The stockWebcampublishes color images with frame idcamera_optical(or{frame_id_prefix}/camera_opticalifframe_id_prefixis set). The no-robot desk blueprint (ticket T5 in the feature backlog) publishes TF so that optical frame is consistent with that naming; keep your loadedCameraInfoheader aligned with whatever your desk stack actually publishes for the optical camera.
dimos/hardware/sensors/camera/zed/single_webcam.yaml. Treat it as a schema reference, not as calibration numbers for your camera.