Exemple #1
0
    def _load_segments(self, image: Image, manifest: dict,
                       tar_file_handle: tarfile.TarFile) -> None:

        for segment_info in manifest["segments"]:

            segment_slug = segment_info["slug"]
            segment_bounding_box = segment_info["bounding_box"]
            segment_identifier = segment_info["identifier"]
            segment_color = tuple(
                segment_info["color"]) if segment_info["color"] else None
            segment_meta_data = MetaData(segment_info["meta_data"])

            # lazy-load non-empty mask
            if segment_slug is not None:

                image_segment_data_loader = ImageSegmentDataLoader(
                    tar_file_handle, segment_slug, manifest["image"]["size"],
                    segment_bounding_box)

                segment = LazyLoadedImageSegment(image_segment_data_loader,
                                                 image, segment_identifier)

            # construct empty mask
            else:

                mask = np.zeros(image.get_image_data().shape, dtype=np.bool)

                segment = ImageSegment(image, segment_identifier, mask=mask)

            segment.set_color(segment_color)
            segment.get_meta_data().update(segment_meta_data)

            image.register_segment(segment)
    def add(self, element: Image) -> None:
        """Adds the given image to the set. Ensures unique identifier."""

        if element.get_identifier() is None:
            raise ValueError()

        if self.has_image_identifier(element.get_identifier()):
            raise KeyError()

        super().add(element)
Exemple #3
0
def test_create_from_list():

    foo = Image(identifier="foo")
    bar = Image(identifier="bar")

    image_set = ImageSet([foo, bar])

    assert len(image_set) == 2
    assert foo == image_set.get_by_identifier("foo")
    assert bar == image_set.get_by_identifier("bar")
Exemple #4
0
    def _build_manifest(self, image: Image, segment_slugs: dict) -> dict:

        manifest = {
            "image": {
                "precision_bytes":
                image.get_image_data().dtype.itemsize,
                "size":
                list(image.get_image_data().shape
                     ),  # the image volume byte sequence does not contain this
                "voxel_size":
                list(image.get_voxel_size())
                if image.get_voxel_size() else None,
                "voxel_spacing":
                list(image.get_voxel_spacing())
                if image.get_voxel_spacing() else None,
            },
            "meta_data":
            image.get_meta_data(),
            "slices": [
                self._build_image_slice_manifest(image_slice)
                for image_slice in image.get_slices()
            ],
            "segments": [
                self._build_image_segment_manifest(
                    image_segment,
                    segment_slugs[image_segment.get_identifier()])
                for image_segment in image.get_segments()
            ],
        }

        # make sure the result will actually be readable
        self._validate_manifest(manifest)

        return manifest
    def read_multiple(self, path: str) -> List[Image]:
        """
        Allows to read in multiple images at once in case they are mixed within a single directory.
        """

        self._logger.info("Reading dicom images/series from: " + path)

        file_paths_by_series_uid = self._build_file_paths_by_series_uid_map(
            path)

        self._logger.info("Reading in {} images with series UIDs: {}".format(
            len(file_paths_by_series_uid),
            ", ".join([uid for uid in file_paths_by_series_uid.keys()])))

        images: List[Image] = list()
        for uid, file_path_list in file_paths_by_series_uid.items():

            self._logger.debug("Reading in series {}".format(uid))

            # read in all slices
            slices = [
                pydicom.dcmread(file_path) for file_path in file_path_list
            ]

            # filter non-image slices
            # todo: validate criterion
            slices = [
                slice for slice in slices if "ImagePositionPatient" in slice
            ]
            if len(slices) == 0:
                continue

            # load image volume and meta-data
            volume = self._build_volume(slices)
            image_meta_data, slice_meta_data_by_index = self._get_meta_data(
                slices)

            image = Image(image_data=volume)
            image.get_meta_data().update(
                {DICOM_META_DATA_KEY: image_meta_data})

            # attach slice-specific meta-data
            for index, slice_meta_data in slice_meta_data_by_index.items():

                image.get_or_add_slice(index).get_meta_data().update(
                    {DICOM_META_DATA_KEY: slice_meta_data})

            # deduce image information from dicom meta-data
            image.set_voxel_size(self._get_voxel_size(image))
            image.set_voxel_spacing(self._get_voxel_spacing(image))

            images.append(image)

        return images
Exemple #6
0
    def _load_slices(self, image: Image, manifest: dict) -> None:

        for slice_info in manifest["slices"]:

            slice_index = slice_info["index"]
            slice_identifier = slice_info["identifier"]
            slice_meta_data = MetaData(slice_info["meta_data"])

            image_slice = ImageSlice(image,
                                     slice_index,
                                     identifier=slice_identifier)
            image_slice.get_meta_data().update(slice_meta_data)

            image.register_slice(image_slice)
Exemple #7
0
    def _read_image(self, mrb_handle: MrbHandle, mrml: ElementTree) -> Image:

        volume_node = mrml.find("./Volume")
        volume_storage_node = mrml.find(
            "./VolumeArchetypeStorage[@id='{}']".format(
                volume_node.attrib["storageNodeRef"]))

        nrrd_data, nrrd_header = mrb_handle.read_nrrd(
            urllib.parse.unquote(volume_storage_node.attrib["fileName"]))

        assert nrrd_header["space"] == "left-posterior-superior",\
            "Unsupported nrrd image volume space: " + nrrd_header["space"]
        assert [np.sign(x) for x in self._get_space_directions(nrrd_header["space directions"])] == [1., 1., 1.],\
            "Unsupported nrrd image volume directions."

        # type-cast
        nrrd_data = nrrd_data.astype(np.int32)

        # switch sagittal-coronal-axial axis-order to axial-coronal-sagittal
        nrrd_data = np.swapaxes(nrrd_data, 0, 2)

        # posterior towards anterior
        nrrd_data = np.flip(nrrd_data, 1)

        image = Image(image_data=nrrd_data,
                      identifier=self._derive_identifier(
                          volume_node.attrib["name"]),
                      voxel_spacing=self._get_voxel_spacing(
                          nrrd_header["space directions"]))

        return image
Exemple #8
0
    def write(self,
              image: Image,
              path: str,
              *,
              override_if_existing: bool = False) -> None:

        self._logger.debug("Writing image to gmh file under: {}".format(path))

        if os.path.exists(path) and not override_if_existing:
            raise ValueError(f"Path does already exist: {path}")

        temporary_file_path = f"{path}.tmp"
        assert not os.path.exists(temporary_file_path)

        with tarfile.open(temporary_file_path, "w") as tar_file_handle:

            def add_file(name: str, content) -> None:

                if isinstance(content, str):
                    content = content.encode()

                member = tarfile.TarInfo(name)
                member.size = len(content)

                tar_file_handle.addfile(member, io.BytesIO(content))

            segment_slugs = self._generate_segment_slugs(image)

            add_file("manifest.json",
                     self._build_manifest_document(image, segment_slugs))
            add_file("image_data.npy", image.get_image_data().tobytes())

            for image_segment in image.get_segments():

                if image_segment.is_empty():
                    continue

                segment_slug = segment_slugs[image_segment.get_identifier()]

                add_file(
                    IMAGE_SEGMENT_MASK_MEMBER_NAME_FORMAT.format(segment_slug),
                    image_segment.get_mask_in_bounding_box().tobytes())

        # swap written file into target path
        if os.path.exists(path):
            os.remove(path)
        os.rename(temporary_file_path, path)
Exemple #9
0
    def _handle_segmentation_identifier_collision(self, image: Image,
                                                  identifier: str) -> str:

        suffix = 0

        while True:

            candidate_identifier = "{}_{}".format(identifier, suffix)

            if not image.has_segment(candidate_identifier):
                return candidate_identifier

            suffix += 1
    def _get_pixel_spacing(self,
                           image: Image) -> Optional[Tuple[float, float]]:

        try:
            # "Pixel Spacing" (0028,0030)
            pixel_spacing = image.get_meta_data()[DICOM_META_DATA_KEY][str(
                0x00280030)]
        except KeyError:
            return None

        assert isinstance(pixel_spacing, list)
        assert len(pixel_spacing) == 2
        assert all(isinstance(value, float) for value in pixel_spacing)

        return (pixel_spacing[0], pixel_spacing[1])
Exemple #11
0
    def _generate_segment_slugs(self, image: Image) -> dict:

        result = {}

        for num, segment in enumerate(image.get_segments()):

            while True:

                slug = generate_random_string()

                if slug not in result:
                    break

            result[segment.get_identifier()] = slug

        return result
Exemple #12
0
    def _get_segmentation_mask_offset(self, nrrd_header: dict,
                                      segmentations_volume: np.ndarray,
                                      image: Image) -> Coordinates3:

        offset = [
            int(s) for s in
            nrrd_header["Segmentation_ReferenceImageExtentOffset"].split()
        ]

        assert len(
            offset
        ) == 3, "Invalid value in Segmentation_ReferenceImageExtentOffset."

        # ensure component order matches image coordinate system
        offset.reverse()
        offset[1] = image.get_image_data().shape[1] - (
            offset[1] + segmentations_volume.shape[2])

        return cast(Coordinates3, tuple(offset))
    def _calculate_z_spacing(self, image: Image) -> Optional[float]:

        ordered_slices = image.get_ordered_slices()

        # Cannot calculate spacing with less then two slices
        if len(ordered_slices) < 2:
            return None

        try:
            # "Slice Location" (0020,1041)
            location1 = ordered_slices[0].get_meta_data()[DICOM_META_DATA_KEY][
                str(0x00201041)]
            location2 = ordered_slices[1].get_meta_data()[DICOM_META_DATA_KEY][
                str(0x00201041)]
        except KeyError:
            return None

        assert isinstance(location1, float)
        assert isinstance(location2, float)

        return abs(location1 - location2)
    def _get_voxel_size(self, image: Image) -> Optional[Vector3]:
        """Deduces the voxel size based on slice thickness and pixel spacing.

        Relies on the assumption of non-overlap and non-sparseness in the xy-plane.
        """

        pixel_spacing = self._get_pixel_spacing(image)

        if pixel_spacing is None:
            return None

        try:
            slice_thickness = image.get_meta_data()["Slice Thickness"]
            assert isinstance(slice_thickness, float)
            assert slice_thickness > 0
        except KeyError:
            return None

        return (
            slice_thickness,
            pixel_spacing[1],
            pixel_spacing[0],
        )
    def _get_voxel_spacing(self, image: Image) -> Optional[Vector3]:
        """Deduces the voxel spacing based on slice increment and pixel spacing."""

        pixel_spacing = self._get_pixel_spacing(image)

        if pixel_spacing is None:
            return None

        explicit_slice_increment: Optional[float] = None

        # Read explicit value from "Spacing Between Slices" (0018,0088)
        try:
            explicit_slice_increment = image.get_meta_data(
            )[DICOM_META_DATA_KEY][str(0x00180088)]
            assert isinstance(explicit_slice_increment, float)
            explicit_slice_increment = abs(explicit_slice_increment)
        except KeyError:
            pass

        implicit_slice_increment = self._calculate_z_spacing(image)

        if explicit_slice_increment is None and implicit_slice_increment is None:
            # Cannot do anything
            return None

        elif explicit_slice_increment is not None and implicit_slice_increment is not None:
            # Assert consistency
            assert explicit_slice_increment == implicit_slice_increment,\
                "Derived slice increment differs from defined value"

        slice_increment = explicit_slice_increment if explicit_slice_increment is not None else implicit_slice_increment

        return (
            slice_increment,
            pixel_spacing[1],
            pixel_spacing[0],
        )
Exemple #16
0
    def _read_segmentations(self, mrb_handle: MrbHandle, mrml: ElementTree,
                            image: Image) -> None:
        """Reads in all segmentations and attaches them to the given image instance."""

        segmentation_nodes = mrml.findall("./Segmentation")
        assert len(segmentation_nodes) == 1,\
            "Expected a single <Segmentation>-node. Actual count: {}".format(len(segmentation_nodes))

        segmentation_node = segmentation_nodes[0]

        # Read 4D segmentation mask
        segmentations_volume, nrrd_header = self._get_segmentation_volume(
            mrb_handle, mrml, segmentation_node)

        # Read segmentation volume offset w.r.t. image volume origin
        offset = self._get_segmentation_mask_offset(nrrd_header,
                                                    segmentations_volume,
                                                    image)
        assert all(
            [
                segmentations_volume.shape[1 + i] + offset[i] <=
                image.get_image_data().shape[i] for i in range(len(offset))
            ]
        ), "Segmentation mask offset is inconsistent with mask size and image volume size."

        # Find segments
        subject_hierarchy_item_node = mrml.find(
            "./SubjectHierarchy//SubjectHierarchyItem[@dataNode='{}']".format(
                segmentation_node.attrib["id"]))
        segment_nodes = subject_hierarchy_item_node.findall(
            ".//SubjectHierarchyItem[@type='Segments']")

        # Read in all mrb "segments" as segmentations
        for segment_index, segment_node in enumerate(segment_nodes):

            mask_partition = segmentations_volume[segment_index]

            # Build 3D mask from partition
            mask = np.zeros(image.get_image_data().shape, dtype=np.bool)
            mask[offset[0]:offset[0] + mask_partition.shape[0],
                 offset[1]:offset[1] + mask_partition.shape[1],
                 offset[2]:offset[2] +
                 mask_partition.shape[2], ] = mask_partition

            header_prefix = "Segment{}_".format(segment_index)

            identifier = self._derive_identifier(nrrd_header[header_prefix +
                                                             "Name"])
            if image.has_segment(identifier):
                identifier = self._handle_segmentation_identifier_collision(
                    image, identifier)

            color = tuple([
                round(float(x) * 255)
                for x in nrrd_header[header_prefix + "Color"].split(" ")
            ])
            assert len(color) == 3, "Invalid segment color: " + nrrd_header[
                header_prefix + "Color"]
            color = cast(Color, color)

            image.add_segment(identifier, mask, color)

        assert [np.sum(segmentations_volume[x]) for x in range(segmentations_volume.shape[0])] ==\
               [np.sum(seg.get_mask()) for seg in image.get_ordered_segments()],\
            "Error during reconstruction of segmentation masks!"