From d0a1adae774ba57c9070fe4f7cedf2a418ae6be7 Mon Sep 17 00:00:00 2001 From: Jonas Teuwen Date: Tue, 13 Aug 2024 12:26:14 +0200 Subject: [PATCH] Adding functionality to reindex polygons --- dlup/annotations_experimental.py | 58 ++++++++++++++++++++++++++------ dlup/geometry.py | 16 +++++++-- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/dlup/annotations_experimental.py b/dlup/annotations_experimental.py index 73dba3ad..fd33227f 100644 --- a/dlup/annotations_experimental.py +++ b/dlup/annotations_experimental.py @@ -133,7 +133,7 @@ def shape( class WsiAnnotationsExperimental: """Class that holds all annotations for a specific image""" - def __init__(self, layers): + def __init__(self, layers: DlupGeometryContainer): self._layers = layers self._tags = [] @@ -151,7 +151,7 @@ def from_geojson( _geojsons: Iterable[Any] = [pathlib.Path(geojsons)] _geojsons = [geojsons] if not isinstance(geojsons, (tuple, list)) else geojsons - layers: list[DlupPolygon | DlupPoint] = [] + geometries: list[DlupPolygon | DlupPoint] = [] for path in _geojsons: path = pathlib.Path(path) if not path.exists(): @@ -172,10 +172,10 @@ def from_geojson( raise ValueError("Could not find label in the GeoJSON properties.") _geometry = shape(x["geometry"], label=_label, color=_color) - layers += _geometry + geometries += _geometry container = DlupGeometryContainer() - for layer in layers: + for layer in geometries: if isinstance(layer, DlupPolygon): container.add_polygon(layer) elif isinstance(layer, DlupPoint): @@ -210,11 +210,12 @@ def as_geojson(self) -> GeoJsonDict: return data - def read_region(self, coordinates, scaling, size): + def read_region(self, coordinates: tuple[int, int], scaling: float, size: tuple[int, int]): return self._layers.read_region(coordinates, scaling, size) def scale(self, scaling: float) -> None: - """Scale the annotations by a multiplication factor. + """ + Scale the annotations by a multiplication factor. This operation will be performed in-place. Parameters @@ -222,6 +223,11 @@ def scale(self, scaling: float) -> None: scaling : float The scaling factor to apply to the annotations. + Notes + ----- + This invalidates the R-tree. You could rebuild this manually using `.rebuild_rtree()`, or have the function + `read_region()` do it for you on-demand. + Returns ------- None @@ -230,14 +236,19 @@ def scale(self, scaling: float) -> None: def set_offset(self, offset: tuple[float, float]) -> None: """Set the offset for the annotations. This operation will be performed in-place. - + For example, if the offset is 1, 1, the annotations will be moved by 1 unit in the x and y direction. Parameters ---------- offset : tuple[float, float] The offset to apply to the annotations. - + + Notes + ----- + This invalidates the R-tree. You could rebuild this manually using `.rebuild_rtree()`, or have the function + `read_region()` do it for you on-demand. + Returns ------- None @@ -245,16 +256,42 @@ def set_offset(self, offset: tuple[float, float]) -> None: self._layers.set_offset(offset) def rebuild_rtree(self): + """ + Rebuild the R-tree for the annotations. This operation will be performed in-place. + The R-tree is used for fast spatial queries on the annotations and is invalidated when the annotations are + modified. This function will rebuild the R-tree. Strictly speaking, this is not required as the R-tree will be + rebuilt on-demand when you invoke a `read_region()`. You could however do this if you want to avoid the `read_region()` + to do it for you the first time it runs. + """ + self._layers.rebuild_rtree() + def reindex_polygons(self, index_map: dict[str, int]): + """ + Reindex the polygons in the annotations. This operation will be performed in-place. + This is useful if you want to change the index of the polygons in the annotations. + + This requires that the `.label` property on the polygons is set. + + Parameters + ---------- + index_map : dict[str, int] + A dictionary that maps the label to the new index. + + Returns + ------- + None + """ + self._layers.reindex_polygons(index_map) + def filter_polygons(self, label: str) -> None: - """Filter polygons in-place. + """Filter polygons in-place. Note ---- This will internally invalidate the R-tree. You could rebuild this manually using `.rebuild_rtree()`, or have the function itself do this on-demand (typically when you invoke a `.read_region()`) - + Parameters ---------- label : str @@ -264,4 +301,3 @@ def filter_polygons(self, label: str) -> None: for polygon in self._layers.polygons: if polygon.label == label: self._layers.remove_polygon(polygon) - diff --git a/dlup/geometry.py b/dlup/geometry.py index cc834b08..e67a4468 100644 --- a/dlup/geometry.py +++ b/dlup/geometry.py @@ -23,17 +23,29 @@ def __init__(self, *args, **kwargs): self.set_field(key, value) @property - def label(self): + def label(self) -> str: return self.get_field("label") + @label.setter + def label(self, value: str) -> None: + self.set_field("label", value) + @property - def index(self): + def index(self) -> int: return self.get_field("index") + @index.setter + def index(self, value: int) -> None: + self.set_field("index", value) + @property def color(self): return self.get_field("color") + @color.setter + def color(self, value: str) -> None: + self.set_field("color", value) + def to_shapely(self): if not SHAPELY_AVAILABLE: raise ImportError(