From 32d347a8b059ff82f6fe3aef0d28d5f81e718c6d Mon Sep 17 00:00:00 2001 From: Erik O Gabrielsson Date: Thu, 30 Nov 2023 09:51:35 +0100 Subject: [PATCH 1/3] Get the available transfer syntaxes from server if possible --- wsidicom/web/wsidicom_web_client.py | 94 ++++++++++++++++++----------- wsidicom/web/wsidicom_web_source.py | 59 +++++++++++++----- 2 files changed, 101 insertions(+), 52 deletions(-) diff --git a/wsidicom/web/wsidicom_web_client.py b/wsidicom/web/wsidicom_web_client.py index e7839711..82a17152 100644 --- a/wsidicom/web/wsidicom_web_client.py +++ b/wsidicom/web/wsidicom_web_client.py @@ -14,7 +14,7 @@ from http import HTTPStatus import logging -from typing import Any, Dict, Iterator, List, Optional, Tuple, Union +from typing import Any, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union from dicomweb_client.api import DICOMfileClient, DICOMwebClient from dicomweb_client.session_utils import create_session_from_auth @@ -30,6 +30,8 @@ SOP_CLASS_UID = "00080016" SOP_INSTANCE_UID = "00080018" +SERIES_INSTANCE_UID = "0020000E" +AVAILABLE_SOP_TRANSFER_SYNTAX_UID = "00083002" class WsiDicomWebClient: @@ -81,42 +83,47 @@ def create_client( return cls(client) - def get_wsi_instances(self, study_uid: UID, series_uid: UID) -> Iterator[UID]: + def get_wsi_instances( + self, study_uid: UID, series_uids: Iterable[UID] + ) -> Iterator[Tuple[UID, UID, Optional[Set[UID]]]]: """ - Get instance uids for WSI instances in a series. + Get instance uids for WSI instances in a study. Parameters ---------- study_uid: UID - Study UID of the series. - series_uid: UID - Series UID of the series. + Study UID of the study. + series_uids: Iterable[UID] + Series UIDs in the study. Returns ---------- - Iterator[UID] - Iterator of instance uids for WSI instances in the series.""" - return self._get_intances(study_uid, series_uid, WSI_SOP_CLASS_UID) + Iterator[Tuple[UID, UID, Optional[Set[UID]]]] + Iterator of series and instance uid and optionally available transfer syntax + uids for WSI instances in the study and series. + """ + return self._get_intances(study_uid, series_uids, WSI_SOP_CLASS_UID) def get_annotation_instances( - self, study_uid: UID, series_uid: UID - ) -> Iterator[UID]: + self, study_uid: UID, series_uids: Iterable[UID] + ) -> Iterator[Tuple[UID, UID, Optional[Set[UID]]]]: """ - Get instance uids of Annotation instancesii in a series. + Get instance uids of Annotation instances in a study. Parameters ---------- study_uid: UID - Study UID of the series. - series_uid: UID - Series UID of the series. + Study UID of the study. + series_uids: Iterable[UID] + Series UIDs in the study. Returns ---------- - Iterator[UID] - Iterator of instance uids for Annotation instances in the series. + Iterator[Tuple[UID, UID, Optional[Set[UID]]]] + Iterator of series and instance uid and optionally available transfer syntax + uids for Annotation instances in the study and series. """ - return self._get_intances(study_uid, series_uid, ANN_SOP_CLASS_UID) + return self._get_intances(study_uid, series_uids, ANN_SOP_CLASS_UID) def get_instance( self, study_uid: UID, series_uid: UID, instance_uid: UID @@ -138,7 +145,6 @@ def get_instance( Dataset Instance metadata. """ - self._client.retrieve_instance instance = self._client.retrieve_instance_metadata( study_uid, series_uid, instance_uid ) @@ -218,49 +224,65 @@ def is_transfer_syntax_supported( return True def _get_intances( - self, study_uid: UID, series_uid: UID, sop_class_uid - ) -> Iterator[UID]: - """Get instance uids for instances of SOP class. + self, study_uid: UID, series_uids: Iterable[UID], sop_class_uid + ) -> Iterator[Tuple[UID, UID, Optional[Set[UID]]]]: + """Get series, instance, and optionally available transfer syntax uids for + instances of SOP class in study. Parameters ---------- study_uid: UID - Study UID of the series. - series_uid: UID - Series UID of the series. + Study UID of the study. + series_uids: Iterable[UID] + Series UIDs in the study. sop_class_uid: UID SOP Class UID of the instances. Returns ---------- - Iterator[UID] - Iterator of instance uids for instances of SOP class in the series. + Iterator[Tuple[UID, UID, Optional[Set[UID]]]] + Iterator of series and instance uid and optionally available transfer syntax + uids for instances in the study and series. """ return ( - self._get_sop_instance_uid_from_response(instance) + self._get_uids_from_response(instance) for instance in self._client.search_for_instances( study_uid, - series_uid, fields=["AvailableTransferSyntaxUID"], - search_filters={SOP_CLASS_UID: sop_class_uid}, + search_filters={ + SOP_CLASS_UID: sop_class_uid, + SERIES_INSTANCE_UID: series_uids, + }, ) ) @staticmethod - def _get_sop_instance_uid_from_response(response: Dict[str, Dict[Any, Any]]) -> UID: - """Get SOP Instance UID from response. + def _get_uids_from_response( + response: Dict[str, Dict[Any, Any]] + ) -> Tuple[UID, UID, Optional[Set[UID]]]: + """Get series, instance, and optionally transfer syntax uids from response. Parameters ---------- response: Dict[str, Dict[Any, Any]] - Response from server. + Response from server for an instance. Returns ---------- - UID - SOP Instance UID from response. + Tuple[UID, UID, Optional[Set[UID]]] + Series and instance uid and optionally available transfer syntax + uids for instances in response. """ - return UID(response[SOP_INSTANCE_UID]["Value"][0]) + available_transfer_syntaxes = response.get( + AVAILABLE_SOP_TRANSFER_SYNTAX_UID, None + ) + return ( + UID(response[SERIES_INSTANCE_UID]["Value"][0]), + UID(response[SOP_INSTANCE_UID]["Value"][0]), + set(available_transfer_syntaxes["Value"]) + if available_transfer_syntaxes + else None, + ) @staticmethod def _get_sop_class_uid_from_response(response: Dict[str, Dict[Any, Any]]) -> UID: diff --git a/wsidicom/web/wsidicom_web_source.py b/wsidicom/web/wsidicom_web_source.py index 74370e6a..3b6a539c 100644 --- a/wsidicom/web/wsidicom_web_source.py +++ b/wsidicom/web/wsidicom_web_source.py @@ -41,6 +41,7 @@ """A source for reading WSI DICOM files from DICOMWeb.""" +# Transfer syntaxes to try in order of preference. PREFERED_WEB_TRANSFER_SYNTAXES = [ JPEGBaseline8Bit, JPEG2000, @@ -88,16 +89,20 @@ def __init__( ImageType, Set[UID] ] = defaultdict(set) - def _create_instance(uids: Tuple[UID, UID, UID]) -> Optional[WsiInstance]: - dataset = client.get_instance(uids[0], uids[1], uids[2]) + def create_instance( + uids: Tuple[UID, UID, UID, Optional[Set[UID]]] + ) -> Optional[WsiInstance]: + study_uid, series_uid, instance_uid, available_transfer_syntaxes = uids + dataset = client.get_instance(study_uid, series_uid, instance_uid) if not WsiDataset.is_supported_wsi_dicom(dataset): - logging.info(f"Non-supported instance {uids[2]}.") + logging.info(f"Non-supported instance {instance_uid}.") return dataset = WsiDataset(dataset) transfer_syntax = self._determine_transfer_syntax( client, dataset, + available_transfer_syntaxes, detected_transfer_syntaxes_by_image_type[dataset.image_type], requested_transfer_syntaxes, ) @@ -106,23 +111,27 @@ def _create_instance(uids: Tuple[UID, UID, UID]) -> Optional[WsiInstance]: f"No supported transfer syntax found for instance {uids[2]}." ) return - + detected_transfer_syntaxes_by_image_type[dataset.image_type].add( + transfer_syntax + ) image_data = WsiDicomWebImageData(client, dataset, transfer_syntax) return WsiInstance(dataset, image_data) instance_uids = ( - (study_uid, series_uid, instance_uid) - for series_uid in series_uids - for instance_uid in client.get_wsi_instances(study_uid, series_uid) + (study_uid, series_uid, instance_uid, available_transfer_syntaxes) + for series_uid, instance_uid, available_transfer_syntaxes in client.get_wsi_instances( + study_uid, series_uids + ) ) annotation_instances = ( client.get_instance(study_uid, series_uid, instance_uid) - for series_uid in series_uids - for instance_uid in client.get_annotation_instances(study_uid, series_uid) + for series_uid, instance_uid, _ in client.get_annotation_instances( + study_uid, series_uids + ) ) with ConditionalThreadPoolExecutor(settings.open_web_theads) as pool: - instances = pool.map(_create_instance, instance_uids) + instances = pool.map(create_instance, instance_uids) for instance in instances: if instance is None: continue @@ -183,8 +192,9 @@ def _determine_transfer_syntax( self, client: WsiDicomWebClient, dataset: WsiDataset, + available_transfer_syntaxes: Optional[Set[UID]], detected_transfer_syntaxes: Set[UID], - requested_transfer_syntaxes: Optional[Iterable[UID]] = None, + requested_transfer_syntaxes: Optional[Iterable[UID]], ) -> Optional[UID]: """Determine transfer syntax to use for image data. @@ -194,9 +204,11 @@ def _determine_transfer_syntax( Client used for DICOMWeb communication. dataset: WsiDataset Dataset to determine transfer syntax for. + available_transfer_syntaxes: Optional[Set[UID]] + Transfer syntaxes available on the server, or None if not known. detected_transfer_syntaxes: Set[UID] Transfer syntaxes that have already been detected. - requested_transfer_syntaxes: Optional[Iterable[UID]] = None + requested_transfer_syntaxes: Optional[Iterable[UID]] Transfer syntaxes to try in order of preference. Returns @@ -207,6 +219,7 @@ def _determine_transfer_syntax( """ if requested_transfer_syntaxes is None: requested_transfer_syntaxes = PREFERED_WEB_TRANSFER_SYNTAXES + supported_transfer_syntaxes = ( transfer_syntax for transfer_syntax in requested_transfer_syntaxes @@ -217,12 +230,29 @@ def _determine_transfer_syntax( dataset.photometric_interpretation, ) ) + if available_transfer_syntaxes is not None: + # Server provided list of available transfer syntaxes. + # Select first one that is supported in order by user or default preference. + transfer_syntax = next( + ( + transfer_syntax + for transfer_syntax in supported_transfer_syntaxes + if transfer_syntax in available_transfer_syntaxes + ), + None, + ) + if transfer_syntax is not None: + return transfer_syntax + + # No server provided list of available transfer syntaxes. + # Detect if any of the supported transfer syntaxes are supported by the server. + # Start with the ones that have already been detected. sorted_transfer_syntaxes = sorted( supported_transfer_syntaxes, key=lambda transfer_syntax: transfer_syntax in detected_transfer_syntaxes, ) - transfer_syntax = next( + return next( ( transfer_syntax for transfer_syntax in sorted_transfer_syntaxes @@ -235,6 +265,3 @@ def _determine_transfer_syntax( ), None, ) - if transfer_syntax is not None: - detected_transfer_syntaxes.add(transfer_syntax) - return transfer_syntax From 1ecc9a7696bf9b05415a4bec39a87269e2257de2 Mon Sep 17 00:00:00 2001 From: Erik O Gabrielsson Date: Thu, 30 Nov 2023 15:44:49 +0100 Subject: [PATCH 2/3] Fix for DICOMfileClient search --- wsidicom/web/wsidicom_web_client.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/wsidicom/web/wsidicom_web_client.py b/wsidicom/web/wsidicom_web_client.py index 82a17152..f77ec3ff 100644 --- a/wsidicom/web/wsidicom_web_client.py +++ b/wsidicom/web/wsidicom_web_client.py @@ -244,6 +244,18 @@ def _get_intances( Iterator of series and instance uid and optionally available transfer syntax uids for instances in the study and series. """ + if isinstance(self._client, DICOMfileClient): + # DICOMfileClient does not support searching for instances by + # series instance uid as search filter + return ( + self._get_uids_from_response(instance, series_uid) + for series_uid in series_uids + for instance in self._client.search_for_instances( + study_uid, + series_uid, + search_filters={SOP_CLASS_UID: sop_class_uid}, + ) + ) return ( self._get_uids_from_response(instance) for instance in self._client.search_for_instances( @@ -258,7 +270,7 @@ def _get_intances( @staticmethod def _get_uids_from_response( - response: Dict[str, Dict[Any, Any]] + response: Dict[str, Dict[Any, Any]], series_uid: Optional[UID] = None ) -> Tuple[UID, UID, Optional[Set[UID]]]: """Get series, instance, and optionally transfer syntax uids from response. @@ -277,7 +289,9 @@ def _get_uids_from_response( AVAILABLE_SOP_TRANSFER_SYNTAX_UID, None ) return ( - UID(response[SERIES_INSTANCE_UID]["Value"][0]), + series_uid + if series_uid is not None + else UID(response[SERIES_INSTANCE_UID]["Value"][0]), UID(response[SOP_INSTANCE_UID]["Value"][0]), set(available_transfer_syntaxes["Value"]) if available_transfer_syntaxes From 640102cf13ee0e6ddc0fb2e1cc912ac90e7fcca2 Mon Sep 17 00:00:00 2001 From: Erik O Gabrielsson Date: Thu, 30 Nov 2023 15:49:00 +0100 Subject: [PATCH 3/3] Update version --- CHANGELOG.md | 8 +++++++- pyproject.toml | 2 +- wsidicom/__init__.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15e8b658..28114fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.15.0] - 2023-11-30 + ### Added - Fallback to EOT when overflowing BOT. +- Use AvailableTransferSyntaxUID if provided to determine transfer syntax to use when opening DICOM Web instances. ### Fixed @@ -226,7 +229,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release of wsidicom -[Unreleased]: https://github.com/imi-bigpicture/wsidicom/compare/0.12.0..HEAD +[Unreleased]: https://github.com/imi-bigpicture/wsidicom/compare/0.15.0..HEAD +[0.15.0]: https://github.com/imi-bigpicture/wsidicom/compare/v0.14.0..v0.15.0 +[0.14.0]: https://github.com/imi-bigpicture/wsidicom/compare/v0.13.0..v0.14.0 +[0.13.0]: https://github.com/imi-bigpicture/wsidicom/compare/v0.12.0..v0.13.0 [0.12.0]: https://github.com/imi-bigpicture/wsidicom/compare/v0.11.0..v0.12.0 [0.11.0]: https://github.com/imi-bigpicture/wsidicom/compare/v0.10.0..v0.11.0 [0.10.0]: https://github.com/imi-bigpicture/wsidicom/compare/v0.9.0..v0.10.0 diff --git a/pyproject.toml b/pyproject.toml index fb9eb7ff..8aab5f74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wsidicom" -version = "0.14.0" +version = "0.15.0" description = "Tools for handling DICOM based whole scan images" authors = ["Erik O Gabrielsson "] license = "Apache-2.0" diff --git a/wsidicom/__init__.py b/wsidicom/__init__.py index c5644f62..7b68afb5 100644 --- a/wsidicom/__init__.py +++ b/wsidicom/__init__.py @@ -51,4 +51,4 @@ from wsidicom.web import WsiDicomWebClient from wsidicom.wsidicom import WsiDicom -__version__ = "0.14.0" +__version__ = "0.15.0"