Skip to content

Commit

Permalink
Fix polygon boundary attributes, return our wrappers instead
Browse files Browse the repository at this point in the history
  • Loading branch information
willGraham01 committed Jan 31, 2025
1 parent 79b03c2 commit 62aacee
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 21 deletions.
26 changes: 22 additions & 4 deletions movement/roi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Literal, TypeAlias

import shapely
from shapely.coords import CoordinateSequence

from movement.utils.logging import log_error

Expand Down Expand Up @@ -44,6 +45,22 @@ class BaseRegionOfInterest:
_name: str | None
_shapely_geometry: __supported_type

@property
def coords(self) -> CoordinateSequence:
"""Coordinates of the points that define the region.
These are the points passed to the constructor argument ``points``.
Note that for Polygonal regions, these are the coordinates of the
exterior boundary, interior boundaries must be accessed via
``self.region.interior.coords``.
"""
return (
self.region.coords
if self.dimensions < 2
else self.region.exterior.coords
)

@property
def dimensions(self) -> int:
"""Dimensionality of the region."""
Expand All @@ -57,7 +74,7 @@ def is_closed(self) -> bool:
- A polygon (2D RoI).
- A 1D LoI whose final point connects back to its first.
"""
return isinstance(self.region, shapely.Polygon) or (
return self.dimensions > 1 or (
self.dimensions == 1
and self.region.coords[0] == self.region.coords[-1]
)
Expand Down Expand Up @@ -136,8 +153,9 @@ def __repr__(self) -> str: # noqa: D105
return str(self)

def __str__(self) -> str: # noqa: D105
display_type = "-gon" if self.dimensions > 1 else "line segment(s)"
display_type = "-gon" if self.dimensions > 1 else " line segment(s)"
n_points = len(self.coords) - 1
return (
f"{self.__class__.__name__} {self.name} "
f"({len(self.region.coords)}{display_type})\n"
) + " -> ".join(f"({c[0]}, {c[1]})" for c in self.region.coords)
f"({n_points}{display_type})\n"
) + " -> ".join(f"({c[0]}, {c[1]})" for c in self.coords)
23 changes: 19 additions & 4 deletions movement/roi/polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,25 @@ def __init__(
super().__init__(points=boundary, dimensions=2, holes=holes, name=name)

@property
def boundary(self) -> LineOfInterest:
"""The boundary of this RoI."""
def exterior(self) -> LineOfInterest:
"""The (exterior) boundary of this RoI."""
return LineOfInterest(
self.region.boundary.coords,
self.region.exterior.coords,
loop=True,
name=f"Boundary of {self.name}",
name=f"Exterior boundary of {self.name}",
)

@property
def interiors(self) -> tuple[LineOfInterest, ...]:
"""The (interior) boundaries of this RoI.
A region with no interior boundaries returns the empty tuple.
"""
return tuple(
LineOfInterest(
int_boundary.coords,
loop=True,
name=f"Interior boundary {i} of {self.name}",
)
for i, int_boundary in enumerate(self.region.interiors)
)
21 changes: 21 additions & 0 deletions tests/test_unit/test_roi/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import numpy as np
import pytest


@pytest.fixture()
def unit_square_pts() -> np.ndarray:
return np.array(
[
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[0.0, 1.0],
],
dtype=float,
)


@pytest.fixture()
def unit_square_hole(unit_square_pts: np.ndarray) -> np.ndarray:
"""Hole in the shape of a 0.25 side-length square centred on 0.5, 0.5."""
return 0.25 + (unit_square_pts.copy() * 0.5)
13 changes: 0 additions & 13 deletions tests/test_unit/test_roi/test_instantiate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,6 @@
from movement.roi.base import BaseRegionOfInterest


@pytest.fixture()
def unit_square_pts() -> np.ndarray:
return np.array(
[
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[0.0, 1.0],
],
dtype=float,
)


@pytest.mark.parametrize(
["input_pts", "kwargs_for_creation", "expected_results"],
[
Expand Down
53 changes: 53 additions & 0 deletions tests/test_unit/test_roi/test_polygon_boundary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import numpy as np
import pytest
import shapely

from movement.roi.line import LineOfInterest
from movement.roi.polygon import PolygonOfInterest


@pytest.mark.parametrize(
["exterior_boundary", "interior_boundaries"],
[
pytest.param("unit_square_pts", tuple(), id="No holes"),
pytest.param(
"unit_square_pts", tuple(["unit_square_hole"]), id="One hole"
),
pytest.param(
"unit_square_pts",
(
np.array([[0.0, 0.0], [0.25, 0.0], [0.0, 0.25]]),
np.array([[0.75, 0.0], [1.0, 0.0], [1.0, 0.25]]),
),
id="Corners shaved off",
),
],
)
def test_boundary(exterior_boundary, interior_boundaries, request) -> None:
if isinstance(exterior_boundary, str):
exterior_boundary = request.getfixturevalue(exterior_boundary)
interior_boundaries = tuple(
request.getfixturevalue(ib) if isinstance(ib, str) else ib
for ib in interior_boundaries
)
tolerance = 1.0e-8

polygon = PolygonOfInterest(
exterior_boundary, holes=interior_boundaries, name="Holey"
)
expected_exterior = shapely.LinearRing(exterior_boundary)
expected_interiors = tuple(
shapely.LinearRing(ib) for ib in interior_boundaries
)

computed_exterior = polygon.exterior
computed_interiors = polygon.interiors

assert isinstance(computed_exterior, LineOfInterest)
assert expected_exterior.equals_exact(computed_exterior.region, tolerance)
assert isinstance(computed_interiors, tuple)
assert len(computed_interiors) == len(expected_interiors)
for i, item in enumerate(computed_interiors):
assert isinstance(item, LineOfInterest)

assert expected_interiors[i].equals_exact(item.region, tolerance)

0 comments on commit 62aacee

Please sign in to comment.