Skip to content

feat(registration): add displacement-field sampling and inversion for B-spline transforms#235

Open
sdiebolt wants to merge 12 commits into
mainfrom
feat/bspline-inversion
Open

feat(registration): add displacement-field sampling and inversion for B-spline transforms#235
sdiebolt wants to merge 12 commits into
mainfrom
feat/bspline-inversion

Conversation

@sdiebolt

@sdiebolt sdiebolt commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds bspline_to_displacement_field and invert_displacement_field to confusius.registration.bspline: sample a B-spline (or composite affine + B-spline) transform into a dense displacement field on an explicit grid, then invert it via SimpleITK's InvertDisplacementFieldImageFilter.
  • resample_volume/resample_like now accept displacement fields directly alongside affines and B-spline DataArrays, so a saved registration transform's inverse can be applied without a closed-form inverse — the missing piece for a future napari "apply inverse transform" hook.
  • Fixes two bugs found while validating against real (anisotropic, singleton-axis) fUSI data:
    • sitk_bspline_to_dataarray/_dataarray_to_sitk_bspline assumed SimpleITK reverses axis order relative to the DataArray; the rest of the module never reverses, so this silently swapped control-point grid geometry between axes on anisotropic images (invisible on isotropic test fixtures). Pre-existing, shipped in released alpha versions — see B-spline control-point grid geometry is swapped between axes on anisotropic images #236.
    • InvertDisplacementFieldImageFilter silently returns an all-zero field when any spatial axis has size 1 (fUSI data is routinely a single 2D slice, e.g. (1, y, x)). invert_displacement_field now reuses expand_thin_dims (promoted from volume.py to the shared registration/_utils.py) to pad and crop degenerate axes — the same trick register_volume already relies on for its own thin-dimension images. New code introduced in this PR, not a prior regression.
  • Extends the same-subject registration example with a B-spline refinement step after the rigid one.
  • Adds atlas mesh warping/resampling support for affine and nonlinear transforms, plus unit-validation coverage for atlas and registration resampling paths.

Closes #229. Closes #233. Closes #236.

… B-spline transforms

Adds `bspline_to_displacement_field` and `invert_displacement_field` to
confusius.registration.bspline, letting a B-spline (or composite affine +
B-spline) transform be sampled into a dense displacement field and inverted
via SimpleITK's InvertDisplacementFieldImageFilter. resample_volume and
resample_like now accept displacement fields directly alongside affines and
B-spline DataArrays, so a saved transform's inverse can be applied without a
closed-form inverse -- needed for future napari "apply inverse transform"
support.

Also fixes two pre-existing bugs surfaced while validating on real
(anisotropic, singleton-axis) fUSI data:
- sitk_bspline_to_dataarray/_dataarray_to_sitk_bspline assumed SimpleITK
  reverses axis order relative to the DataArray; the rest of the module
  never reverses, so this silently swapped control-point grid geometry
  between axes on anisotropic images (invisible on isotropic test fixtures).
- InvertDisplacementFieldImageFilter requires a real spatial neighborhood
  along every axis and silently returns an all-zero field when one has size
  1 (fUSI data is routinely stored as a single 2D slice, e.g. (1, y, x)).
  invert_displacement_field now reuses expand_thin_dims (promoted from
  volume.py to the shared registration/_utils.py) to pad and crop degenerate
  axes, the same trick register_volume already relies on for its own
  thin-dimension images.

Closes #233.
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

📖 Doc preview: https://confusius.tools/pr-preview/pr-235/

Replace the module-level DISPLACEMENT_FIELD_INVERSION_ITERATIONS/MAX_ERROR
constants (only ever used as default values) with plain literal defaults on
invert_displacement_field's max_iterations/max_error_tolerance parameters.

Also add sitk_threads to bspline_to_displacement_field and
invert_displacement_field, matching the convention already used by
resample_volume/resample_like -- both underlying SimpleITK filters
(TransformToDisplacementFieldFilter, InvertDisplacementFieldImageFilter)
support per-call thread control via set_sitk_thread_count.
@sdiebolt sdiebolt self-assigned this Jul 2, 2026
@sdiebolt sdiebolt added the enhancement New feature or request label Jul 2, 2026
@sdiebolt sdiebolt requested a review from FelipeCybis July 2, 2026 14:32
sdiebolt added 9 commits July 2, 2026 15:53
assert_allclose(inverse_field.values[0], 0.0) used the default atol=0, so a
tiny platform-dependent floating-point residual (~1e-16 on macOS CI, exact
0.0 on Linux/Windows) failed the relative-tolerance check against a true
zero. Use atol=1e-9, well above any such noise and well below any real
displacement signal.
…rings

Use commas or true em-dashes (no surrounding spaces) instead of "--" as a
prose separator. Also updates the degenerate-axis test docstring, which
still described the earlier squeeze/reinsert approach rather than the
current expand/crop one.
Extends the same-subject registration example with a B-spline step
initialized from the rigid transform, using the same mesh_size=(6, 6, 6)
that demo_bspline_inversion.py uses. Rigid registration parameters are left
untouched. Tested end-to-end against the actual dataset: rigid alone
reaches a final metric around -0.81, and the B-spline refinement typically
improves it to around -0.85 to -0.89 depending on run-to-run optimizer
variance.

Also points readers at bspline_to_displacement_field/
invert_displacement_field for applying the resulting B-spline transform (or
its inverse), since unlike the rigid transform it has no closed-form
inverse.
Exercises _validate_displacement_field_dataarray's two raise branches
through invert_displacement_field (its public caller), not by importing
the private validator directly.
@sdiebolt

sdiebolt commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

@FelipeCybis Ready to review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

1 participant