Skip to content

Commit 3f4bbf7

Browse files
authored
Merge pull request #462 from effigies/fix/optional-jacobian
ENH: Allow Jacobian correction to be toggled on/off
2 parents d953979 + 9c48489 commit 3f4bbf7

File tree

5 files changed

+40
-15
lines changed

5 files changed

+40
-15
lines changed

sdcflows/interfaces/bspline.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@ class _ApplyCoeffsFieldInputSpec(BaseInterfaceInputSpec):
328328
mandatory=True,
329329
desc="the phase-encoding direction corresponding to in_data",
330330
)
331+
jacobian = traits.Bool(
332+
False,
333+
usedefault=True,
334+
desc="apply Jacobian determinant correction after unwarping",
335+
)
331336
num_threads = traits.Int(nohash=True, desc="number of threads")
332337
approx = traits.Bool(
333338
True,
@@ -421,6 +426,7 @@ def _run_interface(self, runtime):
421426
self.inputs.in_data,
422427
self.inputs.pe_dir,
423428
self.inputs.ro_time,
429+
jacobian=self.inputs.jacobian,
424430
xfms=self.inputs.in_xfms or None,
425431
xfm_data2fmap=data2fmap_xfm,
426432
approx=self.inputs.approx,

sdcflows/transform.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def _sdc_unwarp(
7373
coordinates: np.ndarray,
7474
pe_info: Tuple[int, float],
7575
hmc_xfm: np.ndarray | None,
76+
jacobian: bool,
7677
fmap_hz: np.ndarray,
7778
output_dtype: str | np.dtype | None = None,
7879
order: int = 3,
@@ -95,13 +96,6 @@ def _sdc_unwarp(
9596
vsm = fmap_hz * pe_info[1]
9697
coordinates[pe_info[0], ...] += vsm
9798

98-
# The Jacobian determinant image is the amount of stretching in the PE direction.
99-
# Using central differences accounts for the shift in neighboring voxels.
100-
# The full Jacobian at each voxel would be a 3x3 matrix, but because there is
101-
# only warping in one direction, we end up with a diagonal matrix with two 1s.
102-
# The following is the other entry at each voxel, and hence the determinant.
103-
jacobian = 1 + np.gradient(vsm, axis=pe_info[0])
104-
10599
resampled = ndi.map_coordinates(
106100
data,
107101
coordinates,
@@ -110,7 +104,15 @@ def _sdc_unwarp(
110104
mode=mode,
111105
cval=cval,
112106
prefilter=prefilter,
113-
) * jacobian
107+
)
108+
109+
# The Jacobian determinant image is the amount of stretching in the PE direction.
110+
# Using central differences accounts for the shift in neighboring voxels.
111+
# The full Jacobian at each voxel would be a 3x3 matrix, but because there is
112+
# only warping in one direction, we end up with a diagonal matrix with two 1s.
113+
# The following is the other entry at each voxel, and hence the determinant.
114+
if jacobian:
115+
resampled *= 1 + np.gradient(vsm, axis=pe_info[0])
114116

115117
return resampled
116118

@@ -138,6 +140,7 @@ async def unwarp_parallel(
138140
fmap_hz: np.ndarray,
139141
pe_info: Sequence[Tuple[int, float]],
140142
xfms: Sequence[np.ndarray],
143+
jacobian: bool,
141144
order: int = 3,
142145
mode: str = "constant",
143146
cval: float = 0.0,
@@ -162,6 +165,8 @@ async def unwarp_parallel(
162165
pe_info : :obj:`tuple` of (:obj:`int`, :obj:`float`)
163166
A tuple containing the index of the phase-encoding axis in the data array and
164167
the readout time (including sign, if displacements must be reversed)
168+
jacobian : :class:`bool`
169+
If :obj:`True`, apply Jacobian determinant correction after unwarping.
165170
xfms : :obj:`list` of obj:`~numpy.ndarray`
166171
A list of 4×4 matrices, each one formalizing
167172
the estimated head motion alignment to the scan's reference.
@@ -200,6 +205,7 @@ async def unwarp_parallel(
200205

201206
func = partial(
202207
_sdc_unwarp,
208+
jacobian=jacobian,
203209
fmap_hz=fmap_hz,
204210
output_dtype=output_dtype,
205211
order=order,
@@ -370,6 +376,7 @@ def apply(
370376
pe_dir: str | Sequence[str],
371377
ro_time: float | Sequence[float],
372378
xfms: Sequence[np.ndarray] | None = None,
379+
jacobian: bool = True,
373380
xfm_data2fmap: np.ndarray | None = None,
374381
approx: bool = True,
375382
order: int = 3,
@@ -394,6 +401,8 @@ def apply(
394401
A valid ``PhaseEncodingDirection`` metadata value.
395402
ro_time : :obj:`float` or :obj:`list` of :obj:`float`
396403
The total readout time in seconds.
404+
jacobian : :class:`bool`
405+
If :obj:`True`, apply Jacobian determinant correction after unwarping.
397406
xfms : :obj:`None` or :obj:`list`
398407
A list of 4×4 matrices, each one formalizing
399408
the estimated head motion alignment to the scan's reference.
@@ -528,6 +537,7 @@ def apply(
528537
self.mapped.get_fdata(dtype="float32"), # fieldmap in Hz
529538
pe_info,
530539
xfms,
540+
jacobian,
531541
output_dtype='float32',
532542
order=order,
533543
mode=mode,

sdcflows/workflows/apply/correction.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@
2626
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
2727

2828

29-
def init_unwarp_wf(*, free_mem=None, omp_nthreads=1, debug=False, name="unwarp_wf"):
29+
def init_unwarp_wf(
30+
*,
31+
jacobian=True,
32+
free_mem=None,
33+
omp_nthreads=1,
34+
debug=False,
35+
name="unwarp_wf",
36+
):
3037
r"""
3138
Set up a workflow that unwarps the input :abbr:`EPI (echo-planar imaging)` dataset.
3239
@@ -40,11 +47,13 @@ def init_unwarp_wf(*, free_mem=None, omp_nthreads=1, debug=False, name="unwarp_w
4047
4148
Parameters
4249
----------
43-
omp_nthreads : :obj:`int`
50+
jacobian : :class:`bool`
51+
If :obj:`True`, apply Jacobian determinant correction after unwarping.
52+
omp_nthreads : :class:`int`
4453
Maximum number of threads an individual process may use.
45-
name : :obj:`str`
54+
name : :class:`str`
4655
Unique name of this workflow.
47-
debug : :obj:`bool`
56+
debug : :class:`bool`
4857
Whether to run in *sloppy* mode.
4958
5059
Inputs
@@ -123,7 +132,7 @@ def init_unwarp_wf(*, free_mem=None, omp_nthreads=1, debug=False, name="unwarp_w
123132
num_threads = omp_nthreads
124133

125134
resample = pe.Node(
126-
ApplyCoeffsField(num_threads=num_threads),
135+
ApplyCoeffsField(jacobian=jacobian, num_threads=num_threads),
127136
mem_gb=mem_per_thread * num_threads,
128137
name="resample",
129138
)

sdcflows/workflows/fit/pepolar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def init_topup_wf(
219219
from sdcflows.interfaces.bspline import ApplyCoeffsField
220220

221221
# Separate the runs again, as our ApplyCoeffsField corrects them separately
222-
unwarp = pe.Node(ApplyCoeffsField(), name="unwarp")
222+
unwarp = pe.Node(ApplyCoeffsField(jacobian=True), name="unwarp")
223223
unwarp.interface._always_run = True
224224
concat_corrected = pe.Node(MergeSeries(), name="concat_corrected")
225225

sdcflows/workflows/fit/syn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def init_syn_sdc_wf(
237237
DisplacementsField2Fieldmap(), name="extract_field"
238238
)
239239

240-
unwarp = pe.Node(ApplyCoeffsField(), name="unwarp")
240+
unwarp = pe.Node(ApplyCoeffsField(jacobian=False), name="unwarp")
241241

242242
# Check zooms (avoid very expensive B-Splines fitting)
243243
zooms_field = pe.Node(

0 commit comments

Comments
 (0)