Source code for stereocomplex.api.corner_refinement

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Literal

import numpy as np


RefineMethod = Literal["raw", "rayfield_tps_robust"]


@dataclass(frozen=True)
class CharucoDetections:
    """
    Minimal detections for one view.

    Coordinates are in the dataset pixel-center convention.
    """

    marker_ids: np.ndarray  # (M,)
    marker_corners: list[np.ndarray]  # list of (4,2)
    charuco_ids: np.ndarray  # (K,)
    charuco_xy: np.ndarray  # (K,2)


[docs] def refine_charuco_corners( *, method: RefineMethod, board: Any, detections: CharucoDetections | None = None, marker_ids: np.ndarray | None = None, marker_corners: list[np.ndarray] | None = None, charuco_ids: np.ndarray | None = None, charuco_xy: np.ndarray | None = None, tps_lam: float = 10.0, huber_c: float = 3.0, iters: int = 3, ) -> np.ndarray: """Refine ChArUco corners using geometric priors on the board plane. The function accepts either a bundled :class:`CharucoDetections` object or the four raw detection arrays. ``method="raw"`` returns the ChArUco detector coordinates unchanged; ``method="rayfield_tps_robust"`` fits a robust 2-D board-plane warp from marker corners to image pixels and evaluates it at the requested ChArUco corner IDs. Parameters ---------- method : {"raw", "rayfield_tps_robust"} Refinement strategy. board : CharucoBoardSpec or OpenCV CharucoBoard Board geometry. ``CharucoBoardSpec`` is converted with ``build_charuco_board``; an OpenCV board must provide ``getIds``, ``getObjPoints`` and ``getChessboardCorners``. detections : CharucoDetections, optional Bundled marker and ChArUco detections for one image. If provided, the four raw detection arguments below are ignored. marker_ids : ndarray, shape (M,), optional ArUco marker IDs detected in the image. marker_corners : list of ndarray, optional Marker corner coordinates in pixels, one ``(4, 2)`` array per marker. charuco_ids : ndarray, shape (K,), optional ChArUco corner IDs to refine. charuco_xy : ndarray, shape (K, 2), optional Initial ChArUco corner coordinates in pixels. tps_lam : float Thin-plate-spline smoothing parameter for ``rayfield_tps_robust``. huber_c : float Huber threshold in pixels for robust TPS fitting. iters : int Number of robust reweighting iterations. Returns ------- ndarray, shape (K, 2) Refined corner positions in pixels, in the same order as ``charuco_ids``. """ if detections is not None: marker_ids = detections.marker_ids marker_corners = detections.marker_corners charuco_ids = detections.charuco_ids charuco_xy = detections.charuco_xy if marker_ids is None or marker_corners is None or charuco_ids is None or charuco_xy is None: raise TypeError( "provide either detections=... or " "marker_ids/marker_corners/charuco_ids/charuco_xy" ) if not ( hasattr(board, "getIds") and hasattr(board, "getObjPoints") and hasattr(board, "getChessboardCorners") ): try: from stereocomplex.api.calibration import CharucoBoardSpec, build_charuco_board # noqa: PLC0415 if isinstance(board, CharucoBoardSpec): board = build_charuco_board(board) except Exception: pass charuco_ids = np.asarray(charuco_ids, dtype=np.int32).reshape(-1) charuco_xy = np.asarray(charuco_xy, dtype=np.float64).reshape(-1, 2) if charuco_ids.size == 0: return np.zeros((0, 2), dtype=np.float64) if method == "raw": return charuco_xy.copy() if method != "rayfield_tps_robust": raise ValueError(f"unknown method: {method}") from stereocomplex.core.rayfield2d import predict_points_rayfield_tps_robust # noqa: PLC0415 marker_ids = np.asarray(marker_ids, dtype=np.int32).reshape(-1) board_ids = np.asarray(board.getIds(), dtype=np.int32).reshape(-1) board_obj = board.getObjPoints() id_to_obj2 = { int(i): np.asarray(p, dtype=np.float64)[:, :2] for i, p in zip(board_ids.tolist(), board_obj, strict=True) } obj_pts: list[np.ndarray] = [] img_pts: list[np.ndarray] = [] for mid, mc in zip(marker_ids.tolist(), marker_corners, strict=True): o = id_to_obj2.get(int(mid)) if o is None: continue mc = np.asarray(mc, dtype=np.float64).reshape(-1, 2) # noqa: PLW2901 if mc.shape != (4, 2) or o.shape != (4, 2): continue obj_pts.append(o) img_pts.append(mc) if not obj_pts: return charuco_xy.copy() obj_xy = np.concatenate(obj_pts, axis=0) img_uv = np.concatenate(img_pts, axis=0) chess2 = np.asarray(board.getChessboardCorners(), dtype=np.float64)[:, :2] target_xy = chess2[charuco_ids] pred = predict_points_rayfield_tps_robust( obj_xy, img_uv, target_xy, lam=float(tps_lam), huber_c=float(huber_c), iters=int(iters), ) return np.asarray(pred, dtype=np.float64).reshape(-1, 2)