Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "labelmap" segmentations #234

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
91 changes: 53 additions & 38 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,37 +2396,35 @@ 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 ** 16:
# 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(
'Argument "color" must be either "red", "green", or "blue".'
)
self._attr_name_prefix = f'{color.title()}PaletteColorLookupTable'

if bits_per_entry == 8 and len(lut_data) % 2 != 0:
# Need to pad so that the resulting value has even length
lut_data = np.concatenate(
[lut_data, np.array([0], lut_data.dtype)]
)

# The Palette Color Lookup Table Data attributes have VR OW
# (16-bit other words)
setattr(
self,
f'{self._attr_name_prefix}Data',
lut_data.astype(np.uint16).tobytes()
lut_data.tobytes()
)
setattr(
self,
Expand All @@ -2427,15 +2436,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 +2461,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 +2497,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 +2515,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 +2549,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 +2569,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 @@ -2605,11 +2616,11 @@ def __init__(

self._lut_data = np.array(
expanded_lut_values,
dtype=self._dtype
dtype=segmented_lut_data.dtype,
)

len_data = len(expanded_lut_values)
if len_data == 2**16:
if len_data == 2 ** 16:
number_of_entries = 0
else:
number_of_entries = len_data
Expand All @@ -2624,10 +2635,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 +2666,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
18 changes: 10 additions & 8 deletions src/highdicom/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,13 @@ def encode_frame(
'monochrome image frames with Lossless JPEG 2000 codec.'
)
if photometric_interpretation not in (
'MONOCHROME1', 'MONOCHROME2'
'MONOCHROME1', 'MONOCHROME2', 'PALETTE COLOR',
):
raise ValueError(
'Photometric intpretation must be either "MONOCHROME1" '
'or "MONOCHROME2" for encoding of monochrome image '
'frames with Lossless JPEG 2000 codec.'
'Photometric intpretation must be either '
'"MONOCHROME1", "MONOCHROME2", or "PALETTE COLOR" for '
'encoding of monochrome image frames with Lossless '
'JPEG 2000 codec.'
)
if bits_allocated not in (8, 16):
raise ValueError(
Expand Down Expand Up @@ -267,12 +268,13 @@ def encode_frame(
'monochrome image frames with Lossless JPEG-LS codec.'
)
if photometric_interpretation not in (
'MONOCHROME1', 'MONOCHROME2'
'MONOCHROME1', 'MONOCHROME2', 'PALETTE COLOR',
):
raise ValueError(
'Photometric intpretation must be either "MONOCHROME1" '
'or "MONOCHROME2" for encoding of monochrome image '
'frames with Lossless JPEG-LS codec.'
'Photometric intpretation must be either '
'"MONOCHROME1", "MONOCHROME2", or "PALETTE COLOR" for '
'encoding of monochrome image frames with Lossless '
'JPEG-LS codec.'
)
if bits_allocated not in (8, 16):
raise ValueError(
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
Loading