From 82d8fd35966730c977b534a8d126a2d8573956d0 Mon Sep 17 00:00:00 2001 From: Chris Bridge Date: Tue, 30 Jan 2024 18:46:58 +0000 Subject: [PATCH] Update to allow providing a Palette color LUT or using MONOCHROME2 --- src/highdicom/content.py | 83 +++++++++++++----------- src/highdicom/pr/sop.py | 5 ++ src/highdicom/seg/sop.py | 117 ++++++++++++++++++++++++++++++--- tests/test_content.py | 42 ++++++------ tests/test_seg.py | 136 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 315 insertions(+), 68 deletions(-) diff --git a/src/highdicom/content.py b/src/highdicom/content.py index 4bbc53da..e5e9fadb 100644 --- a/src/highdicom/content.py +++ b/src/highdicom/content.py @@ -2351,7 +2351,7 @@ def __init__( Pixel value that will be mapped to the first value in the lookup table. lut_data: numpy.ndarray - Lookup table data. Must be of type uint16. + Lookup table data. Must be of type uint8 or uint16. color: str Text representing the color (``red``, ``green``, or ``blue``). @@ -2365,15 +2365,26 @@ def __init__( """ super().__init__() + # Note 8 bit LUT data is unsupported for presentation states pending + # clarification on the standard, but is valid for segmentations + if lut_data.dtype.type == np.uint8: + bits_per_entry = 8 + elif lut_data.dtype.type == np.uint16: + bits_per_entry = 16 + else: + raise ValueError( + "Numpy array must have dtype uint8 or uint16." + ) if not isinstance(first_mapped_value, int): raise TypeError('Argument "first_mapped_value" must be an integer.') if first_mapped_value < 0: raise ValueError( 'Argument "first_mapped_value" must be non-negative.' ) - if first_mapped_value >= 2 ** 16: + if first_mapped_value >= 2 ** bits_per_entry: raise ValueError( - 'Argument "first_mapped_value" must be less than 2^16.' + 'Argument "first_mapped_value" must be less than ' + '2^(bits per entry).' ) if not isinstance(lut_data, np.ndarray): @@ -2385,24 +2396,16 @@ def __init__( len_data = lut_data.shape[0] if len_data == 0: raise ValueError('Argument "lut_data" must not be empty.') - if len_data > 2**16: + if len_data > 2 ** bits_per_entry: raise ValueError( 'Length of argument "lut_data" must be no greater than ' - '2^16 elements.' + '2^(bits per entry) elements.' ) - elif len_data == 2**16: + elif len_data == 2 ** bits_per_entry: # Per the standard, this is recorded as 0 number_of_entries = 0 else: number_of_entries = len_data - # Note 8 bit LUT data is unsupported pending clarification on the - # standard - if lut_data.dtype.type == np.uint16: - bits_per_entry = 16 - else: - raise ValueError( - "Numpy array must have dtype uint16." - ) if color.lower() not in ('red', 'green', 'blue'): raise ValueError( @@ -2415,7 +2418,7 @@ def __init__( setattr( self, f'{self._attr_name_prefix}Data', - lut_data.astype(np.uint16).tobytes() + lut_data.tobytes() ) setattr( self, @@ -2427,7 +2430,7 @@ def __init__( def lut_data(self) -> np.ndarray: """numpy.ndarray: lookup table data""" if self.bits_per_entry == 8: - raise RuntimeError("8 bit LUTs are currently unsupported.") + dtype = np.uint8 elif self.bits_per_entry == 16: dtype = np.uint16 else: @@ -2435,7 +2438,7 @@ def lut_data(self) -> np.ndarray: length = self.number_of_entries data = getattr(self, f'{self._attr_name_prefix}Data') # The LUT data attributes have VR OW (16-bit other words) - array = np.frombuffer(data, dtype=np.uint16) + array = np.frombuffer(data, dtype=dtype) # Needs to be casted according to third descriptor value. array = array.astype(dtype) if len(array) != length: @@ -2452,7 +2455,7 @@ def number_of_entries(self) -> int: descriptor = getattr(self, f'{self._attr_name_prefix}Descriptor') value = int(descriptor[0]) if value == 0: - return 2**16 + return 2 ** self.bits_per_entry return value @property @@ -2488,7 +2491,7 @@ def __init__( Pixel value that will be mapped to the first value in the lookup table. segmented_lut_data: numpy.ndarray - Segmented lookup table data. Must be of type uint16. + Segmented lookup table data. Must be of type uint8 or uint16. color: str Free-form text explanation of the color (``red``, ``green``, or ``blue``). @@ -2506,13 +2509,24 @@ def __init__( """ super().__init__() + # Note 8 bit LUT data is unsupported for presentation states pending + # clarification on the standard, but is valid for segmentations + if segmented_lut_data.dtype.type == np.uint8: + bits_per_entry = 8 + elif segmented_lut_data.dtype.type == np.uint16: + bits_per_entry = 16 + else: + raise ValueError( + "Numpy array must have dtype uint8 or uint16." + ) + if not isinstance(first_mapped_value, int): raise TypeError('Argument "first_mapped_value" must be an integer.') if first_mapped_value < 0: raise ValueError( 'Argument "first_mapped_value" must be non-negative.' ) - if first_mapped_value >= 2 ** 16: + if first_mapped_value >= 2 ** bits_per_entry: raise ValueError( 'Argument "first_mapped_value" must be less than 2^16.' ) @@ -2529,23 +2543,14 @@ def __init__( len_data = segmented_lut_data.size if len_data == 0: raise ValueError('Argument "segmented_lut_data" must not be empty.') - if len_data > 2**16: + if len_data > 2 ** bits_per_entry: raise ValueError( 'Length of argument "segmented_lut_data" must be no greater ' 'than 2^16 elements.' ) - elif len_data == 2**16: + elif len_data == 2 ** bits_per_entry: # Per the standard, this is recorded as 0 len_data = 0 - # Note 8 bit LUT data is currently unsupported pending clarification on - # the standard - if segmented_lut_data.dtype.type == np.uint16: - bits_per_entry = 16 - self._dtype = np.uint16 - else: - raise ValueError( - "Numpy array must have dtype uint16." - ) if color.lower() not in ('red', 'green', 'blue'): raise ValueError( @@ -2558,7 +2563,7 @@ def __init__( setattr( self, f'Segmented{self._attr_name_prefix}Data', - segmented_lut_data.astype(np.uint16).tobytes() + segmented_lut_data.tobytes() ) expanded_lut_values = [] @@ -2609,7 +2614,7 @@ def __init__( ) len_data = len(expanded_lut_values) - if len_data == 2**16: + if len_data == 2 ** bits_per_entry: number_of_entries = 0 else: number_of_entries = len_data @@ -2624,10 +2629,14 @@ def segmented_lut_data(self) -> np.ndarray: """numpy.ndarray: segmented lookup table data""" length = self.number_of_entries data = getattr(self, f'Segmented{self._attr_name_prefix}Data') + if self.bits_per_entry == 8: + dtype = np.uint8 + elif self.bits_per_entry == 16: + dtype = np.uint16 + else: + raise RuntimeError("Invalid LUT descriptor.") # The LUT data attributes have VR OW (16-bit other words) - array = np.frombuffer(data, dtype=np.uint16) - # Needs to be casted according to third descriptor value. - array = array.astype(self._dtype) + array = np.frombuffer(data, dtype=dtype) if len(array) != length: raise RuntimeError( 'Length of LUTData does not match the value expected from the ' @@ -2651,7 +2660,7 @@ def number_of_entries(self) -> int: # That's because the descriptor attributes have VR US, which cannot # encode the value of 2^16, but only values in the range [0, 2^16 - 1]. if value == 0: - return 2**16 + return 2 ** self.bits_per_entry else: return value diff --git a/src/highdicom/pr/sop.py b/src/highdicom/pr/sop.py index d3b7afca..e2d2dc91 100644 --- a/src/highdicom/pr/sop.py +++ b/src/highdicom/pr/sop.py @@ -582,6 +582,11 @@ def __init__( ) # Palette Color Lookup Table + if palette_color_lut_transformation.red_lut.bits_per_entry != 16: + raise ValueError( + "palette_color_lut_transformation for presentation states must " + "have 16 bits." + ) _add_palette_color_lookup_table_attributes( self, palette_color_lut_transformation=palette_color_lut_transformation diff --git a/src/highdicom/seg/sop.py b/src/highdicom/seg/sop.py index 8eb88d20..84a8de4d 100644 --- a/src/highdicom/seg/sop.py +++ b/src/highdicom/seg/sop.py @@ -5,6 +5,7 @@ from contextlib import contextmanager from copy import deepcopy from os import PathLike +import pkgutil import sqlite3 from typing import ( Any, @@ -47,6 +48,7 @@ from highdicom.base import SOPClass, _check_little_endian from highdicom.content import ( ContentCreatorIdentificationCodeSequence, + PaletteColorLUTTransformation, PlaneOrientationSequence, PlanePositionSequence, PixelMeasuresSequence @@ -56,6 +58,10 @@ DimensionOrganizationTypeValues, ) from highdicom.frame import encode_frame +from highdicom.pr.content import ( + _add_icc_profile_attributes, + _add_palette_color_lookup_table_attributes, +) from highdicom.utils import ( are_plane_positions_tiled_full, compute_plane_position_tiled_full, @@ -1185,6 +1191,8 @@ def __init__( tile_size: Union[Sequence[int], None] = None, pyramid_uid: Optional[str] = None, pyramid_label: Optional[str] = None, + palette_color_lut_transformation: Optional[PaletteColorLUTTransformation] = None, + icc_profile: Optional[bytes] = None, **kwargs: Any ) -> None: """ @@ -1394,6 +1402,12 @@ def __init__( Human readable label for the pyramid containing this segmentation. Should only be used if this segmentation is part of a multi-resolution pyramid. + palette_color_lut_transformation: Union[highdicom.PaletteColorLUTTransformation, None], optional + A palette color lookup table transformation to apply to the pixels + for display. This is only permitted if segmentation_type is "LABELMAP". + icc_profile: Union[bytes, None] = None + An ICC profile to display the segmentation. This is only permitted + when palette_color_lut_transformation is provided. **kwargs: Any, optional Additional keyword arguments that will be passed to the constructor of `highdicom.base.SOPClass` @@ -1594,10 +1608,8 @@ def __init__( self.ImageType = ['DERIVED', 'PRIMARY'] self.SamplesPerPixel = 1 self.PixelRepresentation = 0 - if segmentation_type == SegmentationTypeValues.LABELMAP: - self.PhotometricInterpretation = 'PALETTE COLOR' - else: - self.PhotometricInterpretation = 'MONOCHROME2' + segmentation_type = SegmentationTypeValues(segmentation_type) + self.SegmentationType = segmentation_type.value if content_label is not None: _check_code_string(content_label) @@ -1620,8 +1632,6 @@ def __init__( self.ContentCreatorIdentificationCodeSequence = \ content_creator_identification - segmentation_type = SegmentationTypeValues(segmentation_type) - self.SegmentationType = segmentation_type.value if self.SegmentationType == SegmentationTypeValues.BINARY.value: self.BitsAllocated = 1 self.HighBit = 0 @@ -1645,9 +1655,25 @@ def __init__( self.MaximumFractionalValue = max_fractional_value elif self.SegmentationType == SegmentationTypeValues.LABELMAP.value: # Decide on the output datatype and update the image metadata - # accordingly + # accordingly. Use the smallest possible type unless there is + # a palette color LUT that says otherwise. labelmap_dtype = _get_unsigned_dtype(len(segment_descriptions)) - self.BitsAllocated = np.iinfo(labelmap_dtype).bits + if labelmap_dtype == np.uint32: + raise ValueError( + "Too many classes to represent with a 16 bit integer." + ) + labelmap_bitdepth = np.iinfo(labelmap_dtype).bits + if palette_color_lut_transformation is not None: + lut_bitdepth = ( + palette_color_lut_transformation.red_lut.bits_per_entry + ) + if lut_bitdepth < labelmap_bitdepth: + raise ValueError( + 'The labelmap provided does not have entries ' + 'to cover the number all specified classes.' + ) + labelmap_bitdepth = lut_bitdepth + self.BitsAllocated = labelmap_bitdepth self.HighBit = self.BitsAllocated - 1 self.BitsStored = self.BitsAllocated @@ -1668,6 +1694,79 @@ def __init__( self.LossyImageCompressionMethod = \ src_img.LossyImageCompressionMethod + # Use PALETTE COLOR photometric interpretation in the case + # of a labelmap segmentation with a provided LUT, MONOCHROME2 + # otherwise + if segmentation_type == SegmentationTypeValues.LABELMAP: + if palette_color_lut_transformation is None: + self.PhotometricInterpretation = 'MONOCHROME2' + if icc_profile is not None: + raise TypeError( + "Argument 'icc_profile' should " + "not be provided if is " + "'palette_color_lut_transformation' " + "is not specified." + ) + else: + # Using photometric interpretation "PALETTE COLOR" + # need to specify the LUT in this case + self.PhotometricInterpretation = 'PALETTE COLOR' + + # Checks on the validity of the LUT + if not isinstance( + palette_color_lut_transformation, + PaletteColorLUTTransformation + ): + raise TypeError( + 'Argument "palette_color_lut_transformation" must be of type ' + 'PaletteColorLUTTransformation.' + ) + + lut = palette_color_lut_transformation.red_lut + lut_entries = lut.number_of_entries + lut_start = lut.first_mapped_value + lut_end = lut_start + lut_entries + + if ( + (lut_start > 1) or lut_end <= len(segment_descriptions) + ): + raise ValueError( + 'The labelmap provided does not have entries ' + 'to cover all segments.' + ) + + # Add the LUT to this instance + _add_palette_color_lookup_table_attributes( + self, + palette_color_lut_transformation, + ) + + if icc_profile is None: + # Use default sRGB profile + icc_profile = pkgutil.get_data( + 'highdicom', + '_icc_profiles/sRGB_v4_ICC_preference.icc' + ) + _add_icc_profile_attributes( + self, + icc_profile=icc_profile + ) + + else: + self.PhotometricInterpretation = 'MONOCHROME2' + if palette_color_lut_transformation is not None: + raise TypeError( + "Argument 'palette_color_lut_transformation' should " + "not be provided when 'segmentation_type' is " + f"'{segmentation_type.value}'." + ) + if icc_profile is not None: + raise TypeError( + "Argument 'icc_profile' should " + "not be provided when 'segmentation_type' is " + f"'{segmentation_type.value}'." + ) + # Multi-Resolution Pyramid if pyramid_uid is not None: if not is_tiled: @@ -2571,7 +2670,7 @@ def _check_and_cast_pixel_array( f'({number_of_segments}).' ) - if pixel_array.dtype in (np.bool_, np.uint8, np.uint16, np.uint32): + if pixel_array.dtype in (np.bool_, np.uint8, np.uint16): max_pixel = pixel_array.max() if pixel_array.ndim == 3: diff --git a/tests/test_content.py b/tests/test_content.py index 5511ac42..f080258d 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -1187,27 +1187,27 @@ def test_construction_16bit(self): np.array_equal(lut.lut_data, lut_data) # Commented out until 8 bit LUTs are reimplemented - # def test_construction_8bit(self): - # lut_data = np.arange(0, 256, dtype=np.uint8) - # first_mapped_value = 0 - # lut = PaletteColorLUT(first_mapped_value, lut_data, color='blue') - - # assert len(lut.BluePaletteColorLookupTableDescriptor) == 3 - # assert lut.BluePaletteColorLookupTableDescriptor[0] == 256 - # assert lut.BluePaletteColorLookupTableDescriptor[1] == 0 - # assert lut.BluePaletteColorLookupTableDescriptor[2] == 8 - # assert not hasattr(lut, 'RedPaletteColorLookupTableDescriptor') - # assert not hasattr(lut, 'GreenPaletteColorLookupTableDescriptor') - # expected_len = lut_data.shape[0] * 2 - # assert len(lut.BluePaletteColorLookupTableData) == expected_len - # assert not hasattr(lut, 'RedPaletteColorLookupTableData') - # assert not hasattr(lut, 'GreenPaletteColorLookupTableData') - - # assert lut.number_of_entries == lut_data.shape[0] - # assert lut.first_mapped_value == first_mapped_value - # assert lut.bits_per_entry == 8 - # assert lut.lut_data.dtype == np.uint8 - # np.array_equal(lut.lut_data, lut_data) + def test_construction_8bit(self): + lut_data = np.arange(0, 256, dtype=np.uint8) + first_mapped_value = 0 + lut = PaletteColorLUT(first_mapped_value, lut_data, color='blue') + + assert len(lut.BluePaletteColorLookupTableDescriptor) == 3 + assert lut.BluePaletteColorLookupTableDescriptor[0] == 0 + assert lut.BluePaletteColorLookupTableDescriptor[1] == 0 + assert lut.BluePaletteColorLookupTableDescriptor[2] == 8 + assert not hasattr(lut, 'RedPaletteColorLookupTableDescriptor') + assert not hasattr(lut, 'GreenPaletteColorLookupTableDescriptor') + expected_len = lut_data.shape[0] + assert len(lut.BluePaletteColorLookupTableData) == expected_len + assert not hasattr(lut, 'RedPaletteColorLookupTableData') + assert not hasattr(lut, 'GreenPaletteColorLookupTableData') + + assert lut.number_of_entries == lut_data.shape[0] + assert lut.first_mapped_value == first_mapped_value + assert lut.bits_per_entry == 8 + assert lut.lut_data.dtype == np.uint8 + np.array_equal(lut.lut_data, lut_data) class TestPaletteColorLUTTransformation(TestCase): diff --git a/tests/test_seg.py b/tests/test_seg.py index 3340de89..db01f579 100644 --- a/tests/test_seg.py +++ b/tests/test_seg.py @@ -4,6 +4,7 @@ import itertools import unittest from pathlib import Path +import pkgutil import warnings import numpy as np @@ -21,7 +22,10 @@ JPEG2000Lossless, JPEGLSLossless, ) - +from highdicom import ( + PaletteColorLUT, + PaletteColorLUTTransformation, +) from highdicom.content import ( AlgorithmIdentificationSequence, PlanePositionSequence, @@ -722,6 +726,26 @@ def setUp(self): ), } + r_lut_data = np.arange(10, 120, dtype=np.uint16) + g_lut_data = np.arange(20, 130, dtype=np.uint16) + b_lut_data = np.arange(30, 140, dtype=np.uint16) + r_first_mapped_value = 0 + g_first_mapped_value = 0 + b_first_mapped_value = 0 + r_lut = PaletteColorLUT(r_first_mapped_value, r_lut_data, color='red') + g_lut = PaletteColorLUT(g_first_mapped_value, g_lut_data, color='green') + b_lut = PaletteColorLUT(b_first_mapped_value, b_lut_data, color='blue') + self._lut_transformation = PaletteColorLUTTransformation( + red_lut=r_lut, + green_lut=g_lut, + blue_lut=b_lut, + palette_color_lut_uid=UID(), + ) + self._icc_profile = pkgutil.get_data( + 'highdicom', + '_icc_profiles/sRGB_v4_ICC_preference.icc' + ) + # Fixtures to use to parametrize segmentation creation # Using this fixture mechanism, we can parametrize class methods @staticmethod @@ -947,6 +971,13 @@ def test_construction(self): with pytest.raises(AttributeError): frame_item.PlanePositionSlideSequence self.check_dimension_index_vals(instance) + assert not hasattr(instance, 'ICCProfile') + assert not hasattr(instance, 'RedPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'RedPaletteColorLookupTableData') + assert not hasattr(instance, 'GreenPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'GreenPaletteColorLookupTableData') + assert not hasattr(instance, 'BluePaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'BluePaletteColorLookupTableData') def test_construction_2(self): instance = Segmentation( @@ -1018,6 +1049,13 @@ def test_construction_2(self): with pytest.raises(AttributeError): frame_item.PlanePositionSequence self.check_dimension_index_vals(instance) + assert not hasattr(instance, 'ICCProfile') + assert not hasattr(instance, 'RedPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'RedPaletteColorLookupTableData') + assert not hasattr(instance, 'GreenPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'GreenPaletteColorLookupTableData') + assert not hasattr(instance, 'BluePaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'BluePaletteColorLookupTableData') def test_construction_3(self): # Segmentation instance from a series of single-frame CT images @@ -1103,6 +1141,13 @@ def test_construction_3(self): with pytest.raises(AttributeError): frame_item.PlanePositionSlideSequence self.check_dimension_index_vals(instance) + assert not hasattr(instance, 'ICCProfile') + assert not hasattr(instance, 'RedPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'RedPaletteColorLookupTableData') + assert not hasattr(instance, 'GreenPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'GreenPaletteColorLookupTableData') + assert not hasattr(instance, 'BluePaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'BluePaletteColorLookupTableData') def test_construction_4(self): # Segmentation instance from an enhanced (multi-frame) CT image @@ -1182,6 +1227,13 @@ def test_construction_4(self): with pytest.raises(AttributeError): frame_item.PlanePositionSlideSequence self.check_dimension_index_vals(instance) + assert not hasattr(instance, 'ICCProfile') + assert not hasattr(instance, 'RedPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'RedPaletteColorLookupTableData') + assert not hasattr(instance, 'GreenPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'GreenPaletteColorLookupTableData') + assert not hasattr(instance, 'BluePaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'BluePaletteColorLookupTableData') def test_construction_5(self): # Segmentation instance from a series of single-frame CT images @@ -1266,6 +1318,13 @@ def test_construction_5(self): with pytest.raises(AttributeError): frame_item.PlanePositionSlideSequence self.check_dimension_index_vals(instance) + assert not hasattr(instance, 'ICCProfile') + assert not hasattr(instance, 'RedPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'RedPaletteColorLookupTableData') + assert not hasattr(instance, 'GreenPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'GreenPaletteColorLookupTableData') + assert not hasattr(instance, 'BluePaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'BluePaletteColorLookupTableData') def test_construction_6(self): # A chest X-ray with no frame of reference @@ -1351,6 +1410,13 @@ def test_construction_6(self): assert len(derivation_image_item.SourceImageSequence) == 1 assert SegmentsOverlapValues[instance.SegmentsOverlap] == \ SegmentsOverlapValues.NO + assert not hasattr(instance, 'ICCProfile') + assert not hasattr(instance, 'RedPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'RedPaletteColorLookupTableData') + assert not hasattr(instance, 'GreenPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'GreenPaletteColorLookupTableData') + assert not hasattr(instance, 'BluePaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'BluePaletteColorLookupTableData') def test_construction_7(self): # A chest X-ray with no frame of reference and multiple segments @@ -1441,6 +1507,13 @@ def test_construction_7(self): assert len(derivation_image_item.SourceImageSequence) == 1 assert SegmentsOverlapValues[instance.SegmentsOverlap] == \ SegmentsOverlapValues.NO + assert not hasattr(instance, 'ICCProfile') + assert not hasattr(instance, 'RedPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'RedPaletteColorLookupTableData') + assert not hasattr(instance, 'GreenPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'GreenPaletteColorLookupTableData') + assert not hasattr(instance, 'BluePaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'BluePaletteColorLookupTableData') def test_construction_8(self): # A chest X-ray with no frame of reference, LABELMAP @@ -1463,6 +1536,7 @@ def test_construction_8(self): instance.DimensionIndexSequence[0].DimensionIndexPointer == tag_for_keyword('FrameLabel') ) + assert instance.PhotometricInterpretation == 'MONOCHROME2' dim_ind_vals = ( instance .PerFrameFunctionalGroupsSequence[0] @@ -1470,6 +1544,66 @@ def test_construction_8(self): .DimensionIndexValues ) assert dim_ind_vals == 1 + assert not hasattr(instance, 'ICCProfile') + assert not hasattr(instance, 'RedPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'RedPaletteColorLookupTableData') + assert not hasattr(instance, 'GreenPaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'GreenPaletteColorLookupTableData') + assert not hasattr(instance, 'BluePaletteColorLookupTableDescriptor') + assert not hasattr(instance, 'BluePaletteColorLookupTableData') + + def test_construction_9(self): + # A label with a palette color LUT + instance = Segmentation( + self._ct_series, + self._ct_series_mask_array, + SegmentationTypeValues.LABELMAP.value, + self._segment_descriptions, + self._series_instance_uid, + self._series_number, + self._sop_instance_uid, + self._instance_number, + self._manufacturer, + self._manufacturer_model_name, + self._software_versions, + self._device_serial_number, + palette_color_lut_transformation=self._lut_transformation, + ) + assert instance.PhotometricInterpretation == 'PALETTE COLOR' + assert hasattr(instance, 'ICCProfile') + assert hasattr(instance, 'RedPaletteColorLookupTableDescriptor') + assert hasattr(instance, 'RedPaletteColorLookupTableData') + assert hasattr(instance, 'GreenPaletteColorLookupTableDescriptor') + assert hasattr(instance, 'GreenPaletteColorLookupTableData') + assert hasattr(instance, 'BluePaletteColorLookupTableDescriptor') + assert hasattr(instance, 'BluePaletteColorLookupTableData') + + def test_construction_10(self): + # A label with a palette color LUT and ICC Profile + instance = Segmentation( + self._ct_series, + self._ct_series_mask_array, + SegmentationTypeValues.LABELMAP.value, + self._segment_descriptions, + self._series_instance_uid, + self._series_number, + self._sop_instance_uid, + self._instance_number, + self._manufacturer, + self._manufacturer_model_name, + self._software_versions, + self._device_serial_number, + palette_color_lut_transformation=self._lut_transformation, + icc_profile=self._icc_profile, + ) + assert instance.PhotometricInterpretation == 'PALETTE COLOR' + assert hasattr(instance, 'ICCProfile') + assert hasattr(instance, 'RedPaletteColorLookupTableDescriptor') + assert hasattr(instance, 'RedPaletteColorLookupTableData') + assert hasattr(instance, 'GreenPaletteColorLookupTableDescriptor') + assert hasattr(instance, 'GreenPaletteColorLookupTableData') + assert hasattr(instance, 'BluePaletteColorLookupTableDescriptor') + assert hasattr(instance, 'BluePaletteColorLookupTableData') def test_construction_workers(self): # Create a segmentation with multiple workers