From 11542a5f16db12d89993f4122a4470223432a118 Mon Sep 17 00:00:00 2001 From: kyinhub Date: Sat, 25 Jul 2026 11:05:05 -0700 Subject: [PATCH] fix: decode native floating-point pixel frames Signed-off-by: kyinhub --- src/highdicom/io.py | 22 +++++++++++++----- tests/test_io.py | 55 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/highdicom/io.py b/src/highdicom/io.py index 6018dd6a3..d7b938cb8 100644 --- a/src/highdicom/io.py +++ b/src/highdicom/io.py @@ -31,6 +31,7 @@ ) from pydicom.uid import UID, DeflatedExplicitVRLittleEndian +from highdicom.enum import PixelDataKeywords from highdicom.frame import decode_frame from highdicom.color import ColorManager from highdicom.uid import UID as hd_UID @@ -41,6 +42,11 @@ _FLOAT_PIXEL_DATA_TAGS = {0x7FE00008, 0x7FE00009, } _UINT_PIXEL_DATA_TAGS = {0x7FE00010, } _PIXEL_DATA_TAGS = _FLOAT_PIXEL_DATA_TAGS.union(_UINT_PIXEL_DATA_TAGS) +_PIXEL_DATA_KEYWORDS = { + 0x7FE00008: PixelDataKeywords.FLOAT_PIXEL_DATA, + 0x7FE00009: PixelDataKeywords.DOUBLE_FLOAT_PIXEL_DATA, + 0x7FE00010: PixelDataKeywords.PIXEL_DATA, +} _JPEG_SOI_MARKER = b'\xFF\xD8' # also JPEG-LS _JPEG_EOI_MARKER = b'\xFF\xD9' # also JPEG-LS @@ -545,9 +551,7 @@ def _read_metadata(self) -> None: raise ValueError( 'Dataset does not represent an image information entity.' ) - self._as_float = False - if int(tag) in _FLOAT_PIXEL_DATA_TAGS: - self._as_float = True + self._pixel_keyword = _PIXEL_DATA_KEYWORDS[int(tag)] # Reset the file pointer to the beginning of the Pixel Data element self._fp.seek(self._pixel_data_offset, 0) @@ -759,6 +763,13 @@ def read_frame(self, index: int, correct_color: bool = True) -> np.ndarray: logger.debug(f'decode frame #{index}') + if self._pixel_keyword == PixelDataKeywords.PIXEL_DATA: + bits_stored = self.metadata.BitsStored + pixel_representation = self.metadata.PixelRepresentation + else: + bits_stored = self.metadata.BitsAllocated + pixel_representation = None + frame_array = decode_frame( frame_data, rows=self.metadata.Rows, @@ -766,13 +777,14 @@ def read_frame(self, index: int, correct_color: bool = True) -> np.ndarray: samples_per_pixel=self.metadata.SamplesPerPixel, transfer_syntax_uid=self.transfer_syntax_uid, bits_allocated=self.metadata.BitsAllocated, - bits_stored=self.metadata.BitsStored, + bits_stored=bits_stored, photometric_interpretation=self.metadata.PhotometricInterpretation, - pixel_representation=self.metadata.PixelRepresentation, + pixel_representation=pixel_representation, planar_configuration=getattr( self.metadata, 'PlanarConfiguration', None ), index=index, + pixel_keyword=self._pixel_keyword, ) # We don't use the color_correct_frame() function here, since we cache diff --git a/tests/test_io.py b/tests/test_io.py index e90d77ed6..4c2e8347c 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -6,7 +6,13 @@ import numpy as np from pydicom import dcmread from pydicom.data import get_testdata_file +from pydicom.dataset import FileDataset, FileMetaDataset from pydicom.filebase import DicomBytesIO, DicomFileLike, DicomFile +from pydicom.uid import ( + ExplicitVRLittleEndian, + ParametricMapStorage, + generate_uid, +) import pytest from highdicom.io import ImageFileReader @@ -39,6 +45,55 @@ def test_read_single_frame_ct_image_native(self): ) np.testing.assert_array_equal(frame, pixel_array) + def test_read_float_pixel_data(self): + cases = [ + (np.float32, 'FloatPixelData', 32), + (np.float64, 'DoubleFloatPixelData', 64), + ] + with TemporaryDirectory() as temp_dir: + for dtype, pixel_keyword, bits_allocated in cases: + filename = Path(temp_dir) / f"{pixel_keyword}.dcm" + file_meta = FileMetaDataset() + file_meta.TransferSyntaxUID = ExplicitVRLittleEndian + file_meta.MediaStorageSOPClassUID = ParametricMapStorage + file_meta.MediaStorageSOPInstanceUID = generate_uid() + file_meta.ImplementationClassUID = generate_uid() + dataset = FileDataset( + filename, + {}, + file_meta=file_meta, + preamble=b"\0" * 128, + ) + dataset.SOPClassUID = file_meta.MediaStorageSOPClassUID + dataset.SOPInstanceUID = file_meta.MediaStorageSOPInstanceUID + dataset.Rows = 3 + dataset.Columns = 4 + dataset.SamplesPerPixel = 1 + dataset.PhotometricInterpretation = 'MONOCHROME2' + dataset.NumberOfFrames = 2 + dataset.BitsAllocated = bits_allocated + pixel_array = np.linspace( + -3.5, + 2.25, + num=24, + dtype=dtype, + ).reshape(2, 3, 4) + setattr(dataset, pixel_keyword, pixel_array.tobytes()) + dataset.save_as( + filename, + implicit_vr=False, + little_endian=True, + ) + + with ImageFileReader(filename) as reader: + for frame_index in range(2): + frame = reader.read_frame(frame_index) + assert frame.dtype == dtype + np.testing.assert_array_equal( + frame, + pixel_array[frame_index], + ) + def test_read_multi_frame_ct_image_native(self): filename = str(get_testdata_file('eCT_Supplemental.dcm')) dataset = dcmread(filename)