pathology/dicom_proxy/downsample_util.py (520 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. # ============================================================================== """Util that generates transcoded and/or downsampled frame images.""" import collections import dataclasses import math import os import tempfile import typing from typing import Dict, Iterator, List, Optional, Union import numpy as np from pathology.dicom_proxy import color_conversion_util from pathology.dicom_proxy import dicom_image_coordinate_util from pathology.dicom_proxy import dicom_instance_frame_patch_util from pathology.dicom_proxy import dicom_instance_request from pathology.dicom_proxy import dicom_proxy_flags from pathology.dicom_proxy import enum_types from pathology.dicom_proxy import frame_retrieval_util from pathology.dicom_proxy import icc_color_transform from pathology.dicom_proxy import image_util from pathology.dicom_proxy import metadata_util from pathology.dicom_proxy import parameters_exceptions_and_return_types from pathology.dicom_proxy import proxy_const from pathology.dicom_proxy import render_frame_params from pathology.shared_libs.logging_lib import cloud_logging_client # Types _DicomInstanceFramePatch = ( dicom_instance_frame_patch_util.DicomInstanceFramePatch ) _IccColorTransform = icc_color_transform.IccColorTransform _RenderedDicomFrames = ( parameters_exceptions_and_return_types.RenderedDicomFrames ) _RenderFrameParams = render_frame_params.RenderFrameParams _LocalDicomInstance = parameters_exceptions_and_return_types.LocalDicomInstance _DicomInstanceRequest = dicom_instance_request.DicomInstanceRequest _DownsamplingFrameRequestError = ( parameters_exceptions_and_return_types.DownsamplingFrameRequestError ) _Metrics = parameters_exceptions_and_return_types.Metrics _FrameImages = frame_retrieval_util.FrameImages _ERROR_RETRIEVING_DICOM_FRAMES = 'Error occurred retrieving DICOM frame(s).' @dataclasses.dataclass class _MiniBatch: """Represents one mini-batch frame request. Attributes: frame_patches: List of downsampled image patches (describes coordinates in and required frames in source imaging) required_frame_indexes: Dict frame indexes in source imaging that are required to generate all patches(images) in the mini-batch. """ frame_patches: List[_DicomInstanceFramePatch] required_frame_indexes: Dict[int, None] @dataclasses.dataclass class _MiniBatchFrameImages: """DICOM frames retrieved and still needed to generate images in mini-batch. Attributes: frames_retrieved: Frame images retrieved. frame_indexes_to_get: List of frame indexes in source imaging not defined in frames_retrieved that need to be retrieved. """ frames_retrieved: Optional[_FrameImages] frame_indexes_to_get: List[int] class _TranscodedRenderedDicomFrames(_RenderedDicomFrames): """Return type for transcoded requests to get_rendered_dicom_frame.""" def __init__( self, frames: List[Union[bytes, _DicomInstanceFramePatch]], icc_profile_transform: Optional[_IccColorTransform], params: _RenderFrameParams, source_frames: Optional[frame_retrieval_util.FrameImages], metrics: _Metrics, source_image_compression: Optional[enum_types.Compression], ): """Constructor. Args: frames: List of compressed images (bytes) or downsampled frame patches. Images in order requested. icc_profile_transform: color profile transform to apply to imaging. params: Parameters defining downsampled rendered frame requests. source_frames: Source frame imaging used to construct images from downsampled patches. None of the frames is List[bytes]. metrics: metrics to measure implementation source_image_compression: Compression format source imaging is encoded in. """ if source_frames is None: is_frame_patch = False decoded_source_frames = None else: is_frame_patch = True # Images decoded using OpenCV into BGR ordering decoded_source_frames = { num: image_util.decode_image_bytes(img, source_image_compression) for num, img in source_frames.images.items() } encoded_images = [] for image in frames: if is_frame_patch: image = typing.cast( _DicomInstanceFramePatch, image ).get_downsampled_image(decoded_source_frames) elif ( icc_profile_transform is None and source_image_compression == enum_types.Compression.JPEG_TRANSCODED_TO_JPEGXL and params.compression == enum_types.Compression.JPEG ): # transcoding JPEGXL derieved from JPEG to JPEG with no icc profile # correction try: encoded_images.append(image_util.transcode_jpxl_to_jpeg(image)) continue except image_util.JpegxlToJpegTranscodeError: image = image_util.decode_image_bytes(image, source_image_compression) elif ( icc_profile_transform is None and source_image_compression == enum_types.Compression.JPEG and ( params.compression == enum_types.Compression.JPEGXL or params.compression == enum_types.Compression.JPEG_TRANSCODED_TO_JPEGXL ) ): # transcoding JPEG to JPEGXL with no icc profile correction encoded_images.append(image_util.transcode_jpeg_to_jpxl(image)) continue else: image = image_util.decode_image_bytes(image, source_image_compression) if icc_profile_transform is None: icc_profile = None else: image = color_conversion_util.transform_image_color( image, icc_profile_transform ) icc_profile = icc_profile_transform.rendered_icc_profile encoded_images.append( image_util.encode_image( image, params.compression, params.quality, icc_profile if params.embed_iccprofile else None, ) ) super().__init__(encoded_images, params, metrics) def _metadata_colorspace_equals( instance_icc_profile_color_space: str, name: str ) -> bool: return name in instance_icc_profile_color_space.strip().upper().split(' ') def _optimize_iccprofile_transform_colorspace( params: _RenderFrameParams, instance_icc_profile_color_space: str ) -> _RenderFrameParams: """If DICOM allready in desired color space, skips transform. Args: params: Downsampling rendering parameters. instance_icc_profile_color_space: String representation if color space in DICOM instance metadata. Returns: Downsampling rendering parameters adjusted based on DICOM colorspace. """ if _metadata_colorspace_equals( instance_icc_profile_color_space, params.icc_profile.upper() ): return dataclasses.replace(params, icc_profile=proxy_const.ICCProfile.YES) return params def _get_minibatch( minibatch_index: int, dicom_frame_indexes: list[int], dicom_instance_source: _DicomInstanceRequest, params: _RenderFrameParams, img_filter: image_util.GaussianImageFilter, downsample_metadata: metadata_util.DicomInstanceMetadata, ) -> _MiniBatch: """Returns next mini-batch of images to downsample. Returns mini-batches of frames (N <= _MAX_MINI_BATCH_FRAME_REQUEST_SIZE) Args: minibatch_index: Starting index in the frame list to begin generating the mini-batch. dicom_frame_indexes: Downsampled frame indexes to generate. dicom_instance_source: DICOM source for downsampled imaging. params: Downsampling parameters. img_filter: Image filter to apply before downsampling to reduce aliasing. downsample_metadata: Metadata for the downsampled instance. Returns: _MiniBatch Raises: DownsamplingFrameRequestError: Number of frames in source imaging required to generate a single downsampled image exceeds value of MAX_MINI_BATCH_FRAME_REQUEST_SIZE_FLG. """ frame_patches = [] required_frame_indexes = collections.OrderedDict() frame_index_count = len(dicom_frame_indexes) # scaling require to transform between # source dimensions and downsampled dimensions != params.downsample # if frame dimensions are not perfect multiples. Ensures source patches # fully sample source imaging downsampled_frame_coord_scale_factor = ( dicom_instance_frame_patch_util.get_downsampled_frame_coord_scale_factor( dicom_instance_source.metadata, downsample_metadata ) ) while minibatch_index < frame_index_count: downsample_frame_pixel_coord = ( dicom_image_coordinate_util.get_instance_frame_pixel_coordinates( downsample_metadata, dicom_frame_indexes[minibatch_index] ) ) frame_patch = _DicomInstanceFramePatch( downsample_frame_pixel_coord, params.downsample, downsampled_frame_coord_scale_factor, dicom_instance_source.metadata, params.interpolation, img_filter, ) patch_frame_count = len(frame_patch.frame_indexes) if ( patch_frame_count > dicom_proxy_flags.MAX_MINI_BATCH_FRAME_REQUEST_SIZE_FLG.value ): raise _DownsamplingFrameRequestError( 'Number of frames required to create a single downsampled frame ' 'exceeds max number of frames supported by the DICOM Proxy. ' 'Reduce frame downsampling. Downsample requires ' f'{patch_frame_count} source frames; Max mini-batch request size: ' f'{dicom_proxy_flags.MAX_MINI_BATCH_FRAME_REQUEST_SIZE_FLG.value}.' ) # if worst case estimate for total size of required frames exceeds # limit process existing batch if ( patch_frame_count + len(required_frame_indexes) > dicom_proxy_flags.MAX_MINI_BATCH_FRAME_REQUEST_SIZE_FLG.value ): break frame_patches.append(frame_patch) # multiple frames are being requested only get ones which haven't # already been requested in mini-batch for fi in frame_patch.frame_indexes: required_frame_indexes[fi] = None minibatch_index += 1 return _MiniBatch(frame_patches, required_frame_indexes) def _create_minibatch_frame_request( required_frame_indexes: Iterator[int], previous_batch: Optional[_MiniBatchFrameImages], ) -> _MiniBatchFrameImages: """Creates a mini-batch frame request. Args: required_frame_indexes: List of frames required from source imaging to generate downsampled patches. previous_batch: Previous mini-batch. If defined will bring any previously retrieved required frames forward into next mini-batch to reduce the number of frames which need to be retrieved for the returned mini-batch. Returns: _MiniBatchFrameImages """ if previous_batch is None or previous_batch.frames_retrieved is None: return _MiniBatchFrameImages(None, list(required_frame_indexes)) # if frames previously retrieved in preceding mini-batch. # scan frames list for frames requested in mini-batch and carry previously # retrieved and required frames forward into next mini-batch. required_retrieved_images = {} indexes_required = [] for fi in required_frame_indexes: frame_found = previous_batch.frames_retrieved.images.get(fi) if frame_found is None: indexes_required.append(fi) else: required_retrieved_images[fi] = frame_found previous_batch.frames_retrieved.images = required_retrieved_images previous_batch.frame_indexes_to_get = indexes_required return previous_batch def _get_missing_minibatch_frames( dicom_instance_source: _DicomInstanceRequest, params: _RenderFrameParams, frame_request: _MiniBatchFrameImages, metrics: _Metrics, ) -> None: """Retrieves mini-batch frames that have not been retrieved. Args: dicom_instance_source: DICOM instance to retrieve frames from. params: Parameters to use. frame_request: Mini-batch frame request. metrics: Metrics to measure implementation. Returns: None Raises: DownsamplingFrameRequestError: Error occurred retrieving DICOM frame(s). DicomFrameRequestError: Error occurred retrieving DICOM frame(s). """ if not frame_request.frame_indexes_to_get: return metrics.frame_requests += len(frame_request.frame_indexes_to_get) try: source_frames = dicom_instance_source.get_dicom_frames( params, frame_request.frame_indexes_to_get ) except frame_retrieval_util.BaseFrameRetrievalError as exp: raise _DownsamplingFrameRequestError( _ERROR_RETRIEVING_DICOM_FRAMES ) from exp metrics.number_of_frames_downloaded_from_store += ( source_frames.number_of_frames_downloaded_from_store ) if frame_request.frames_retrieved is None: frame_request.frames_retrieved = source_frames else: frame_request.frames_retrieved.images.update(source_frames.images) frame_request.frame_indexes_to_get = [] def _get_max_downsample(metadata: metadata_util.DicomInstanceMetadata) -> float: """Returns maximum downsampling factor for DICOM instance metadata.""" return float( max(metadata.total_pixel_matrix_columns, metadata.total_pixel_matrix_rows) ) def _get_frames_no_downsampling( frame_indexes: List[int], dicom_instance_source: _DicomInstanceRequest, params: _RenderFrameParams, icc_profile_transform: Optional[_IccColorTransform], metrics: _Metrics, ) -> _RenderedDicomFrames: """Returns DICOM frames without downsampling, optionally transcoded. Args: frame_indexes: List of frame indexes to retrieve from source instance. dicom_instance_source: DICOM instance to retrieve frames from. params: Parameters to use. icc_profile_transform: ICC profile transform to apply to frames. metrics: Metrics to measure implementation. Returns: _RenderedDicomFrames (encoded frame imaging bytes and description of encoding) Raises: DownsamplingFrameRequestError: Error occurred retrieving DICOM frame(s). DicomFrameRequestError: Error occurred retrieving DICOM frame(s). """ dedup_frame_indexes = list( collections.OrderedDict.fromkeys(frame_indexes).keys() ) metrics.frame_requests += len(dedup_frame_indexes) try: source_frames = dicom_instance_source.get_dicom_frames( params, dedup_frame_indexes ) except frame_retrieval_util.BaseFrameRetrievalError as exp: raise _DownsamplingFrameRequestError( _ERROR_RETRIEVING_DICOM_FRAMES ) from exp result = [ source_frames.images[dicom_frame_index] for dicom_frame_index in frame_indexes ] metrics.number_of_frames_downloaded_from_store += ( source_frames.number_of_frames_downloaded_from_store ) if ( params.compression == source_frames.compression and icc_profile_transform is None ): metrics.images_transcoded = False return _RenderedDicomFrames(result, params, metrics) # transcode images metrics.images_transcoded = True return _TranscodedRenderedDicomFrames( result, icc_profile_transform, params, None, metrics, dicom_instance_source.metadata.image_compression, ) def get_rendered_dicom_frames( dicom_instance_source: _DicomInstanceRequest, params: _RenderFrameParams, dicom_frame_indexes: List[int], ) -> _RenderedDicomFrames: """Returns downsampled and/or transcoded WSI frame imaging. Function accepts a list of downsampled frames and performs downsampling using params defined parameters. Frame retrieval is conducted in parallel and utilizes extensive caching to reduce DICOM store transactions both within and across requests. RenderedFrameParams.downsample < 1 implies zooming into imaging and is not supported. If RenderedFrameParams.downsample == 1 frames will be retrieved in parallel, transcoded as needed, but not resized. Number of frames returned will equal the number of frames requested. If RenderedFrameParams.downsample > 1 implies imaging downsampling, e.g. 2 = 50% reduction. Downsampled frame generation requires retrieval of multiple higher magnification frames (4 - 16). Higher level frames are first identified and de-duplicated. The set of required frames is then retrieved. If the set of frames exceeds the servers _MAX_FRAME_BATCH_REQUEST_SIZE_FLG, then the request will be split into mini-batches. This may cause duplicate store transactions. Frame retrieval is cached (LRU_, so it is possible that frames across non-consecutive mini-batches may not require retrieval from the store. If this occurs the compressed frame imaging require duplicate decompression. The set of required frames not in memory is requested in parallel and decompressed to RGB for downsampled image generation. Transcoding Image transcoding is supported for (JPEG, WEBP, PNG, GIF). JPEG is a native format supported by DICOM. If the DICOM is encoded in JPEG and requested in JPEG then the image will be returned without transcoding for higher quality and performance. ICC_Color profile correction requires image transcoding. Security All requests perform an uncached metadata transaction against the DICOM store to retrieve the instance metadata required for downsampling. This operation also functions to validate that the requesting user has read access to the DICOM store. Optimally, if all other store requests are cached then this will be the only transaction the user performs against the store. Limitations * Total number of frames requests <= _MAX_NUMBER_OF_FRAMES_PER_REQUEST_FLG Ramifications It may require multiple tile-server transactions to request frames. Total number of frames requested in a mini-batch <= _MAX_MINI_BATCH_FRAME_REQUEST_SIZE_FLG * Total number of frames requested in a mini-batch <= _MAX_MINI_BATCH_FRAME_REQUEST_SIZE_FLG (Applies to requests with downsampling factor > 1.0) Ramifications 1) By default _MAX_MINI_BATCH_FRAME_REQUEST_SIZE_FLG = 500 server should support downsamples up to ~16x for images of any size. By default, images with less than 500 will support downsampling by an factor, e.g. 32x or 100x. 2) Performance will drop as the number of mini-batches required to fulfill the request increases. Assumptions The LRU caching and limits are based on frame numbers. It is assumed frame dimensions will be relatively consistent across the DICOM (200 - 512 pixels square). DICOM Proxy memory requirements will increase as frame size increases. Recommendations Source DICOM: Source DICOM instance for frame generation should optimally be the instance which requires the least downsampling. Example, if the DICOM store has a 40x, and 10x instance stored and tile representing a 5x instance is desired then 2x down sample from the 10x should be requested. Frame Request: Batch frame requests will improve performance even if the request is internally broken into multiple mini-batches. Compression: JPEG compression (Improved performance for JPEG encoded files) Downsampling Algorithm: AREA = Recommended for quality and speed. Args: dicom_instance_source: DICOM instance being returned (DICOMweb or Local). params: Parameters for downsampling and transcoding dicom_frame_indexes: List of frame indexs to return. Frame index’s relative to downsampled imaging dimensions. First frame # = 1 Returns: RenderedDicomFrames (list of generated frame imaging and compression format that the images are encoded in). Raises: DownsamplingFrameRequestError: Invalid downsampling frame request. """ metrics = _Metrics() if not dicom_frame_indexes: raise _DownsamplingFrameRequestError('No frames requested.') requested_count = len(dicom_frame_indexes) if ( dicom_proxy_flags.MAX_NUMBER_OF_FRAMES_PER_REQUEST_FLG.value < requested_count ): raise _DownsamplingFrameRequestError( 'Number of frames requested exceeds max number of frames that can be' f' returned in one batch; Requested frame count: {requested_count}; Max' ' supported:' f' {dicom_proxy_flags.MAX_NUMBER_OF_FRAMES_PER_REQUEST_FLG.value}.' ) if params.downsample < 1.0: raise _DownsamplingFrameRequestError( 'DICOM Proxy does not support downsampling factors less than 1.0; ' f'downsampling factor requested: {params.downsample}.' ) if min(dicom_frame_indexes) <= 0: raise _DownsamplingFrameRequestError( 'Requesting frame # < 1; ' f'Frame numbers requested: {dicom_frame_indexes}' ) # cannot downsample smaller than 1 x 1 pixel params.downsample = min( params.downsample, _get_max_downsample(dicom_instance_source.metadata) ) # Uncached metadata request # functions as a test that a user has read access to dicom store. downsample_metadata = dicom_instance_source.metadata.downsample( params.downsample ) if max(dicom_frame_indexes) > downsample_metadata.number_of_frames: msg = ( 'Requesting frame # > metadata number of frames; ' f'Number of frames: {downsample_metadata.number_of_frames} ' f'Frame numbers requested: {dicom_frame_indexes}' ) cloud_logging_client.error( msg, { 'downsample_factor': params.downsample, 'source_instance_metadata': dataclasses.asdict( dicom_instance_source.metadata ), 'downsampled_instance_metadata': dataclasses.asdict( downsample_metadata ), }, ) raise _DownsamplingFrameRequestError(msg) params = _optimize_iccprofile_transform_colorspace( params, dicom_instance_source.icc_profile_color_space() ) try: icc_profile_transform = dicom_instance_source.icc_profile_transform( params.icc_profile ) except color_conversion_util.UnableToLoadIccProfileError as exp: raise _DownsamplingFrameRequestError(str(exp)) from exp dicom_frame_indexes = [index - 1 for index in dicom_frame_indexes] if params.downsample == 1.0: return _get_frames_no_downsampling( dicom_frame_indexes, dicom_instance_source, params, icc_profile_transform, metrics, ) img_filter = image_util.GaussianImageFilter( params.downsample, params.interpolation ) # if necessary batch frame requests into mini-batches to control total number # of source imaging frames in memory at one time. downsample_image_list = [] minibatch_start_index = 0 minibatch_frame_request: Optional[_MiniBatchFrameImages] = None while minibatch_start_index < len(dicom_frame_indexes): minibatch = _get_minibatch( minibatch_start_index, dicom_frame_indexes, dicom_instance_source, params, img_filter, downsample_metadata, ) minibatch_start_index += len(minibatch.frame_patches) minibatch_frame_request = _create_minibatch_frame_request( iter(minibatch.required_frame_indexes.keys()), minibatch_frame_request ) _get_missing_minibatch_frames( dicom_instance_source, params, minibatch_frame_request, metrics ) transcoded_frames = _TranscodedRenderedDicomFrames( minibatch.frame_patches, icc_profile_transform, params, minibatch_frame_request.frames_retrieved, metrics, dicom_instance_source.metadata.image_compression, ) metrics.mini_batch_requests += 1 downsample_image_list.extend(transcoded_frames.images) metrics.images_transcoded = True return _RenderedDicomFrames(downsample_image_list, params, metrics) def downsample_dicom_instance( dicom_instance: _DicomInstanceRequest, params: _RenderFrameParams, batch_mode: bool = False, decode_image_as_numpy: bool = True, clip_image_dim: bool = True, ) -> _RenderedDicomFrames: """Downsample the entire DICOM instance and return as image. Function will require large amounts of memory for high magnification imaging. Args: dicom_instance: DICOM instance to downsample. params: Parameters to use. batch_mode: Flag indicates if all imaging frames should be requested in one batched transaction. Minimizes duplication of frame decoding computational effort at the expense of memory. False, recommend for instances with large frame numbers. decode_image_as_numpy: If true imaging is not encoded at the end of get_rendered_dicom_frames (True = optimization). False models calls to get_rendered_dicom_frames. clip_image_dim: Clip dimensions to dimensions of downsampled image. Returns: _RenderedDicomFrames encoding whole instance as a single frame image. """ if params.downsample < 1.0: raise _DownsamplingFrameRequestError( 'DICOM Proxy does not support downsampling factors less than 1.0; ' f'downsampling factor requested: {params.downsample}.' ) params = _optimize_iccprofile_transform_colorspace( params, dicom_instance.icc_profile_color_space() ) orig_params = params # original unmodified params. if decode_image_as_numpy: params = params.copy() # copy params to preserve parameter values params.compression = enum_types.Compression.NUMPY ds_meta = dicom_instance.metadata.downsample(params.downsample) width = ds_meta.total_pixel_matrix_columns height = ds_meta.total_pixel_matrix_rows tile_width = ds_meta.columns tile_height = ds_meta.rows frame_width_count = int(math.ceil(float(width) / float(tile_width))) frame_height_count = int(math.ceil(float(height) / float(tile_height))) ds_img_width = frame_width_count * tile_width ds_img_height = frame_height_count * tile_height instance_image = None image_list = None if batch_mode: batch_frame_block = [] max_frame_count = frame_height_count * frame_width_count else: max_frame_count = None batch_frame_block = None framenumber = 1 py = 0 py_end = tile_height return_metrics = _Metrics() for _ in range(frame_height_count): px = 0 px_end = tile_width for _ in range(frame_width_count): if batch_mode: if ( not batch_frame_block or framenumber < batch_frame_block[0] or framenumber > batch_frame_block[-1] ): batch_frame_block = list( range( framenumber, min( framenumber + dicom_proxy_flags.MAX_NUMBER_OF_FRAMES_PER_REQUEST_FLG.value, max_frame_count + 1, ), ) ) rendered_frames = get_rendered_dicom_frames( dicom_instance, params, batch_frame_block ) return_metrics.add_metrics(rendered_frames.metrics) image_list = rendered_frames.images # request frames in reverse order. Rendered frames poped from list. image_list.reverse() del rendered_frames undecoded_image = image_list.pop() # pytype: disable=attribute-error else: rendered_frame = get_rendered_dicom_frames( dicom_instance, params, [framenumber] ) return_metrics.add_metrics(rendered_frame.metrics) undecoded_image = rendered_frame.images[0] del rendered_frame if params.compression == enum_types.Compression.NUMPY: decoded_image = typing.cast(np.ndarray, undecoded_image) elif params.compression == enum_types.Compression.RAW: decoded_image = np.ndarray( (tile_height, tile_width, 3), dtype=np.uint8, buffer=undecoded_image ) else: decoded_image = image_util.decode_image_bytes( undecoded_image, dicom_instance.metadata.image_compression ) if instance_image is None: instance_image = np.zeros( ( ds_img_height, ds_img_width, decoded_image.shape[-1], ), # type: ignore dtype=np.uint8, ) instance_image[py:py_end, px:px_end] = decoded_image[...] del undecoded_image, decoded_image framenumber += 1 px = px_end px_end += tile_width py = py_end py_end += tile_height if clip_image_dim: # crop image to actual image dimensions instance_image = instance_image[:height, :width, ...] icc_profile_transform = dicom_instance.icc_profile_transform( orig_params.icc_profile ) if icc_profile_transform is None: icc_profile = None else: icc_profile = icc_profile_transform.rendered_icc_profile return_metrics.images_transcoded = True # all images re-compressed here. return _RenderedDicomFrames( [ image_util.encode_image( instance_image, orig_params.compression, # type: ignore orig_params.quality, icc_profile if orig_params.embed_iccprofile else None, ) ], orig_params, return_metrics, ) def downsample_dicom_web_instance( dicom_instance: _DicomInstanceRequest, params: _RenderFrameParams, batch_mode: bool = False, decode_image_as_numpy: bool = True, clip_image_dim: bool = True, ) -> _RenderedDicomFrames: """Downsample the entire DICOM instance and return as image. Tests metadata to determine if DICOM appears to be encoded as Baseline JPG Yes: Downloads instance to container and generates image locally. No: Generates downsampled frames via rendered frame requests against store. Args: dicom_instance: DICOM Store instance to downsample. params: Parameters to use. batch_mode: Flag indicates if all imaging frames should be requested in one batched transaction. Minimizes duplication of frame decoding computational effort at the expense of memory. False, recommend for instances with large frame numbers. decode_image_as_numpy: If true imaging is not encoded at the end of get_rendered_dicom_frames (True = optimization). False models calls to get_rendered_dicom_frames. clip_image_dim: Clip dimensions to dimensions of downsampled image. Returns: _RenderedDicomFrames encoding whole instance as a single frame image. """ if not ( dicom_instance.metadata.is_baseline_jpeg or dicom_instance.metadata.is_jpeg2000 or dicom_instance.metadata.is_jpegxl ): return downsample_dicom_instance( dicom_instance, params, batch_mode, decode_image_as_numpy, clip_image_dim, ) with tempfile.TemporaryDirectory() as temp_dir: path = os.path.join(temp_dir, 'instance.dcm') dicom_instance.download_instance(path) return downsample_dicom_instance( _LocalDicomInstance(path), params, batch_mode, decode_image_as_numpy, clip_image_dim, )