diff --git a/dlup/annotations_experimental.py b/dlup/annotations_experimental.py
index e79dded5..cd7cd926 100644
--- a/dlup/annotations_experimental.py
+++ b/dlup/annotations_experimental.py
@@ -73,11 +73,11 @@ class DarwinV7Metadata(NamedTuple):
@functools.lru_cache(maxsize=None)
def get_v7_metadata(filename: pathlib.Path) -> Optional[dict[tuple[str, str], DarwinV7Metadata]]:
if not DARWIN_SDK_AVAILABLE:
- raise RuntimeError("`darwin` is not available. Install using `python -m pip install darwin-py`.")
+ raise ImportError("`darwin` is not available. Install using `python -m pip install darwin-py`.")
import darwin.path_utils
if not filename.is_dir():
- raise RuntimeError("Provide the path to the root folder of the Darwin V7 annotations")
+ raise ValueError("Provide the path to the root folder of the Darwin V7 annotations")
v7_metadata_fn = filename / ".v7" / "metadata.json"
if not v7_metadata_fn.exists():
@@ -129,7 +129,6 @@ def to_sorting_params(self) -> tuple[Callable[[Polygon], Optional[int | float |
if self == AnnotationSorting.Z_INDEX:
return lambda x: x.get_field("z_index"), False
- raise ValueError(f"Unsupported sorting {self}")
def _geometry_to_geojson(
@@ -383,6 +382,10 @@ def from_asap_xml(
-------
SlideAnnotations
"""
+ path = pathlib.Path(asap_xml)
+ if not path.exists():
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(path))
+
tree = ET.parse(asap_xml)
opened_annotation = tree.getroot()
collection: GeometryCollection = GeometryCollection()
@@ -447,6 +450,9 @@ def from_darwin_json(
import darwin
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))
+
darwin_an = darwin.utils.parse_darwin_json(darwin_json_fn, None)
v7_metadata = get_v7_metadata(darwin_json_fn.parent)
@@ -513,10 +519,15 @@ def from_darwin_json(
else:
raise ValueError(f"Annotation type {annotation_type} is not supported.")
- for polygon, _ in sorted(polygons, key=lambda x: x[1]):
- collection.add_polygon(polygon)
+ if sorting == "Z_INDEX":
+ for polygon, _ in sorted(polygons, key=lambda x: x[1]):
+ collection.add_polygon(polygon)
+ else:
+ _ = [collection.add_polygon(polygon) for polygon, _ in polygons]
- SlideAnnotations._in_place_sort_and_scale(collection, scaling, sorting)
+ SlideAnnotations._in_place_sort_and_scale(
+ collection, scaling, sorting="NONE" if sorting == "Z_INDEX" else sorting
+ )
return cls(layers=collection, tags=tuple(tags), sorting=sorting)
@classmethod
@@ -533,6 +544,10 @@ def from_dlup_xml(cls: Type[_TSlideAnnotations], dlup_xml: PathLike) -> _TSlideA
-------
SlideAnnotations
"""
+ path = pathlib.Path(dlup_xml)
+ if not path.exists():
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(path))
+
parser = XmlParser()
with open(dlup_xml, "rb") as f:
dlup_annotations = parser.from_bytes(f.read(), XMLDlupAnnotations)
@@ -628,6 +643,10 @@ def from_halo_xml(
-------
SlideAnnotations
"""
+ path = pathlib.Path(halo_xml)
+ if not path.exists():
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(path))
+
if not PYHALOXML_AVAILABLE:
raise RuntimeError("`pyhaloxml` is not available. Install using `python -m pip install pyhaloxml`.")
import pyhaloxml.shapely
@@ -657,14 +676,19 @@ def from_halo_xml(
raise NotImplementedError(f"Regiontype {region.type} is not implemented in dlup")
SlideAnnotations._in_place_sort_and_scale(collection, scaling, sorting)
+
def offset_function(slide):
return slide.slide_bounds[0] - slide.slide_bounds[0] % 256
+
return cls(collection, tags=None, sorting=sorting, offset_function=offset_function)
@staticmethod
def _in_place_sort_and_scale(
collection: GeometryCollection, scaling: Optional[float], sorting: Optional[AnnotationSorting | str]
) -> None:
+ if sorting == "REVERSE":
+ raise NotImplementedError("This doesn't work for now.")
+
if scaling != 1.0 and scaling is not None:
collection.scale(scaling)
if sorting == AnnotationSorting.NONE or sorting is None:
@@ -815,7 +839,6 @@ def __contains__(self, item: str | Point | Polygon) -> bool:
return item in self.available_classes
if isinstance(item, Point):
return item in self._layers.points
-
if isinstance(item, Polygon):
return item in self._layers.polygons
diff --git a/dlup/geometry.py b/dlup/geometry.py
index 4fe045d2..b92f3cf5 100644
--- a/dlup/geometry.py
+++ b/dlup/geometry.py
@@ -371,6 +371,7 @@ def _point_factory(point: _dg.Point) -> Point:
_dg.set_point_factory(_point_factory)
+# TODO: Allow to construct geometry collection from a list of polygons, bypassing the python loop
class GeometryCollection(_dg.GeometryCollection):
def __init__(self) -> None:
super().__init__()
@@ -418,6 +419,15 @@ def __setstate__(self, state: dict[str, list[dict[str, Any]]]) -> None:
for point in points:
self.add_point(point)
+ def __copy__(self):
+ collection = GeometryCollection()
+ for polygon in self.polygons:
+ collection.add_polygon(polygon.__copy__())
+ for point in self.points:
+ collection.add_point(point.__copy__())
+ collection.rebuild_rtree()
+ return collection
+
def __eq__(self, other: Any) -> bool:
if not isinstance(other, type(self)):
return False
diff --git a/tests/infer_ann.py b/tests/infer_ann.py
deleted file mode 100644
index e7be73c0..00000000
--- a/tests/infer_ann.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from dlup.annotations import WsiAnnotations
-
-ann = WsiAnnotations.from_geojson("/Users/jteuwen/annotations.json")
diff --git a/tests/test_slide_annotations.py b/tests/test_slide_annotations.py
index 730c54b5..81ea4dae 100644
--- a/tests/test_slide_annotations.py
+++ b/tests/test_slide_annotations.py
@@ -11,7 +11,7 @@
import numpy as np
import pytest
-from dlup.annotations_experimental import SlideAnnotations, geojson_to_dlup
+from dlup.annotations_experimental import GeometryCollection, SlideAnnotations, geojson_to_dlup, get_v7_metadata
from dlup.geometry import Point as Point
from dlup.geometry import Polygon as Polygon
from dlup.utils.imports import DARWIN_SDK_AVAILABLE
@@ -93,6 +93,15 @@
+
+
+
+
+
+
+
+
+
@@ -106,6 +115,14 @@
"""
+polygons = [
+ Polygon([(0, 0), (0, 3), (3, 3), (3, 0)], []),
+ Polygon([(2, 2), (2, 5), (5, 5), (5, 2)], []),
+ Polygon([(4, 2), (4, 7), (7, 7), (7, 4)], []),
+ Polygon([(6, 6), (6, 9), (9, 9), (9, 6)], []),
+]
+
+
class TestAnnotations:
with tempfile.NamedTemporaryFile(suffix=".xml") as asap_file:
asap_file.write(ASAP_XML_EXAMPLE)
@@ -155,8 +172,8 @@ def test_conversion_geojson_v7(self):
assert self.v7_annotations.num_points == annotations.num_points
assert self.v7_annotations.num_polygons == annotations.num_polygons
- assert self.v7_annotations._layers.polygons == annotations._layers.polygons
- assert self.v7_annotations._layers.points == annotations._layers.points
+ assert self.v7_annotations.layers.polygons == annotations.layers.polygons
+ assert self.v7_annotations.layers.points == annotations.layers.points
self.v7_annotations.rebuild_rtree()
annotations.rebuild_rtree()
@@ -190,6 +207,12 @@ def test_reading_qupath05_geojson_export(self):
annotations = SlideAnnotations.from_geojson(pathlib.Path("tests/files/qupath05.geojson"))
assert len(annotations.available_classes) == 2
+ @pytest.mark.parametrize("class_method", ["from_geojson", "from_halo_xml", "from_dlup_xml", "from_asap_xml"])
+ def test_missing_file_constructor(self, class_method):
+ constructor = getattr(SlideAnnotations, class_method)
+ with pytest.raises(FileNotFoundError):
+ constructor("doesnotexist.xml.json")
+
def test_asap_to_geojson(self):
# TODO: Make sure that the annotations hit the border of the region.
asap_geojson = self.asap_annotations.as_geojson()
@@ -435,3 +458,40 @@ def test_add_with_invalid_type(self):
annotations += "invalid type"
with pytest.raises(TypeError):
_ = "invalid type" + annotations
+
+ def test_v7_metadata(self, monkeypatch):
+ with pytest.raises(ValueError):
+ get_v7_metadata(pathlib.Path("../tests"))
+
+ monkeypatch.setattr("dlup.annotations_experimental.DARWIN_SDK_AVAILABLE", False)
+ with pytest.raises(ImportError):
+ get_v7_metadata(pathlib.Path("."))
+
+ @pytest.mark.parametrize("sorting_type", ["NONE", "REVERSE", "AREA", "Z_INDEX", "NON_EXISTENT"])
+ def test_sorting(self, sorting_type):
+ collection = GeometryCollection()
+ for polygon in polygons:
+ collection.add_polygon(polygon)
+
+ if sorting_type == "NONE":
+ curr_collection = collection.__copy__()
+ SlideAnnotations._in_place_sort_and_scale(curr_collection, scaling=1.0, sorting=sorting_type)
+ assert curr_collection == collection
+
+ if sorting_type == "REVERSE":
+ with pytest.raises(NotImplementedError):
+ curr_collection = collection.__copy__()
+ SlideAnnotations._in_place_sort_and_scale(curr_collection, scaling=1.0, sorting=sorting_type)
+ # Needs fixing
+ # assert curr_collection.polygons == collection.polygons[::-1]
+
+ if sorting_type == "Z_INDEX":
+ curr_collection = collection.__copy__()
+ for idx, polygon in enumerate(curr_collection.polygons):
+ polygon.set_field("z_index", len(curr_collection.polygons) - idx)
+ SlideAnnotations._in_place_sort_and_scale(curr_collection, scaling=1.0, sorting=sorting_type)
+ assert curr_collection.polygons == collection.polygons[::-1]
+
+ if sorting_type == "NON_EXISTENT":
+ with pytest.raises(KeyError):
+ SlideAnnotations._in_place_sort_and_scale(collection, scaling=1.0, sorting=sorting_type)
diff --git a/tests/utils/test_annotation_utils.py b/tests/utils/test_annotation_utils.py
index 0d0093d3..b2d4966d 100644
--- a/tests/utils/test_annotation_utils.py
+++ b/tests/utils/test_annotation_utils.py
@@ -1,7 +1,8 @@
# Copyright (c) dlup contributors
import pytest
-from dlup.utils.annotations_utils import rgb_to_hex, hex_to_rgb
+from dlup.utils.annotations_utils import hex_to_rgb, rgb_to_hex
+
@pytest.mark.parametrize("rgb", [(0, 0, 0), (255, 10, 255), (255, 127, 0), (0, 28, 0), (0, 0, 255)])
def test_rgb_to_hex_to_rgb(rgb):
@@ -9,9 +10,11 @@ def test_rgb_to_hex_to_rgb(rgb):
rgb2 = hex_to_rgb(hex_repr)
assert rgb == rgb2
+
def test_fixed_colors():
assert hex_to_rgb("black") == (0, 0, 0)
+
def test_exceptions():
with pytest.raises(ValueError):
rgb_to_hex(256, 0, 0)
@@ -42,4 +45,4 @@ def test_exceptions():
with pytest.raises(ValueError):
hex_to_rgb("#")
with pytest.raises(ValueError):
- hex_to_rgb("")
\ No newline at end of file
+ hex_to_rgb("")