ez_wsi_dicomweb/dicom_store.py (113 lines of code) (raw):

# Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== """Dicom Web abstraction layer.""" from typing import Optional from ez_wsi_dicomweb import credential_factory as credential_factory_module from ez_wsi_dicomweb import dicom_slide from ez_wsi_dicomweb import dicom_web_interface from ez_wsi_dicomweb import ez_wsi_errors from ez_wsi_dicomweb import ez_wsi_logging_factory from ez_wsi_dicomweb import local_dicom_slide_cache from ez_wsi_dicomweb import local_dicom_slide_cache_types from ez_wsi_dicomweb import pixel_spacing from ez_wsi_dicomweb.ml_toolkit import dicom_path class DicomStore: """Abstraction on top of DicomWeb. Designed to be the layer of internal PathDB. Provides the single point of access to the hierarchy of slide/image/patch. """ def __init__( self, dicomstore_path: str, enable_client_slide_frame_decompression: bool = True, credential_factory: Optional[ credential_factory_module.AbstractCredentialFactory ] = None, pixel_spacing_diff_tolerance: float = pixel_spacing.PIXEL_SPACING_DIFF_TOLERANCE, logging_factory: Optional[ ez_wsi_logging_factory.AbstractLoggingInterfaceFactory ] = None, slide_frame_cache: Optional[ local_dicom_slide_cache.InMemoryDicomSlideCache ] = None, ): """Creates a DicomStore object. Args: dicomstore_path: The path to the dicom store. enable_client_slide_frame_decompression: determines whether frames should be decompressed server side or client side. Client side reduces data transfer. credential_factory: The factory that EZ WSI uses to construct the credentials needed to access the DICOM store pixel_spacing_diff_tolerance: The tolerance (percentage difference) for difference between row and column pixel spacings. This will be used when creating DicomSlide objects. logging_factory: The factory that EZ WSI uses to construct a logging interface. slide_frame_cache: Slide cache, init to share cache across stores. Returns: A DicomStore object. """ self._enable_client_slide_frame_decompression = ( enable_client_slide_frame_decompression ) self.dicomstore_path = dicomstore_path if credential_factory is None: credential_factory = credential_factory_module.CredentialFactory() if logging_factory is None: self._logging_factory = ez_wsi_logging_factory.BasePythonLoggerFactory( ez_wsi_logging_factory.DEFAULT_EZ_WSI_PYTHON_LOGGER_NAME ) else: self._logging_factory = logging_factory self.dicomweb = dicom_web_interface.DicomWebInterface(credential_factory) self._pixel_spacing_diff_tolerance = pixel_spacing_diff_tolerance self._slide_frame_cache = slide_frame_cache @property def slide_frame_cache( self, ) -> Optional[local_dicom_slide_cache.InMemoryDicomSlideCache]: """Returns DICOM slide frame cache used by slide.""" return self._slide_frame_cache @slide_frame_cache.setter def slide_frame_cache( self, slide_frame_cache: Optional[ local_dicom_slide_cache.InMemoryDicomSlideCache ], ) -> None: """Sets DICOM slide frame cache used by the store. Shared cache's configured using max_cache_frame_memory_lru_cache_size_bytes can be used to limit total cache memory utilization across multiple stores. It is not recommended to share non-LRU frame caches. Args: slide_frame_cache: Reference to slide frame cache. """ self._slide_frame_cache = slide_frame_cache def init_slide_frame_cache( self, max_cache_frame_memory_lru_cache_size_bytes: int, number_of_frames_to_read: int = local_dicom_slide_cache.DEFAULT_NUMBER_OF_FRAMES_TO_READ_ON_CACHE_MISS, max_instance_number_of_frames_to_prefer_whole_instance_download: int = local_dicom_slide_cache.MAX_INSTANCE_NUMBER_OF_FRAMES_TO_PREFER_WHOLE_INSTANCE_DOWNLOAD, optimization_hint: local_dicom_slide_cache_types.CacheConfigOptimizationHint = local_dicom_slide_cache_types.CacheConfigOptimizationHint.MINIMIZE_DICOM_STORE_QPM, ) -> local_dicom_slide_cache.InMemoryDicomSlideCache: """Init a shared DICOM slide frame cache that will back store slides. Args: max_cache_frame_memory_lru_cache_size_bytes: Maximum size of cache in bytes. Ideally should be in hundreds of megabyts-to-gigabyte size. If None, no limit to size. number_of_frames_to_read: Number of frames to read on cache miss. max_instance_number_of_frames_to_prefer_whole_instance_download: Max number of frames to prefer downloading whole instances over retrieving frames in batch (Typically faster for small instances e.g. < 10,0000). Optimal threshold will depend on average size of instance frame data and the size of non frame instance metadata. optimization_hint: Optimize cache to minimize data latency or total queries to the DICOM store. Returns: DICOM slide frame cache initialized for the store. """ self._slide_frame_cache = local_dicom_slide_cache.InMemoryDicomSlideCache( credential_factory=self.dicomweb.credential_factory, max_cache_frame_memory_lru_cache_size_bytes=max_cache_frame_memory_lru_cache_size_bytes, number_of_frames_to_read=number_of_frames_to_read, max_instance_number_of_frames_to_prefer_whole_instance_download=max_instance_number_of_frames_to_prefer_whole_instance_download, optimization_hint=optimization_hint, logging_factory=self._logging_factory, ) return self._slide_frame_cache def remove_slide_frame_cache(self) -> None: """Remove reference to store level DICOM cache.""" self._slide_frame_cache = None def get_slide_by_accession_number( self, accession_number: str ) -> dicom_slide.DicomSlide: """Searches a DicomSlide object by accession number. Args: accession_number: DICOM tag, e.g. AccessionNumber=123. Returns: A DicomSlide object. Raises: UnexpectedDicomSlideCountError if the slide count for the series is not 1. """ dicom = self.dicomweb.get_series( dicom_path.FromString(self.dicomstore_path), {'AccessionNumber': accession_number}, ) if len(dicom) != 1: raise ez_wsi_errors.UnexpectedDicomSlideCountError( f'Expect single slide for {accession_number}, len(dicom)={len(dicom)}' ) return dicom_slide.DicomSlide( self.dicomweb, dicom[0].path, enable_client_slide_frame_decompression=self._enable_client_slide_frame_decompression, accession_number=accession_number, pixel_spacing_diff_tolerance=self._pixel_spacing_diff_tolerance, logging_factory=self._logging_factory, slide_frame_cache=self._slide_frame_cache, ) def get_slide( self, study_instance_uid: str, series_instance_uid: str ) -> dicom_slide.DicomSlide: """Gets a DicomSlide object. Args: study_instance_uid: DICOM study instance UID. series_instance_uid: DICOM study instance UID. Returns: A DicomSlide object. Raises: DicomSlideMissingError if the slide is not constructed correctly. """ store_path = dicom_path.FromString(self.dicomstore_path) series_path = dicom_path.FromPath( store_path, study_uid=study_instance_uid, series_uid=series_instance_uid ) slide = dicom_slide.DicomSlide( self.dicomweb, series_path, enable_client_slide_frame_decompression=self._enable_client_slide_frame_decompression, pixel_spacing_diff_tolerance=self._pixel_spacing_diff_tolerance, logging_factory=self._logging_factory, slide_frame_cache=self._slide_frame_cache, ) if slide is None: raise ez_wsi_errors.DicomSlideMissingError( 'Error constructing DicomSlide for slide StudyInstanceUID: ' f' {study_instance_uid}; SeriesInstanceUID: {series_instance_uid}.' ) return slide