Skip to content

Commit 6f6095d

Browse files
authored
FIX: Derive field from SyN displacements using EPI affine (#421)
Theoretically, a fieldmap in Hz is the number of voxels/second that signal is shifted from the true location. SyN-SDC calculates the shift in mm from the true location. In order to find the equivalent fieldmap, we need the size of the voxels, readout time and phase-encoding direction of the EPI image that is to be corrected. Previously, we were deriving the fieldmap from the readout time and phase-encoding direction of the EPI image, but the voxel size of the displacement field. This causes an overestimate of the number of voxels to be shifted, and thus exaggerated correction.
1 parent 503d4e1 commit 6f6095d

File tree

4 files changed

+21
-15
lines changed

4 files changed

+21
-15
lines changed

sdcflows/interfaces/fmap.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ def _run_interface(self, runtime):
161161

162162
class _DisplacementsField2FieldmapInputSpec(BaseInterfaceInputSpec):
163163
transform = File(exists=True, mandatory=True, desc="input displacements field")
164+
epi = File(exists=True, mandatory=True, desc="source EPI image")
164165
ro_time = traits.Float(mandatory=True, desc="total readout time")
165166
pe_dir = traits.Enum(
166167
"j-", "j", "i", "i-", "k", "k-", mandatory=True, desc="phase encoding direction"
@@ -189,6 +190,7 @@ def _run_interface(self, runtime):
189190
)
190191
fmapnii = disp_to_fmap(
191192
nb.load(self.inputs.transform),
193+
nb.load(self.inputs.epi),
192194
ro_time=self.inputs.ro_time,
193195
pe_dir=self.inputs.pe_dir,
194196
itk_format=self.inputs.itk_transform,

sdcflows/tests/test_transform.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,11 +327,11 @@ def test_conversions(tmpdir, testdata_dir, pe_dir):
327327
ro_time=0.2,
328328
pe_dir=pe_dir,
329329
),
330+
fmap_nii,
330331
ro_time=0.2,
331332
pe_dir=pe_dir,
332333
)
333334

334-
new_nii.to_filename("test.nii.gz")
335335
assert np.allclose(
336336
fmap_nii.get_fdata(dtype="float32"),
337337
new_nii.get_fdata(dtype="float32"),

sdcflows/transform.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -617,20 +617,22 @@ def fmap_to_disp(fmap_nii, ro_time, pe_dir, itk_format=True):
617617
return xyz_nii
618618

619619

620-
def disp_to_fmap(xyz_nii, ro_time, pe_dir, itk_format=True):
620+
def disp_to_fmap(xyz_nii, epi_nii, ro_time, pe_dir, itk_format=True):
621621
"""
622622
Convert a displacements field into a fieldmap in Hz.
623623
624624
This is the inverse operation to the previous function.
625625
626626
Parameters
627627
----------
628-
xyz_nii : :obj:`os.pathlike`
629-
Path to a displacements field in NIfTI format.
628+
xyz_nii : :class:`nibabel.Nifti1Image`
629+
Displacements field in NIfTI format.
630+
epi_nii : :class:`nibabel.Nifti1Image`
631+
Source EPI image.
630632
ro_time : :obj:`float`
631-
The total readout time in seconds.
633+
The total readout time of the EPI image in seconds.
632634
pe_dir : :obj:`str`
633-
The ``PhaseEncodingDirection`` metadata value.
635+
The ``PhaseEncodingDirection`` metadata value of the EPI image.
634636
635637
Returns
636638
-------
@@ -644,12 +646,13 @@ def disp_to_fmap(xyz_nii, ro_time, pe_dir, itk_format=True):
644646
# ITK displacement vectors are in LPS orientation
645647
xyz_deltas[:, (0, 1)] *= -1
646648

647-
inv_aff = np.linalg.inv(xyz_nii.affine)
648-
inv_aff[:3, 3] = 0 # Translations MUST NOT be applied.
649+
# Use the EPI's affine to determine voxel spacing, axis ordering and flips
650+
inv_aff = np.linalg.inv(epi_nii.affine)
651+
inv_mat = inv_aff[:3, :3]
649652

650653
# Convert displacements from mm to voxel units
651654
# Using the inverse affine accounts for reordering of axes, etc.
652-
ijk_deltas = nb.affines.apply_affine(inv_aff, xyz_deltas).astype("float32")
655+
ijk_deltas = (xyz_deltas @ inv_mat.T).astype("float32")
653656
pe_axis = "ijk".index(pe_dir[0])
654657
vsm = ijk_deltas[:, pe_axis].reshape(xyz_nii.shape[:3])
655658
scale_factor = -ro_time if pe_dir.endswith("-") else ro_time

sdcflows/workflows/fit/syn.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ def init_syn_sdc_wf(
247247

248248
# Extract the corresponding fieldmap in Hz
249249
extract_field = pe.Node(
250-
DisplacementsField2Fieldmap(demean=True), name="extract_field"
250+
DisplacementsField2Fieldmap(), name="extract_field"
251251
)
252252

253253
unwarp = pe.Node(ApplyCoeffsField(), name="unwarp")
@@ -267,14 +267,15 @@ def init_syn_sdc_wf(
267267
)
268268

269269
# Regularize with B-Splines
270-
bs_filter = pe.Node(BSplineApprox(), name="bs_filter")
270+
bs_filter = pe.Node(
271+
BSplineApprox(recenter=False, debug=debug, extrapolate=not debug),
272+
name="bs_filter",
273+
)
271274
bs_filter.interface._always_run = debug
272275
bs_filter.inputs.bs_spacing = (
273276
[DEFAULT_LF_ZOOMS_MM, DEFAULT_HF_ZOOMS_MM] if not sloppy else [DEFAULT_ZOOMS_MM]
274277
)
275-
bs_filter.inputs.extrapolate = not debug
276278

277-
# fmt: off
278279
workflow.connect([
279280
(inputnode, readout_time, [(("epi_ref", _pop), "in_file"),
280281
(("epi_ref", _pull), "metadata")]),
@@ -314,6 +315,7 @@ def init_syn_sdc_wf(
314315
(epi_merge, syn, [("out", "moving_image")]),
315316
(moving_masks, syn, [("out", "moving_image_masks")]),
316317
(syn, extract_field, [(("forward_transforms", _pop), "transform")]),
318+
(clip_epi, extract_field, [("out_file", "epi")]),
317319
(readout_time, extract_field, [("readout_time", "ro_time"),
318320
("pe_direction", "pe_dir")]),
319321
(extract_field, zooms_field, [("out_file", "input_image")]),
@@ -329,8 +331,7 @@ def init_syn_sdc_wf(
329331
(bs_filter, outputnode, [("out_coeff", "fmap_coeff")]),
330332
(unwarp, outputnode, [("out_corrected", "fmap_ref"),
331333
("out_field", "fmap")]),
332-
])
333-
# fmt: on
334+
]) # fmt:skip
334335

335336
return workflow
336337

0 commit comments

Comments
 (0)