Skip to content

Commit

Permalink
Improve test coverage to 90%
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasteuwen committed Aug 21, 2024
1 parent b656d78 commit 5f6efe9
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 15 deletions.
37 changes: 30 additions & 7 deletions dlup/annotations_experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions dlup/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__()
Expand Down Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions tests/infer_ann.py

This file was deleted.

66 changes: 63 additions & 3 deletions tests/test_slide_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -93,6 +93,15 @@
<Point x="0.0" y="4.0"/>
<Point x="0.0" y="0.0"/>
</Exterior>
<Interiors>
<Interior>
<Point x="0.5" y="0.5"/>
<Point x="3.5" y="0.5"/>
<Point x="3.5" y="3.5"/>
<Point x="0.5" y="3.5"/>
<Point x="0.5" y="0.5"/>
</Interior>
</Interiors>
</Polygon>
<Box xMin="5.0" yMin="5.0" xMax="10.0" yMax="10.0" label="Box1" color="#33FF57" order="1" />
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
7 changes: 5 additions & 2 deletions tests/utils/test_annotation_utils.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# 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):
hex_repr = rgb_to_hex(*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)
Expand Down Expand Up @@ -42,4 +45,4 @@ def test_exceptions():
with pytest.raises(ValueError):
hex_to_rgb("#")
with pytest.raises(ValueError):
hex_to_rgb("")
hex_to_rgb("")

0 comments on commit 5f6efe9

Please sign in to comment.