Exemplo n.º 1
0
 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)
Exemplo n.º 2
0
 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]
Exemplo n.º 3
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)
Exemplo n.º 4
0
 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)
Exemplo n.º 6
0
 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
Exemplo n.º 8
0
    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'
Exemplo n.º 10
0
    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")
Exemplo n.º 11
0
 def test_empty_sequence(self):
     seq = DimensionOrganizationSequence()
     assert len(seq) == 0
Exemplo n.º 12
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
Exemplo n.º 13
0
 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