@@ -190,3 +190,56 @@ def test_densefield_oob_resampling(is_deltas):
190190 assert np .allclose (mapped [0 ], points [0 ])
191191 assert np .allclose (mapped [2 ], points [2 ])
192192 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