"""Value types for camera geometry: pixels, boxes, intrinsics and observations."""
from dataclasses import dataclass
from datetime import datetime
from typing import Protocol
from olo.spatial import Point, Transform, Vector
class FrameLookup(Protocol):
"""Protocol for namespace-scoped frame lookup handles."""
def lookup(
self,
target_frame: str,
source_frame: str,
*,
at: datetime | None = None,
timeout: float = 5.0,
) -> Transform: ...
[docs]
@dataclass(frozen=True, slots=True)
class Pixel:
"""Pixel coordinate, zero-based from the top-left of the image."""
x: float
y: float
[docs]
@dataclass(frozen=True, slots=True)
class BoundingBox:
"""Half-open image bounding box with XYXY pixel coordinates."""
minimum: Pixel
maximum: Pixel
def __post_init__(self) -> None:
if self.maximum.x <= self.minimum.x or self.maximum.y <= self.minimum.y:
raise ValueError("BoundingBox maximum must be greater than minimum")
[docs]
@classmethod
def from_xyxy(cls, x_min: float, y_min: float, x_max: float, y_max: float) -> "BoundingBox":
"""Create a half-open bounding box from ``x_min, y_min, x_max, y_max``."""
return cls(minimum=Pixel(x_min, y_min), maximum=Pixel(x_max, y_max))
[docs]
@classmethod
def from_xywh(cls, x: float, y: float, width: float, height: float) -> "BoundingBox":
"""Create a half-open bounding box from ``x, y, width, height``."""
if width <= 0 or height <= 0:
raise ValueError("BoundingBox width and height must be positive")
return cls.from_xyxy(x, y, x + width, y + height)
@property
def width(self) -> float:
"""Return the box width in pixels."""
return self.maximum.x - self.minimum.x
@property
def height(self) -> float:
"""Return the box height in pixels."""
return self.maximum.y - self.minimum.y
@property
def center(self) -> Pixel:
"""Return the box centre pixel."""
return Pixel(x=(self.minimum.x + self.maximum.x) / 2.0, y=(self.minimum.y + self.maximum.y) / 2.0)
[docs]
def to_xyxy(self) -> tuple[float, float, float, float]:
"""Return ``x_min, y_min, x_max, y_max``."""
return (self.minimum.x, self.minimum.y, self.maximum.x, self.maximum.y)
[docs]
def to_xywh(self) -> tuple[float, float, float, float]:
"""Return ``x, y, width, height``."""
return (self.minimum.x, self.minimum.y, self.width, self.height)
[docs]
@dataclass(frozen=True, slots=True)
class CameraIntrinsics:
"""Pinhole camera calibration parsed from sensor_msgs/CameraInfo."""
width: int
height: int
fx: float
fy: float
cx: float
cy: float
frame_id: str = ""
stamp: datetime | None = None
distortion_model: str = ""
distortion: tuple[float, ...] = ()
[docs]
@dataclass(frozen=True, slots=True)
class PointObservation:
"""Stamped 3D point observation in the camera optical frame."""
point: Point
frame_id: str
pixel: Pixel
depth_metres: float
stamp: datetime | None = None
[docs]
@dataclass(frozen=True, slots=True)
class AlignmentError:
"""Image-space alignment error."""
x_px: float
y_px: float
distance_px: float
converged: bool
[docs]
@dataclass(frozen=True, slots=True)
class PositionError:
"""Cartesian position error."""
vector: Vector
distance: float
converged: bool
frame_id: str = ""
__all__ = [
"AlignmentError",
"BoundingBox",
"CameraIntrinsics",
"FrameLookup",
"Pixel",
"PointObservation",
"PositionError",
]