Per-camera intrinsics (focal length, principal point, lens distortion) for the 6-camera rig, from images of a plain black/white checkerboard. Used to feed known intrinsics into COLMAP / 3DGS reconstruction.
Two interchangeable implementations that produce the same OpenCV/COLMAP-compatible output
(I prefer MATLAB since it seems to get better results, but both are good)
| Script | Runs all 6 | |
|---|---|---|
| OpenCV (Python) | checkerboard_calib.py |
scripts/run_checkerboard_calib.ps1 (Windows) / scripts/run_checkerboard_calib.sh (Linux/macOS) |
| MATLAB | checkerboard_calib_matlab.m |
run_checkerboard_calib_matlab.m |
Intrinsics only. The rig extrinsics (relative camera poses) are a separate step — see
extrinsics/. A plain checkerboard is fine for intrinsics but not for multi-camera extrinsics.
Hardware this was set up for: Lucid Triton 2.3 MP (IMX392) + Edmund Optics 4 mm fixed lens, images at 1920×1200.
mach-e2-manual-calib/
├─ checkerboard_calib.py # OpenCV (Python) calibrator
├─ checkerboard_calib_matlab.m # MATLAB calibrator
├─ run_checkerboard_calib_matlab.m # MATLAB : all 6 cameras
├─ scripts/ # Windows PowerShell + Linux/mac Bash helpers
│ ├─ run_checkerboard_calib.ps1 # all 6 cameras (OpenCV)
│ ├─ run_checkerboard_calib.sh # all 6 cameras (OpenCV)
│ ├─ fetch_intrinsics_images.ps1 # download images from S3 into images<N>/raw
│ ├─ fetch_intrinsics_images.sh # download images from S3 into images<N>/raw
│ ├─ clear_intrinsics_images.ps1 # empty the images<N> folders
│ ├─ clear_intrinsics_images.sh # empty the images<N> folders
│ ├─ run_full_calib.ps1 # clear -> fetch -> calibrate, in order
│ └─ run_full_calib.sh # clear -> fetch -> calibrate, in order
└─ intrinsics/
├─ images1/raw/ <- put camera 1 PNGs here
├─ images2/raw/ <- put camera 2 PNGs here
├─ ...
└─ images6/raw/ <- put camera 6 PNGs here
Input: raw images go in intrinsics/imagesX/raw/ — one folder per camera.
Output: results are written to the parent intrinsics/imagesX/ (next to raw/).
- Format: lossless PNG, at the camera's native resolution — the same resolution you will use downstream. (These intrinsics are only valid for that resolution.)
- Whole board visible with a light margin around it in every shot. Detection is all-or-nothing; a board touching the frame edge is rejected.
- 30+ views (around 60 is most likely good), varied: different angles, tilts, distances, and positions — including the board pushed into all four corners of the frame (this is what constrains lens distortion at the edges; see the coverage map in §5). Make sure to get pitch and yaw throughout each spot of the picture at different distances.
- Lock focus and aperture on the lens, and calibrate at roughly the working distance you'll use. Re-focusing after calibration invalidates the intrinsics.
The detector counts inner corners (where 4 squares meet), not squares: a board of N×M squares has (N−1)×(M−1) inner corners. Partial/cut-off border squares still form valid corners, so count what the detector sees, not the squares.
- OpenCV: set these at the top of
checkerboard_calib.py:Not sure of the count? Let the script find it (see §4,CHESS_COLS = 13 # inner corners across CHESS_ROWS = 8 # inner corners down SQUARE_LENGTH = 0.079375 # one square's side, in METERS (measure it!)
--probe). - MATLAB: auto-detects the board size — nothing to set. Only
squareSize(optional arg, default0.079375) matters, and only for board pose, not the intrinsics themselves.
Requires Python 3.9+ (3.13 is fine):
pip install "opencv-contrib-python>=4.7" numpy(Optional, recommended — isolate in a venv first:)
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install "opencv-contrib-python>=4.7" numpypython ./checkerboard_calib.py intrinsics/images1/raw --probeCopy the ==> Most likely full board numbers into CHESS_COLS / CHESS_ROWS.
python ./checkerboard_calib.py intrinsics/images2/rawWindows (PowerShell):
.\scripts\run_checkerboard_calib.ps1If you see "running scripts is disabled on this system":
powershell -ExecutionPolicy Bypass -File .\scripts\run_checkerboard_calib.ps1Linux/macOS:
bash scripts/run_checkerboard_calib.sh& "C:\Program Files\MATLAB\R2025b\bin\matlab.exe" -batch "run_checkerboard_calib_matlab"intrinsics_opencv.json— fx, fy, cx, cy, distortion, camera matrixcoverage_opencv.png— where corners landed across all views (see §5)detections/ok_*.jpg/skip_*.jpg— per-image overlays of what was detected
- MATLAB R2021a+ recommended
- Computer Vision Toolbox (calibration) + Image Processing Toolbox (coverage map)
From the repo root in MATLAB:
checkerboard_calib_matlab('intrinsics/images2/raw')
% with an explicit square size (meters):
checkerboard_calib_matlab('intrinsics/images2/raw', 0.079375)run_checkerboard_calib_matlabintrinsics_matlab_camX.json— camera numberXis taken from theimagesXfoldercoverage_matlab.pngboard_positions_matlab.png— camera-centric 3D map of every board pose (see §6)detections_matlab/ok_*.jpg/skip_*.jpg
The _matlab outputs sit next to the _opencv ones so you can compare them.
The board-position map is MATLAB-only (it needs the per-view extrinsics that the
toolbox estimates); the OpenCV path does not emit an equivalent.
- Reprojection error: aim for < 0.5 px (OpenCV prints RMS; MATLAB prints mean, so they won't match exactly even for an identical calibration.)
- Sanity check fx/fy: for the 4 mm lens at 1920×1200, expect fx ≈ fy ≈ 1160–1190 px and cx, cy ≈ 960, 600. Wildly different values mean a bad square size or bad detections.
- Coverage map: dark grey = no corners ever landed there; bright = many. You want color reaching into all four frame corners — that's where the wide lens distorts most. If the edges/corners are dark, capture more views with the board pushed into those corners and re-run.
- Board-position map (
board_positions_matlab.png): the camera sits at the origin (blue frustum, looking down +Z) and every detected board is drawn as a translucent numbered rectangle at its estimated pose, in centimeters. Coverage shows where corners landed in the image; this shows where the board was held in space. You want a spread of depths and tilts, not a flat wall of boards all at one distance — varied range and orientation is what makes focal length and distortion observable. The PNG is a fixed snapshot; MATLAB's built-inshowExtrinsics(cameraParams, 'CameraCentric')renders the same camera-centric view as a live, rotatable figure if you have thecameraParametersobject.
| Symptom | Likely cause / fix |
|---|---|
Every image skip (0 corners) |
Wrong CHESS_COLS/ROWS → run --probe; or the board is cut off at the frame edge in every shot |
skip (unreadable) |
Corrupt/truncated PNG (capture or copy interrupted). Check file size vs. good files and re-export |
skip (resolution mismatch) |
All images for a camera must be the same resolution |
Few usable views (< 8) |
Board not fully visible / too oblique / motion-blurred in most frames |
| High reproj error | Mixed focus, unlocked lens, wrong square size, or a few bad views (inspect detections/) |
PowerShell won't run the .ps1 |
Use the -ExecutionPolicy Bypass form in §4 |
- Pixel origin: MATLAB is 1-based, OpenCV is 0-based. The MATLAB script
exports the principal point as
(cx−1, cy−1)so both JSONs are directly comparable / COLMAP-ready. Focal lengths are unaffected. - Board size: OpenCV needs
CHESS_COLS/ROWS; MATLAB auto-detects it. - Error metric: RMS (OpenCV) vs mean (MATLAB) — see §6.
- Distortion model: both estimate
k1, k2, p1, p2, k3(OpenCVOPENCV/ COLMAPOPENCVmodel order).