@@ -190,3 +190,56 @@ def test_densefield_oob_resampling(is_deltas):
190
190
assert np .allclose (mapped [0 ], points [0 ])
191
191
assert np .allclose (mapped [2 ], points [2 ])
192
192
assert np .allclose (mapped [1 ], points [1 ] + 1 )
193
+
194
+
195
+ def test_bspline_map_gridpoints ():
196
+ """BSpline mapping matches dense field on grid points."""
197
+ ref = nb .Nifti1Image (np .zeros ((5 , 5 , 5 ), dtype = "uint8" ), np .eye (4 ))
198
+ coeff = nb .Nifti1Image (
199
+ np .random .RandomState (0 ).rand (9 , 9 , 9 , 3 ).astype ("float32" ), np .eye (4 )
200
+ )
201
+
202
+ bspline = BSplineFieldTransform (coeff , reference = ref )
203
+ dense = bspline .to_field ()
204
+
205
+ # Use a couple of voxel centers from the reference grid
206
+ ijk = np .array ([[1 , 1 , 1 ], [2 , 3 , 0 ]])
207
+ pts = nb .affines .apply_affine (ref .affine , ijk )
208
+
209
+ assert np .allclose (bspline .map (pts ), dense .map (pts ), atol = 1e-6 )
210
+
211
+
212
+ def test_bspline_map_manual ():
213
+ """BSpline interpolation agrees with manual computation."""
214
+ ref = nb .Nifti1Image (np .zeros ((5 , 5 , 5 ), dtype = "uint8" ), np .eye (4 ))
215
+ rng = np .random .RandomState (0 )
216
+ coeff = nb .Nifti1Image (rng .rand (9 , 9 , 9 , 3 ).astype ("float32" ), np .eye (4 ))
217
+
218
+ bspline = BSplineFieldTransform (coeff , reference = ref )
219
+
220
+ from nitransforms .base import _as_homogeneous
221
+ from nitransforms .interp .bspline import _cubic_bspline
222
+
223
+ def manual_map (x ):
224
+ ijk = (bspline ._knots .inverse @ _as_homogeneous (x ).squeeze ())[:3 ]
225
+ w_start = np .floor (ijk ).astype (int ) - 1
226
+ w_end = w_start + 3
227
+ w_start = np .maximum (w_start , 0 )
228
+ w_end = np .minimum (w_end , np .array (bspline ._coeffs .shape [:3 ]) - 1 )
229
+
230
+ window = []
231
+ for i in range (w_start [0 ], w_end [0 ] + 1 ):
232
+ for j in range (w_start [1 ], w_end [1 ] + 1 ):
233
+ for k in range (w_start [2 ], w_end [2 ] + 1 ):
234
+ window .append ([i , j , k ])
235
+ window = np .array (window )
236
+
237
+ dist = np .abs (window - ijk )
238
+ weights = _cubic_bspline (dist ).prod (1 )
239
+ coeffs = bspline ._coeffs [window [:, 0 ], window [:, 1 ], window [:, 2 ]]
240
+
241
+ return x + coeffs .T @ weights
242
+
243
+ pts = np .array ([[1.2 , 1.5 , 2.0 ], [3.3 , 1.7 , 2.4 ]])
244
+ expected = np .vstack ([manual_map (p ) for p in pts ])
245
+ assert np .allclose (bspline .map (pts ), expected , atol = 1e-6 )
0 commit comments