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 __calc_quant_vals__(self, quant_map: MedicalVolume, map_type: QuantitativeValueType): """Helper method to get quantitative values for tissue - implemented per tissue. Different tissues should override this as they see fit. Args: quant_map (MedicalVolume): 3D map of pixel-wise quantitative measures (T2, T2*, T1-rho, etc.). Volume should have ``np.nan`` values for all pixels unable to be calculated. map_type (QuantitativeValueType): Type of quantitative value to analyze. Raises: TypeError: If `quant_map` is not of type `MedicalVolume` or `map_type` is not of type `QuantitativeValueType`. ValueError: If no mask is found for tissue. """ if not isinstance(quant_map, MedicalVolume): raise TypeError("`Expected type 'MedicalVolume' for `quant_map`") if not isinstance(map_type, QuantitativeValueType): raise TypeError( "`Expected type 'QuantitativeValueType' for `map_type`") if self.__mask__ is None: raise ValueError("Please initialize mask for {}".format( self.FULL_NAME)) quant_map.reformat(self.__mask__.orientation, inplace=True) pass
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 _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 _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_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_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 set_mask(self, mask: MedicalVolume): """Set mask for tissue. Args: mask (MedicalVolume): Binary mask of segmented tissue. """ assert type( mask ) is MedicalVolume, "mask for tissue must be of type MedicalVolume" mask.reformat(SAGITTAL, inplace=True) self.__mask__ = mask
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_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 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 __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_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 _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_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 test_from_sitk(self): mv = MedicalVolume(np.random.rand(10, 20, 30), self._AFFINE) filepath = os.path.join(ututils.TEMP_PATH, "med_vol_from_sitk.nii.gz") NiftiWriter().save(mv, filepath) nr = NiftiReader() expected = nr.load(filepath) img = sitk.ReadImage(filepath) mv = MedicalVolume.from_sitk(img) assert np.allclose(mv.affine, expected.affine) assert mv.shape == expected.shape assert np.all(mv.volume == expected.volume) img = sitk.Image([10, 20, 1], sitk.sitkVectorFloat32, 3) mv = MedicalVolume.from_sitk(img) assert np.all(mv.volume == 0) assert mv.shape == (10, 20, 1, 3)
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 _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_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 check_orientations(self, mv: MedicalVolume, orientations): """ Apply each orientation specified in orientations to the Medical Volume mv Assert if mv --> apply orientation --> apply original orientation != mv original position coordinates. Args: mv: a Medical Volume orientations: a list or tuple of orientation tuples """ o_base, so_base, ps_base = mv.orientation, mv.scanner_origin, mv.pixel_spacing ps_affine = np.array(mv.affine) for o in orientations: # Reorient to some orientation mv.reformat(o, inplace=True) # Reorient to original orientation mv.reformat(o_base, inplace=True) assert mv.orientation == o_base, "Orientation mismatch: Expected %s, got %s" % ( str(o_base), str(mv.orientation), ) assert mv.scanner_origin == so_base, "Scanner Origin mismatch: Expected %s, got %s" % ( str(so_base), str(mv.scanner_origin), ) assert mv.pixel_spacing == ps_base, "Pixel Spacing mismatch: Expected %s, got %s" % ( str(ps_base), str(mv.pixel_spacing), ) assert (mv.affine == ps_affine).all( ), "Affine matrix mismatch: Expected\n%s\ngot\n%s" % ( str(ps_affine), str(mv.affine), )
def _process_mask(self, mask, y: MedicalVolume): """Process mask into appropriate shape.""" arr_types = (np.ndarray, cp.ndarray) if env.cupy_available() else (np.ndarray, ) if isinstance(mask, arr_types): mask = y._partial_clone(volume=mask, headers=None) elif not isinstance(mask, MedicalVolume): raise TypeError("`mask` must be a MedicalVolume or ndarray") mask = mask.reformat_as(y) if not mask.is_same_dimensions(y, defaults.AFFINE_DECIMAL_PRECISION): raise RuntimeError("`mask` and `y` dimension mismatch") return mask > 0
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 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