Skip to content

Commit

Permalink
Add property to count number of tile frames (#116)
Browse files Browse the repository at this point in the history
* Add property to count number of tile frames

The "NumberOfFrames" attribute on a dataset takes into account not just
the number of frames corresponding to tiles, but also the number of
focal planes and optical paths (for example, see [here](https://github.com/imi-bigpicture/wsidicom/blob/774fe3ca00096e3fd2556fc956520dbfabc81311/wsidicom/instance/dataset.py#L651-L655)).

When I was trying to view a dataset with multiple optical paths, I would
encounter errors in the `image_size` property since it did not distinguish
frames that came from optical paths and tiles.

This fixes viewing [this example](https://imagingdatacommons.github.io/slim/studies/2.25.23897195960526781231486877255455994829/series/2.25.83282858720704132758110891374375550907) via DICOMweb in [large_image](https://github.com/girder/large_image).

I am new to the field, so please feel free to correct any naming issues
or misconceptions...

Signed-off-by: Patrick Avery <[email protected]>

* Add option to provide a session to the web client (#117)

* Add option to provide web client a session

We would like to create the `requests.Session` object on our own
and pass it into `WsiDicomWebClient`, since we are not using an object
derived from `AuthBase` to create it.

This PR adds support to pass a session to `WsiDicomWebClient` directly.

Signed-off-by: Patrick Avery <[email protected]>

* Refactor __init__ method of WsiDicomWebClient

The `__init__` method now accepts a `DICOMwebClient` object.

The previous `__init__` method was moved into a `create_client()`
class method.

Signed-off-by: Patrick Avery <[email protected]>

* Remove `WsiDicomFileClient` and update README.md

Signed-off-by: Patrick Avery <[email protected]>

---------

Signed-off-by: Patrick Avery <[email protected]>

* Dicomweb get multiple frames (#121)

* Get multiple frames from dicom web

* Allow loading multiple series with DICOMweb (#122)

* Allow loading multiple series with DICOMweb

`WsiDicom.open_web()` was modified to be able to either take a single
series uid (as before) or a list of series uids. If a list of series
uids is passed, their instances are all loaded together. This can be
useful for cases where, for instance, different optical paths are located
in different series.

This also loosens the UID matching to only check the frame of references.

This also adds `TotalPixelMatrixOriginSequence` matching when comparing
datasets.

Fixes: #118

Signed-off-by: Patrick Avery <[email protected]>

* Fix `isinstance()` check for series uids

Signed-off-by: Patrick Avery <[email protected]>

* Fix logic in `SlideUids.matches()`

Signed-off-by: Patrick Avery <[email protected]>

* Remove unused import

Signed-off-by: Patrick Avery <[email protected]>

---------

Signed-off-by: Patrick Avery <[email protected]>

* Remove unsafe number_of_focal_planes and number_of_optical_paths properties

* FIx if

* Skip frame count check for concatenated instances

---------

Signed-off-by: Patrick Avery <[email protected]>
Co-authored-by: Erik O Gabrielsson <[email protected]>
Co-authored-by: Erik O Gabrielsson <[email protected]>
  • Loading branch information
3 people authored Nov 9, 2023
1 parent 61fbe0d commit 41e07f7
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 18 deletions.
52 changes: 35 additions & 17 deletions wsidicom/instance/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from dataclasses import dataclass
from enum import Enum, IntEnum, auto
from functools import cached_property
from re import U
from typing import Any, List, Optional, Sequence, Tuple, Union, cast

from pydicom.dataset import Dataset
Expand Down Expand Up @@ -256,10 +257,18 @@ def tile_type(self) -> TileType:
"""
tile_type = self._get_dicom_attribute("DimensionOrganizationType")
if tile_type == "TILED_FULL":
# By the standard it should be tiled full.
return TileType.FULL
elif "PerFrameFunctionalGroupsSequence" in self:
if "PerFrameFunctionalGroupsSequence" in self:
# If no per frame functional sequence we cant make a sparse tile index.
return TileType.SPARSE
elif self.frame_count == 1:
if self.image_type == ImageType.LABEL:
# Labels are expected to only have one frame and can be treated as tiled full.
return TileType.FULL
number_of_focal_planes = getattr(self, "TotalPixelMatrixFocalPlanes", 1)
number_of_optical_paths = getattr(self, "NumberOfOpticalPaths", 1)
if self.frame_count == number_of_focal_planes * number_of_optical_paths:
# One frame per focal plane and optical path, treat as tiled full.
return TileType.FULL
raise WsiDicomError("Undetermined tile type.")

Expand Down Expand Up @@ -315,14 +324,6 @@ def spacing_between_slices(self) -> Optional[float]:
return None
return getattr(self.pixel_measure, "SpacingBetweenSlices", None)

@cached_property
def number_of_focal_planes(self) -> int:
"""Return number of focal planes."""
number_of_focal_planes = self._get_dicom_attribute(
"TotalPixelMatrixFocalPlanes"
)
return cast(int, number_of_focal_planes)

@cached_property
def frame_sequence(self) -> DicomSequence:
"""Return per frame functional group sequene if present, otherwise
Expand Down Expand Up @@ -376,16 +377,30 @@ def image_size(self) -> Size:
image_size = Size(self.TotalPixelMatrixColumns, self.TotalPixelMatrixRows)
if image_size.width <= 0 or image_size.height <= 0:
raise WsiDicomError("Image size is zero")
if self.tile_type == TileType.FULL:
if self.tile_type == TileType.FULL and self.uids.concatenation is None:
# Check that the number of frames match the image size and tile size.
# Dont check concantenated instances as the frame count is ambiguous.
expected_tiled_size = image_size.ceil_div(self.tile_size)
if expected_tiled_size.area != self.frame_count:
number_of_focal_planes = getattr(self, "TotalPixelMatrixFocalPlanes", 1)
number_of_optical_paths = getattr(self, "NumberOfOpticalPaths", 1)
expected_frame_count = (
expected_tiled_size.area
* number_of_focal_planes
* number_of_optical_paths
)
if expected_frame_count != self.frame_count:
error = (
f"Image size {image_size} does not match tile size "
f"{self.tile_size} and number of frames {self.frame_count} "
f"for tile type {TileType.FULL}."
)
if self.image_type == ImageType.VOLUME or self.frame_count != 1:
# Be strict on volume images.
if (
self.image_type == ImageType.VOLUME
and self.frame_count
!= number_of_focal_planes * number_of_optical_paths
):
# Be strict on volume images if more than one frame per focal plane
# and optical path.
raise WsiDicomError(error)
# Labels and overviews are likely to have only one tile.
error += " Overriding image size to tile size."
Expand Down Expand Up @@ -454,7 +469,8 @@ def slice_thickness(self) -> Optional[float]:
return self._get_dicom_attribute("SliceThickness", self.pixel_measure)
except AttributeError:
if self.mm_depth is not None:
return self.mm_depth / self.number_of_focal_planes
number_of_focal_planes = getattr(self, "TotalPixelMatrixFocalPlanes", 1)
return self.mm_depth / number_of_focal_planes
return None

@cached_property
Expand Down Expand Up @@ -556,8 +572,10 @@ def matches_instance(self, other_dataset: "WsiDataset") -> bool:
and self.image_size == other_dataset.image_size
and self.tile_size == other_dataset.tile_size
and self.tile_type == other_dataset.tile_type
and (getattr(self, 'TotalPixelMatrixOriginSequence', None) ==
getattr(other_dataset, 'TotalPixelMatrixOriginSequence', None))
and (
getattr(self, "TotalPixelMatrixOriginSequence", None)
== getattr(other_dataset, "TotalPixelMatrixOriginSequence", None)
)
)

def matches_series(self, uids: SlideUids, tile_size: Optional[Size] = None) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion wsidicom/instance/tile_index/full_tile_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def _read_focal_planes_from_datasets(
focal_planes: Set[float] = set()
for dataset in self._datasets:
slice_spacing = dataset.spacing_between_slices
number_of_focal_planes = dataset.number_of_focal_planes
number_of_focal_planes = getattr(dataset, "TotalPixelMatrixFocalPlanes", 1)
if slice_spacing is None:
if number_of_focal_planes == 1:
slice_spacing = 0.0
Expand Down

0 comments on commit 41e07f7

Please sign in to comment.