Source code for pyetsimul.types.imaging

"""Dataclasses for camera images, pupil data, and imaging results.

Provides structured dataclasses for imaging operations to replace
the Dict[str, Any] pattern with type-safe alternatives.
"""

from dataclasses import dataclass

import numpy as np

from ..core.default_configs import CameraDefaults
from .geometry import Point2D, Point3D


[docs] @dataclass class CameraImage: """Result of camera.take_image() operation.""" corneal_reflections: list[Point2D | None] # CR positions for each light pupil_boundary: list[Point2D] | None # Pupil boundary points as structured types pupil_center: Point2D | None # Pupil center position resolution: Point2D # Camera resolution glint_sizes_px: list[float | None] | None = None # Glint diameters in pixels for each light
[docs] @classmethod def empty(cls, resolution: Point2D, num_lights: int) -> "CameraImage": """Create empty camera image with no detected features.""" return cls( corneal_reflections=[None] * num_lights, pupil_boundary=None, pupil_center=None, resolution=resolution )
[docs] @dataclass class PupilData: """Result of pupil detection/analysis operations.""" boundary_points: np.ndarray | None = None # 2xM matrix of boundary points center: Point2D | None = None # Pupil center position ellipse_params: np.ndarray | None = None # Ellipse fitting parameters area: float | None = None # Pupil area in pixels @property def is_valid(self) -> bool: """Check if pupil data contains valid measurements.""" return self.boundary_points is not None and self.center is not None
[docs] @classmethod def empty(cls) -> "PupilData": """Create empty pupil data indicating no detection.""" return cls()
[docs] @dataclass class EyeMeasurement: """Complete eye measurement from camera.""" camera_image: CameraImage pupil_data: PupilData gaze_direction: Point3D | None = None # 3D gaze direction vector timestamp: float | None = None # Measurement timestamp @property def is_valid(self) -> bool: """Check if measurement contains valid eye tracking data.""" return self.pupil_data.is_valid and any(cr is not None for cr in self.camera_image.corneal_reflections)
[docs] @dataclass class ProjectionResult: """Result of camera projection operation.""" image_points: np.ndarray # 2xn matrix of image coordinates (NaN for invalid points) distances: np.ndarray # 1xn array of distances from camera along optical axis valid_mask: np.ndarray # 1xn boolean array indicating points within image bounds @property def num_points(self) -> int: """Number of projected points.""" return self.image_points.shape[1] if self.image_points.ndim > 1 else 1 @property def valid_points(self) -> np.ndarray: """Get only the valid image points.""" if self.image_points.ndim == 1: return self.image_points if self.valid_mask else np.array([np.nan, np.nan]) return self.image_points[:, self.valid_mask]
[docs] class CameraMatrix: """3x3 camera matrix with convenient properties for focal_length and resolution."""
[docs] def __init__(self, matrix: np.ndarray | None = None) -> None: """Initialize camera matrix. Args: matrix: Optional 3x3 camera matrix. If None, uses defaults. """ if matrix is not None: self._matrix = np.asarray(matrix, dtype=np.float64) else: self._matrix = np.array( [ [CameraDefaults.FOCAL_LENGTH, 0.0, CameraDefaults.PRINCIPAL_POINT_X], [0.0, CameraDefaults.FOCAL_LENGTH, CameraDefaults.PRINCIPAL_POINT_Y], [0.0, 0.0, 1.0], ], dtype=np.float64, )
@property def focal_length(self) -> float: """Get the camera focal length in pixels.""" return float(self._matrix[0, 0]) @focal_length.setter def focal_length(self, value: float | list[float]) -> None: if isinstance(value, (int, float)): self._matrix[0, 0] = float(value) self._matrix[1, 1] = float(value) else: self._matrix[0, 0] = float(value[0]) self._matrix[1, 1] = float(value[1]) @property def resolution(self) -> Point2D: """Get the camera resolution in pixels.""" cx, cy = self._matrix[0, 2], self._matrix[1, 2] return Point2D(x=int(2 * cx), y=int(2 * cy)) @resolution.setter def resolution(self, value: Point2D) -> None: self._matrix[0, 2] = value.x / 2.0 self._matrix[1, 2] = value.y / 2.0 @property def matrix(self) -> np.ndarray: """Get the camera intrinsics matrix.""" return self._matrix def __array__(self) -> np.ndarray: """Enable numpy array conversion for camera intrinsics.""" return self._matrix