|
22 | 22 | # |
23 | 23 | """Test EPI manipulation routines.""" |
24 | 24 | import numpy as np |
| 25 | +import pytest |
25 | 26 | import nibabel as nb |
26 | | -from ..tools import brain_masker |
| 27 | +from nitransforms.linear import Affine |
| 28 | +from sdcflows.utils.tools import brain_masker, deoblique_and_zooms |
27 | 29 |
|
28 | 30 |
|
29 | 31 | def test_epi_mask(tmpdir, testdata_dir): |
30 | 32 | """Check mask algorithm.""" |
31 | 33 | tmpdir.chdir() |
32 | 34 | mask = brain_masker(testdata_dir / "epi.nii.gz")[-1] |
33 | 35 | assert abs(np.asanyarray(nb.load(mask).dataobj).sum() - 166476) < 10 |
| 36 | + |
| 37 | + |
| 38 | +@pytest.mark.parametrize("padding", [0, 1, 4]) |
| 39 | +@pytest.mark.parametrize("factor", [1, 4, 0.8]) |
| 40 | +@pytest.mark.parametrize("centered", [True, False]) |
| 41 | +@pytest.mark.parametrize( |
| 42 | + "rotate", |
| 43 | + [ |
| 44 | + np.eye(4), |
| 45 | + # Rotate 90 degrees around x-axis |
| 46 | + np.array([[0, 1, 0, 0], [0, 0, 1, 0], [1, 0, 0, 0], [0, 0, 0, 1]]), |
| 47 | + # Rotate 30 degrees around x-axis |
| 48 | + nb.affines.from_matvec( |
| 49 | + nb.eulerangles.euler2mat(0, 0, 30 * np.pi / 180), |
| 50 | + (0, 0, 0), |
| 51 | + ), |
| 52 | + # Rotate 48 degrees around y-axis and translation |
| 53 | + nb.affines.from_matvec( |
| 54 | + nb.eulerangles.euler2mat(0, 48 * np.pi / 180, 0), |
| 55 | + (2.0, 1.2, 0.7), |
| 56 | + ), |
| 57 | + ], |
| 58 | +) |
| 59 | +def test_deoblique_and_zooms(tmpdir, padding, factor, centered, rotate, debug=False): |
| 60 | + """Check deoblique and denser.""" |
| 61 | + tmpdir.chdir() |
| 62 | + |
| 63 | + # Generate an example reference image |
| 64 | + ref_shape = (80, 128, 160) if factor < 1 else (20, 32, 40) |
| 65 | + ref_data = np.zeros(ref_shape, dtype=np.float32) |
| 66 | + ref_data[1:-2, 10:-11, 1:-2] = 1 |
| 67 | + ref_affine = np.diag([2.0, 1.25, 1.0, 1.0]) |
| 68 | + ref_zooms = nb.affines.voxel_sizes(ref_affine) |
| 69 | + if centered: |
| 70 | + ref_affine[:3, 3] -= nb.affines.apply_affine( |
| 71 | + ref_affine, |
| 72 | + 0.5 * (np.array(ref_data.shape) - 1), |
| 73 | + ) |
| 74 | + ref_img = nb.Nifti1Image(ref_data, ref_affine) |
| 75 | + ref_img.header.set_qform(ref_affine, 1) |
| 76 | + ref_img.header.set_sform(ref_affine, 0) |
| 77 | + |
| 78 | + # Generate an example oblique image |
| 79 | + mov_affine = rotate @ ref_affine |
| 80 | + mov_img = nb.Nifti1Image(ref_data, mov_affine) |
| 81 | + mov_img.header.set_qform(mov_affine, 1) |
| 82 | + mov_img.header.set_sform(mov_affine, 0) |
| 83 | + |
| 84 | + # Call function with default parameters |
| 85 | + out_img = deoblique_and_zooms(ref_img, mov_img, padding=padding, factor=factor) |
| 86 | + |
| 87 | + # Check output shape and zooms |
| 88 | + assert np.allclose(out_img.header.get_zooms()[:3], ref_zooms / factor) |
| 89 | + |
| 90 | + # Check idempotency with a lot of tolerance |
| 91 | + ref_resampled = Affine(reference=out_img).apply(ref_img, order=0) |
| 92 | + ref_back = Affine(reference=ref_img).apply(ref_resampled, order=0) |
| 93 | + resampled = Affine(reference=out_img).apply(mov_img, order=2) |
| 94 | + if debug: |
| 95 | + ref_img.to_filename("reference.nii.gz") |
| 96 | + ref_back.to_filename("reference_back.nii.gz") |
| 97 | + ref_resampled.to_filename("reference_resampled.nii.gz") |
| 98 | + mov_img.to_filename("moving.nii.gz") |
| 99 | + resampled.to_filename("resampled.nii.gz") |
| 100 | + |
| 101 | + # Allow up to 3% pixels wrong after up(down)sampling and walk back. |
| 102 | + assert ( |
| 103 | + np.abs(np.clip(ref_back.get_fdata(), 0, 1) - ref_data).sum() |
| 104 | + < ref_data.size * 0.03 |
| 105 | + ) |
| 106 | + |
| 107 | + vox_vol_out = np.prod(out_img.header.get_zooms()) |
| 108 | + vox_vol_mov = np.prod(mov_img.header.get_zooms()) |
| 109 | + vol_factor = vox_vol_out / vox_vol_mov |
| 110 | + |
| 111 | + ref_volume = ref_data.sum() |
| 112 | + res_volume = np.clip(resampled.get_fdata(), 0, 1).sum() * vol_factor |
| 113 | + # Tolerate up to 2% variation of the volume of the moving image |
| 114 | + assert abs(1 - res_volume / ref_volume) < 0.02 |
0 commit comments