Skip to content

Commit ac2daa4

Browse files
committed
enh: add a fieldmap report to replace old 3 reports.
1 parent 60c42b7 commit ac2daa4

File tree

4 files changed

+92
-13
lines changed

4 files changed

+92
-13
lines changed

sdcflows/interfaces/fmap.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,63 @@ def _run_interface(self, runtime):
230230
return runtime
231231

232232

233+
class _FUGUEvsm2ANTSwarpInputSpec(BaseInterfaceInputSpec):
234+
in_file = File(exists=True, mandatory=True,
235+
desc='input displacements field map')
236+
pe_dir = traits.Enum('i', 'i-', 'j', 'j-', 'k', 'k-',
237+
desc='phase-encoding axis')
238+
239+
240+
class _FUGUEvsm2ANTSwarpOutputSpec(TraitedSpec):
241+
out_file = File(desc='the output warp field')
242+
fieldmap = File(desc='field map in mm')
243+
244+
245+
class FUGUEvsm2ANTSwarp(SimpleInterface):
246+
"""Convert a voxel-shift-map to ants warp."""
247+
248+
_dtype = '<f4'
249+
input_spec = _FUGUEvsm2ANTSwarpInputSpec
250+
output_spec = _FUGUEvsm2ANTSwarpOutputSpec
251+
252+
def _run_interface(self, runtime):
253+
phaseEncDim = {'i': 0, 'j': 1, 'k': 2}[self.inputs.pe_dir[0]]
254+
phaseEncSign = [1.0, -1.0][len(self.inputs.pe_dir) != 2]
255+
256+
# Create new header
257+
nii = nb.load(self.inputs.in_file)
258+
hdr = nii.header.copy()
259+
hdr.set_data_dtype(self._dtype)
260+
261+
# Get data, convert to mm
262+
data = nii.get_fdata(dtype=self._dtype)
263+
aff = np.diag([1.0, 1.0, -1.0])
264+
if np.linalg.det(aff) < 0 and phaseEncDim != 0:
265+
# Reverse direction since ITK is LPS
266+
aff *= -1.0
267+
268+
aff = aff.dot(nii.affine[:3, :3])
269+
data *= phaseEncSign * nii.header.get_zooms()[phaseEncDim]
270+
self._results['fieldmap'] = fname_presuffix(
271+
self.inputs.in_file, suffix='_units-mm_fieldmap', newpath=runtime.cwd)
272+
nb.Nifti1Image(data, nii.affine, hdr).to_filename(self._results['fieldmap'])
273+
274+
# Compose a vector field
275+
zeros = np.zeros_like(data, dtype=self._dtype)
276+
field = [zeros, zeros]
277+
field.insert(phaseEncDim, data)
278+
field = np.stack(field, -1)
279+
280+
hdr.set_intent('vector', (), '')
281+
# Write out
282+
self._results['out_file'] = fname_presuffix(
283+
self.inputs.in_file, suffix='_desc-field_sdcwarp', newpath=runtime.cwd)
284+
nb.Nifti1Image(field[:, :, :, np.newaxis, :], nii.affine, hdr).to_filename(
285+
self._results['out_file'])
286+
287+
return runtime
288+
289+
233290
def _despike2d(data, thres, neigh=None):
234291
"""Despike axial slices, as done in FSL's ``epiunwarp``."""
235292
if neigh is None:

sdcflows/workflows/fmap.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@
2222
from nipype.pipeline import engine as pe
2323
from nipype.interfaces import utility as niu, fsl
2424
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
25-
from niworkflows.interfaces import itk
2625
from niworkflows.interfaces.images import IntraModalMerge
2726
from niworkflows.interfaces.registration import ANTSApplyTransformsRPT, ANTSRegistrationRPT
2827

29-
from ..interfaces.fmap import get_ees as _get_ees, FieldToRadS
28+
from ..interfaces.fmap import get_ees as _get_ees, FieldToRadS, FUGUEvsm2ANTSwarp
3029
from .gre import init_fmap_postproc_wf, init_magnitude_wf
3130

3231

@@ -111,7 +110,8 @@ def init_fmap_wf(omp_nthreads, fmap_bspline, name='fmap_wf'):
111110
return workflow
112111

113112

114-
def init_fmap2field_wf(omp_nthreads, debug, name='fmap2field_wf'):
113+
def init_fmap2field_wf(omp_nthreads, debug, name='fmap2field_wf',
114+
generate_report=True):
115115
"""
116116
Convert the estimated fieldmap in Hz into a displacements field.
117117
@@ -187,13 +187,13 @@ def init_fmap2field_wf(omp_nthreads, debug, name='fmap2field_wf'):
187187
'sdcflows', 'data/fmap-any_registration_testing.json')
188188

189189
fmap2ref_reg = pe.Node(
190-
ANTSRegistrationRPT(generate_report=True, from_file=ants_settings,
190+
ANTSRegistrationRPT(generate_report=False, from_file=ants_settings,
191191
output_inverse_warped_image=True, output_warped_image=True),
192192
name='fmap2ref_reg', n_procs=omp_nthreads)
193193

194194
# Map the VSM into the EPI space
195195
fmap2ref_apply = pe.Node(ANTSApplyTransformsRPT(
196-
generate_report=True, dimension=3, interpolation='BSpline', float=True),
196+
generate_report=False, dimension=3, interpolation='BSpline', float=True),
197197
name='fmap2ref_apply')
198198

199199
fmap_mask2ref_apply = pe.Node(ANTSApplyTransformsRPT(
@@ -209,7 +209,7 @@ def init_fmap2field_wf(omp_nthreads, debug, name='fmap2field_wf'):
209209
gen_vsm = pe.Node(fsl.FUGUE(save_unmasked_shift=True), name='gen_vsm')
210210
# Convert the VSM into a DFM (displacements field map)
211211
# or: FUGUE shift to ANTS warping.
212-
vsm2dfm = pe.Node(itk.FUGUEvsm2ANTSwarp(), name='vsm2dfm')
212+
vsm2dfm = pe.Node(FUGUEvsm2ANTSwarp(), name='vsm2dfm')
213213

214214
workflow.connect([
215215
(inputnode, fmap2ref_reg, [('fmap_ref', 'moving_image'),
@@ -228,11 +228,32 @@ def init_fmap2field_wf(omp_nthreads, debug, name='fmap2field_wf'):
228228
('composite_transform', 'transforms')]),
229229
(fmap2ref_apply, torads, [('output_image', 'in_file')]),
230230
(fmap_mask2ref_apply, gen_vsm, [('output_image', 'mask_file')]),
231-
(get_ees, gen_vsm, [('ees', 'dwell_time')]),
232231
(gen_vsm, vsm2dfm, [('shift_out_file', 'in_file')]),
232+
(get_ees, gen_vsm, [('ees', 'dwell_time')]),
233233
(torads, gen_vsm, [('out_file', 'fmap_in_file')]),
234234
(vsm2dfm, outputnode, [('out_file', 'out_warp')]),
235235
])
236+
237+
if generate_report:
238+
from niworkflows.interfaces.bids import DerivativesDataSink
239+
from ..interfaces.reportlets import FieldmapReportlet
240+
241+
fmap_rpt = pe.Node(FieldmapReportlet(
242+
reference_label='EPI Reference',
243+
moving_label='Magnitude', show='both'), name='fmap_rpt')
244+
ds_report_sdc = pe.Node(
245+
DerivativesDataSink(desc='fieldmap', suffix='bold'),
246+
name='ds_report_fmap', mem_gb=0.01, run_without_submitting=True
247+
)
248+
249+
workflow.connect([
250+
(inputnode, fmap_rpt, [('in_reference', 'reference')]),
251+
(fmap2ref_reg, fmap_rpt, [('warped_image', 'moving')]),
252+
(fmap_mask2ref_apply, fmap_rpt, [('output_image', 'mask')]),
253+
(vsm2dfm, fmap_rpt, [('fieldmap', 'fieldmap')]),
254+
(fmap_rpt, ds_report_sdc, [('out_report', 'in_file')]),
255+
])
256+
236257
return workflow
237258

238259

sdcflows/workflows/gre.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def init_magnitude_wf(omp_nthreads, name='magnitude_wf'):
8484
return workflow
8585

8686

87-
def init_fmap_postproc_wf(omp_nthreads, fmap_bspline, median_kernel_size=3,
87+
def init_fmap_postproc_wf(omp_nthreads, fmap_bspline, median_kernel_size=5,
8888
name='fmap_postproc_wf'):
8989
"""
9090
Postprocess a B0 map estimated elsewhere.

sdcflows/workflows/outputs.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ def init_sdc_unwarp_report_wf(name='sdc_unwarp_report_wf', forcedsyn=False):
1313
This workflow generates and saves a reportlet showing the effect of fieldmap
1414
unwarping a BOLD image.
1515
16-
.. workflow::
17-
:graph2use: orig
18-
:simple_form: yes
16+
Workflow Graph
17+
.. workflow::
18+
:graph2use: orig
19+
:simple_form: yes
1920
20-
from sdcflows.workflows.outputs import init_sdc_unwarp_report_wf
21-
wf = init_sdc_unwarp_report_wf()
21+
from sdcflows.workflows.outputs import init_sdc_unwarp_report_wf
22+
wf = init_sdc_unwarp_report_wf()
2223
2324
Parameters
2425
----------

0 commit comments

Comments
 (0)