-
Notifications
You must be signed in to change notification settings - Fork 17
ENH: Base implementation of B-Spline transforms #138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
4ff32ac
enh: base implementation of B-Spline transforms
oesteban 9d1c97c
fix: import of nonexistent function from scipy.sparse
oesteban a01653d
enh: implement mapping of scattered, off-grid points
oesteban 1c6986e
enh: finalized implementation
oesteban cc2ea73
sty: pacify flake8
oesteban a467b65
enh: add test comparing bsplines and displacements fields
oesteban 52a68e5
DOC: Add nitransforms.interp to API docs
effigies 997cbac
Apply suggestions from code review
oesteban 4eca2fb
fix: address review comments
oesteban 07ade2b
fix: final amends to ensure tests pass
oesteban f232acf
fix: fixed error types and increased test coverage
oesteban File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
===================== | ||
Interpolation methods | ||
===================== | ||
|
||
.. automodule:: nitransforms.interp | ||
:members: | ||
|
||
------------- | ||
Method groups | ||
------------- | ||
|
||
^^^^^^^^^ | ||
B-Splines | ||
^^^^^^^^^ | ||
|
||
.. automodule:: nitransforms.interp.bspline | ||
:members: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- | ||
# vi: set ft=python sts=4 ts=4 sw=4 et: | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
# | ||
# See COPYING file distributed along with the NiBabel package for the | ||
# copyright and license terms. | ||
# | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
"""Interpolate with 3D tensor-product B-Spline basis.""" | ||
import numpy as np | ||
import nibabel as nb | ||
from scipy.sparse import csr_matrix, kron | ||
|
||
|
||
def _cubic_bspline(d, order=3): | ||
"""Evaluate the cubic bspline at distance d from the center.""" | ||
if order != 3: | ||
raise NotImplementedError | ||
|
||
return np.piecewise( | ||
d, | ||
[d < 1.0, d >= 1.0], | ||
[ | ||
lambda d: (4.0 - 6.0 * d ** 2 + 3.0 * d ** 3) / 6.0, | ||
lambda d: (2.0 - d) ** 3 / 6.0, | ||
], | ||
) | ||
|
||
|
||
def grid_bspline_weights(target_grid, ctrl_grid): | ||
r""" | ||
Evaluate tensor-product B-Spline weights on a grid. | ||
|
||
For each of the :math:`N` input locations :math:`\mathbf{x} = (x_i, x_j, x_k)` | ||
and :math:`K` control points or *knots* :math:`\mathbf{c} =(c_i, c_j, c_k)`, | ||
the tensor-product cubic B-Spline kernel weights are calculated: | ||
|
||
.. math:: | ||
\Psi^3(\mathbf{x}, \mathbf{c}) = | ||
\beta^3(x_i - c_i) \cdot \beta^3(x_j - c_j) \cdot \beta^3(x_k - c_k), | ||
\label{eq:bspline_weights}\tag{1} | ||
|
||
where each :math:`\beta^3` represents the cubic B-Spline for one dimension. | ||
The 1D B-Spline kernel implementation uses :obj:`numpy.piecewise`, and is based on the | ||
closed-form given by Eq. (6) of [Unser1999]_. | ||
|
||
By iterating over dimensions, the data samples that fall outside of the compact | ||
support of the tensor-product kernel associated to each control point can be filtered | ||
out and dismissed to lighten computation. | ||
|
||
Finally, the resulting weights matrix :math:`\Psi^3(\mathbf{k}, \mathbf{s})` | ||
can be easily identified in Eq. :math:`\eqref{eq:1}` and used as the design matrix | ||
for approximation of data. | ||
|
||
Parameters | ||
---------- | ||
target_grid : :obj:`~nitransforms.base.ImageGrid` or :obj:`nibabel.spatialimages` | ||
Regular grid of :math:`N` locations at which tensor B-Spline basis will be evaluated. | ||
ctrl_grid : :obj:`~nitransforms.base.ImageGrid` or :obj:`nibabel.spatialimages` | ||
Regular grid of :math:`K` control points (knot) where B-Spline basis are defined. | ||
|
||
Returns | ||
------- | ||
weights : :obj:`numpy.ndarray` (:math:`K \times N`) | ||
A sparse matrix of interpolating weights :math:`\Psi^3(\mathbf{k}, \mathbf{s})` | ||
for the *N* voxels of the target EPI, for each of the total *K* knots. | ||
This sparse matrix can be directly used as design matrix for the fitting | ||
step of approximation/extrapolation. | ||
|
||
""" | ||
shape = target_grid.shape[:3] | ||
ctrl_sp = nb.affines.voxel_sizes(ctrl_grid.affine)[:3] | ||
ras2ijk = np.linalg.inv(ctrl_grid.affine) | ||
# IJK index in the control point image of the first index in the target image | ||
origin = nb.affines.apply_affine(ras2ijk, [tuple(target_grid.affine[:3, 3])])[0] | ||
|
||
wd = [] | ||
for i, (o, n, sp) in enumerate( | ||
zip(origin, shape, nb.affines.voxel_sizes(target_grid.affine)[:3]) | ||
): | ||
# Locations of voxels in target image in control point image | ||
locations = np.arange(0, n, dtype="float16") * sp / ctrl_sp[i] + o | ||
knots = np.arange(0, ctrl_grid.shape[i], dtype="float16") | ||
distance = np.abs(locations[np.newaxis, ...] - knots[..., np.newaxis]) | ||
|
||
within_support = distance < 2.0 | ||
d_vals, d_idxs = np.unique(distance[within_support], return_inverse=True) | ||
bs_w = _cubic_bspline(d_vals) | ||
weights = np.zeros_like(distance, dtype="float32") | ||
weights[within_support] = bs_w[d_idxs] | ||
wd.append(csr_matrix(weights)) | ||
|
||
return kron(kron(wd[0], wd[1]), wd[2]) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.