ez_wsi_dicomweb/dicom_frame_decoder.py (53 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.
# ==============================================================================
"""Image encoding, decoding, and downsampling utility."""
import enum
import io
from typing import Optional
import cv2
import imagecodecs
import numpy as np
import PIL.Image
# DICOM Transfer syntax define the encoding of the pixel data in an instance.
# https://www.dicomlibrary.com/dicom/transfer-syntax/
class DicomTransferSyntax(enum.Enum):
JPEG_BASELINE = '1.2.840.10008.1.2.4.50'
JPEG_2000_LOSSLESS = '1.2.840.10008.1.2.4.90'
JPEG_2000 = '1.2.840.10008.1.2.4.91'
JPEGXL_LOSSLESS = '1.2.840.10008.1.2.4.110'
JPEGXL_JPEG = '1.2.840.10008.1.2.4.111'
JPEGXL = '1.2.840.10008.1.2.4.112'
_JPEG2000_TRANSFER_SYNTAXS = {
DicomTransferSyntax.JPEG_2000_LOSSLESS.value,
DicomTransferSyntax.JPEG_2000.value,
}
_JPEGXL_TRANSFER_SYNTAXS = {
DicomTransferSyntax.JPEGXL_LOSSLESS.value,
DicomTransferSyntax.JPEGXL_JPEG.value,
DicomTransferSyntax.JPEGXL.value,
}
def can_decompress_dicom_transfer_syntax(transfer_syntax: str) -> bool:
return transfer_syntax in (
supported_syntax.value for supported_syntax in DicomTransferSyntax
)
def _pad_frame(frame: Optional[np.ndarray]) -> Optional[np.ndarray]:
"""Pad monochrome frames to 1 channel if channel is ommited."""
if frame is not None and len(frame.shape) == 2:
return np.expand_dims(frame, 2)
return frame
def decode_dicom_compressed_frame_bytes(
frame: bytes, transfer_syntax: str
) -> Optional[np.ndarray]:
"""Decode compressed frame bytes to RGB image.
Args:
frame: Raw image bytes (compressed blob).
transfer_syntax: DICOM transfer syntax frame pixels are encoded in.
Returns:
Decompressed image or None if decompression fails.
"""
if transfer_syntax == DicomTransferSyntax.JPEGXL_JPEG.value:
# if JPGXL is encoded JPEG then extract JPG and decode using
# using JPG pipeline.
try:
frame = imagecodecs.jpegxl_encode_jpeg(frame, numthreads=1)
transfer_syntax = DicomTransferSyntax.JPEG_BASELINE.value
except ValueError:
pass
if transfer_syntax in _JPEGXL_TRANSFER_SYNTAXS:
return _pad_frame(imagecodecs.jpegxl_decode(frame, numthreads=1))
if transfer_syntax not in _JPEG2000_TRANSFER_SYNTAXS:
result = cv2.imdecode(
np.frombuffer(frame, dtype=np.uint8), cv2.IMREAD_COLOR
)
if result is not None:
cv2.cvtColor(result, cv2.COLOR_BGR2RGB, dst=result)
return _pad_frame(result)
try:
return _pad_frame(np.asarray(PIL.Image.open(io.BytesIO(frame))))
except PIL.UnidentifiedImageError:
return None