Skip to content

Commit 6ed47ae

Browse files
committed
ENH: Massage TOPUP's fieldcoeff files to be compatible with ours
TOUP-generated "fieldcoeff" files are just B-Spline fields, where the shape of the field is fixated to be a decimated grid of the original image by a factor of 2x and added 3 pixels on each dimension. This is one root reason why TOPUP errors (FSL 6) or segfaults (FSL5), when the input image has odd number of voxels along one or more directions. These "fieldcoeff" are fixated to be zero-centered, and have "plumb" orientation (as in, aligned with cardinal/imaging axes). The q-form of these NIfTI files is always diagonal, with the decimation factors set on the diagonal (and hence, the voxel zooms). The origin of the q-form is set to the reference image's shape. This interface modifies these coefficient files to be fully-fledged NIfTI images aligned with the reference image. Therefore, the s-form header of the coefficients file is updated to match that of the reference file. The s-form header is used because the imaging axes may be oblique. The q-form retains the original header and is marked with code 0.
1 parent 3361f8a commit 6ed47ae

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

sdcflows/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""py.test configuration"""
1+
"""py.test configuration."""
22
import os
33
from pathlib import Path
44
import numpy

sdcflows/interfaces/bspline.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,54 @@ def _run_interface(self, runtime):
317317
return runtime
318318

319319

320+
class _TOPUPCoeffReorientInputSpec(BaseInterfaceInputSpec):
321+
in_coeff = InputMultiObject(
322+
File(exist=True), mandatory=True, desc="input coefficients file(s) from TOPUP"
323+
)
324+
fmap_ref = File(exists=True, mandatory=True, desc="the fieldmap reference")
325+
326+
327+
class _TOPUPCoeffReorientOutputSpec(TraitedSpec):
328+
out_coeff = OutputMultiObject(File(exists=True), desc="patched coefficients")
329+
330+
331+
class TOPUPCoeffReorient(SimpleInterface):
332+
"""
333+
Revise the orientation of TOPUP-generated B-Spline coefficients.
334+
335+
TOUP-generated "fieldcoeff" files are just B-Spline fields, where the shape
336+
of the field is fixated to be a decimated grid of the original image by an
337+
integer factor and added 3 pixels on each dimension.
338+
This is one root reason why TOPUP errors (FSL 6) or segfaults (FSL5), when the
339+
input image has odd number of voxels along one or more directions.
340+
341+
These "fieldcoeff" are fixated to be zero-centered, and have "plumb" orientation
342+
(as in, aligned with cardinal/imaging axes).
343+
The q-form of these NIfTI files is always diagonal, with the decimation factors
344+
set on the diagonal (and hence, the voxel zooms).
345+
The origin of the q-form is set to the reference image's shape.
346+
347+
This interface modifies these coefficient files to be fully-fledged NIfTI images
348+
aligned with the reference image.
349+
Therefore, the s-form header of the coefficients file is updated to match that
350+
of the reference file.
351+
The s-form header is used because the imaging axes may be oblique.
352+
353+
The q-form retains the original header and is marked with code 0.
354+
355+
"""
356+
357+
input_spec = _TOPUPCoeffReorientInputSpec
358+
output_spec = _TOPUPCoeffReorientOutputSpec
359+
360+
def _run_interface(self, runtime):
361+
self._results["out_coeff"] = [
362+
str(_fix_topup_fieldcoeff(in_coeff, self.inputs.fmap_ref,))
363+
for in_coeff in self.inputs.in_coeff
364+
]
365+
return runtime
366+
367+
320368
def bspline_grid(img, control_zooms_mm=DEFAULT_ZOOMS_MM):
321369
"""Create a :obj:`~nibabel.nifti1.Nifti1Image` embedding the location of control points."""
322370
if isinstance(img, (str, Path)):
@@ -437,3 +485,36 @@ def _move_coeff(in_coeff, fmap_ref, transform):
437485
img.__class__(img.dataobj, newaff, img.header).to_filename(out[-1])
438486

439487
return out
488+
489+
490+
def _fix_topup_fieldcoeff(in_coeff, fmap_ref, out_file=None):
491+
"""Read in a coefficients file generated by TOPUP and fix x-form headers."""
492+
from pathlib import Path
493+
import numpy as np
494+
import nibabel as nb
495+
496+
if out_file is None:
497+
out_file = Path("coefficients.nii.gz").absolute()
498+
499+
coeffnii = nb.load(in_coeff)
500+
refnii = nb.load(fmap_ref)
501+
502+
coeff_shape = np.array(coeffnii.shape[:3])
503+
ref_shape = np.array(refnii.shape[:3])
504+
factors = coeffnii.header.get_zooms()[:3]
505+
if not np.all(coeff_shape == ref_shape // factors + 3):
506+
raise ValueError(
507+
f"Shape of coefficients file {coeff_shape} does not meet the "
508+
f"expectation given the reference's shape {ref_shape}."
509+
)
510+
newaff = np.eye(4)
511+
newaff[:3, :3] = refnii.affine[:3, :3] * factors
512+
c_ref = nb.affines.apply_affine(refnii.affine, 0.5 * (ref_shape - 1))
513+
c_coeff = nb.affines.apply_affine(newaff, 0.5 * (coeff_shape - 1))
514+
newaff[:3, 3] = c_ref - c_coeff
515+
header = coeffnii.header.copy()
516+
coeffnii.header.set_qform(coeffnii.header.get_qform(coded=False), code=0)
517+
coeffnii.header.set_sform(newaff, code=1)
518+
519+
coeffnii.__class__(coeffnii.dataobj, newaff, header).to_filename(out_file)
520+
return out_file

sdcflows/workflows/fit/pepolar.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def init_topup_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
6969

7070
from ...interfaces.epi import GetReadoutTime, EPIMask
7171
from ...interfaces.utils import Flatten
72+
from ...interfaces.bspline import TOPUPCoeffReorient
7273

7374
workflow = Workflow(name=name)
7475
workflow.__desc__ = f"""\
@@ -113,6 +114,11 @@ def init_topup_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
113114
)
114115

115116
epi_mask = pe.Node(EPIMask(), name="epi_mask")
117+
118+
fix_coeff = pe.Node(
119+
TOPUPCoeffReorient(), name="fix_coeff", run_without_submitting=True
120+
)
121+
116122
# fmt: off
117123
workflow.connect([
118124
(inputnode, flatten, [("in_data", "in_data"),
@@ -124,14 +130,16 @@ def init_topup_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
124130
(readout_time, topup, [("readout_time", "readout_times")]),
125131
(concat_blips, topup, [("out_file", "in_file")]),
126132
(topup, merge_corrected, [("out_corrected", "in_files")]),
133+
(topup, fix_coeff, [("out_fieldcoef", "in_coeff"),
134+
("out_corrected", "fmap_ref")]),
127135
(topup, outputnode, [("out_field", "fmap"),
128-
("out_fieldcoef", "fmap_coeff"),
129136
("out_jacs", "jacobians"),
130137
("out_mats", "xfms"),
131138
("out_warps", "out_warps")]),
132139
(merge_corrected, epi_mask, [("out_avg", "in_file")]),
133140
(merge_corrected, outputnode, [("out_avg", "fmap_ref")]),
134141
(epi_mask, outputnode, [("out_file", "fmap_mask")]),
142+
(fix_coeff, outputnode, [("out_coeff", "fmap_coeff")]),
135143
])
136144
# fmt: on
137145

0 commit comments

Comments
 (0)