From de12f1815eda25e1bb1029e0c4c7da87560c9414 Mon Sep 17 00:00:00 2001 From: Mac Prible Date: Mon, 20 Jul 2026 12:22:39 -0500 Subject: [PATCH] fix: OpenCV 5 compatibility for charuco detection and intrinsic calibration OpenCV 5 changed two things that broke caliscope: 1. ArUco/Charuco detectBoard returns ids as (N,) instead of (N, 1), and img_loc as (N, 2) instead of (N, 1, 2). The [:, 0] squeeze raised IndexError. Fixed with np.squeeze, which handles both shapes. 2. The default calibrateCamera solver switched to a Schur-complement LM (PR #28461) that converges to bad local minima with sparse data. Fixed by passing CALIB_USE_INTRINSIC_GUESS with a reasonable initial camera matrix, giving both solvers the right basin of convergence. Intrinsic test uses 30 frames instead of 20 to ensure the principal point is well-constrained across solver implementations. Bump to 0.11.3. --- pyproject.toml | 2 +- src/caliscope/core/calibrate_intrinsics.py | 8 ++++++-- src/caliscope/trackers/charuco_tracker.py | 4 ++-- tests/test_calibrate_intrinsics.py | 3 +-- uv.lock | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fe71f7174..a9b7a29cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "caliscope" -version = "0.11.2" +version = "0.11.3" description = "Rapid and reliable multicamera calibration with GUI and scripting API" authors = [{name = "Mac Prible", email = "prible@gmail.com"}] license = { text = "BSD-2-Clause" } diff --git a/src/caliscope/core/calibrate_intrinsics.py b/src/caliscope/core/calibrate_intrinsics.py index 2ef411f7e..5782ea50f 100644 --- a/src/caliscope/core/calibrate_intrinsics.py +++ b/src/caliscope/core/calibrate_intrinsics.py @@ -152,8 +152,11 @@ def calibrate_intrinsics( obj_pts = [p.astype(np.float32) for p in obj_points_list] img_pts = [p.astype(np.float32) for p in img_points_list] - # Pre-initialize output matrices (required by type checker, works at runtime) - camera_matrix = np.zeros((3, 3), dtype=np.float64) + camera_matrix = np.eye(3, dtype=np.float64) + camera_matrix[0, 0] = max(width, height) + camera_matrix[1, 1] = max(width, height) + camera_matrix[0, 2] = (width - 1) * 0.5 + camera_matrix[1, 2] = (height - 1) * 0.5 dist_coeffs = np.zeros(5, dtype=np.float64) error, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( @@ -162,6 +165,7 @@ def calibrate_intrinsics( (width, height), camera_matrix, dist_coeffs, + flags=cv2.CALIB_USE_INTRINSIC_GUESS, ) # calibrateCamera returns dist as (1, 5), flatten it dist = dist.ravel() diff --git a/src/caliscope/trackers/charuco_tracker.py b/src/caliscope/trackers/charuco_tracker.py index 756addbfa..7359562b9 100644 --- a/src/caliscope/trackers/charuco_tracker.py +++ b/src/caliscope/trackers/charuco_tracker.py @@ -110,8 +110,8 @@ def find_corners_single_frame(self, gray_frame, mirror): except Exception as e: logger.debug(f"Sub pixel detection failed: {e}") - ids = _ids[:, 0] - img_loc = _img_loc[:, 0] + ids = _ids.squeeze() + img_loc = _img_loc.squeeze(axis=1) if _img_loc.ndim == 3 else _img_loc # flip coordinates if mirrored image fed in frame_width = gray_frame.shape[1] diff --git a/tests/test_calibrate_intrinsics.py b/tests/test_calibrate_intrinsics.py index bd3f348d9..6159d1456 100644 --- a/tests/test_calibrate_intrinsics.py +++ b/tests/test_calibrate_intrinsics.py @@ -43,8 +43,7 @@ def test_calibrate_returns_valid_result(self): """Calibration produces reasonable camera matrix and distortions.""" image_points, cam0_frames = _load_test_data() - # Use first 20 frames for calibration - selected = cam0_frames[:20] + selected = cam0_frames[:30] result = calibrate_intrinsics( image_points, diff --git a/uv.lock b/uv.lock index 36dcb3be3..7a0d0331c 100644 --- a/uv.lock +++ b/uv.lock @@ -104,7 +104,7 @@ wheels = [ [[package]] name = "caliscope" -version = "0.11.2" +version = "0.11.3" source = { editable = "." } dependencies = [ { name = "av" },