Skip to content
This repository was archived by the owner on Jan 27, 2025. It is now read-only.

Commit 68f199d

Browse files
authored
Merge pull request #256 from nipreps/enh/bold-registration-experiment
ENH: Add bold realignment notebook (ISMRM'25 Experiment 2)
2 parents 3cba47d + 5123f50 commit 68f199d

File tree

6 files changed

+1835
-47
lines changed

6 files changed

+1835
-47
lines changed

docs/notebooks/bold_realignment.ipynb

Lines changed: 1777 additions & 0 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ test = [
7676

7777
antsopt = [
7878
"ConfigSpace",
79+
"nipreps",
80+
"skimage",
7981
"smac",
8082
]
8183

src/eddymotion/data/dmri.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
33
#
4-
# Copyright 2022 The NiPreps Developers <[email protected]>
4+
# Copyright The NiPreps Developers <[email protected]>
55
#
66
# Licensed under the Apache License, Version 2.0 (the "License");
77
# you may not use this file except in compliance with the License.

src/eddymotion/estimator.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class EddyMotionEstimator:
3939

4040
@staticmethod
4141
def estimate(
42-
dwdata,
42+
data,
4343
*,
4444
align_kwargs=None,
4545
iter_kwargs=None,
@@ -53,7 +53,7 @@ def estimate(
5353
5454
Parameters
5555
----------
56-
dwdata : :obj:`~eddymotion.dmri.DWI`
56+
data : :obj:`~eddymotion.dmri.DWI`
5757
The target DWI dataset, represented by this tool's internal
5858
type. The object is used in-place, and will contain the estimated
5959
parameters in its ``em_affines`` property, as well as the rotated
@@ -88,7 +88,7 @@ def estimate(
8888
"seed": None,
8989
"bvals": None, # TODO: extract b-vals here if pertinent
9090
} | iter_kwargs
91-
iter_kwargs["size"] = len(dwdata)
91+
iter_kwargs["size"] = len(data)
9292

9393
iterfunc = getattr(eutils, f'{iter_kwargs.pop("strategy", "random")}_iterator')
9494
index_order = list(iterfunc(**iter_kwargs))
@@ -107,9 +107,9 @@ def estimate(
107107

108108
for i_iter, model in enumerate(models):
109109
# When downsampling these need to be set per-level
110-
bmask_img = _prepare_brainmask_data(dwdata.brainmask, dwdata.affine)
110+
bmask_img = _prepare_brainmask_data(data.brainmask, data.affine)
111111

112-
_prepare_kwargs(dwdata, kwargs)
112+
_prepare_kwargs(data, kwargs)
113113

114114
single_model = model.lower() in (
115115
"b0",
@@ -130,7 +130,7 @@ def estimate(
130130
model=model,
131131
**kwargs,
132132
)
133-
dwmodel.fit(dwdata.dataobj, n_jobs=n_jobs)
133+
dwmodel.fit(data.dataobj, n_jobs=n_jobs)
134134

135135
with TemporaryDirectory() as tmp_dir:
136136
print(f"Processing in <{tmp_dir}>")
@@ -141,12 +141,12 @@ def estimate(
141141
pbar.set_description_str(
142142
f"Pass {i_iter + 1}/{n_iter} | Fit and predict b-index <{i}>"
143143
)
144-
data_train, data_test = lovo_split(dwdata, i, with_b0=True)
144+
data_train, data_test = lovo_split(data, i, with_b0=True)
145145
grad_str = f"{i}, {data_test[1][:3]}, b={int(data_test[1][3])}"
146146
pbar.set_description_str(f"[{grad_str}], {n_jobs} jobs")
147147

148148
if not single_model: # A true LOGO estimator
149-
if hasattr(dwdata, "gradients"):
149+
if hasattr(data, "gradients"):
150150
kwargs["gtab"] = data_train[1]
151151
# Factory creates the appropriate model and pipes arguments
152152
dwmodel = ModelFactory.init(
@@ -166,7 +166,7 @@ def estimate(
166166

167167
# prepare data for running ANTs
168168
fixed, moving = _prepare_registration_data(
169-
data_test[0], predicted, dwdata.affine, i, ptmp_dir, reg_target_type
169+
data_test[0], predicted, data.affine, i, ptmp_dir, reg_target_type
170170
)
171171

172172
pbar.set_description_str(
@@ -177,11 +177,11 @@ def estimate(
177177
fixed,
178178
moving,
179179
bmask_img,
180-
dwdata.em_affines,
181-
dwdata.affine,
182-
dwdata.dataobj.shape[:3],
180+
data.em_affines,
181+
data.affine,
182+
data.dataobj.shape[:3],
183183
data_test[1][3],
184-
dwdata.fieldmap,
184+
data.fieldmap,
185185
i_iter,
186186
i,
187187
ptmp_dir,
@@ -190,10 +190,10 @@ def estimate(
190190
)
191191

192192
# update
193-
dwdata.set_transform(i, xform.matrix)
193+
data.set_transform(i, xform.matrix)
194194
pbar.update()
195195

196-
return dwdata.em_affines
196+
return data.em_affines
197197

198198

199199
def _prepare_brainmask_data(brainmask, affine):
@@ -219,7 +219,7 @@ def _prepare_brainmask_data(brainmask, affine):
219219
return bmask_img
220220

221221

222-
def _prepare_kwargs(dwdata, kwargs):
222+
def _prepare_kwargs(data, kwargs):
223223
"""Prepare the keyword arguments depending on the DWI data: add attributes corresponding to
224224
the ``brainmask``, ``bzero``, ``gradients``, ``frame_time``, and ``total_duration`` DWI data
225225
properties.
@@ -228,24 +228,24 @@ def _prepare_kwargs(dwdata, kwargs):
228228
229229
Parameters
230230
----------
231-
dwdata : :class:`eddymotion.data.dmri.DWI`
231+
data : :class:`eddymotion.data.dmri.DWI`
232232
DWI data object.
233233
kwargs : :obj:`dict`
234234
Keyword arguments.
235235
"""
236236
from eddymotion.data.filtering import advanced_clip as _advanced_clip
237237

238-
if dwdata.brainmask is not None:
239-
kwargs["mask"] = dwdata.brainmask
238+
if data.brainmask is not None:
239+
kwargs["mask"] = data.brainmask
240240

241-
if hasattr(dwdata, "bzero") and dwdata.bzero is not None:
242-
kwargs["S0"] = _advanced_clip(dwdata.bzero)
241+
if hasattr(data, "bzero") and data.bzero is not None:
242+
kwargs["S0"] = _advanced_clip(data.bzero)
243243

244-
if hasattr(dwdata, "gradients"):
245-
kwargs["gtab"] = dwdata.gradients
244+
if hasattr(data, "gradients"):
245+
kwargs["gtab"] = data.gradients
246246

247-
if hasattr(dwdata, "frame_time"):
248-
kwargs["timepoints"] = dwdata.frame_time
247+
if hasattr(data, "frame_time"):
248+
kwargs["timepoints"] = data.frame_time
249249

250-
if hasattr(dwdata, "total_duration"):
251-
kwargs["xlim"] = dwdata.total_duration
250+
if hasattr(data, "total_duration"):
251+
kwargs["xlim"] = data.total_duration

src/eddymotion/model/gpr.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,12 @@ class EddyMotionGPR(GaussianProcessRegressor):
104104
The reason for that is that such methods typically use fewer steps, and
105105
when the cost of calculating the derivatives is small/moderate compared
106106
to calculating the functions itself (as is the case for Eq. (12)) then
107-
execution time can be much shorter. However, we found that for the
108-
multi-shell case a heuristic optimisation method such as the Nelder-Mead
109-
simplex method (Nelder and Mead, 1965) was frequently better at avoiding
110-
local maxima. Hence, that was the method we used for all optimisations
111-
in the present paper.
107+
execution time can be much shorter.
108+
However, we found that for the multi-shell case a heuristic optimisation
109+
method such as the Nelder-Mead simplex method (Nelder and Mead, 1965) was
110+
frequently better at avoiding local maxima.
111+
Hence, that was the method we used for all optimisations in the present
112+
paper.
112113
113114
**Multi-shell regression (TODO).**
114115
For multi-shell modeling, the kernel :math:`k(\textbf{x}, \textbf{x'})`
@@ -135,8 +136,10 @@ class EddyMotionGPR(GaussianProcessRegressor):
135136
\mathbf{K} = \left[
136137
\begin{matrix}
137138
\lambda C_{\theta}(\theta (\mathbf{G}_{1}); a) + \sigma_{1}^{2} \mathbf{I} &
138-
\lambda C_{\theta}(\theta (\mathbf{G}_{2}, \mathbf{G}_{1}); a) C_{b}(b_{2}, b_{1}; \ell) \\
139-
\lambda C_{\theta}(\theta (\mathbf{G}_{1}, \mathbf{G}_{2}); a) C_{b}(b_{1}, b_{2}; \ell) &
139+
\lambda C_{\theta}(\theta (\mathbf{G}_{2},
140+
\mathbf{G}_{1}); a) C_{b}(b_{2}, b_{1}; \ell) \\
141+
\lambda C_{\theta}(\theta (\mathbf{G}_{1}, \mathbf{G}_{2});
142+
a) C_{b}(b_{1}, b_{2}; \ell) &
140143
\lambda C_{\theta}(\theta (\mathbf{G}_{2}); a) + \sigma_{2}^{2} \mathbf{I} \\
141144
\end{matrix}
142145
\right]

test/test_integration.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@
3030

3131
from eddymotion.data.dmri import DWI
3232
from eddymotion.estimator import EddyMotionEstimator
33+
from eddymotion.registration.utils import displacements_within_mask
3334

3435

35-
def test_proximity_estimator_trivial_model(datadir):
36+
def test_proximity_estimator_trivial_model(datadir, tmp_path):
3637
"""Check the proximity of transforms estimated by the estimator with a trivial B0 model."""
3738

3839
dwdata = DWI.from_filename(datadir / "dwi.h5")
3940
b0nii = nb.Nifti1Image(dwdata.bzero, dwdata.affine, None)
41+
masknii = nb.Nifti1Image(dwdata.brainmask.astype(np.uint8), dwdata.affine, None)
4042

4143
# Generate a list of large-yet-plausible bulk-head motion.
4244
xfms = nt.linear.LinearTransformsMapping(
@@ -56,8 +58,8 @@ def test_proximity_estimator_trivial_model(datadir):
5658
moved_nii = (~xfms).apply(b0nii, reference=b0nii)
5759

5860
# Uncomment to see the moved dataset
59-
# moved_nii.to_filename(tmp_path / "test.nii.gz")
60-
# xfms.apply(moved_nii).to_filename(tmp_path / "ground_truth.nii.gz")
61+
moved_nii.to_filename(tmp_path / "test.nii.gz")
62+
xfms.apply(moved_nii).to_filename(tmp_path / "ground_truth.nii.gz")
6163

6264
# Wrap into dataset object
6365
dwi_motion = DWI(
@@ -70,7 +72,7 @@ def test_proximity_estimator_trivial_model(datadir):
7072

7173
estimator = EddyMotionEstimator()
7274
em_affines = estimator.estimate(
73-
dwdata=dwi_motion,
75+
data=dwi_motion,
7476
models=("b0",),
7577
seed=None,
7678
align_kwargs={
@@ -81,14 +83,18 @@ def test_proximity_estimator_trivial_model(datadir):
8183
)
8284

8385
# Uncomment to see the realigned dataset
84-
# nt.linear.LinearTransformsMapping(
85-
# em_affines,
86-
# reference=b0nii,
87-
# ).apply(moved_nii).to_filename(tmp_path / "realigned.nii.gz")
86+
nt.linear.LinearTransformsMapping(
87+
em_affines,
88+
reference=b0nii,
89+
).apply(moved_nii).to_filename(tmp_path / "realigned.nii.gz")
8890

8991
# For each moved b0 volume
90-
coords = xfms.reference.ndcoords.T
9192
for i, est in enumerate(em_affines):
92-
xfm = nt.linear.Affine(xfms.matrix[i], reference=b0nii)
93-
est = nt.linear.Affine(est, reference=b0nii)
94-
assert np.sqrt(((xfm.map(coords) - est.map(coords)) ** 2).sum(1)).mean() < 0.2
93+
assert (
94+
displacements_within_mask(
95+
masknii,
96+
nt.linear.Affine(est),
97+
xfms[i],
98+
).max()
99+
< 0.2
100+
)

0 commit comments

Comments
 (0)