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)
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")
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
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)
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
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)
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])
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
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], )
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!"