Skip to content

Commit

Permalink
Update to allow providing a Palette color LUT or using MONOCHROME2
Browse files Browse the repository at this point in the history
  • Loading branch information
CPBridge committed Jan 30, 2024
1 parent 43841f8 commit 82d8fd3
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 68 deletions.
83 changes: 46 additions & 37 deletions src/highdicom/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``).
Expand All @@ -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):
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -2427,15 +2430,15 @@ 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:
raise RuntimeError("Invalid LUT descriptor.")
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:
Expand All @@ -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
Expand Down Expand Up @@ -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``).
Expand All @@ -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.'
)
Expand All @@ -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(
Expand All @@ -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 = []
Expand Down Expand Up @@ -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
Expand All @@ -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 '
Expand All @@ -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

Expand Down
5 changes: 5 additions & 0 deletions src/highdicom/pr/sop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 82d8fd3

Please sign in to comment.