Skip to content

Commit

Permalink
Added ROIs to output xml and parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasteuwen committed Sep 4, 2024
1 parent 8dbcffc commit 7a86527
Show file tree
Hide file tree
Showing 4 changed files with 350 additions and 75 deletions.
6 changes: 5 additions & 1 deletion dlup/_geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,14 @@ def set_point_factory(factory: Callable[[Point_], Point_]) -> None: ...
class Box:
@property
def fields(self) -> list[str]: ...
def as_polygon(self) -> Polygon: ...
def as_polygon(self) -> Polygon_: ...
@property
def area(self) -> float: ...
def scale(self, scaling: float) -> None: ...
@property
def coordinates(self) -> tuple[float, float]: ...
@property
def size(self) -> tuple[float, float]: ...

def set_box_factory(factory: Callable[[Box_], Box_]) -> None: ...

Expand Down
131 changes: 77 additions & 54 deletions dlup/annotations_experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@
from dlup._types import GenericNumber, PathLike
from dlup.geometry import Box, GeometryCollection, Point, Polygon
from dlup.utils.annotations_utils import get_geojson_color, hex_to_rgb, rgb_to_hex
from dlup.utils.geometry_xml import create_xml_geometries, create_xml_rois
from dlup.utils.geometry_xml import (
create_xml_geometries,
create_xml_rois,
parse_dlup_xml_polygon,
parse_dlup_xml_roi_box,
)
from dlup.utils.imports import DARWIN_SDK_AVAILABLE, PYHALOXML_AVAILABLE
from dlup.utils.schemas.generated import DlupAnnotations as XMLDlupAnnotations
from dlup.utils.schemas.generated import Metadata as XMLMetadata
Expand Down Expand Up @@ -334,6 +339,7 @@ def from_geojson(
geojsons: PathLike,
scaling: float | None = None,
sorting: AnnotationSorting | str = AnnotationSorting.NONE,
roi_names: Optional[list[str]] = None,
) -> _TSlideAnnotations:
"""
Read annotations from a GeoJSON file.
Expand All @@ -346,14 +352,18 @@ def from_geojson(
scaling : float, optional
Scaling factor. Sometimes required when GeoJSON annotations are stored in a different resolution than the
original image.
sorting: AnnotationSorting
sorting : AnnotationSorting
The sorting to apply to the annotations. Check the `AnnotationSorting` enum for more information.
By default, the annotations are sorted by area.
roi_names : list[str], optional
List of names that should be considered as regions of interest. If set, these will be added as ROIs rather
than polygons.
Returns
-------
SlideAnnotations
"""
roi_names = [] if roi_names is None else roi_names
collection = GeometryCollection()
path = pathlib.Path(geojsons)
if not path.exists():
Expand All @@ -376,7 +386,10 @@ def from_geojson(
_geometries = geojson_to_dlup(x["geometry"], label=_label, color=_color)
for geometry in _geometries:
if isinstance(geometry, Polygon):
collection.add_polygon(geometry)
if geometry.label in roi_names:
collection.add_roi(geometry)

Check warning on line 390 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L390

Added line #L390 was not covered by tests
else:
collection.add_polygon(geometry)
elif isinstance(geometry, Point):
collection.add_point(geometry)
else:
Expand All @@ -391,6 +404,7 @@ def from_asap_xml(
asap_xml: PathLike,
scaling: float | None = None,
sorting: AnnotationSorting | str = AnnotationSorting.AREA,
roi_names: Optional[list[str]] = None,
) -> _TSlideAnnotations:
"""
Read annotations as an ASAP [1] XML file. ASAP is a tool for viewing and annotating whole slide images.
Expand All @@ -405,6 +419,9 @@ def from_asap_xml(
sorting: AnnotationSorting
The sorting to apply to the annotations. Check the `AnnotationSorting` enum for more information.
By default, the annotations are sorted by area.
roi_names : list[str], optional
List of names that should be considered as regions of interest. If set, these will be added as ROIs rather
than polygons.
References
----------
Expand All @@ -415,6 +432,7 @@ def from_asap_xml(
SlideAnnotations
"""
path = pathlib.Path(asap_xml)
roi_names = [] if roi_names is None else roi_names
if not path.exists():
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(path))

Expand All @@ -436,8 +454,10 @@ def from_asap_xml(
collection.add_point(Point(point, label=label, color=color))

Check warning on line 454 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L454

Added line #L454 was not covered by tests

elif annotation_type == "polygon":
polygon = Polygon(coordinates, [], label=label, color=color)
collection.add_polygon(polygon)
if label in roi_names:
collection.add_roi(Polygon(coordinates, [], label=label, color=color))

Check warning on line 458 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L458

Added line #L458 was not covered by tests
else:
collection.add_polygon(Polygon(coordinates, [], label=label, color=color))

SlideAnnotations._in_place_sort_and_scale(collection, scaling, sorting)
return cls(layers=collection)
Expand All @@ -449,6 +469,7 @@ def from_darwin_json(
scaling: float | None = None,
sorting: AnnotationSorting | str = AnnotationSorting.NONE,
z_indices: Optional[dict[str, int]] = None,
roi_names: Optional[list[str]] = None,
) -> _TSlideAnnotations:
"""
Read annotations as a V7 Darwin [1] JSON file. If available will read the `.v7/metadata.json` file to extract
Expand All @@ -467,6 +488,9 @@ def from_darwin_json(
than the original image.
z_indices: dict[str, int], optional
If set, these z_indices will be used rather than the default order.
roi_names : list[str], optional
List of names that should be considered as regions of interest. If set, these will be added as ROIs rather
than polygons.
References
----------
Expand All @@ -481,6 +505,8 @@ def from_darwin_json(
raise RuntimeError("`darwin` is not available. Install using `python -m pip install darwin-py`.")

Check warning on line 505 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L505

Added line #L505 was not covered by tests
import darwin

roi_names = [] if roi_names is None else roi_names

darwin_json_fn = pathlib.Path(darwin_json)
if not darwin_json_fn.exists():
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(darwin_json_fn))

Check warning on line 512 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L512

Added line #L512 was not covered by tests
Expand Down Expand Up @@ -553,10 +579,16 @@ def from_darwin_json(

if sorting == "Z_INDEX":
for polygon, _ in sorted(polygons, key=lambda x: x[1]):
collection.add_polygon(polygon)
if polygon.label in roi_names:
collection.add_roi(polygon)

Check warning on line 583 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L583

Added line #L583 was not covered by tests
else:
collection.add_polygon(polygon)

Check warning on line 585 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L585

Added line #L585 was not covered by tests
else:
for polygon, _ in polygons:
collection.add_polygon(polygon)
if polygon.label in roi_names:
collection.add_roi(polygon)

Check warning on line 589 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L589

Added line #L589 was not covered by tests
else:
collection.add_polygon(polygon)

SlideAnnotations._in_place_sort_and_scale(
collection, scaling, sorting="NONE" if sorting == "Z_INDEX" else sorting
Expand Down Expand Up @@ -629,17 +661,38 @@ def from_dlup_xml(cls: Type[_TSlideAnnotations], dlup_xml: PathLike) -> _TSlideA
if dlup_annotations.geometries.multi_point:
raise NotImplementedError("Multipoints are not supported.")

Check warning on line 662 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L662

Added line #L662 was not covered by tests

for curr_box in dlup_annotations.geometries.box:
# mypy struggles
assert isinstance(curr_box.x_min, float)
assert isinstance(curr_box.y_min, float)
assert isinstance(curr_box.x_max, float)
assert isinstance(curr_box.y_max, float)
box = Box(
(curr_box.x_min, curr_box.y_min),
(curr_box.x_max - curr_box.x_min, curr_box.y_max - curr_box.y_min),
label=curr_box.label,
color=hex_to_rgb(curr_box.color) if curr_box.color else None,
)
collection.add_box(box)

rois: list[tuple[Polygon, int]] = []
# Regions of interest
if dlup_annotations.regions_of_interest:
for region_of_interest in dlup_annotations.regions_of_interest.multi_polygon:
raise NotImplementedError("MultiPolygon regions of interest are not supported.")
raise NotImplementedError(

Check warning on line 681 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L681

Added line #L681 was not covered by tests
"MultiPolygon regions of interest are not yet supported. "
"If you have a use case for this, "
"please open an issue at https://github.com/NKI-AI/dlup/issues."
)

if dlup_annotations.regions_of_interest.polygon:
rois += parse_dlup_xml_polygon(dlup_annotations.regions_of_interest.polygon)

Check warning on line 688 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L688

Added line #L688 was not covered by tests

if dlup_annotations.regions_of_interest.box:
raise NotImplementedError("Box regions of interest are not supported.")
for _curr_box in dlup_annotations.regions_of_interest.box:
box, curr_order = parse_dlup_xml_roi_box(_curr_box)
rois.append((box.as_polygon(), curr_order))

Check warning on line 693 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L692-L693

Added lines #L692 - L693 were not covered by tests
for roi, _ in sorted(rois, key=lambda x: x[1]):
collection.add_roi(roi)

Check warning on line 695 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L695

Added line #L695 was not covered by tests

return cls(layers=collection, tags=tuple(tags), metadata=metadata)

Expand All @@ -650,6 +703,7 @@ def from_halo_xml(
scaling: float | None = None,
sorting: AnnotationSorting | str = AnnotationSorting.NONE,
box_as_polygon: bool = False,
roi_names: Optional[list[str]] = None,
) -> _TSlideAnnotations:
"""
Read annotations as a Halo [1] XML file.
Expand All @@ -667,6 +721,9 @@ def from_halo_xml(
box_as_polygon : bool
If True, rectangles are converted to polygons, and added as such.
This is useful when the rectangles are actually implicitly bounding boxes.
roi_names : list[str], optional
List of names that should be considered as regions of interest. If set, these will be added as ROIs rather
than polygons.
References
----------
Expand All @@ -678,6 +735,8 @@ def from_halo_xml(
SlideAnnotations
"""
path = pathlib.Path(halo_xml)
roi_names = [] if roi_names is None else roi_names

if not path.exists():
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(path))

Expand All @@ -703,7 +762,10 @@ def from_halo_xml(

if box_as_polygon:
polygon = curr_box.as_polygon()

Check warning on line 764 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L764

Added line #L764 was not covered by tests
collection.add_polygon(polygon)
if polygon.label in roi_names:
collection.add_roi(polygon)

Check warning on line 766 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L766

Added line #L766 was not covered by tests
else:
collection.add_polygon(polygon)

Check warning on line 768 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L768

Added line #L768 was not covered by tests
else:
collection.add_box(curr_box)
continue

Check warning on line 771 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L770-L771

Added lines #L770 - L771 were not covered by tests
Expand All @@ -712,7 +774,10 @@ def from_halo_xml(
polygon = Polygon(
region.getvertices(), [x.getvertices() for x in region.holes], label=layer.name, color=color
)
collection.add_polygon(polygon)
if polygon.label in roi_names:
collection.add_roi(polygon)

Check warning on line 778 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L778

Added line #L778 was not covered by tests
else:
collection.add_polygon(polygon)

Check warning on line 780 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L780

Added line #L780 was not covered by tests
elif region.type == pyhaloxml.RegionType.Pin:
point = Point(*(region.getvertices()[0]), label=layer.name, color=color)
collection.add_point(point)

Check warning on line 783 in dlup/annotations_experimental.py

View check run for this annotation

Codecov / codecov/patch

dlup/annotations_experimental.py#L782-L783

Added lines #L782 - L783 were not covered by tests
Expand Down Expand Up @@ -1418,45 +1483,3 @@ def _parse_darwin_complex_polygon(
polygon.label = label
polygon.color = color
yield polygon


def parse_dlup_xml_polygon(
polygons: list[Any], order: Optional[int] = None, label: Optional[str] = None, index: Optional[int] = None
) -> list[tuple[Polygon, int]]:
output = []
print(type(polygons[0]))
for curr_polygon in polygons:
if not order and curr_polygon.order is None:
raise ValueError("Polygon does not have an order.")
order = order if order else curr_polygon.order

if not curr_polygon.exterior:
raise ValueError("Polygon does not have an exterior.")
exterior = [(point.x, point.y) for point in curr_polygon.exterior.point]
if curr_polygon.interiors:
interiors = [
[(point.x, point.y) for point in interior.point] for interior in curr_polygon.interiors.interior
]
else:
interiors = []

label = label if label else curr_polygon.label
if hasattr(curr_polygon, "index"):
index = curr_polygon.index
else:
index = None

if hasattr(curr_polygon, "color"):
color = hex_to_rgb(curr_polygon.color)
else:
color = None

polygon = Polygon(
exterior,
interiors,
label=label,
index=index,
color=color,
)
output.append((polygon, order))
return output
Loading

0 comments on commit 7a86527

Please sign in to comment.