ds_msp.detect¶
Detection adapters (checkerboard / ChArUco / AprilGrid) — the OpenCV-facing layer that turns raw images into 3D↔2D correspondences. See detecting every AprilGrid tag for the multi-scale detection deep-dive.
ds_msp.detect ¶
Detection adapters (ChArUco / AprilGrid).
The OpenCV-facing layer: turn images into 3D<->2D correspondences as :mod:ds_msp.data
records. This is the only place under the calibration stack (besides io) where cv2 is
allowed — the geometry/solver path stays NumPy-native. Depends on data + core.
BoardSpec
dataclass
¶
The ChArUco geometry from MC-Calib's config (one entry per board).
Source code in ds_msp/detect/charuco.py
CheckerboardSpec
dataclass
¶
Plain checkerboard geometry: interior-corner counts + metric spacing.
Source code in ds_msp/detect/checkerboard.py
board_object_points ¶
(n_corners, 3) interior-corner positions at square_size spacing, row-major
— identical to MC-Calib's single-board calibrated_objects_data.yml.
Source code in ds_msp/detect/charuco.py
checkerboard_object_points ¶
(rows*cols, 3) interior-corner positions at square_size spacing, row-major with
cols fastest -- identical convention to
:func:ds_msp.detect.charuco.board_object_points, and the direct successor to
ds_msp.utils.build_checkerboard_points.
Source code in ds_msp/detect/checkerboard.py
detect_aprilgrid ¶
detect_aprilgrid(image_paths: Sequence[str], *, family: str = 't36h11', min_tags: int = 6, refine: bool = True, subpix_window: int = 5, scales: Sequence[float] = (1, 2, 3), target: Optional[object] = None, recover: bool = False, recover_neighbors: int = 6, recover_roi_pad: float = 0.6, recover_roi_px: float = 160.0) -> List[Dict[int, np.ndarray]]
Detect AprilGrid tags in a list of images, robust to the fisheye periphery.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image_paths
|
sequence of str
|
Paths to the calibration frames. |
required |
family
|
str
|
AprilTag family of the board (TUM-VI / Kalibr default is |
't36h11'
|
min_tags
|
int
|
Skip frames where fewer than this many tags are found (a near-empty frame contributes little and risks outliers). |
6
|
refine
|
bool
|
Apply |
True
|
subpix_window
|
int
|
Half-window (pixels) for |
5
|
scales
|
sequence of float
|
Image scales to detect at, unioned by tag id (default |
(1, 2, 3)
|
target
|
AprilGridTarget
|
Board geometry; required when |
None
|
recover
|
bool
|
Enable board-guided recovery of still-missing tags (needs |
False
|
recover_neighbors
|
int
|
Recovery knobs — nearest detected tags used for the local homography, ROI padding (× tag size), and the pixel size each ROI tag is up-scaled to. |
6
|
recover_roi_pad
|
int
|
Recovery knobs — nearest detected tags used for the local homography, ROI padding (× tag size), and the pixel size each ROI tag is up-scaled to. |
6
|
recover_roi_px
|
int
|
Recovery knobs — nearest detected tags used for the local homography, ROI padding (× tag size), and the pixel size each ROI tag is up-scaled to. |
6
|
Returns:
| Type | Description |
|---|---|
list of dict
|
One |
Source code in ds_msp/detect/detect.py
detect_checkerboard_corners ¶
Detect one checkerboard in one grayscale image. Returns (rows*cols, 2) pixel
corners in the same row-major, cols-fastest order as :func:board_object_points, or
None on failure. findChessboardCornersSB is all-or-nothing per image (no partial
detection) -- the same contract as ChArUco's detectBoard per board.
Source code in ds_msp/detect/checkerboard.py
detect_folder ¶
detect_folder(image_dir: str, specs: List[BoardSpec], obj: Object3D, cam_id: int, *, legacy: bool = True, min_corners: int = 4, pattern: str = '*', progress_cb: Optional[Callable[[int, int, int, str], None]] = None) -> List[ObjectObs]
Detect ChArUco corners over every image in image_dir for one camera, sequentially.
Returns one :class:ObjectObs per (frame, board), with point_rows indexing
obj.pts_3d via (board_id, corner_id) — the same observation objects the
keypoint reader produces, so the rest of the pipeline is untouched. frame_id is the
raw filename index; :func:detect_rig rebases it to MC-Calib's 0-indexed convention.
progress_cb(cam_id, i, n, path), if given, fires before each image is detected — the
per-image OpenCV detection pass is the part of a real calibration run that otherwise
stays silent for minutes on a large image set. This is the single-camera building block;
:func:detect_rig parallelizes across every camera's images at once rather than calling
this once per camera.
Source code in ds_msp/detect/charuco.py
detect_image ¶
detect_image(detectors, gray: ndarray, *, min_corners: int = 4, subpix: bool = False) -> List[Tuple[int, np.ndarray, np.ndarray]]
Detect every board in one image. Returns [(board_id, corner_ids, pts_2d), ...]
with corner_ids shape (m,) and pts_2d shape (m, 2).
subpix=True runs cv2.cornerSubPix on the interpolated ChArUco corners. OFF by
default: CharucoDetector already sub-pixel-interpolates, and a second pass measurably
wandered on this wide-fisheye rig (RMS 0.89→0.93 px) — the 5px window straddles
neighbouring corners under heavy distortion. Useful on mild-distortion images.
Source code in ds_msp/detect/charuco.py
detect_rig ¶
detect_rig(root_path: str, cam_ids: List[int], specs: List[BoardSpec], obj: Object3D, *, cam_prefix: str = 'Cam_', legacy: bool = True, min_corners: int = 4, progress_cb: Optional[Callable[[int, int, int, str], None]] = None, workers: Optional[int] = None) -> Tuple[List[ObjectObs], Dict[int, Tuple[int, int]]]
Detect over <root_path>/<cam_prefix><cam+1:03d>/ for every camera (MC-Calib's
1-indexed layout). Returns (object_obs, img_size_per_cam).
Detection is parallelised across cameras and images together: one flat
(cam_id, path) task list spanning every camera, run on a single
:class:~concurrent.futures.ThreadPoolExecutor — the same pattern (and the same
measured ~3.5x on an 8-camera rig) as rig/reconstruct.py::detect_board_obs_images.
OpenCV's C++ detection work releases the GIL, so threads scale near-linearly with cores
without needing process-pool pickling of the (potentially large) fused Object3D.
Flat per-image tasks, not per-camera ones, keep every core busy despite very uneven
per-camera image counts (e.g. a 2592px camera with 116 frames next to a 640px one with
58 — a per-camera-only split would leave two workers doing 2x the work of the rest).
workers=None (default) uses os.cpu_count(); 1 forces the original serial
per-camera loop (useful for debugging, or when there's only one camera / one image
total, where pool start-up would only add overhead).
progress_cb(cam_id, i, n, path), if given, still fires once per image, from whichever
worker thread is currently detecting it — serialized by an internal lock so terminal
output from concurrent workers doesn't interleave mid-line. i counts that camera's own
images processed so far (as before). Tasks are round-robin interleaved across cameras
(image-index-major, not camera-major): a naive flat list built camera-by-camera drains
camera 0's entire directory before camera 1's first task is even queued, so the live
progress readout would show a strictly sequential "cam 0 done, then cam 1 starts, ..."
sweep even though the underlying work is genuinely concurrent — measured, not assumed
(rig/reconstruct.py::detect_board_obs_images had the exact same bug).
Source code in ds_msp/detect/charuco.py
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 | |
make_detectors ¶
One :class:cv2.aruco.CharucoDetector per board, with MC-Calib's id offsets.
tuned=True enables the detection tricks that recover more corners a basic detector
drops — tryRefineMarkers (re-find markers missed on the first pass via the board
layout), minMarkers=1 (accept a ChArUco corner adjacent to a single decoded marker,
not two — the board-edge corners), and per-marker CORNER_REFINE_SUBPIX. On the real
8-camera rig it lifts the corner count ~24% (e.g. the 640px fisheyes 87/558 → 397/971).
It is OFF by default because, measured on that rig, the extra corners HURT accuracy: the recovered corners are the strongly-distorted wide-fisheye edge corners, which are noisier — reprojection RMS rose 0.61→0.89 px. Turn it on only for a near-pinhole / mild- distortion rig, or when corner coverage (not accuracy) is the binding constraint.
Source code in ds_msp/detect/charuco.py
single_board_object ¶
Build an :class:Object3D for a single ChArUco board straight from the config.