def _format_volume_to_header(volume: MedicalVolume) -> MedicalVolume: """Reformats the volume according to its header. Args: volume (MedicalVolume): The volume to reformat. Must be 3D and have headers of shape (1, 1, volume.shape[2]). Returns: MedicalVolume: The reformatted volume. """ headers = volume.headers() assert headers.shape == (1, 1, volume.shape[2]) affine = to_RAS_affine(headers.flatten()) orientation = stdo.orientation_nib_to_standard(nib.aff2axcodes(affine)) # Currently do not support mismatch in scanner_origin. if tuple(affine[:3, 3]) != volume.scanner_origin: raise ValueError( "Scanner origin mismatch. " "Currently we do not handle mismatch in scanner origin " "(i.e. cannot flip across axis)") volume = volume.reformat(orientation) assert volume.headers().shape == (1, 1, volume.shape[2]) return volume
def test_metadata(self): field, field_val = "EchoTime", 4.0 volume = np.random.rand(10, 20, 30, 40) headers = ututils.build_dummy_headers(volume.shape[2:], {field: field_val}) mv_no_headers = MedicalVolume(volume, self._AFFINE) mv = MedicalVolume(volume, self._AFFINE, headers=headers) assert mv_no_headers.headers() is None assert mv_no_headers.headers(flatten=True) is None with self.assertRaises(ValueError): mv.get_metadata("foobar") assert mv.get_metadata("foobar", default=0) == 0 echo_time = mv.get_metadata(field) assert echo_time == field_val new_val = 5.0 mv2 = mv.clone(headers=True) mv2.set_metadata(field, new_val) assert mv.get_metadata(field, type(field_val)) == field_val assert mv2.get_metadata(field, type(new_val)) == new_val for h in mv2.headers(flatten=True): assert h[field].value == new_val new_val = 6.0 mv2 = mv.clone(headers=True) mv2[..., 1].set_metadata(field, new_val) assert mv2[..., 0].get_metadata(field) == field_val assert mv2[..., 1].get_metadata(field) == new_val headers = mv2.headers() for h in headers[..., 0].flatten(): assert h[field].value == field_val for h in headers[..., 1].flatten(): assert h[field].value == new_val # Set metadata when volume has no headers. mv_nh = MedicalVolume(volume, self._AFFINE, headers=None) with self.assertRaises(ValueError): mv_nh.set_metadata("EchoTime", 40.0) with self.assertWarns(UserWarning): mv_nh.set_metadata("EchoTime", 40.0, force=True) assert mv_nh._headers.shape == (1, ) * len(mv_nh.shape) assert mv_nh.get_metadata("EchoTime") == 40.0 assert mv_nh[:1, :2, :3]._headers.shape == (1, ) * len(mv_nh.shape)
def test_clone(self): mv = MedicalVolume(np.random.rand(10, 20, 30), self._AFFINE) mv2 = mv.clone() assert mv.is_identical(mv2) # expected identical volumes mv = MedicalVolume( np.random.rand(10, 20, 30), self._AFFINE, headers=ututils.build_dummy_headers((1, 1, 30)), ) mv2 = mv.clone(headers=False) assert mv.is_identical(mv2) # expected identical volumes assert id(mv.headers(flatten=True)[0]) == id( mv2.headers(flatten=True) [0]), "headers not cloned, expected same memory address" mv3 = mv.clone(headers=True) assert mv.is_identical(mv3) # expected identical volumes assert id(mv.headers(flatten=True)[0]) != id( mv3.headers(flatten=True) [0]), "headers cloned, expected different memory address"
def save( self, volume: MedicalVolume, dir_path: str, fname_fmt: str = np._NoValue, sort_by: Union[str, int, Sequence[Union[str, int]]] = np._NoValue, ): """Save `medical volume` in dicom format. This function assumes headers for the volume (``volume.headers()``) exist for one spatial dimension. Headers for non-spatial dimensions are optional, but highly recommended. If provided, they will be used to write the volume. If not, headers will be appropriately broadcast to these dimensions. Note, this means that multiple files will have the same header information and will not be able to be loaded automatically. Currently header spatial information (orientation, origin, slicing between spaces, etc.) is not overwritten nor validated. All data must correspond to the same spatial information as specified in the headers to produce valid DICOM files. Args: volume (MedicalVolume): Volume to save. dir_path: Directory path to store dicom files. Dicoms are stored in directories, as multiple files are needed to store the volume. fname_fmt (str, optional): Formatting string for filenames. Must contain ``%d``, which correspopnds to slice number. Defaults to ``self.fname_fmt``. sort_by (``str``(s) or ``int``(s), optional): DICOM attribute(s) used to define ordering of slices prior to writing. If ``None``, this ordering will be defined by the order of blocks in ``volume``. Defaults to ``self.sort_by``. Raises: ValueError: If `im` does not have initialized headers. Or if `im` was flipped across any axis. Flipping changes scanner origin, which is currently not handled. """ fname_fmt = fname_fmt if fname_fmt != np._NoValue else self.fname_fmt sort_by = sort_by if sort_by != np._NoValue else self.sort_by # Get orientation indicated by headers. headers = volume.headers() if headers is None: raise ValueError( "MedicalVolume headers must be initialized to save as a dicom") sort_by = _wrap_as_tuple(sort_by, default=()) # Reformat to put headers in last dimensions. single_dim = [] full_dim = [] for i, dim in enumerate(headers.shape[:3]): if dim == 1: single_dim.append(i) else: full_dim.append(i) if len(full_dim) > 1: raise ValueError( f"Only one spatial dimension can have headers. Got {len(full_dim)} - " f"headers.shape={headers.shape[:3]}") new_orientation = (volume.orientation[x] for x in single_dim + full_dim) volume = volume.reformat(new_orientation) assert volume.headers().shape[:3] == (1, 1, volume.shape[2]) # Reformat medical volume to expected orientation specified by dicom headers. # NOTE: This is temporary. Future fixes will allow us to modify header # data to match affine matrix. if len(volume.shape) > 3: shape = volume.shape[3:] multi_volumes = np.empty(shape, dtype=object) for dims in itertools.product( *[list(range(0, x)) for x in multi_volumes.shape]): multi_volumes[dims] = _format_volume_to_header( volume[(Ellipsis, ) + dims]) multi_volumes = multi_volumes.flatten() volume_arr = np.concatenate([v.volume for v in multi_volumes], axis=-1) headers = np.concatenate( [v.headers(flatten=True) for v in multi_volumes], axis=-1) else: volume = _format_volume_to_header(volume) volume_arr = volume.volume headers = volume.headers(flatten=True) assert headers.ndim == 1 assert volume_arr.shape[2] == len( headers ), "Dimension mismatch - {:d} slices but {:d} headers".format( volume_arr.shape[-1], len(headers)) if sort_by: idxs = np.asarray( index_natsorted( headers, key=lambda h: tuple( _unpack_dicom_attr(h, k, required=True) for k in sort_by), )) headers = headers[idxs] volume_arr = volume_arr[..., idxs] # Check if dir_path exists. os.makedirs(dir_path, exist_ok=True) num_slices = len(headers) if not fname_fmt: filename_format = "I%0" + str(max(4, ceil( log10(num_slices)))) + "d.dcm" else: filename_format = fname_fmt filepaths = [ os.path.join(dir_path, filename_format % (s + 1)) for s in range(num_slices) ] if self.num_workers: slices = [volume_arr[..., s] for s in range(num_slices)] if self.verbose: process_map(_write_dicom_file, slices, headers, filepaths) else: with mp.Pool(self.num_workers) as p: out = p.starmap_async(_write_dicom_file, zip(slices, headers, filepaths)) out.wait() else: for s in tqdm(range(num_slices), disable=not self.verbose): _write_dicom_file(volume_arr[..., s], headers[s], filepaths[s])
def test_numpy(self): mv = MedicalVolume(np.ones((10, 20, 30)), np.eye(4)) assert np.all(np.exp(mv.volume) == np.exp(mv).volume) mv[np.where(mv == 1)] = 5 assert np.all(mv == 5) assert not np.any(mv == 1) assert all(mv == 5) assert not any(mv == 5) _ = mv + np.ones(mv.shape) shape = (10, 20, 30, 2) headers = np.stack( [ ututils.build_dummy_headers(shape[2], {"EchoTime": 2}), ututils.build_dummy_headers(shape[2], {"EchoTime": 10}), ], axis=-1, ) # Reduce functions mv = MedicalVolume(np.random.rand(*shape), np.eye(4), headers=headers) mv2 = np.add.reduce(mv, -1) assert np.all(mv2 == np.add.reduce(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.add.reduce(mv, axis=None) assert np.all(mv2 == np.add.reduce(mv.volume, axis=None)) assert np.isscalar(mv2) mv2 = np.sum(mv, axis=-1) assert np.all(mv2 == np.sum(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.sum(mv) assert np.all(mv2 == np.sum(mv.volume)) assert np.isscalar(mv2) mv2 = mv.sum(axis=-1) assert np.all(mv2 == np.sum(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = mv.sum() assert np.all(mv2 == np.sum(mv.volume)) assert np.isscalar(mv2) mv2 = np.mean(mv, axis=-1) assert np.all(mv2 == np.mean(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.mean(mv) assert np.all(mv2 == np.mean(mv.volume)) assert np.isscalar(mv2) mv2 = mv.mean(axis=-1) assert np.all(mv2 == np.mean(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = mv.mean() assert np.all(mv2 == np.mean(mv.volume)) assert np.isscalar(mv2) mv2 = np.std(mv, axis=-1) assert np.all(mv2 == np.std(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.std(mv) assert np.all(mv2 == np.std(mv.volume)) assert np.isscalar(mv2) # Min/max functions mv2 = np.amin(mv, axis=-1) assert np.all(mv2 == np.amin(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.amin(mv) assert np.all(mv2 == np.amin(mv.volume)) assert np.isscalar(mv2) mv2 = np.amax(mv, axis=-1) assert np.all(mv2 == np.amax(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.amax(mv) assert np.all(mv2 == np.amax(mv.volume)) assert np.isscalar(mv2) mv2 = np.argmin(mv, axis=-1) assert np.all(mv2 == np.argmin(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.argmin(mv) assert np.all(mv2 == np.argmin(mv.volume)) mv2 = np.argmax(mv, axis=-1) assert np.all(mv2 == np.argmax(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.argmax(mv) assert np.all(mv2 == np.argmax(mv.volume)) # NaN functions vol_nan = np.ones(shape) vol_nan[..., 1] = np.nan mv = MedicalVolume(vol_nan, np.eye(4), headers=headers) mv2 = np.nansum(mv, axis=-1) assert np.all(mv2 == np.nansum(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.nansum(mv) assert np.all(mv2 == np.nansum(mv.volume)) assert np.isscalar(mv2) mv2 = np.nanmean(mv, axis=-1) assert np.all(mv2 == np.nanmean(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.nanmean(mv) assert np.all(mv2 == np.nanmean(mv.volume)) assert np.isscalar(mv2) mv2 = np.nanstd(mv, axis=-1) assert np.all(mv2 == np.nanstd(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.nanstd(mv) assert np.all(mv2 == np.nanstd(mv.volume)) assert np.isscalar(mv2) mv2 = np.nan_to_num(mv) assert np.unique(mv2.volume).tolist() == [0, 1] mv2 = np.nan_to_num(mv, copy=False) assert id(mv2) == id(mv) mv2 = np.nanmin(mv, axis=-1) assert np.all(mv2 == np.nanmin(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.nanmin(mv) assert np.all(mv2 == np.nanmin(mv.volume)) assert np.isscalar(mv2) mv2 = np.nanmax(mv, axis=-1) assert np.all(mv2 == np.nanmax(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.nanmax(mv) assert np.all(mv2 == np.nanmax(mv.volume)) assert np.isscalar(mv2) mv2 = np.nanargmin(mv, axis=-1) assert np.all(mv2 == np.nanargmin(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.nanargmin(mv) assert np.all(mv2 == np.nanargmin(mv.volume)) mv2 = np.nanargmax(mv, axis=-1) assert np.all(mv2 == np.nanargmax(mv.volume, axis=-1)) assert mv2.shape == mv.shape[:3] mv2 = np.nanargmax(mv) assert np.all(mv2 == np.nanargmax(mv.volume)) # Round shape = (10, 20, 30, 2) affine = np.concatenate([np.random.rand(3, 4), [[0, 0, 0, 1]]], axis=0) mv = MedicalVolume(np.random.rand(*shape), affine, headers=headers) mv2 = mv.round() assert np.allclose(mv2.affine, affine) assert np.unique(mv2.volume).tolist() == [0, 1] mv2 = mv.round(affine=True) assert np.unique(mv2.affine).tolist() == [0, 1] assert np.unique(mv2.volume).tolist() == [0, 1] # Clip shape = (10, 20, 30) mv = MedicalVolume(np.random.rand(*shape), np.eye(4)) mv2 = np.clip(mv, 0.4, 0.6) assert np.all((mv2.volume >= 0.4) & (mv2.volume <= 0.6)) mv_lower = MedicalVolume(np.ones(mv.shape) * 0.4, mv.affine) mv_upper = MedicalVolume(np.ones(mv.shape) * 0.6, mv.affine) mv2 = np.clip(mv, mv_lower, mv_upper) assert np.all((mv2.volume >= 0.4) & (mv2.volume <= 0.6)) # Array like shape = (10, 20, 30) mv = MedicalVolume(np.random.rand(*shape), np.eye(4)) mv2 = np.zeros_like(mv) assert np.all(mv2.volume == 0) mv2 = np.ones_like(mv) assert np.all(mv2.volume == 1) # Shares memory shape = (10, 20, 30, 2) headers = np.stack( [ ututils.build_dummy_headers(shape[2], {"EchoTime": 2}), ututils.build_dummy_headers(shape[2], {"EchoTime": 10}), ], axis=-1, ) mv = MedicalVolume(np.random.rand(*shape), np.eye(4), headers=headers) mv2 = MedicalVolume(mv.A, affine=mv.affine, headers=mv.headers()) assert np.shares_memory(mv, mv) assert np.shares_memory(mv, mv2)