-
Notifications
You must be signed in to change notification settings - Fork 37
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 support for instance segmentation with LABELED segmentation type #184
base: master
Are you sure you want to change the base?
Changes from 5 commits
e587424
4b7c326
0d07605
f1a93d7
bfc4ded
f9818c1
61aaa50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -274,6 +274,8 @@ def __init__( | |
if len(source_images) == 0: | ||
raise ValueError('At least one source image is required.') | ||
|
||
segmentation_type = SegmentationTypeValues(segmentation_type) | ||
|
||
uniqueness_criteria = set( | ||
( | ||
image.StudyInstanceUID, | ||
|
@@ -298,17 +300,49 @@ def __init__( | |
'are multi-frame images.' | ||
) | ||
is_tiled = hasattr(src_img, 'TotalPixelMatrixRows') | ||
supported_transfer_syntaxes = { | ||
ImplicitVRLittleEndian, | ||
ExplicitVRLittleEndian, | ||
|
||
supported_compressed_transfer_syntaxes = { | ||
JPEG2000Lossless, | ||
JPEGLSLossless, | ||
RLELossless, | ||
} | ||
supported_native_transfer_syntaxes = { | ||
ImplicitVRLittleEndian, | ||
ExplicitVRLittleEndian, | ||
} | ||
supported_transfer_syntaxes = set() | ||
supported_transfer_syntaxes.update( | ||
supported_compressed_transfer_syntaxes | ||
) | ||
supported_transfer_syntaxes.update( | ||
supported_native_transfer_syntaxes | ||
) | ||
hackermd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if transfer_syntax_uid not in supported_transfer_syntaxes: | ||
raise ValueError( | ||
f'Transfer syntax "{transfer_syntax_uid}" is not supported.' | ||
) | ||
if segmentation_type == SegmentationTypeValues.BINARY: | ||
if transfer_syntax_uid not in supported_native_transfer_syntaxes: | ||
raise ValueError( | ||
f'Transfer syntax "{transfer_syntax_uid}" is not supported ' | ||
'for segmentation type BINARY.' | ||
'Supported are "{}"'.format( | ||
'", "'.join(supported_native_transfer_syntaxes) | ||
) | ||
) | ||
if segmentation_type == SegmentationTypeValues.LABELED: | ||
if ( | ||
pixel_array.dtype.itemsize > 2 and | ||
transfer_syntax_uid not in supported_native_transfer_syntaxes | ||
): | ||
raise ValueError( | ||
f'Transfer syntax "{transfer_syntax_uid}" is not supported ' | ||
'for segmentation type LABELED with more than 16 bits ' | ||
'allocated per sample.' | ||
'Supported are "{}"'.format( | ||
'", "'.join(supported_native_transfer_syntaxes) | ||
) | ||
) | ||
|
||
if pixel_array.ndim == 2: | ||
pixel_array = pixel_array[np.newaxis, ...] | ||
|
@@ -442,11 +476,9 @@ 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 | ||
if self.file_meta.TransferSyntaxUID.is_encapsulated: | ||
raise ValueError( | ||
'The chosen transfer syntax ' | ||
|
@@ -455,22 +487,31 @@ def __init__( | |
) | ||
elif self.SegmentationType == SegmentationTypeValues.FRACTIONAL.value: | ||
self.BitsAllocated = 8 | ||
self.HighBit = 7 | ||
segmentation_fractional_type = SegmentationFractionalTypeValues( | ||
fractional_type | ||
) | ||
self.SegmentationFractionalType = segmentation_fractional_type.value | ||
if max_fractional_value > 2**8: | ||
raise ValueError( | ||
'Maximum fractional value must not exceed image bit depth.' | ||
'Maximum fractional value must not exceed ' | ||
'the number allocated bits.' | ||
) | ||
self.MaximumFractionalValue = max_fractional_value | ||
elif self.SegmentationType == SegmentationTypeValues.LABELED.value: | ||
bit_depth = pixel_array.dtype.itemsize * 8 | ||
if bit_depth not in (8, 16, 32): | ||
raise ValueError( | ||
'The number of allocated bits must be a multiple of 8 and ' | ||
'less than or equal to 32.' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 24 is a multiple of 8 and less than or equal to 32 ;) |
||
) | ||
self.BitsAllocated = bit_depth | ||
else: | ||
raise ValueError( | ||
'Unknown segmentation type "{}"'.format(segmentation_type) | ||
f'Unknown segmentation type "{segmentation_type}".' | ||
) | ||
|
||
self.BitsStored = self.BitsAllocated | ||
self.HighBit = self.BitsAllocated - 1 | ||
self.LossyImageCompression = getattr( | ||
src_img, | ||
'LossyImageCompression', | ||
|
@@ -753,7 +794,7 @@ def __init__( | |
|
||
for i, segment_number in enumerate(described_segment_numbers): | ||
# Pixel array for just this segment | ||
if pixel_array.dtype in (np.float_, np.float32, np.float64): | ||
if pixel_array.dtype.kind == 'f': | ||
# Floating-point numbers must be mapped to 8-bit integers in | ||
# the range [0, max_fractional_value]. | ||
if pixel_array.ndim == 4: | ||
|
@@ -764,27 +805,34 @@ def __init__( | |
segment_array * float(self.MaximumFractionalValue) | ||
) | ||
planes = planes.astype(np.uint8) | ||
elif pixel_array.dtype in (np.uint8, np.uint16): | ||
# Note that integer arrays with segments stacked down the last | ||
# dimension will already have been converted to bool, leaving | ||
# only "label maps" here, which must be converted to binary | ||
# masks. | ||
planes = np.zeros(pixel_array.shape, dtype=np.uint8) | ||
planes[pixel_array == segment_number] = 1 | ||
elif pixel_array.dtype == np.bool_: | ||
elif pixel_array.dtype.kind in 'u' or pixel_array.dtype == np.bool_: | ||
if pixel_array.ndim == 4: | ||
planes = pixel_array[:, :, :, segment_number - 1] | ||
else: | ||
planes = pixel_array | ||
planes = planes.astype(np.uint8) | ||
# It may happen that a boolean array is passed that should be | ||
# interpreted as fractional segmentation type. In this case, we | ||
# also need to stretch pixel valeus to 8-bit unsigned integer | ||
# range by multiplying with the maximum fractional value. | ||
if segmentation_type == SegmentationTypeValues.BINARY: | ||
# Note that integer arrays with segments stacked down | ||
# the last dimension will already have been converted | ||
# to bool, leaving only "label maps" here, which must | ||
# be converted to binary masks. | ||
planes = np.zeros(pixel_array.shape, dtype=np.uint8) | ||
planes[pixel_array == segment_number] = 1 | ||
else: | ||
planes = pixel_array | ||
Comment on lines
+808
to
+816
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is wrong: the top branch (if branch) should be used if segmentation type is either BINARY or FRACTIONAL. We allow users to pass "label map" style pixel arrays regardless of the BINARY/FRACTIONAL segmentation type. |
||
if segmentation_type == SegmentationTypeValues.FRACTIONAL: | ||
planes *= int(self.MaximumFractionalValue) | ||
# It may happen that a boolean array is passed that | ||
# should be interpreted as fractional segmentation | ||
# type. In this case, we also need to stretch pixel | ||
# values to 8-bit unsigned integer range by multiplying | ||
# with the maximum fractional value. | ||
planes = planes.astype(np.float32) | ||
planes *= np.float32(self.MaximumFractionalValue) | ||
planes = planes.astype(np.uint8) | ||
else: | ||
raise TypeError('Pixel array has an invalid data type.') | ||
raise TypeError( | ||
'Pixel array has an invalid data type. ' | ||
'Data type must be either np.bool_, numpy.float32, ' | ||
'numpy.float64, numpy.uint8, numpy.uint16, or numpy.uint32.' | ||
) | ||
|
||
contained_plane_index = [] | ||
for j in plane_sort_index: | ||
|
@@ -1038,48 +1086,58 @@ def _check_and_cast_pixel_array( | |
f'({len(described_segment_numbers)}).' | ||
) | ||
|
||
if pixel_array.dtype in (np.bool_, np.uint8, np.uint16): | ||
if pixel_array.dtype.kind == 'u' or pixel_array.dtype == np.bool_: | ||
if pixel_array.ndim == 3: | ||
# A label-map style array where pixel values represent | ||
# segment associations | ||
segments_present = np.unique( | ||
pixel_array[pixel_array > 0].astype(np.uint16) | ||
) | ||
|
||
# The pixel values in the pixel array must all belong to | ||
# a described segment | ||
if not np.all( | ||
if segmentation_type == SegmentationTypeValues.BINARY: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be perfectly permissible for a user to pass an unsigned integer array and request that it be stored as a FRACTIONAL seg, e.g. in order to use an efficient lossless codec or to ensure individual frames may be efficiently read. Therefore this should include |
||
# A label-map style array where pixel values represent | ||
# segment associations | ||
segments_present = np.unique( | ||
pixel_array[pixel_array > 0].astype(np.uint16) | ||
) | ||
# The pixel values in the pixel array must all belong to | ||
# a described segment | ||
if not np.all( | ||
np.in1d(segments_present, described_segment_numbers) | ||
): | ||
raise ValueError( | ||
'Pixel array contains segments that lack ' | ||
'descriptions.' | ||
) | ||
raise ValueError( | ||
'Pixel array contains segments that lack ' | ||
'descriptions.' | ||
) | ||
elif segmentation_type == SegmentationTypeValues.LABELED: | ||
if pixel_array.max() >= 2 ** 32: | ||
raise ValueError( | ||
'When passing segments for segmentation type ' | ||
'LABELED with an integer data type, ' | ||
'pixels must not exceed 32-bit depth.' | ||
) | ||
|
||
# By construction of the pixel array, we know that the segments | ||
# cannot overlap | ||
segments_overlap = SegmentsOverlapValues.NO | ||
|
||
else: | ||
# Pixel array is 4D where each segment is stacked down | ||
# the last dimension | ||
# In this case, each segment of the pixel array should be binary | ||
if pixel_array.max() > 1: | ||
raise ValueError( | ||
'When passing a 4D stack of segments with an integer ' | ||
'pixel type, the pixel array must be binary.' | ||
) | ||
pixel_array = pixel_array.astype(np.bool_) | ||
if segmentation_type == SegmentationTypeValues.BINARY: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, this needs to include FRACTIONAL |
||
# Pixel array is 4D where each segment is stacked down the | ||
# last dimension. In this case, each segment of the pixel | ||
# array should be binary. | ||
if pixel_array.max() > 1: | ||
raise ValueError( | ||
'When passing a 4D stack of segments for ' | ||
'segmentation type BINARY with an integer data ' | ||
'type, the pixel array must be binary.' | ||
) | ||
pixel_array = pixel_array.astype(np.bool_) | ||
|
||
# Need to check whether or not segments overlap | ||
if pixel_array.shape[-1] == 1: | ||
# A single segment does not overlap | ||
segments_overlap = SegmentsOverlapValues.NO | ||
elif pixel_array.sum(axis=-1).max() > 1: | ||
elif (pixel_array > 0).sum(axis=-1).max() > 1: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see why this change is required. To get here, the pixel array must have an unsigned integer type, so the new and old version of this line are logical equivalent as far as I can tell |
||
segments_overlap = SegmentsOverlapValues.YES | ||
else: | ||
segments_overlap = SegmentsOverlapValues.NO | ||
|
||
elif (pixel_array.dtype in (np.float_, np.float32, np.float64)): | ||
elif pixel_array.dtype.kind == 'f': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You'll probably need some extra logic in this section for the LABELED case. You may want to either disallow floating point data types for LABELED, or check that the values are actually integer values (like the BINARY case where we allow users to pass a float array that contains only the vales 0.0 and 1.0 exactly) |
||
unique_values = np.unique(pixel_array) | ||
if np.min(unique_values) < 0.0 or np.max(unique_values) > 1.0: | ||
raise ValueError( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the speculative nature of this PR, we should just fix this on master