from typing import Iterable, Literal
import attrs
import numpy as np
from matplotlib.patches import Polygon
from typing_extensions import TypeAlias, Self
__all__ = ['ObjectiveFov']
Coordinates: TypeAlias = np.ndarray
"""Array[float, 2] in XY"""
[docs]
@attrs.define
class ObjectiveFov:
"""
Class for 2P Field Of View coordinates
Use IBL coordinates space ::
AP (+), anterior to the Bregma. (-) posterior to the Bregma
ML (+), right hemisphere. (-) left hemisphere
.. seealso:: `<https://int-brain-lab.github.io/iblenv/notebooks_external/atlas_working_with_ibllib_atlas.html#Coordinate-systems>`_
"""
region_name: str
"""FOV name"""
am: Coordinates
"""anteromedial (default in mm). `Array[float, 2]` in XY"""
pm: Coordinates
"""posteromedial (default in mm). `Array[float, 2]` in XY"""
al: Coordinates
"""anterolateral (default in mm). `Array[float, 2]` in XY"""
pl: Coordinates
"""posterolateral (default in mm). `Array[float, 2]` in XY"""
#
rotation_angle_ml: float = attrs.field(default=0, kw_only=True)
"""objective ml direction rotation in deg"""
rotation_angle_ap: float = attrs.field(default=0, kw_only=True)
"""objective ap direction rotation in deg"""
#
unit: Literal['mm', 'um'] = attrs.field(default='mm', kw_only=True, validator=attrs.validators.in_(('mm', 'um')))
def __iter__(self) -> Iterable[Coordinates]:
for c in (self.am, self.pm, self.pl, self.al): # order for polygon
yield c
@property
def ap_distance(self) -> float:
"""anterior to posterior distance of the FOV"""
a = np.max([self.am[1], self.al[1]])
p = np.max([self.pm[1], self.pl[1]])
return np.abs(p - a)
@property
def ml_distance(self) -> float:
"""medial to lateral distance of the FOV"""
m = np.max([self.am[0], self.pm[0]])
l = np.max([self.al[0], self.pl[0]]) # noqa: E741
return np.abs(l - m)
[docs]
def to_um(self) -> Self:
"""unit from mm to um"""
if self.unit == 'um':
raise RuntimeError('unit already in um')
factor = 1000
return attrs.evolve(
self,
am=self.am * factor,
pm=self.pm * factor,
al=self.al * factor,
pl=self.pl * factor,
unit='um'
)
[docs]
def ap_invert(self) -> Self:
"""anterior posterior coordinates invert"""
for c in self:
c[1] = c[1] * -1
return attrs.evolve(self, self.am, self.pm, self.al, self.pl)
[docs]
def ml_invert(self) -> Self:
"""medial lateral coordinates invert"""
for c in self:
c[0] = c[0] * -1
return attrs.evolve(self, self.am, self.pm, self.al, self.pl)
[docs]
def invert_axis(self) -> Self:
"""invert both ap and ml axes"""
self.ap_invert()
self.ml_invert()
return attrs.evolve(self)
[docs]
def to_polygon(self, **kwargs) -> Polygon:
"""coordinates to ``matplotlib.patches.Polygon``"""
x = [c[0] for c in self]
y = [c[1] for c in self]
return Polygon(list(zip(x, y)), closed=True, edgecolor='r', facecolor='none', **kwargs)