|
7 | 7 |
|
8 | 8 | import numpy as np |
9 | 9 | import nibabel as nb |
10 | | -from nitransforms.resampling import apply |
11 | 10 | from nitransforms.base import TransformError, ImageGrid |
12 | 11 | from nitransforms.io.base import TransformFileError |
13 | 12 | from nitransforms.nonlinear import ( |
|
17 | 16 | from ..io.itk import ITKDisplacementsField |
18 | 17 |
|
19 | 18 |
|
20 | | -SOME_TEST_POINTS = np.array([ |
21 | | - [0.0, 0.0, 0.0], |
22 | | - [1.0, 2.0, 3.0], |
23 | | - [10.0, -10.0, 5.0], |
24 | | - [-5.0, 7.0, -2.0], |
25 | | - [12.0, 0.0, -11.0], |
26 | | -]) |
| 19 | +SOME_TEST_POINTS = np.array( |
| 20 | + [ |
| 21 | + [0.0, 0.0, 0.0], |
| 22 | + [1.0, 2.0, 3.0], |
| 23 | + [10.0, -10.0, 5.0], |
| 24 | + [-5.0, 7.0, -2.0], |
| 25 | + [12.0, 0.0, -11.0], |
| 26 | + ] |
| 27 | +) |
| 28 | + |
27 | 29 |
|
28 | 30 | @pytest.mark.parametrize("size", [(20, 20, 20), (20, 20, 20, 3)]) |
29 | 31 | def test_itk_disp_load(size): |
@@ -88,109 +90,114 @@ def test_bsplines_references(testdata_path): |
88 | 90 | testdata_path / "someones_bspline_coefficients.nii.gz" |
89 | 91 | ).to_field() |
90 | 92 |
|
91 | | - with pytest.raises(TransformError): |
92 | | - apply( |
93 | | - BSplineFieldTransform( |
94 | | - testdata_path / "someones_bspline_coefficients.nii.gz" |
95 | | - ), |
96 | | - testdata_path / "someones_anatomy.nii.gz", |
97 | | - ) |
98 | | - |
99 | | - apply( |
100 | | - BSplineFieldTransform(testdata_path / "someones_bspline_coefficients.nii.gz"), |
101 | | - testdata_path / "someones_anatomy.nii.gz", |
| 93 | + BSplineFieldTransform( |
| 94 | + testdata_path / "someones_bspline_coefficients.nii.gz", |
102 | 95 | reference=testdata_path / "someones_anatomy.nii.gz", |
103 | 96 | ) |
104 | 97 |
|
105 | 98 |
|
106 | | -@pytest.mark.xfail( |
107 | | - reason="GH-267: disabled while debugging", |
108 | | - strict=False, |
109 | | -) |
110 | | -def test_bspline(tmp_path, testdata_path): |
| 99 | +def test_map_bspline_vs_displacement(tmp_path, testdata_path): |
111 | 100 | """Cross-check B-Splines and deformation field.""" |
112 | 101 | os.chdir(str(tmp_path)) |
113 | 102 |
|
114 | 103 | img_name = testdata_path / "someones_anatomy.nii.gz" |
115 | 104 | disp_name = testdata_path / "someones_displacement_field.nii.gz" |
116 | 105 | bs_name = testdata_path / "someones_bspline_coefficients.nii.gz" |
117 | 106 |
|
118 | | - bsplxfm = BSplineFieldTransform(bs_name, reference=img_name) |
| 107 | + bsplxfm = BSplineFieldTransform(bs_name, reference=img_name).to_field() |
119 | 108 | dispxfm = DenseFieldTransform(disp_name) |
| 109 | + # Interpolating field should be reasonably similar |
| 110 | + np.testing.assert_allclose(dispxfm._field, bsplxfm._field, atol=1e-1, rtol=1e-4) |
120 | 111 |
|
121 | | - out_disp = apply(dispxfm, img_name) |
122 | | - out_bspl = apply(bsplxfm, img_name) |
123 | | - |
124 | | - out_disp.to_filename("resampled_field.nii.gz") |
125 | | - out_bspl.to_filename("resampled_bsplines.nii.gz") |
126 | | - |
127 | | - assert ( |
128 | | - np.sqrt( |
129 | | - (out_disp.get_fdata(dtype="float32") - out_bspl.get_fdata(dtype="float32")) |
130 | | - ** 2 |
131 | | - ).mean() |
132 | | - < 0.2 |
133 | | - ) |
134 | 112 |
|
135 | 113 | @pytest.mark.parametrize("image_orientation", ["RAS", "LAS", "LPS", "oblique"]) |
136 | 114 | @pytest.mark.parametrize("ongrid", [True, False]) |
137 | | -def test_densefield_map(tmp_path, get_testdata, image_orientation, ongrid): |
| 115 | +def test_densefield_map(get_testdata, image_orientation, ongrid): |
138 | 116 | """Create a constant displacement field and compare mappings.""" |
139 | 117 |
|
140 | 118 | nii = get_testdata[image_orientation] |
141 | 119 |
|
| 120 | + # Get sampling indices |
| 121 | + rng = np.random.default_rng() |
| 122 | + |
142 | 123 | # Create a reference centered at the origin with various axis orders/flips |
143 | 124 | shape = nii.shape |
144 | 125 | ref_affine = nii.affine.copy() |
145 | 126 | reference = ImageGrid(nb.Nifti1Image(np.zeros(shape), ref_affine, None)) |
146 | | - indices = reference.ndindex |
147 | | - |
148 | | - gridpoints = reference.ras(indices) |
149 | | - points = gridpoints if ongrid else SOME_TEST_POINTS |
150 | | - |
151 | | - coordinates = gridpoints.reshape(*shape, 3) |
152 | | - deltas = np.stack(( |
153 | | - np.zeros(np.prod(shape), dtype="float32").reshape(shape), |
154 | | - np.linspace(-80, 80, num=np.prod(shape), dtype="float32").reshape(shape), |
155 | | - np.linspace(-50, 50, num=np.prod(shape), dtype="float32").reshape(shape), |
156 | | - ), axis=-1) |
157 | | - |
158 | | - atol = 1e-4 if image_orientation == "oblique" else 1e-7 |
| 127 | + grid_ijk = reference.ndindex |
| 128 | + grid_xyz = reference.ras(grid_ijk) |
| 129 | + |
| 130 | + subsample = rng.choice(grid_ijk.shape[0], 5000) |
| 131 | + points_ijk = grid_ijk.copy() if ongrid else grid_ijk[subsample] |
| 132 | + coords_xyz = ( |
| 133 | + grid_xyz |
| 134 | + if ongrid |
| 135 | + else reference.ras(points_ijk) + rng.normal(size=points_ijk.shape) |
| 136 | + ) |
159 | 137 |
|
160 | | - # Build an identity transform (deltas) |
161 | | - id_xfm_deltas = DenseFieldTransform(reference=reference) |
162 | | - np.testing.assert_array_equal(coordinates, id_xfm_deltas._field) |
163 | | - np.testing.assert_allclose(points, id_xfm_deltas.map(points), atol=atol) |
| 138 | + coords_map = grid_xyz.reshape(*shape, 3) |
| 139 | + deltas = np.stack( |
| 140 | + ( |
| 141 | + np.zeros(np.prod(shape), dtype="float32").reshape(shape), |
| 142 | + np.linspace(-80, 80, num=np.prod(shape), dtype="float32").reshape(shape), |
| 143 | + np.linspace(-50, 50, num=np.prod(shape), dtype="float32").reshape(shape), |
| 144 | + ), |
| 145 | + axis=-1, |
| 146 | + ) |
164 | 147 |
|
165 | | - # Build an identity transform (deformation) |
166 | | - id_xfm_field = DenseFieldTransform(coordinates, is_deltas=False, reference=reference) |
167 | | - np.testing.assert_array_equal(coordinates, id_xfm_field._field) |
168 | | - np.testing.assert_allclose(points, id_xfm_field.map(points), atol=atol) |
| 148 | + if ongrid: |
| 149 | + atol = 1e-3 if image_orientation == "oblique" or not ongrid else 1e-7 |
| 150 | + # Build an identity transform (deltas) |
| 151 | + id_xfm_deltas = DenseFieldTransform(reference=reference) |
| 152 | + np.testing.assert_array_equal(coords_map, id_xfm_deltas._field) |
| 153 | + np.testing.assert_allclose(coords_xyz, id_xfm_deltas.map(coords_xyz)) |
| 154 | + |
| 155 | + # Build an identity transform (deformation) |
| 156 | + id_xfm_field = DenseFieldTransform( |
| 157 | + coords_map, is_deltas=False, reference=reference |
| 158 | + ) |
| 159 | + np.testing.assert_array_equal(coords_map, id_xfm_field._field) |
| 160 | + np.testing.assert_allclose(coords_xyz, id_xfm_field.map(coords_xyz), atol=atol) |
169 | 161 |
|
170 | | - # Collapse to zero transform (deltas) |
171 | | - zero_xfm_deltas = DenseFieldTransform(-coordinates, reference=reference) |
172 | | - np.testing.assert_array_equal(np.zeros_like(zero_xfm_deltas._field), zero_xfm_deltas._field) |
173 | | - np.testing.assert_allclose(np.zeros_like(points), zero_xfm_deltas.map(points), atol=atol) |
| 162 | + # Collapse to zero transform (deltas) |
| 163 | + zero_xfm_deltas = DenseFieldTransform(-coords_map, reference=reference) |
| 164 | + np.testing.assert_array_equal( |
| 165 | + np.zeros_like(zero_xfm_deltas._field), zero_xfm_deltas._field |
| 166 | + ) |
| 167 | + np.testing.assert_allclose( |
| 168 | + np.zeros_like(coords_xyz), zero_xfm_deltas.map(coords_xyz), atol=atol |
| 169 | + ) |
174 | 170 |
|
175 | | - # Collapse to zero transform (deformation) |
176 | | - zero_xfm_field = DenseFieldTransform(np.zeros_like(deltas), is_deltas=False, reference=reference) |
177 | | - np.testing.assert_array_equal(np.zeros_like(zero_xfm_field._field), zero_xfm_field._field) |
178 | | - np.testing.assert_allclose(np.zeros_like(points), zero_xfm_field.map(points), atol=atol) |
| 171 | + # Collapse to zero transform (deformation) |
| 172 | + zero_xfm_field = DenseFieldTransform( |
| 173 | + np.zeros_like(deltas), is_deltas=False, reference=reference |
| 174 | + ) |
| 175 | + np.testing.assert_array_equal( |
| 176 | + np.zeros_like(zero_xfm_field._field), zero_xfm_field._field |
| 177 | + ) |
| 178 | + np.testing.assert_allclose( |
| 179 | + np.zeros_like(coords_xyz), zero_xfm_field.map(coords_xyz), atol=atol |
| 180 | + ) |
179 | 181 |
|
180 | 182 | # Now let's apply a transform |
181 | 183 | xfm = DenseFieldTransform(deltas, reference=reference) |
182 | 184 | np.testing.assert_array_equal(deltas, xfm._deltas) |
183 | | - np.testing.assert_array_equal(coordinates + deltas, xfm._field) |
| 185 | + np.testing.assert_array_equal(coords_map + deltas, xfm._field) |
184 | 186 |
|
185 | | - mapped = xfm.map(points) |
186 | | - nit_deltas = mapped - points |
| 187 | + mapped = xfm.map(coords_xyz) |
| 188 | + nit_deltas = mapped - coords_xyz |
187 | 189 |
|
188 | 190 | if ongrid: |
189 | 191 | mapped_image = mapped.reshape(*shape, 3) |
190 | | - np.testing.assert_allclose(deltas + coordinates, mapped_image) |
| 192 | + np.testing.assert_allclose(deltas + coords_map, mapped_image) |
191 | 193 | np.testing.assert_allclose(deltas, nit_deltas.reshape(*shape, 3), atol=1e-4) |
192 | 194 | np.testing.assert_allclose(xfm._field, mapped_image) |
193 | | - |
| 195 | + else: |
| 196 | + ongrid_xyz = xfm.map(grid_xyz[subsample]) |
| 197 | + assert ( |
| 198 | + (np.linalg.norm(ongrid_xyz - mapped, axis=1) > 2).sum() |
| 199 | + / ongrid_xyz.shape[0] |
| 200 | + ) < 0.5 |
194 | 201 |
|
195 | 202 |
|
196 | 203 | @pytest.mark.parametrize("is_deltas", [True, False]) |
|
0 commit comments