@@ -219,84 +219,8 @@ def test_densefield_map(get_testdata, image_orientation, ongrid):
219
219
) < 0.5
220
220
221
221
222
- @pytest .mark .parametrize ("is_deltas" , [True , False ])
223
- def test_densefield_oob_resampling (is_deltas ):
224
- """Ensure mapping outside the field returns input coordinates."""
225
- ref = nb .Nifti1Image (np .zeros ((2 , 2 , 2 ), dtype = "uint8" ), np .eye (4 ))
226
-
227
- if is_deltas :
228
- field = nb .Nifti1Image (np .ones ((2 , 2 , 2 , 3 ), dtype = "float32" ), np .eye (4 ))
229
- else :
230
- grid = np .stack (
231
- np .meshgrid (* [np .arange (2 ) for _ in range (3 )], indexing = "ij" ),
232
- axis = - 1 ,
233
- ).astype ("float32" )
234
- field = nb .Nifti1Image (grid + 1.0 , np .eye (4 ))
235
-
236
- xfm = DenseFieldTransform (field , is_deltas = is_deltas , reference = ref )
237
-
238
- points = np .array ([[- 1.0 , - 1.0 , - 1.0 ], [0.5 , 0.5 , 0.5 ], [3.0 , 3.0 , 3.0 ]])
239
- mapped = xfm .map (points )
240
-
241
- assert np .allclose (mapped [0 ], points [0 ])
242
- assert np .allclose (mapped [2 ], points [2 ])
243
- assert np .allclose (mapped [1 ], points [1 ] + 1 )
244
-
245
-
246
- def test_bspline_map_gridpoints ():
247
- """BSpline mapping matches dense field on grid points."""
248
- ref = nb .Nifti1Image (np .zeros ((5 , 5 , 5 ), dtype = "uint8" ), np .eye (4 ))
249
- coeff = nb .Nifti1Image (
250
- np .random .RandomState (0 ).rand (9 , 9 , 9 , 3 ).astype ("float32" ), np .eye (4 )
251
- )
252
-
253
- bspline = BSplineFieldTransform (coeff , reference = ref )
254
- dense = bspline .to_field ()
255
-
256
- # Use a couple of voxel centers from the reference grid
257
- ijk = np .array ([[1 , 1 , 1 ], [2 , 3 , 0 ]])
258
- pts = nb .affines .apply_affine (ref .affine , ijk )
259
-
260
- assert np .allclose (bspline .map (pts ), dense .map (pts ), atol = 1e-6 )
261
222
262
-
263
- def test_bspline_map_manual ():
264
- """BSpline interpolation agrees with manual computation."""
265
- ref = nb .Nifti1Image (np .zeros ((5 , 5 , 5 ), dtype = "uint8" ), np .eye (4 ))
266
- rng = np .random .RandomState (0 )
267
- coeff = nb .Nifti1Image (rng .rand (9 , 9 , 9 , 3 ).astype ("float32" ), np .eye (4 ))
268
-
269
- bspline = BSplineFieldTransform (coeff , reference = ref )
270
-
271
- from nitransforms .base import _as_homogeneous
272
- from nitransforms .interp .bspline import _cubic_bspline
273
-
274
- def manual_map (x ):
275
- ijk = (bspline ._knots .inverse @ _as_homogeneous (x ).squeeze ())[:3 ]
276
- w_start = np .floor (ijk ).astype (int ) - 1
277
- w_end = w_start + 3
278
- w_start = np .maximum (w_start , 0 )
279
- w_end = np .minimum (w_end , np .array (bspline ._coeffs .shape [:3 ]) - 1 )
280
-
281
- window = []
282
- for i in range (w_start [0 ], w_end [0 ] + 1 ):
283
- for j in range (w_start [1 ], w_end [1 ] + 1 ):
284
- for k in range (w_start [2 ], w_end [2 ] + 1 ):
285
- window .append ([i , j , k ])
286
- window = np .array (window )
287
-
288
- dist = np .abs (window - ijk )
289
- weights = _cubic_bspline (dist ).prod (1 )
290
- coeffs = bspline ._coeffs [window [:, 0 ], window [:, 1 ], window [:, 2 ]]
291
-
292
- return x + coeffs .T @ weights
293
-
294
- pts = np .array ([[1.2 , 1.5 , 2.0 ], [3.3 , 1.7 , 2.4 ]])
295
- expected = np .vstack ([manual_map (p ) for p in pts ])
296
- assert np .allclose (bspline .map (pts ), expected , atol = 1e-6 )
297
-
298
-
299
- def test_densefield_map_against_ants (testdata_path , tmp_path ):
223
+ def test_densefield_map_vs_ants (testdata_path , tmp_path ):
300
224
"""Map points with DenseFieldTransform and compare to ANTs."""
301
225
warpfile = (
302
226
testdata_path
@@ -419,3 +343,81 @@ def test_constant_field_vs_ants(tmp_path, get_testdata, image_orientation, gridp
419
343
mask = np .argwhere (np .abs (diff ) > 1e-2 )[:, 0 ]
420
344
421
345
assert len (mask ) == 0 , f"A total of { len (mask )} /{ ants_pts .shape [0 ]} contained errors:\n { diff [mask ]} "
346
+
347
+
348
+
349
+ @pytest .mark .parametrize ("is_deltas" , [True , False ])
350
+ def test_densefield_oob_resampling (is_deltas ):
351
+ """Ensure mapping outside the field returns input coordinates."""
352
+ ref = nb .Nifti1Image (np .zeros ((2 , 2 , 2 ), dtype = "uint8" ), np .eye (4 ))
353
+
354
+ if is_deltas :
355
+ field = nb .Nifti1Image (np .ones ((2 , 2 , 2 , 3 ), dtype = "float32" ), np .eye (4 ))
356
+ else :
357
+ grid = np .stack (
358
+ np .meshgrid (* [np .arange (2 ) for _ in range (3 )], indexing = "ij" ),
359
+ axis = - 1 ,
360
+ ).astype ("float32" )
361
+ field = nb .Nifti1Image (grid + 1.0 , np .eye (4 ))
362
+
363
+ xfm = DenseFieldTransform (field , is_deltas = is_deltas , reference = ref )
364
+
365
+ points = np .array ([[- 1.0 , - 1.0 , - 1.0 ], [0.5 , 0.5 , 0.5 ], [3.0 , 3.0 , 3.0 ]])
366
+ mapped = xfm .map (points )
367
+
368
+ assert np .allclose (mapped [0 ], points [0 ])
369
+ assert np .allclose (mapped [2 ], points [2 ])
370
+ assert np .allclose (mapped [1 ], points [1 ] + 1 )
371
+
372
+
373
+ def test_bspline_map_gridpoints ():
374
+ """BSpline mapping matches dense field on grid points."""
375
+ ref = nb .Nifti1Image (np .zeros ((5 , 5 , 5 ), dtype = "uint8" ), np .eye (4 ))
376
+ coeff = nb .Nifti1Image (
377
+ np .random .RandomState (0 ).rand (9 , 9 , 9 , 3 ).astype ("float32" ), np .eye (4 )
378
+ )
379
+
380
+ bspline = BSplineFieldTransform (coeff , reference = ref )
381
+ dense = bspline .to_field ()
382
+
383
+ # Use a couple of voxel centers from the reference grid
384
+ ijk = np .array ([[1 , 1 , 1 ], [2 , 3 , 0 ]])
385
+ pts = nb .affines .apply_affine (ref .affine , ijk )
386
+
387
+ assert np .allclose (bspline .map (pts ), dense .map (pts ), atol = 1e-6 )
388
+
389
+
390
+ def test_bspline_map_manual ():
391
+ """BSpline interpolation agrees with manual computation."""
392
+ ref = nb .Nifti1Image (np .zeros ((5 , 5 , 5 ), dtype = "uint8" ), np .eye (4 ))
393
+ rng = np .random .RandomState (0 )
394
+ coeff = nb .Nifti1Image (rng .rand (9 , 9 , 9 , 3 ).astype ("float32" ), np .eye (4 ))
395
+
396
+ bspline = BSplineFieldTransform (coeff , reference = ref )
397
+
398
+ from nitransforms .base import _as_homogeneous
399
+ from nitransforms .interp .bspline import _cubic_bspline
400
+
401
+ def manual_map (x ):
402
+ ijk = (bspline ._knots .inverse @ _as_homogeneous (x ).squeeze ())[:3 ]
403
+ w_start = np .floor (ijk ).astype (int ) - 1
404
+ w_end = w_start + 3
405
+ w_start = np .maximum (w_start , 0 )
406
+ w_end = np .minimum (w_end , np .array (bspline ._coeffs .shape [:3 ]) - 1 )
407
+
408
+ window = []
409
+ for i in range (w_start [0 ], w_end [0 ] + 1 ):
410
+ for j in range (w_start [1 ], w_end [1 ] + 1 ):
411
+ for k in range (w_start [2 ], w_end [2 ] + 1 ):
412
+ window .append ([i , j , k ])
413
+ window = np .array (window )
414
+
415
+ dist = np .abs (window - ijk )
416
+ weights = _cubic_bspline (dist ).prod (1 )
417
+ coeffs = bspline ._coeffs [window [:, 0 ], window [:, 1 ], window [:, 2 ]]
418
+
419
+ return x + coeffs .T @ weights
420
+
421
+ pts = np .array ([[1.2 , 1.5 , 2.0 ], [3.3 , 1.7 , 2.4 ]])
422
+ expected = np .vstack ([manual_map (p ) for p in pts ])
423
+ assert np .allclose (bspline .map (pts ), expected , atol = 1e-6 )
0 commit comments