Generated from the full canonical file for this source snapshot. Line numbers match the library source.
Source SHA256: befa482ccc8cafccb45f480d6067ca7184eea87e81645865e4355bba61c6b70d
1"""Independent geometric recovery metrics; objective decrease is a separate fact.23These CPU reports consume explicit small landmark sets and rigid transforms.4They never estimate a reference pose from the image used by the optimiser.5"""67from __future__ import annotations89import math10from collections.abc import Sequence11from dataclasses import dataclass1213from dpt.contracts import ContractError, NumericalError14from dpt.geometry import DetectorGeometry, RigidTransform, Vector3, vector3151617@dataclass(frozen=True, slots=True)18class PoseError:19translation_mm: float20rotation_radians: float21landmark_rms_mm: float22landmark_max_mm: float23landmarks: int242526# region book:recovery-geometric-metrics27def pose_error(28recovered: RigidTransform,29reference: RigidTransform,30landmarks_object_mm: Sequence[Vector3],31) -> PoseError:32"""Report origin displacement, geodesic rotation and held-out target errors.3334A translation norm depends on the chosen object origin. Landmark errors also35expose rotation about that origin, so they are reported rather than hidden36inside a single weighted pose norm with arbitrary mixed units.37"""38if not landmarks_object_mm:39raise ContractError("recovery metrics need at least one declared landmark")40# Relative rotation R_reference.T @ R_recovered, independent scalar indexing.41relative = tuple(42math.fsum(reference.rotation[3 * k + i] * recovered.rotation[3 * k + j] for k in range(3))43for i in range(3)44for j in range(3)45)46cosine = max(-1.0, min(1.0, (relative[0] + relative[4] + relative[8] - 1.0) / 2.0))47sine = 0.5 * math.hypot(48relative[7] - relative[5], relative[2] - relative[6], relative[3] - relative[1]49)50distances: list[float] = []51for landmark in landmarks_object_mm:52point = vector3(landmark, "landmark")53a, b = recovered.point(point), reference.point(point)54distances.append(math.hypot(*(x - y for x, y in zip(a, b, strict=True))))55translation = math.hypot(56*(a - b for a, b in zip(recovered.translation_mm, reference.translation_mm, strict=True))57)58rms = math.hypot(*(value / math.sqrt(len(distances)) for value in distances))59if not math.isfinite(translation) or not math.isfinite(rms):60raise NumericalError("recovery metric exceeds the finite coordinate range")61return PoseError(translation, math.atan2(sine, cosine), rms, max(distances), len(distances))626364# endregion book:recovery-geometric-metrics656667def detector_coordinate(geometry: DetectorGeometry, point_world_mm: Vector3) -> tuple[float, float]:68"""Continuous (column, row) of a point's perspective projection.6970Coordinates outside the detector remain meaningful residuals. A point on71the source-parallel plane or behind the source is rejected; no clipped pixel72is substituted for an undefined projection.73"""74point = vector3(point_world_mm, "world landmark")75u, v = geometry.u, geometry.v76normal = (u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0])77offset = tuple(o - s for o, s in zip(geometry.origin_mm, geometry.source_mm, strict=True))78ray = tuple(p - s for p, s in zip(point, geometry.source_mm, strict=True))79plane_distance = math.fsum(a * b for a, b in zip(offset, normal, strict=True))80direction = math.fsum(a * b for a, b in zip(ray, normal, strict=True))81if direction == 0 or not math.isfinite(direction):82raise ContractError("landmark projection is undefined on a source-parallel plane")83scale = plane_distance / direction84if not math.isfinite(scale) or scale <= 0:85raise ContractError("landmark must project onto the forward detector plane")86hit = tuple(scale * r - o for r, o in zip(ray, offset, strict=True))87coordinate = (88math.fsum(a * b for a, b in zip(hit, u, strict=True)) / geometry.spacing_mm[0],89math.fsum(a * b for a, b in zip(hit, v, strict=True)) / geometry.spacing_mm[1],90)91if not all(map(math.isfinite, coordinate)):92raise NumericalError("projected landmark exceeds the finite pixel range")93return coordinate949596def reprojection_rms_pixels(97recovered: RigidTransform,98reference: RigidTransform,99geometry: DetectorGeometry,100landmarks_object_mm: Sequence[Vector3],101) -> float:102if not landmarks_object_mm:103raise ContractError("reprojection metrics need at least one declared landmark")104differences: list[float] = []105for landmark in landmarks_object_mm:106point = vector3(landmark, "landmark")107a = detector_coordinate(geometry, recovered.point(point))108b = detector_coordinate(geometry, reference.point(point))109differences.extend(x - y for x, y in zip(a, b, strict=True))110result = math.hypot(*(x / math.sqrt(len(landmarks_object_mm)) for x in differences))111if not math.isfinite(result):112raise NumericalError("reprojection residual exceeds the finite pixel range")113return result114