def test_add_dimension_with_functional_group_pointer(self) -> None: seq = DimensionOrganizationSequence() seq.add_dimension("ReferencedSegmentNumber", "SegmentIdentificationSequence") assert len(seq) == 1 assert seq[0].DimensionOrganizationUID == seq[0].DimensionOrganizationUID assert seq[0].DimensionIndexPointer == pydicom.tag.Tag(0x0062, 0x000B) assert seq[0].FunctionalGroupPointer == pydicom.tag.Tag(0x0062, 0x000A)
def test_add_dimension_without_functional_group_pointer(self): seq = DimensionOrganizationSequence() seq.add_dimension('ReferencedSegmentNumber') assert len(seq) == 1 assert seq[0].DimensionOrganizationUID == seq[ 0].DimensionOrganizationUID assert seq[0].DimensionIndexPointer == pydicom.tag.Tag(0x0062, 0x000b) assert 'FunctionalGroupPointer' not in seq[0]
def test_add_dimension_with_functional_group_pointer_tag_based(self): seq = DimensionOrganizationSequence() seq.add_dimension(pydicom.tag.Tag(0x0062, 0x000b), pydicom.tag.Tag(0x0062, 0x000a)) assert len(seq) == 1 assert seq[0].DimensionOrganizationUID == seq[ 0].DimensionOrganizationUID assert seq[0].DimensionIndexPointer == pydicom.tag.Tag(0x0062, 0x000b) assert seq[0].FunctionalGroupPointer == pydicom.tag.Tag(0x0062, 0x000a)
def test_add_multiple_dimensions_copies_same_organization_uid(self): seq = DimensionOrganizationSequence() seq.add_dimension('ReferencedSegmentNumber', 'SegmentIdentificationSequence') seq.add_dimension('ImagePositionPatient', 'PlanePositionSequence') assert len(seq) == 2 assert seq[0].DimensionOrganizationUID == seq[ 1].DimensionOrganizationUID
def test_add_dimension_organization_duplicate(self): seq = DimensionOrganizationSequence() seq.add_dimension('ReferencedSegmentNumber', 'SegmentIdentificationSequence') seq.add_dimension('ImagePositionPatient', 'PlanePositionSequence') self.dataset.add_dimension_organization(seq) with pytest.raises(ValueError, match='Dimension organization with UID.*'): self.dataset.add_dimension_organization(seq)
def test_add_dimension_organization_duplicate(self) -> None: seq = DimensionOrganizationSequence() seq.add_dimension("ReferencedSegmentNumber", "SegmentIdentificationSequence") seq.add_dimension("ImagePositionPatient", "PlanePositionSequence") self.dataset.add_dimension_organization(seq) with pytest.raises(ValueError, match="Dimension organization with UID.*"): self.dataset.add_dimension_organization(seq)
def test_add_multiple_dimension_organizations(self): for _ in range(2): seq = DimensionOrganizationSequence() seq.add_dimension('ReferencedSegmentNumber', 'SegmentIdentificationSequence') seq.add_dimension('ImagePositionPatient', 'PlanePositionSequence') self.dataset.add_dimension_organization(seq) assert len(self.dataset.DimensionOrganizationSequence) == 2 assert len(self.dataset.DimensionIndexSequence) == 4
def test_add_multiple_dimension_organizations(self) -> None: for _ in range(2): seq = DimensionOrganizationSequence() seq.add_dimension("ReferencedSegmentNumber", "SegmentIdentificationSequence") seq.add_dimension("ImagePositionPatient", "PlanePositionSequence") self.dataset.add_dimension_organization(seq) assert len(self.dataset.DimensionOrganizationSequence) == 2 assert len(self.dataset.DimensionIndexSequence) == 4
def test_add_dimension_organization(self): assert 'DimensionOrganizationSequence' not in self.dataset assert 'DimensionIndexSequence' not in self.dataset seq = DimensionOrganizationSequence() seq.add_dimension('ReferencedSegmentNumber', 'SegmentIdentificationSequence') seq.add_dimension('ImagePositionPatient', 'PlanePositionSequence') self.dataset.add_dimension_organization(seq) assert len(self.dataset.DimensionOrganizationSequence) == 1 assert len(self.dataset.DimensionIndexSequence) == 2 assert self.dataset.DimensionIndexSequence[ 0].DimensionDescriptionLabel == 'ReferencedSegmentNumber' assert self.dataset.DimensionIndexSequence[ 1].DimensionDescriptionLabel == 'ImagePositionPatient'
def test_add_dimension_organization(self) -> None: assert "DimensionOrganizationSequence" not in self.dataset assert "DimensionIndexSequence" not in self.dataset seq = DimensionOrganizationSequence() seq.add_dimension("ReferencedSegmentNumber", "SegmentIdentificationSequence") seq.add_dimension("ImagePositionPatient", "PlanePositionSequence") self.dataset.add_dimension_organization(seq) assert len(self.dataset.DimensionOrganizationSequence) == 1 assert len(self.dataset.DimensionIndexSequence) == 2 assert (self.dataset.DimensionIndexSequence[0]. DimensionDescriptionLabel == "ReferencedSegmentNumber") assert (self.dataset.DimensionIndexSequence[1]. DimensionDescriptionLabel == "ImagePositionPatient")
def test_empty_sequence(self): seq = DimensionOrganizationSequence() assert len(seq) == 0
def write(self, segmentation: sitk.Image, source_images: List[pydicom.Dataset]) -> pydicom.Dataset: """Writes a DICOM-SEG dataset from a segmentation image and the corresponding DICOM source images. Args: segmentation: A `SimpleITK.Image` with integer labels and a single component per spatial location. source_images: A list of `pydicom.Dataset` which are the source images for the segmentation image. Returns: A `pydicom.Dataset` instance with all necessary information and meta information for writing the dataset to disk. """ if segmentation.GetDimension() != 3: raise ValueError("Only 3D segmentation data is supported") if segmentation.GetNumberOfComponentsPerPixel() > 1: raise ValueError("Multi-class segmentations can only be " "represented with a single component per voxel") if segmentation.GetPixelID() not in [ sitk.sitkUInt8, sitk.sitkUInt16, sitk.sitkUInt32, sitk.sitkUInt64, ]: raise ValueError("Unsigned integer data type required") # TODO Add further checks if source images are from the same series slice_to_source_images = self._map_source_images_to_segmentation( segmentation, source_images) # Compute unique labels and their respective bounding boxes label_statistics_filter = sitk.LabelStatisticsImageFilter() label_statistics_filter.Execute(segmentation, segmentation) unique_labels = set( [x for x in label_statistics_filter.GetLabels() if x != 0]) if len(unique_labels) == 0: raise ValueError("Segmentation does not contain any labels") # Check if all present labels where declared in the DICOM template declared_segments = set( [x.SegmentNumber for x in self._template.SegmentSequence]) missing_declarations = unique_labels.difference(declared_segments) if missing_declarations: missing_segment_numbers = ", ".join( [str(x) for x in missing_declarations]) message = ( f"Skipping segment(s) {missing_segment_numbers}, since their " "declaration is missing in the DICOM template") if not self._skip_missing_segment: raise ValueError(message) logger.warning(message) labels_to_process = unique_labels.intersection(declared_segments) if not labels_to_process: raise ValueError("No segments found for encoding as DICOM-SEG") # Compute bounding boxes for each present label and optionally restrict # the volume to serialize to the joined maximum extent bboxs = { x: label_statistics_filter.GetBoundingBox(x) for x in labels_to_process } if self._inplane_cropping: min_x, min_y, _ = np.min([x[::2] for x in bboxs.values()], axis=0).tolist() max_x, max_y, _ = ( np.max([x[1::2] for x in bboxs.values()], axis=0) + 1).tolist() logger.info( "Serializing cropped image planes starting at coordinates " f"({min_x}, {min_y}) with size ({max_x - min_x}, {max_y - min_y})" ) else: min_x, min_y = 0, 0 max_x, max_y = segmentation.GetWidth(), segmentation.GetHeight() logger.info( f"Serializing image planes at full size ({max_x}, {max_y})") # Create target dataset for storing serialized data result = SegmentationDataset( reference_dicom=source_images[0] if source_images else None, rows=max_y - min_y, columns=max_x - min_x, segmentation_type=SegmentationType.BINARY, ) dimension_organization = DimensionOrganizationSequence() dimension_organization.add_dimension("ReferencedSegmentNumber", "SegmentIdentificationSequence") dimension_organization.add_dimension("ImagePositionPatient", "PlanePositionSequence") result.add_dimension_organization(dimension_organization) writer_utils.copy_segmentation_template( target=result, template=self._template, segments=labels_to_process, skip_missing_segment=self._skip_missing_segment, ) writer_utils.set_shared_functional_groups_sequence( target=result, segmentation=segmentation) # FIX - Use ImageOrientationPatient value from DICOM source rather than the segmentation result.SharedFunctionalGroupsSequence[0].PlaneOrientationSequence[ 0].ImageOrientationPatient = source_images[ 0].ImageOrientationPatient buffer = sitk.GetArrayFromImage(segmentation) for segment in labels_to_process: logger.info(f"Processing segment {segment}") if self._skip_empty_slices: bbox = bboxs[segment] min_z, max_z = bbox[4], bbox[5] + 1 else: min_z, max_z = 0, segmentation.GetDepth() logger.info( "Total number of slices that will be processed for segment " f"{segment} is {max_z - min_z} (inclusive from {min_z} to {max_z})" ) skipped_slices = [] for slice_idx in range(min_z, max_z): frame_index = (min_x, min_y, slice_idx) frame_position = segmentation.TransformIndexToPhysicalPoint( frame_index) frame_data = np.equal( buffer[slice_idx, min_y:max_y, min_x:max_x], segment) if self._skip_empty_slices and not frame_data.any(): skipped_slices.append(slice_idx) continue frame_fg_item = result.add_frame( data=frame_data.astype(np.uint8), referenced_segment=segment, referenced_images=slice_to_source_images[slice_idx], ) frame_fg_item.FrameContentSequence = [pydicom.Dataset()] frame_fg_item.FrameContentSequence[0].DimensionIndexValues = [ segment, # Segment number slice_idx - min_z + 1, # Slice index within cropped volume ] frame_fg_item.PlanePositionSequence = [pydicom.Dataset()] frame_fg_item.PlanePositionSequence[0].ImagePositionPatient = [ f"{x:e}" for x in frame_position ] if skipped_slices: logger.info(f"Skipped empty slices for segment {segment}: " f'{", ".join([str(x) for x in skipped_slices])}') # Encode all frames into a bytearray if self._inplane_cropping or self._skip_empty_slices: num_encoded_bytes = len(result.PixelData) max_encoded_bytes = (segmentation.GetWidth() * segmentation.GetHeight() * segmentation.GetDepth() * len(result.SegmentSequence) // 8) savings = (1 - num_encoded_bytes / max_encoded_bytes) * 100 logger.info( f"Optimized frame data length is {num_encoded_bytes:,}B " f"instead of {max_encoded_bytes:,}B (saved {savings:.2f}%)") result.SegmentsOverlap = "NO" return result
def test_add_multiple_dimensions_copies_same_organization_uid(self) -> None: seq = DimensionOrganizationSequence() seq.add_dimension("ReferencedSegmentNumber", "SegmentIdentificationSequence") seq.add_dimension("ImagePositionPatient", "PlanePositionSequence") assert len(seq) == 2 assert seq[0].DimensionOrganizationUID == seq[1].DimensionOrganizationUID