From 832a28f493598b090120f78b80ec775abffaab18 Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Tue, 10 Oct 2023 02:29:41 +0100 Subject: [PATCH 1/4] easier qupath imports --- tests/test_annotation_stores.py | 53 ++++++++++++++++++++++++++++++++ tiatoolbox/annotation/storage.py | 27 ++++++++++++++-- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/tests/test_annotation_stores.py b/tests/test_annotation_stores.py index 02ee6bacd..ea08a9ff6 100644 --- a/tests/test_annotation_stores.py +++ b/tests/test_annotation_stores.py @@ -2823,3 +2823,56 @@ def test_query_min_area( _, store = fill_store(store_cls, ":memory:") result = store.query((0, 0, 1000, 1000), min_area=1) assert len(result) == 100 # should only get cells, pts are too small + + @staticmethod + def test_import_from_qupath( + tmp_path: Path, + store_cls: type[AnnotationStore], + ) -> None: + """Test importing from a QuPath annotations file with measurements.""" + # make a simple example of a .geojson exported from QuPath + anns = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [1076, 2322.55], + [1073.61, 2323.23], + [1072.58, 2323.88], + [1070.93, 2325.61], + [1076, 2322.55], + ], + ], + }, + "properties": { + "object_type": "detection", + "isLocked": "false", + "measurements": [ + { + "name": "Detection probability", + "value": 0.847621500492096, + }, + {"name": "Area µm^2", "value": 27.739423751831055}, + ], + }, + }, + ], + } + with (tmp_path / "test_annotations.geojson").open("w") as f: + json.dump(anns, f) + store = store_cls.from_geojson( + tmp_path / "test_annotations.geojson", + unpack_qupath_measurements=True, + ) + assert len(store) == 1 + ann = next(iter(store.values())) + assert ann.properties == { + "object_type": "detection", + "isLocked": "false", + "Detection probability": 0.847621500492096, + "Area µm^2": 27.739423751831055, + } diff --git a/tiatoolbox/annotation/storage.py b/tiatoolbox/annotation/storage.py index 3440b5aec..917763d0e 100644 --- a/tiatoolbox/annotation/storage.py +++ b/tiatoolbox/annotation/storage.py @@ -1724,6 +1724,8 @@ def from_geojson( fp: IO | str, scale_factor: tuple[float, float] = (1, 1), origin: tuple[float, float] = (0, 0), + *, + unpack_qupath_measurements: bool = False, ) -> AnnotationStore: """Create a new database with annotations loaded from a geoJSON file. @@ -1736,6 +1738,9 @@ def from_geojson( annotations saved at non-baseline resolution. origin (Tuple[float, float]): The x and y coordinates to use as the origin for the annotations. + unpack_qupath_measurements (bool): + If True, unpack QuPath measurements into individual properties of each + annotation. Defaults to False. Use only for .geojson exported by QuPath. Returns: AnnotationStore: @@ -1743,7 +1748,12 @@ def from_geojson( """ store = cls() - store.add_from_geojson(fp, scale_factor, origin=origin) + store.add_from_geojson( + fp, + scale_factor, + origin=origin, + unpack_qupath_measurements=unpack_qupath_measurements, + ) return store def add_from_geojson( @@ -1751,6 +1761,8 @@ def add_from_geojson( fp: IO | str, scale_factor: tuple[float, float] = (1, 1), origin: tuple[float, float] = (0, 0), + *, + unpack_qupath_measurements: bool = False, ) -> None: """Add annotations from a .geojson file to an existing store. @@ -1765,6 +1777,9 @@ def add_from_geojson( at non-baseline resolution. origin (Tuple[float, float]): The x and y coordinates to use as the origin for the annotations. + unpack_qupath_measurements (bool): + If True, unpack QuPath measurements into individual properties of each + annotation. Defaults to False. Use only for .geojson exported by QuPath. """ @@ -1782,6 +1797,14 @@ def transform_geometry(geom: Geometry) -> Geometry: ) return geom + def unpack_qpath(props: dict) -> dict: + """Helper function to unpack QuPath measurements.""" + if unpack_qupath_measurements and "measurements" in props: + measurements = props.pop("measurements") + for m in measurements: + props[m["name"]] = m["value"] + return props + geojson = self._load_cases( fp=fp, string_fn=json.loads, @@ -1793,7 +1816,7 @@ def transform_geometry(geom: Geometry) -> Geometry: transform_geometry( feature2geometry(feature["geometry"]), ), - feature["properties"], + unpack_qpath(feature["properties"]), ) for feature in geojson["features"] ] From d50fee2a7f74edb591a511cb1d27a038718a14d7 Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Thu, 2 Nov 2023 23:46:19 +0000 Subject: [PATCH 2/4] generic import_transform --- tests/test_annotation_stores.py | 15 ++++++++-- tiatoolbox/annotation/storage.py | 48 +++++++++++++++++--------------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/tests/test_annotation_stores.py b/tests/test_annotation_stores.py index ea08a9ff6..195d2b64a 100644 --- a/tests/test_annotation_stores.py +++ b/tests/test_annotation_stores.py @@ -2825,11 +2825,11 @@ def test_query_min_area( assert len(result) == 100 # should only get cells, pts are too small @staticmethod - def test_import_from_qupath( + def test_import_transform( tmp_path: Path, store_cls: type[AnnotationStore], ) -> None: - """Test importing from a QuPath annotations file with measurements.""" + """Test importing with an application-specific transform.""" # make a simple example of a .geojson exported from QuPath anns = { "type": "FeatureCollection", @@ -2864,9 +2864,18 @@ def test_import_from_qupath( } with (tmp_path / "test_annotations.geojson").open("w") as f: json.dump(anns, f) + + def unpack_qpath(ann: Annotation) -> Annotation: + """Helper function to unpack QuPath measurements.""" + props = ann.properties + measurements = props.pop("measurements") + for m in measurements: + props[m["name"]] = m["value"] + return ann + store = store_cls.from_geojson( tmp_path / "test_annotations.geojson", - unpack_qupath_measurements=True, + import_transform=unpack_qpath, ) assert len(store) == 1 ann = next(iter(store.values())) diff --git a/tiatoolbox/annotation/storage.py b/tiatoolbox/annotation/storage.py index 917763d0e..5e54932f8 100644 --- a/tiatoolbox/annotation/storage.py +++ b/tiatoolbox/annotation/storage.py @@ -1724,8 +1724,7 @@ def from_geojson( fp: IO | str, scale_factor: tuple[float, float] = (1, 1), origin: tuple[float, float] = (0, 0), - *, - unpack_qupath_measurements: bool = False, + import_transform: Callable[[Annotation], Annotation] | None = None, ) -> AnnotationStore: """Create a new database with annotations loaded from a geoJSON file. @@ -1738,9 +1737,11 @@ def from_geojson( annotations saved at non-baseline resolution. origin (Tuple[float, float]): The x and y coordinates to use as the origin for the annotations. - unpack_qupath_measurements (bool): - If True, unpack QuPath measurements into individual properties of each - annotation. Defaults to False. Use only for .geojson exported by QuPath. + import_transform (Callable): + A function to apply to each annotation after loading. Should take an + annotation as input and return an annotation. Defaults to None. + Intended to facilitate modifying the way annotations are loaded to + accomodate the specifics of different annotation formats. Returns: AnnotationStore: @@ -1748,11 +1749,17 @@ def from_geojson( """ store = cls() + if import_transform is None: + + def import_transform(annotation: Annotation) -> Annotation: + """Default import transform. Does Nothing.""" + return annotation + store.add_from_geojson( fp, scale_factor, origin=origin, - unpack_qupath_measurements=unpack_qupath_measurements, + import_transform=import_transform, ) return store @@ -1761,8 +1768,7 @@ def add_from_geojson( fp: IO | str, scale_factor: tuple[float, float] = (1, 1), origin: tuple[float, float] = (0, 0), - *, - unpack_qupath_measurements: bool = False, + import_transform: Callable[[Annotation], Annotation] | None = None, ) -> None: """Add annotations from a .geojson file to an existing store. @@ -1777,9 +1783,11 @@ def add_from_geojson( at non-baseline resolution. origin (Tuple[float, float]): The x and y coordinates to use as the origin for the annotations. - unpack_qupath_measurements (bool): - If True, unpack QuPath measurements into individual properties of each - annotation. Defaults to False. Use only for .geojson exported by QuPath. + import_transform (Callable): + A function to apply to each annotation after loading. Should take an + annotation as input and return an annotation. Defaults to None. + Intended to facilitate modifying the way annotations are loaded to + accomodate the specifics of different annotation formats. """ @@ -1797,14 +1805,6 @@ def transform_geometry(geom: Geometry) -> Geometry: ) return geom - def unpack_qpath(props: dict) -> dict: - """Helper function to unpack QuPath measurements.""" - if unpack_qupath_measurements and "measurements" in props: - measurements = props.pop("measurements") - for m in measurements: - props[m["name"]] = m["value"] - return props - geojson = self._load_cases( fp=fp, string_fn=json.loads, @@ -1812,11 +1812,13 @@ def unpack_qpath(props: dict) -> dict: ) annotations = [ - Annotation( - transform_geometry( - feature2geometry(feature["geometry"]), + import_transform( + Annotation( + transform_geometry( + feature2geometry(feature["geometry"]), + ), + feature["properties"], ), - unpack_qpath(feature["properties"]), ) for feature in geojson["features"] ] From 1d985b2ebf03286a317058a1eb18a64df2a97ee3 Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:18:36 +0000 Subject: [PATCH 3/4] add example --- tiatoolbox/annotation/storage.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tiatoolbox/annotation/storage.py b/tiatoolbox/annotation/storage.py index 5e54932f8..f3c5eaee6 100644 --- a/tiatoolbox/annotation/storage.py +++ b/tiatoolbox/annotation/storage.py @@ -1747,6 +1747,24 @@ def from_geojson( AnnotationStore: A new annotation store with the annotations loaded from the file. + Example: + To load annotations from a GeoJSON exported by QuPath, with measurements + stored in a 'measurements' property as a list of name-value pairs, and + unpack those measurements into a flat dictionary of properties of + each annotation: + >>> from tiatoolbox.annotation.storage import SQLiteStore + >>> def unpack_qpath(ann: Annotation) -> Annotation: + >>> #Helper function to unpack QuPath measurements. + >>> props = ann.properties + >>> measurements = props.pop("measurements") + >>> for m in measurements: + >>> props[m["name"]] = m["value"] + >>> return ann + >>> store = SQLiteStore.from_geojson( + ... "exported_file.geojson", + ... import_transform=unpack_qpath, + ... ) + """ store = cls() if import_transform is None: From dd16891f21c00639583e2fdd7817fb0b6c06621f Mon Sep 17 00:00:00 2001 From: Shan E Ahmed Raza <13048456+shaneahmed@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:57:14 +0000 Subject: [PATCH 4/4] :recycle: Rename import_transform to transform and unpack_qpath to unpack_qupath Signed-off-by: Shan E Ahmed Raza <13048456+shaneahmed@users.noreply.github.com> --- tests/test_annotation_stores.py | 4 ++-- tiatoolbox/annotation/storage.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_annotation_stores.py b/tests/test_annotation_stores.py index 195d2b64a..049bcf99c 100644 --- a/tests/test_annotation_stores.py +++ b/tests/test_annotation_stores.py @@ -2865,7 +2865,7 @@ def test_import_transform( with (tmp_path / "test_annotations.geojson").open("w") as f: json.dump(anns, f) - def unpack_qpath(ann: Annotation) -> Annotation: + def unpack_qupath(ann: Annotation) -> Annotation: """Helper function to unpack QuPath measurements.""" props = ann.properties measurements = props.pop("measurements") @@ -2875,7 +2875,7 @@ def unpack_qpath(ann: Annotation) -> Annotation: store = store_cls.from_geojson( tmp_path / "test_annotations.geojson", - import_transform=unpack_qpath, + transform=unpack_qupath, ) assert len(store) == 1 ann = next(iter(store.values())) diff --git a/tiatoolbox/annotation/storage.py b/tiatoolbox/annotation/storage.py index f3c5eaee6..414efe426 100644 --- a/tiatoolbox/annotation/storage.py +++ b/tiatoolbox/annotation/storage.py @@ -1724,7 +1724,7 @@ def from_geojson( fp: IO | str, scale_factor: tuple[float, float] = (1, 1), origin: tuple[float, float] = (0, 0), - import_transform: Callable[[Annotation], Annotation] | None = None, + transform: Callable[[Annotation], Annotation] | None = None, ) -> AnnotationStore: """Create a new database with annotations loaded from a geoJSON file. @@ -1737,7 +1737,7 @@ def from_geojson( annotations saved at non-baseline resolution. origin (Tuple[float, float]): The x and y coordinates to use as the origin for the annotations. - import_transform (Callable): + transform (Callable): A function to apply to each annotation after loading. Should take an annotation as input and return an annotation. Defaults to None. Intended to facilitate modifying the way annotations are loaded to @@ -1753,7 +1753,7 @@ def from_geojson( unpack those measurements into a flat dictionary of properties of each annotation: >>> from tiatoolbox.annotation.storage import SQLiteStore - >>> def unpack_qpath(ann: Annotation) -> Annotation: + >>> def unpack_qupath(ann: Annotation) -> Annotation: >>> #Helper function to unpack QuPath measurements. >>> props = ann.properties >>> measurements = props.pop("measurements") @@ -1762,14 +1762,14 @@ def from_geojson( >>> return ann >>> store = SQLiteStore.from_geojson( ... "exported_file.geojson", - ... import_transform=unpack_qpath, + ... transform=unpack_qupath, ... ) """ store = cls() - if import_transform is None: + if transform is None: - def import_transform(annotation: Annotation) -> Annotation: + def transform(annotation: Annotation) -> Annotation: """Default import transform. Does Nothing.""" return annotation @@ -1777,7 +1777,7 @@ def import_transform(annotation: Annotation) -> Annotation: fp, scale_factor, origin=origin, - import_transform=import_transform, + transform=transform, ) return store @@ -1786,7 +1786,7 @@ def add_from_geojson( fp: IO | str, scale_factor: tuple[float, float] = (1, 1), origin: tuple[float, float] = (0, 0), - import_transform: Callable[[Annotation], Annotation] | None = None, + transform: Callable[[Annotation], Annotation] | None = None, ) -> None: """Add annotations from a .geojson file to an existing store. @@ -1801,11 +1801,11 @@ def add_from_geojson( at non-baseline resolution. origin (Tuple[float, float]): The x and y coordinates to use as the origin for the annotations. - import_transform (Callable): + transform (Callable): A function to apply to each annotation after loading. Should take an annotation as input and return an annotation. Defaults to None. Intended to facilitate modifying the way annotations are loaded to - accomodate the specifics of different annotation formats. + accommodate the specifics of different annotation formats. """ @@ -1830,7 +1830,7 @@ def transform_geometry(geom: Geometry) -> Geometry: ) annotations = [ - import_transform( + transform( Annotation( transform_geometry( feature2geometry(feature["geometry"]),