Skip to content
62 changes: 62 additions & 0 deletions tests/test_annotation_stores.py
Original file line number Diff line number Diff line change
Expand Up @@ -2823,3 +2823,65 @@ 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_transform(
tmp_path: Path,
store_cls: type[AnnotationStore],
) -> None:
"""Test importing with an application-specific transform."""
# 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)

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",
import_transform=unpack_qpath,
)
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,
}
53 changes: 48 additions & 5 deletions tiatoolbox/annotation/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -1724,6 +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,
) -> AnnotationStore:
"""Create a new database with annotations loaded from a geoJSON file.

Expand All @@ -1736,21 +1737,56 @@ 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):
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:
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()
store.add_from_geojson(fp, scale_factor, origin=origin)
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,
import_transform=import_transform,
)
return store

def add_from_geojson(
self: AnnotationStore,
fp: IO | str,
scale_factor: tuple[float, float] = (1, 1),
origin: tuple[float, float] = (0, 0),
import_transform: Callable[[Annotation], Annotation] | None = None,
) -> None:
"""Add annotations from a .geojson file to an existing store.

Expand All @@ -1765,6 +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):
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.

"""

Expand All @@ -1789,11 +1830,13 @@ def transform_geometry(geom: Geometry) -> Geometry:
)

annotations = [
Annotation(
transform_geometry(
feature2geometry(feature["geometry"]),
import_transform(
Annotation(
transform_geometry(
feature2geometry(feature["geometry"]),
),
feature["properties"],
),
feature["properties"],
)
for feature in geojson["features"]
]
Expand Down