CV Stereo Calibrate (Circle Grid)
The Circle Grid Exists Because a Chessboard Looks the Same Upside Down
- images_left
- images_right
- K_left
- dist_left
- K_right
- dist_right
- K_left
- dist_left
- K_right
- dist_right
- R
- T
- rms_error
- found
This is the same job as CV Stereo Calibrate (Chessboard) - estimate the rotation and translation between two cameras from matched views of a target - but the target is a circle grid, and the reason is geometry, not taste.
A symmetric grid looks identical when you rotate it 180°. If your board is tilted and two cameras see it from different angles, there's no reliable way to tell which corner is which. An asymmetric (staggered) grid breaks that: each row is offset by half a spacing, so the pattern has no rotational symmetry and the detector always knows which way is up. That's why staggered circle grids are the standard target for a stereo rig, and it's why grid_layout defaults to asymmetric (staggered) here.
How it works
Each stereo pair is circle-grid detected independently, and a pair is skipped unless both views find the full grid. The good pairs accumulate into one cv2.stereoCalibrate call, same as the chessboard node.
The intrinsics rules are identical, and they're the part that catches people:
- Connect both
K_leftandK_right(fromCV Calibrate Camera (Circle Grid)) → intrinsics are pinned withCALIB_FIX_INTRINSIC, and only R and T get solved. This is the two-stage workflow you want. - Connect one or neither → the intrinsics are estimated or refined here, and whatever K you passed is only a starting point. One K alone does not hold anything.
What you actually set
images_left / images_right: same order, same length. The tooltip's guidance is 10–20 usable pairs for a good fit; below 3 the node gives up gracefully.
pattern_cols and pattern_rows mean circle centres, and the defaults here are 4 × 11 - a tall staggered grid, not the wide one you'd expect from a chessboard. Get the orientation wrong (rows vs columns swapped) and detection just never fires.
grid_layout picks asymmetric or symmetric geometry. Only switch to symmetric if you're deliberately reusing a symmetric target, and expect the ambiguity above when the board is tilted.
square_size is the physical centre-to-centre spacing, in whatever unit you care about; leave it at 1.0 and the calibration is relative-only.
Outputs are K_left, dist_left, K_right, dist_right, R, T, rms_error and found - a drop-in match for what cv2_stereoRectify wants, after which you remap both frames and hand the rectified pair to CV Stereo Disparity (SGBM).
Install
ComfyUI Manager → search ComfyUI CV, or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Then restart ComfyUI. Python ≥ 3.12 and a recent V3-API ComfyUI build are required. Keep the contrib wheel: a plain opencv-python install shares the same site-packages/cv2 and silently strips the contrib submodules, which takes a whole family of nodes with it. tools/repair_opencv_contrib.py --check will tell you if that already happened.
The repo ships sample target photos - run workflows/01_install_example_inputs.json once, reload the page, and the circle-grid set (circle_calib_00.png …) is in your input folder. 52_circle_grid_calibration.json is the playground.
Where it bites
Under 3 usable pairs you get found=false, an identity R, a zero T and rms_error of 0.0 - no exception. So branch on found; an rms of zero is a failure signal here, not a perfect one.
Print your target properly. A staggered grid printed at 97% scale or slightly stretched is a scale error you've hidden inside a target the detector will still happily find, and no amount of extra views will fix it.
Finally, keep expectations calibrated about the pack itself. bmad4ever's README is unusually blunt: heavy LLM-assisted development, updates not planned, workflows tuned to specific datasets (StereoGeo-CARLA for the stereo side) and not production-grade. The circles-grid detection and the calibration call are textbook OpenCV; it's the surrounding workflow heuristics that are prototype-quality.
Inputs (10)
| Name | Type | Default | Description |
|---|---|---|---|
| images_left | IMAGE | Batch of LEFT circle-grid views (>= 3 usable pairs; ~10-20 gives a good fit). Same order as images_right. | |
| images_right | IMAGE | Batch of RIGHT circle-grid views, same order/length as images_left. | |
| pattern_cols | INT | 42–40 | Circle centers per row (width of the grid). |
| pattern_rows | INT | 112–40 | Circle centers per column (height of the grid). |
| grid_layout | COMBO | asymmetric (staggered) | Grid geometry. Asymmetric staggers each row by half a spacing, which removes the 180-degree rotational ambiguity of symmetric targets - essential for matching a tilted board across two views. |
| square_sizeopt | FLOAT | 1.000.0001–1000000 | Physical center-to-center spacing (e.g. mm); sets the world scale. Leave 1.0 for relative calibration. |
| K_leftopt | NPARRAY | 3x3 intrinsics for the left camera from 'Calibrate Camera (Circle Grid)'. Connect BOTH K_left and K_right to hold the intrinsics FIXED and solve only for R/T; on its own it is just an initial guess and still gets refined. Leave unconnected for a fresh estimate. | |
| dist_leftopt | NPARRAY | Left distortion coefficients from 'Calibrate Camera (Circle Grid)'. Also held fixed when both K matrices are connected. | |
| K_rightopt | NPARRAY | 3x3 intrinsics for the right camera. See K_left: both connected = intrinsics pinned, one alone = initial guess only. | |
| dist_rightopt | NPARRAY | Right distortion coefficients. |
Outputs (8)
| Name | Type | Description |
|---|---|---|
| K_left | NPARRAY | Refined 3x3 intrinsic matrix for the left camera. |
| dist_left | NPARRAY | Refined distortion coefficients for the left camera. |
| K_right | NPARRAY | Refined 3x3 intrinsic matrix for the right camera. |
| dist_right | NPARRAY | Refined distortion coefficients for the right camera. |
| R | NPARRAY | 3x3 rotation matrix from left to right camera. |
| T | NPARRAY | 3x1 translation vector from left to right camera. |
| rms_error | FLOAT | Mean reprojection error in pixels; lower is better. |
| found | BOOLEAN | — |