Skip to content

Commit f03d54a

Browse files
committed
fix: finalize the implementation of a test
after long time scratching my head, it seems mango does not always interpret orientation correctly. freeview seems to do so though.
1 parent ac80266 commit f03d54a

File tree

2 files changed

+65
-30
lines changed

2 files changed

+65
-30
lines changed

sdcflows/utils/tests/test_tools.py

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,43 +38,77 @@ def test_epi_mask(tmpdir, testdata_dir):
3838
@pytest.mark.parametrize("padding", [0, 1, 4])
3939
@pytest.mark.parametrize("factor", [1, 4, 0.8])
4040
@pytest.mark.parametrize("centered", [True, False])
41-
@pytest.mark.parametrize("rotate", [
42-
np.eye(4),
43-
# Rotate 90 degrees around x-axis
44-
np.array([[0, 1, 0, 0], [0, 0, 1, 0], [1, 0, 0, 0], [0, 0, 0, 1]]),
45-
])
46-
def test_deoblique_and_zooms(tmpdir, padding, factor, centered, rotate):
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):
4760
"""Check deoblique and denser."""
4861
tmpdir.chdir()
4962

5063
# Generate an example reference image
51-
ref_data = np.zeros((20, 32, 40), dtype=np.float32)
64+
ref_shape = (80, 128, 160) if factor < 1 else (20, 32, 40)
65+
ref_data = np.zeros(ref_shape, dtype=np.float32)
5266
ref_data[1:-2, 10:-11, 1:-2] = 1
5367
ref_affine = np.diag([2.0, 1.25, 1.0, 1.0])
5468
ref_zooms = nb.affines.voxel_sizes(ref_affine)
5569
if centered:
5670
ref_affine[:3, 3] -= nb.affines.apply_affine(
57-
ref_affine, 0.5 * (np.array(ref_data.shape) - 1),
71+
ref_affine,
72+
0.5 * (np.array(ref_data.shape) - 1),
5873
)
5974
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)
6077

6178
# Generate an example oblique image
62-
ob_img = nb.Nifti1Image(ref_data, rotate @ ref_affine)
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)
6383

6484
# Call function with default parameters
65-
out_img = deoblique_and_zooms(ref_img, ob_img, padding=padding, factor=factor)
85+
out_img = deoblique_and_zooms(ref_img, mov_img, padding=padding, factor=factor)
6686

6787
# Check output shape and zooms
6888
assert np.allclose(out_img.header.get_zooms()[:3], ref_zooms / factor)
6989

90+
# Check idempotency with a lot of tolerance
7091
ref_resampled = Affine(reference=out_img).apply(ref_img, order=0)
71-
resampled = Affine(reference=out_img).apply(ob_img, order=0)
72-
ref_img.to_filename("reference.nii.gz")
73-
ob_img.to_filename("moving.nii.gz")
74-
ref_resampled.to_filename("ref_resampled.nii.gz")
75-
resampled.to_filename("resampled.nii.gz")
76-
# import pdb; pdb.set_trace()
77-
ref_volume = ref_data.sum() * ref_zooms.prod()
78-
res_volume = resampled.get_fdata().sum() * np.prod(resampled.header.get_zooms())
79-
# 20% of change in volume is too high, must be an error somewhere
80-
assert abs(ref_volume - res_volume) < ref_volume * 0.2
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

sdcflows/utils/tools.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,37 +43,38 @@ def deoblique_and_zooms(in_reference, oblique, factor=4, padding=1):
4343
"""
4444
from itertools import product
4545
import numpy as np
46-
from nibabel.affines import apply_affine
46+
from nibabel.affines import apply_affine, rescale_affine
4747

4848
# Reference space metadata
4949
hdr = in_reference.header.copy()
50-
affine = in_reference.affine
50+
affine = in_reference.affine.copy()
5151
ref_shape = np.array(in_reference.shape[:3])
52+
ref_zooms = np.array(hdr.get_zooms()[:3])
5253
_, scode = in_reference.get_sform(coded=True)
5354
_, qcode = in_reference.get_qform(coded=True)
5455

5556
# Calculate the 8 most extreme coordinates of oblique in in_reference space
5657
extent = apply_affine(
5758
oblique.affine,
58-
np.array(list(product((0, 1), repeat=3))) * (ref_shape - 1),
59+
np.array(list(product((0, 1), repeat=3))) * (np.array(oblique.shape[:3]) - 1),
5960
)
6061
extent_ijk = apply_affine(np.linalg.inv(affine), extent)
6162

6263
underflow = np.clip(extent_ijk.min(0) - padding, None, 0).astype(int)
63-
overflow = np.ceil(np.clip(extent_ijk.max(0) + padding + 1 - ref_shape, 0, None)).astype(int)
64+
overflow = np.ceil(
65+
np.clip(extent_ijk.max(0) + padding + 1 - ref_shape, 0, None)
66+
).astype(int)
6467
if np.any(underflow < 0) or np.any(overflow > 0):
6568
# Add under/overflow voxels
6669
ref_shape += np.abs(underflow) + overflow
6770
# Consistently update origin
6871
affine[:-1, -1] = apply_affine(affine, underflow)
6972

7073
# Make grid denser
71-
if abs(1.0 - factor) > 1e-5:
72-
cur_center = apply_affine(affine, 0.5 * (ref_shape - 1))
73-
affine[:3, :3] /= factor
74-
ref_shape = np.rint(ref_shape * factor)
75-
new_center = affine[:3, :3] @ (0.5 * (ref_shape - 1))
76-
affine[:3, 3] = cur_center - new_center
74+
if abs(1.0 - factor) > 1e-4:
75+
new_shape = np.rint(ref_shape * factor)
76+
affine = rescale_affine(affine, ref_shape, ref_zooms / factor, new_shape)
77+
ref_shape = new_shape
7778

7879
# Generate new reference
7980
hdr.set_sform(affine, scode)

0 commit comments

Comments
 (0)