def _generate_mock_data(self, shape=None, metadata=True): """Generates arbitrary mock data for QDess sequence. Metadata values were extracted from a real qDESS sequence. """ if shape is None: shape = (10, 10, 10) e1 = MedicalVolume(np.random.rand(*shape) * 80 + 0.1, affine=np.eye(4)) e2 = MedicalVolume(np.random.rand(*shape) * 40 + 0.1, affine=np.eye(4)) ys = [e1, e2] ts = [8, 42] if metadata: with warnings.catch_warnings(): warnings.simplefilter("ignore") for idx, (y, t) in enumerate(zip(ys, ts)): y.set_metadata("EchoTime", t, force=True) y.set_metadata("EchoNumber", idx + 1, force=True) y.set_metadata("RepetitionTime", 25.0, force=True) y.set_metadata("FlipAngle", 30.0, force=True) y.set_metadata(Tag(0x001910B6), 3132.0, force=True) # gradient time y.set_metadata(Tag(0x001910B7), 1560.0, force=True) # gradient area return ys, ts, None
def test_comparison(self): mv1 = MedicalVolume(np.ones((10, 20, 30)), self._AFFINE) mv2 = MedicalVolume(2 * np.ones((10, 20, 30)), self._AFFINE) assert np.all((mv1 == mv1.clone()).volume) assert np.all((mv1 != mv2).volume) assert np.all((mv1 < mv2).volume) assert np.all((mv1 <= mv2).volume) assert np.all((mv2 > mv1).volume) assert np.all((mv2 >= mv1).volume)
def load(self, file_path): """Load volume from NIfTI file path. A NIfTI file should only correspond to one volume. Args: file_path (str): File path to NIfTI file. Returns: MedicalVolume: Loaded volume. Raises: FileNotFoundError: If `file_path` not found. ValueError: If `file_path` does not end in a supported NIfTI extension. """ if not os.path.isfile(file_path): raise FileNotFoundError("{} not found".format(file_path)) if not self.data_format_code.is_filetype(file_path): raise ValueError( "{} must be a file with extension '.nii' or '.nii.gz'".format( file_path)) nib_img = nib.load(file_path) nib_img_affine = nib_img.affine nib_img_affine = self.__normalize_affine(nib_img_affine) np_img = nib_img.get_fdata() return MedicalVolume(np_img, nib_img_affine)
def test_mask(self): x, y, b = _generate_monoexp_data((10, 10, 20)) mask_arr = np.random.rand(*y[0].shape) > 0.5 mask = MedicalVolume(mask_arr, y[0].affine) fitter = CurveFitter(monoexponential) popt = fitter.fit(x, y, mask=mask)[0] a_hat, b_hat = popt[..., 0], popt[..., 1] assert np.allclose(a_hat.volume[mask_arr != 0], 1.0) assert np.allclose(b_hat.volume[mask_arr != 0], b[mask_arr != 0]) assert np.all(np.isnan(a_hat.volume[mask_arr == 0])) assert np.all(np.isnan(b_hat.volume[mask_arr == 0])) fitter = CurveFitter(monoexponential) popt = fitter.fit(x, y, mask=mask_arr)[0] a_hat, b_hat = popt[..., 0], popt[..., 1] assert np.allclose(a_hat.volume[mask_arr != 0], 1.0) assert np.allclose(b_hat.volume[mask_arr != 0], b[mask_arr != 0]) with self.assertRaises(TypeError): fitter = CurveFitter(monoexponential) popt = fitter.fit(x, y, mask="foo")[0] with self.assertRaises(RuntimeError): mask_incorrect_shape = np.random.rand(5, 5, 5) > 0.5 fitter = CurveFitter(monoexponential) popt = fitter.fit(x, y, mask=mask_incorrect_shape)[0]
def test_device_gpu(self): import cupy as cp mv = MedicalVolume(np.ones((10, 20, 30)), self._AFFINE) mv_gpu = mv.to(Device(0)) assert mv_gpu.device == Device(0) assert isinstance(mv_gpu.volume, cp.ndarray) assert isinstance(mv_gpu.affine, np.ndarray) assert mv_gpu.is_same_dimensions(mv) assert cp.all((mv_gpu + 1).volume == 2) assert cp.all((mv_gpu - 1).volume == 0) assert cp.all((mv_gpu * 2).volume == 2) assert cp.all((mv_gpu / 2).volume == 0.5) assert cp.all((mv_gpu > 0).volume) assert cp.all((mv_gpu >= 0).volume) assert cp.all((mv_gpu < 2).volume) assert cp.all((mv_gpu <= 2).volume) ornt = tuple(x[::-1] for x in mv_gpu.orientation[::-1]) mv2 = mv_gpu.reformat(ornt) assert mv2.orientation == ornt mv_cpu = mv_gpu.cpu() assert mv_cpu.device == Device(-1) assert mv_cpu.is_identical(mv) with self.assertRaises(RuntimeError): mv_gpu.save_volume( os.path.join(self._TEMP_PATH, "test_device.nii.gz"))
def test_array_gpu(self): import cupy as cp mv = MedicalVolume(np.ones((10, 20, 30)), self._AFFINE) mv_gpu = mv.to(Device(0)) data = cp.asarray(mv_gpu) assert cp.shares_memory(data, mv_gpu.volume)
def test_t2_star_map(self): ys, _, _, _ = self._generate_mock_data() scan = Cones(ys) # No mask tissue = FemoralCartilage() map1 = scan.generate_t2_star_map(tissue, num_workers=util.num_workers()) assert map1 is not None, "map should not be None" mask = MedicalVolume(np.ones(ys[0].shape), np.eye(4)) # Use a mask tissue.set_mask(mask) map2 = scan.generate_t2_star_map(tissue, num_workers=util.num_workers()) assert map2 is not None, "map should not be None" assert map1.volumetric_map.is_identical(map2.volumetric_map) # Use a mask as a path tissue = FemoralCartilage() mask_path = os.path.join(self.data_dirpath, "test_t2_star_map_mask.nii.gz") NiftiWriter().save(mask, mask_path) map2 = scan.generate_t2_star_map(tissue, num_workers=util.num_workers(), mask_path=mask_path) assert map2 is not None, "map should not be None" assert map1.volumetric_map.is_identical(map2.volumetric_map)
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_slice_with_headers(self): vol = np.stack([np.ones((10, 20, 30)), 2 * np.ones((10, 20, 30))], axis=-1) headers = np.stack( [ ututils.build_dummy_headers(vol.shape[2], {"EchoTime": 2}), ututils.build_dummy_headers(vol.shape[2], {"EchoTime": 10}), ], axis=-1, ) mv = MedicalVolume(vol, self._AFFINE, headers=headers) mv2 = mv[..., 0] assert mv2._headers.shape == (1, 1, 30) for h in mv2.headers(flatten=True): assert h["EchoTime"].value == 2 mv2 = mv[..., 1] assert mv2._headers.shape == (1, 1, 30) for h in mv2.headers(flatten=True): assert h["EchoTime"].value == 10 mv2 = mv[:10, :5, 8:10, :1] assert mv2._headers.shape == (1, 1, 2, 1) mv2 = mv[:10] assert mv2._headers.shape == (1, 1, 30, 2) mv2 = mv[:, :10] assert mv2._headers.shape == (1, 1, 30, 2) mv2 = mv[..., 0:1] assert mv2._headers.shape == (1, 1, 30, 1) vol = np.stack([np.ones((10, 20, 30)), 2 * np.ones((10, 20, 30))], axis=-1) headers = ututils.build_dummy_headers(vol.shape[2], {"EchoTime": 2})[..., np.newaxis] mv = MedicalVolume(vol, self._AFFINE, headers=headers) mv1 = mv[..., 0] mv2 = mv[..., 1] assert mv1._headers.shape == (1, 1, 30) assert mv2._headers.shape == (1, 1, 30) for h1, h2 in zip(mv1.headers(flatten=True), mv2.headers(flatten=True)): assert id(h1) == id(h2)
def test_dtype(self): vol = np.ones((10, 20, 30)) mv = MedicalVolume(vol, self._AFFINE) assert mv.volume.dtype == vol.dtype mv2 = mv.astype("int32") assert id(mv) == id(mv2) assert mv2.volume.dtype == np.int32
def test_basic(self): vol = np.zeros((10, 10, 10)) mv = MedicalVolume(vol, self._AFFINE) with self.assertRaises(TypeError): _ = T2(vol) qv = T2(mv) qv.add_additional_volume("r2", mv + 1) assert np.all(qv.additional_volumes["r2"] == mv + 1)
def test_spacing(self): """Test affine matrix with pixel spacing.""" ornt = ("AP", "SI", "RL") spacing = np.random.rand(3) + 0.1 # avoid pixel spacing of 0 affine = to_affine(ornt, spacing) mv = MedicalVolume(np.ones((10, 20, 30)), affine) assert mv.orientation == ornt assert np.all(np.asarray(mv.pixel_spacing) == spacing) assert np.all(np.asarray(mv.scanner_origin) == 0) spacing = np.random.rand(1) + 0.1 # avoid pixel spacing of 0 expected_spacing = np.asarray(list(spacing) + [1.0, 1.0]) affine = to_affine(ornt, spacing) mv = MedicalVolume(np.ones((10, 20, 30)), affine) assert mv.orientation == ornt assert np.all(np.asarray(mv.pixel_spacing) == expected_spacing) assert np.all(np.asarray(mv.scanner_origin) == 0)
def test_to_device(self): arr = np.ones((3, 3, 3)) mv = MedicalVolume(arr, affine=np.eye(4)) arr2 = to_device(arr, -1) assert get_device(arr2) == cpu_device mv2 = to_device(mv, -1) assert get_device(mv2) == cpu_device
def _generate_translated_vols(n=3): """Generate mock data that is translated diagonally by 1 pixel.""" mvs = [] affine = to_affine(("SI", "AP"), (0.3, 0.3, 0.5)) for offset in range(n): arr = np.zeros((250, 250, 10)) arr[15 + offset : 35 + offset, 15 + offset : 35 + offset] = 1 mvs.append(MedicalVolume(arr, affine)) return mvs
def test_origin(self): """Test affine matrix with scanner origin.""" ornt = ("AP", "SI", "RL") origin = np.random.rand(3) affine = to_affine(ornt, spacing=None, origin=origin) mv = MedicalVolume(np.ones((10, 20, 30)), affine) assert mv.orientation == ornt assert np.all(np.asarray(mv.pixel_spacing) == 1) assert np.all(np.asarray(mv.scanner_origin) == origin) origin = np.random.rand(1) expected_origin = np.asarray(list(origin) + [0.0, 0.0]) affine = to_affine(ornt, spacing=None, origin=origin) mv = MedicalVolume(np.ones((10, 20, 30)), affine) assert mv.orientation == ornt assert np.all(np.asarray(mv.pixel_spacing) == 1) assert np.all(np.asarray(mv.scanner_origin) == expected_origin)
def test_special_affine(self): """Test creation of affine matrix for special cases.""" # Patient orientation (useful for xray data). header = ututils.build_dummy_headers( 1, fields={"PatientOrientation": ["P", "F"], "PixelSpacing": [0.2, 0.5]} ) affine = to_RAS_affine(header) mv = MedicalVolume(np.ones((10, 20, 30)), affine=affine) assert mv.orientation == ("SI", "AP", "LR") assert mv.pixel_spacing == (0.5, 0.2, 1.0) assert mv.scanner_origin == (0.0, 0.0, 0.0)
def test_slice(self): mv = MedicalVolume(np.ones((10, 20, 30)), self._AFFINE) with self.assertRaises(IndexError): mv[4] mv_slice = mv[4:5] assert mv_slice.shape == (1, 20, 30) mv = MedicalVolume(np.ones((10, 20, 30)), self._AFFINE) mv[:5, ...] = 2 assert np.all(mv._volume[:5, ...] == 2) & np.all(mv._volume[5:, ...] == 1) assert np.all(mv[:5, ...].volume == 2) mv = MedicalVolume(np.ones((10, 20, 30)), self._AFFINE) mv2 = mv[:5, ...].clone() mv2 += 2 mv[:5, ...] = mv2 assert np.all(mv._volume[:5, ...] == 3) & np.all(mv._volume[5:, ...] == 1) assert np.all(mv[:5, ...].volume == 3)
def test_reformat_header(self): volume = np.random.rand(10, 20, 30, 40) headers = ututils.build_dummy_headers(volume.shape[2:]) mv = MedicalVolume(volume, self._AFFINE, headers=headers) new_orientation = tuple(x[::-1] for x in mv.orientation[::-1]) mv2 = mv.reformat(new_orientation) assert mv2._headers.shape == (30, 1, 1, 40) mv2 = mv.clone() mv2.reformat(new_orientation, inplace=True) assert mv2._headers.shape == (30, 1, 1, 40) volume = np.random.rand(10, 20, 30, 40) headers = ututils.build_dummy_headers((volume.shape[2], 1)) mv = MedicalVolume(volume, self._AFFINE, headers=headers) new_orientation = tuple(x[::-1] for x in mv.orientation[::-1]) mv2 = mv.reformat(new_orientation) assert mv2._headers.shape == (30, 1, 1, 1)
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 _generate_linear_data(shape=None, x=None, a=None): """Generate sample linear data. ``a`` is randomly generated in interval [0.1, 1.1). """ if a is None: a = np.random.rand(*shape) + 0.1 else: shape = a.shape if x is None: x = np.asarray([0.5, 1.0, 2.0, 4.0]) y = [MedicalVolume(_linear(t, a), affine=np.eye(4)) for t in x] return x, y, a
def test_to_sitk(self): mv = MedicalVolume(np.random.rand(10, 20, 30), self._AFFINE) filepath = os.path.join(ututils.TEMP_PATH, "med_vol_to_sitk.nii.gz") NiftiWriter().save(mv, filepath) expected = sitk.ReadImage(filepath) nr = NiftiReader() mv = nr.load(filepath) img = mv.to_sitk() assert np.allclose(sitk.GetArrayViewFromImage(img), sitk.GetArrayViewFromImage(expected)) assert img.GetSize() == mv.shape assert np.allclose(img.GetOrigin(), expected.GetOrigin()) assert img.GetSpacing() == img.GetSpacing() assert img.GetDirection() == expected.GetDirection() mv = MedicalVolume(np.zeros((10, 20, 1, 3)), affine=self._AFFINE) img = mv.to_sitk(vdim=-1) assert np.all(sitk.GetArrayViewFromImage(img) == 0) assert img.GetSize() == (10, 20, 1)
def test_complex(self): ornt = ("AP", "SI", "RL") spacing = (0.5, 0.7) origin = (100, -54) expected_spacing = np.asarray(list(spacing) + [1.0]) expected_origin = np.asarray(list(origin) + [0.0]) affine = to_affine(ornt, spacing=spacing, origin=origin) mv = MedicalVolume(np.ones((10, 20, 30)), affine) assert mv.orientation == ornt assert np.all(np.asarray(mv.pixel_spacing) == expected_spacing) assert np.all(np.asarray(mv.scanner_origin) == expected_origin)
def generate_monoexp_data(shape=None, x=None, a=1.0, b=None): """Generate sample monoexponetial data. ``a=1.0``, ``b`` is randomly generated in interval [0.1, 1.1). The equation is :math:`y = a * \\exp (b*x)`. """ if b is None: b = np.random.rand(*shape) + 0.1 else: shape = b.shape if x is None: x = np.asarray([0.5, 1.0, 2.0, 4.0]) y = [MedicalVolume(monoexponential(t, a, b), affine=np.eye(4)) for t in x] return x, y, a, b
def test_basic(self): """Basic transposes and flips in RAS+ coordinate system.""" orientations = [ ("LR", "PA", "IS"), # standard RAS+ ("RL", "AP", "SI"), # flipped ("IS", "LR", "PA"), # transposed ("AP", "SI", "RL"), # transposed + flipped ] for ornt in orientations: affine = to_affine(ornt) mv = MedicalVolume(np.ones((10, 20, 30)), affine) assert mv.orientation == ornt assert np.all(np.asarray(mv.pixel_spacing) == 1) assert np.all(np.asarray(mv.scanner_origin) == 0)
def test_save(self): filepath = get_testdata_file("MR_small.dcm") dr = DicomReader(group_by=None) mv_base = dr.load(filepath)[0] out_dir = os.path.join(self.data_dirpath, "test_save_sort_by") dw = DicomWriter() dw.save(mv_base, out_dir, sort_by="InstanceNumber") mv2 = dr.load(filepath)[0] assert mv2.is_identical(mv_base) out_dir = os.path.join(self.data_dirpath, "test_save_no_headers") mv = MedicalVolume(np.ones((10, 10, 10)), np.eye(4)) dw = DicomWriter() with self.assertRaises(ValueError): dw.save(mv, out_dir)
def _generate_affine(shape=None, x=None, a=None, b=1.0, as_med_vol=False): """Generate data of the form :math:`y = a*x + b`.""" if a is None: a = np.random.rand(*shape) + 0.1 else: shape = a.shape if x is None: x = np.asarray([0.5, 1.0, 2.0, 4.0]) if b is None: b = np.random.rand(*shape) if as_med_vol: y = [MedicalVolume(a * t + b, affine=np.eye(4)) for t in x] else: y = [a * t + b for t in x] return x, y, a, b
def __dilate_mask__( self, mask_path: str, temp_path: str, dil_rate: float = preferences.mask_dilation_rate, dil_threshold: float = preferences.mask_dilation_threshold, ): """Dilate mask using gaussian blur and write to disk to use with Elastix. Args: mask_path (str | MedicalVolume): File path for mask or mask to use to use as focus points for registration. Mask must be binary. temp_path (str): Directory path to store temporary data. dil_rate (`float`, optional): Dilation rate (sigma). Defaults to ``preferences.mask_dilation_rate``. dil_threshold (`float`, optional): Threshold to binarize dilated mask. Must be between [0, 1]. Defaults to ``preferences.mask_dilation_threshold``. Returns: str: File path of dilated mask. Raises: FileNotFoundError: If `mask_path` not valid file. ValueError: If `dil_threshold` not in range [0, 1]. """ if dil_threshold < 0 or dil_threshold > 1: raise ValueError("'dil_threshold' must be in range [0, 1]") if isinstance(mask_path, MedicalVolume): mask = mask_path elif os.path.isfile(mask_path): mask = fio_utils.generic_load(mask_path, expected_num_volumes=1) else: raise FileNotFoundError("File {} not found".format(mask_path)) dilated_mask = (sni.gaussian_filter(np.asarray(mask.volume, dtype=np.float32), sigma=dil_rate) > dil_threshold) fixed_mask = np.asarray(dilated_mask, dtype=np.int8) fixed_mask_filepath = os.path.join(io_utils.mkdirs(temp_path), "dilated-mask.nii.gz") dilated_mask_volume = MedicalVolume(fixed_mask, affine=mask.affine) dilated_mask_volume.save_volume(fixed_mask_filepath) return fixed_mask_filepath
def test_hdf5(self): shape = (10, 20, 30) volume = np.reshape(list(range(np.product(shape))), shape) hdf5_file = os.path.join(self._TEMP_PATH, "unittest.h5") with h5py.File(hdf5_file, "w") as f: f.create_dataset("volume", data=volume) f = h5py.File(hdf5_file, "r") mv = MedicalVolume(f["volume"], np.eye(4)) assert mv.device == Device("cpu") assert mv.dtype == f["volume"].dtype mv2 = mv[:, :, :1] assert np.all(mv2.volume == volume[:, :, :1]) assert mv2.device == Device("cpu") assert mv2.dtype == volume.dtype
def stack(xs, axis: int = -1): """Stack medical images across non-spatial dimensions. Images will be auto-oriented to the orientation of the first medical volume. Args: xs (array-like[MedicalVolume]): 1D array-like of aligned medical images to stack. axis (int, optional): Axis to stack along. Returns: MedicalVolume: The stacked medical image. Note: Unlike NumPy, the default stacking axis is ``-1``. Note: Headers are not set unless all inputs have headers of the same shape. This functionality may change in the future. Examples: >>> mv = dm.MedicalVolume([[[0,1,2,3]]], affine=np.eye(4)) >>> np.stack([mv, mv], axis=-1) # Stacks along last axis. MedicalVolume(volume=[[[[0 0], [1 1], [2 2], [3 3]]]]) """ if not isinstance(axis, int): raise TypeError(f"'{type(axis)}' cannot be interpreted as int") xs = [x.reformat(xs[0].orientation) for x in xs] affine = xs[0].affine for x in xs[1:]: assert x.is_same_dimensions(xs[0], err=True) try: axis = _to_positive_axis(axis, len(xs[0].shape), grow=True, invalid_axis="spatial") except ValueError: raise ValueError(f"Cannot stack across spatial dimension (axis={axis})") assert axis >= 0 vol = np.stack([x.volume for x in xs], axis=axis) headers = [x.headers() for x in xs] if any(x is None for x in headers): headers = None else: headers = np.stack(headers, axis=axis) return MedicalVolume(vol, affine, headers=headers)
def test_save_load_data(self): out_dir = os.path.join(ututils.TEMP_PATH, "quant_val_save") vol = np.zeros((10, 10, 10)) mv = MedicalVolume(vol, self._AFFINE) qv = T2(mv) qv.add_additional_volume("r2", mv + 1) qv.save_data(out_dir, ImageDataFormat.nifti) assert os.path.isfile(os.path.join(out_dir, "t2", "t2.nii.gz")) assert os.path.isfile(os.path.join(out_dir, "t2", "t2-r2.nii.gz")) with self.assertWarns(UserWarning): qv.save_data(out_dir, ImageDataFormat.dicom) qv2 = T2() qv2.load_data(out_dir) assert qv2.volumetric_map.is_identical(qv.volumetric_map)