Skip to content

Commit

Permalink
Merge pull request #176 from jleuschn/pyramids_with_tolerance
Browse files Browse the repository at this point in the history
Group instances into same pyramid if origin difference is below a threshold
  • Loading branch information
erikogabrielsson authored Nov 21, 2024
2 parents 613fd08 + 7095b0c commit 6d962d8
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Default to placing the image at the middle of the slide if no `TotalPixelMatrixOriginSequence` is set when producing DICOM metadata .
- Use Unix epoch as default datetime when producing DICOM metadata.
- Group instances to pyramids with a configurable threshold to allow small differences in `TotalPixelMatrixOriginSequence`.

### Fixed

Expand Down
7 changes: 7 additions & 0 deletions tests/test_pyramids.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ class TestPyramids:
],
2,
],
[
[
(PointMm(0, 0), Orientation((0, -1, 0, 1, 0, 0)), None),
(PointMm(0.001, 0.001), Orientation((0, -1, 0, 1, 0, 0)), None),
],
1,
],
],
)
def test_open_number_of_created_pyramids(
Expand Down
74 changes: 68 additions & 6 deletions tests/testdata/region_definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -621,11 +621,73 @@
"label": "692c84fcb25130abcb65169ec4f4d37f",
"overview": "2274f918d7ca4b3f0951de77d1c48d04",
"tiled": "sparse",
"read_region": [],
"read_region_mm": [],
"read_region_mpp": [],
"read_tile": [],
"read_encoded_tile": [],
"read_thumbnail": []
"read_region": [
{
"location": {
"x": 800,
"y": 500
},
"level": 4,
"size": {
"width": 200,
"height": 300
},
"md5": "de274a86b15ed4fca984f6619c6e5547"
}],
"read_region_mm": [
{
"location": {
"x": 21.0,
"y": 10.0
},
"level": 2,
"size": {
"width": 1.0,
"height": 2.0
},
"md5": "ec4e7d038f1de1021fcb3e0027f0105b"
}],
"read_region_mpp": [
{
"location": {
"x": 21.0,
"y": 10.0
},
"mpp": 8.0,
"size": {
"width": 1.0,
"height": 2.0
},
"md5": "439021d08a2e2fb755b1b65519eb2836"
}],
"read_tile": [
{
"location": {
"x": 16,
"y": 8
},
"level": 2,
"md5": "81e1da2f3e629e95f67aabb9dca67174"
}
],
"read_encoded_tile": [
{
"location": {
"x": 16,
"y": 8
},
"level": 2,
"md5": "077ca7ea325beefc43ad7b057f1dd8ce"
}
],
"read_thumbnail": [
{
"size": {
"width": 512,
"height": 512
},
"md5": "3a4d79e664225e1ea9b80c4e647f6b2c"
}
]
}
}
12 changes: 12 additions & 0 deletions wsidicom/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def __init__(self) -> None:
self._strict_uid_check = False
self._strict_attribute_check = False
self._focal_plane_distance_threshold = 0.000001
self._pyramids_origin_threshold = 0.02
self._prefered_decoder: Optional[str] = None
self._open_web_theads: Optional[int] = None
self._pillow_resampling_filter = Pillow.Resampling.BILINEAR
Expand Down Expand Up @@ -63,6 +64,17 @@ def focal_plane_distance_threshold(self) -> float:
def focal_plane_distance_threshold(self, value: float) -> None:
self._focal_plane_distance_threshold = value

@property
def pyramids_origin_threshold(self) -> float:
"""Threshold in mm for the distance between origins of instances
to group them into the same pyramid. Default is 0.02 mm.
"""
return self._pyramids_origin_threshold

@pyramids_origin_threshold.setter
def pyramids_origin_threshold(self, value: float) -> None:
self._pyramids_origin_threshold = value

@property
def prefered_decoder(self) -> Optional[str]:
"""Name of preferred decoder to use."""
Expand Down
12 changes: 12 additions & 0 deletions wsidicom/metadata/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import datetime
from dataclasses import dataclass, replace
from enum import Enum
from math import sqrt
from typing import Optional, Sequence, TypeVar

from wsidicom.codec import LossyCompressionIsoStandard
Expand Down Expand Up @@ -181,6 +182,17 @@ def from_middle_of_slide(
z_offset=z_offset,
)

def origin_and_rotation_match(
self, other: "ImageCoordinateSystem", origin_threshold: float
) -> bool:
if self.rotation != other.rotation:
return False
origin_distance = sqrt(
(self.origin.x - other.origin.x) ** 2
+ (self.origin.y - other.origin.y) ** 2
)
return origin_distance <= origin_threshold


@dataclass(frozen=True)
class LossyCompression:
Expand Down
27 changes: 26 additions & 1 deletion wsidicom/series/pyramids.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections import defaultdict
from typing import Dict, Iterable, List, Optional, Tuple
from wsidicom.config import settings
from wsidicom.errors import WsiDicomNotFoundError
from wsidicom.instance.instance import WsiInstance
from wsidicom.series.pyramid import Pyramid
Expand Down Expand Up @@ -91,7 +92,9 @@ def _group_instances_into_pyramids(
Tuple[Optional[Tuple[float, float, float]], Optional[Tuple[int, float]]],
List[WsiInstance],
] = defaultdict(list)
for instance in instances:
for instance in sorted(
instances, key=lambda x: x.size.to_tuple(), reverse=True
):
if instance.image_coordinate_system is not None:
image_coordinate_system = (
instance.image_coordinate_system.origin.x,
Expand All @@ -109,6 +112,28 @@ def _group_instances_into_pyramids(
)
else:
ext_depth_of_field = None

if instance.image_coordinate_system is not None:
existing_group_match = next(
(
(image_cs, ext_dof)
for (image_cs, ext_dof), group in grouped_instances.items()
if (
all(
instance.image_coordinate_system.origin_and_rotation_match(
inst.image_coordinate_system,
origin_threshold=settings.pyramids_origin_threshold,
)
for inst in group
)
and ext_depth_of_field == ext_dof
)
),
None,
)
if existing_group_match is not None:
image_coordinate_system, ext_depth_of_field = existing_group_match

grouped_instances[image_coordinate_system, ext_depth_of_field].append(
instance
)
Expand Down

0 comments on commit 6d962d8

Please sign in to comment.