Skip to content

Commit 27424c9

Browse files
mattcieslaktsalo
andauthored
Add synthstrip for ASLRef (#605)
Co-authored-by: Taylor Salo <[email protected]>
1 parent 513ee5e commit 27424c9

File tree

5 files changed

+383
-6
lines changed

5 files changed

+383
-6
lines changed

aslprep/interfaces/freesurfer.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""
4+
SynthStrip interfaces
5+
~~~~~~~~~~~~~~~~~~~~~
6+
7+
"""
8+
9+
import os.path as op
10+
11+
import nibabel as nb
12+
from nipype.interfaces.afni import Zeropad
13+
from nipype.interfaces.base import (
14+
BaseInterfaceInputSpec,
15+
File,
16+
SimpleInterface,
17+
TraitedSpec,
18+
traits,
19+
)
20+
from nipype.interfaces.freesurfer.base import FSCommandOpenMP, FSTraitedSpec
21+
from nipype.utils.filemanip import fname_presuffix
22+
from niworkflows.utils.images import _copyxform
23+
24+
25+
class FSTraitedSpecOpenMP(FSTraitedSpec):
26+
num_threads = traits.Int(desc='allows for specifying more threads', nohash=True)
27+
28+
29+
class _PrepareSynthStripGridInputSpec(BaseInterfaceInputSpec):
30+
input_image = File(exists=True, mandatory=True)
31+
32+
33+
class _PrepareSynthStripGridOutputSpec(TraitedSpec):
34+
prepared_image = File(exists=True)
35+
36+
37+
class PrepareSynthStripGrid(SimpleInterface):
38+
input_spec = _PrepareSynthStripGridInputSpec
39+
output_spec = _PrepareSynthStripGridOutputSpec
40+
41+
def _run_interface(self, runtime):
42+
out_fname = fname_presuffix(
43+
self.inputs.input_image,
44+
newpath=runtime.cwd,
45+
suffix='_SynthStripGrid.nii',
46+
use_ext=False,
47+
)
48+
self._results['prepared_image'] = out_fname
49+
50+
# possibly downsample the image for sloppy mode. Always ensure float32
51+
img = nb.load(self.inputs.input_image)
52+
if not img.ndim == 3:
53+
raise Exception('3D inputs are required for Synthstrip')
54+
xvoxels, yvoxels, zvoxels = img.shape
55+
56+
def get_padding(nvoxels):
57+
extra_slices = nvoxels % 64
58+
if extra_slices == 0:
59+
return 0
60+
complete_64s = nvoxels // 64
61+
return 64 * (complete_64s + 1) - nvoxels
62+
63+
def split_padding(padding):
64+
halfpad = padding // 2
65+
return halfpad, halfpad + halfpad % 2
66+
67+
spad = get_padding(zvoxels)
68+
rpad, lpad = split_padding(get_padding(xvoxels))
69+
apad, ppad = split_padding(get_padding(yvoxels))
70+
71+
zeropad = Zeropad(
72+
S=spad,
73+
R=rpad,
74+
L=lpad,
75+
A=apad,
76+
P=ppad,
77+
in_files=self.inputs.input_image,
78+
out_file=out_fname,
79+
)
80+
81+
_ = zeropad.run()
82+
if not op.exists(out_fname):
83+
raise Exception('zeropad failed! You may need to increase the memory limit.')
84+
return runtime
85+
86+
87+
class _SynthStripInputSpec(FSTraitedSpecOpenMP):
88+
input_image = File(argstr='-i %s', exists=True, mandatory=True)
89+
no_csf = traits.Bool(argstr='--no-csf', desc='Exclude CSF from brain border.')
90+
border = traits.Int(argstr='-b %d', desc='Mask border threshold in mm. Default is 1.')
91+
gpu = traits.Bool(argstr='-g')
92+
out_brain = File(
93+
argstr='-o %s',
94+
name_template='%s_brain.nii.gz',
95+
name_source=['input_image'],
96+
keep_extension=False,
97+
desc='skull stripped image with corrupt sform',
98+
)
99+
out_brain_mask = File(
100+
argstr='-m %s',
101+
name_template='%s_mask.nii.gz',
102+
name_source=['input_image'],
103+
keep_extension=False,
104+
desc='mask image with corrupt sform',
105+
)
106+
107+
108+
class _SynthStripOutputSpec(TraitedSpec):
109+
out_brain = File(exists=True)
110+
out_brain_mask = File(exists=True)
111+
112+
113+
class SynthStrip(FSCommandOpenMP):
114+
input_spec = _SynthStripInputSpec
115+
output_spec = _SynthStripOutputSpec
116+
_cmd = 'mri_synthstrip'
117+
118+
def _num_threads_update(self):
119+
if self.inputs.num_threads:
120+
self.inputs.environ.update({'OMP_NUM_THREADS': '1'})
121+
122+
123+
class FixHeaderSynthStrip(SynthStrip):
124+
def _run_interface(self, runtime, correct_return_codes=(0,)):
125+
# Run normally
126+
runtime = super()._run_interface(runtime, correct_return_codes)
127+
128+
outputs = self._list_outputs()
129+
if not op.exists(outputs['out_brain']):
130+
raise Exception('mri_synthstrip failed!')
131+
132+
if outputs.get('out_brain_mask'):
133+
_copyxform(self.inputs.input_image, outputs['out_brain_mask'])
134+
135+
_copyxform(self.inputs.input_image, outputs['out_brain'])
136+
137+
return runtime
138+
139+
140+
class MockSynthStrip(SimpleInterface):
141+
input_spec = _SynthStripInputSpec
142+
output_spec = _SynthStripOutputSpec
143+
144+
def _run_interface(self, runtime):
145+
from nipype.interfaces.fsl import BET
146+
147+
this_bet = BET(
148+
mask=True,
149+
in_file=self.inputs.input_image,
150+
output_type='NIFTI_GZ',
151+
)
152+
result = this_bet.run()
153+
self._results['out_brain'] = result.outputs.out_file
154+
self._results['out_brain_mask'] = result.outputs.mask_file
155+
156+
return runtime

aslprep/workflows/asl/cbf.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
)
3434
from aslprep.utils.atlas import get_atlas_nifti
3535
from aslprep.utils.bids import find_atlas_entities
36-
from aslprep.workflows.asl.reference import init_enhance_and_skullstrip_asl_wf
36+
from aslprep.workflows.asl.reference import init_synthstrip_aslref_wf
3737

3838

3939
def init_cbf_wf(
@@ -395,9 +395,8 @@ def _getfiledir(file):
395395
(mean_m0, extract_deltam, [('out_file', 'm0scan')]),
396396
]) # fmt:skip
397397

398-
enhance_and_skullstrip_m0scan_wf = init_enhance_and_skullstrip_asl_wf(
398+
enhance_and_skullstrip_m0scan_wf = init_synthstrip_aslref_wf(
399399
disable_n4=config.workflow.disable_n4,
400-
omp_nthreads=1,
401400
name='enhance_and_skullstrip_m0scan_wf',
402401
)
403402
workflow.connect([

aslprep/workflows/asl/fit.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
from aslprep.utils.asl import select_processing_target
5454
from aslprep.workflows.asl.hmc import init_asl_hmc_wf
5555
from aslprep.workflows.asl.outputs import init_asl_fit_reports_wf, init_ds_aslref_wf
56-
from aslprep.workflows.asl.reference import init_enhance_and_skullstrip_asl_wf, init_raw_aslref_wf
56+
from aslprep.workflows.asl.reference import init_raw_aslref_wf, init_synthstrip_aslref_wf
5757

5858

5959
def get_sbrefs(
@@ -621,9 +621,8 @@ def init_asl_fit_wf(
621621
)
622622
workflow.connect(raw_sbref_wf, 'outputnode.aslref', fmapref_buffer, 'sbref_files')
623623

624-
enhance_aslref_wf = init_enhance_and_skullstrip_asl_wf(
624+
enhance_aslref_wf = init_synthstrip_aslref_wf(
625625
disable_n4=config.workflow.disable_n4,
626-
omp_nthreads=omp_nthreads,
627626
)
628627

629628
ds_coreg_aslref_wf = init_ds_aslref_wf(

aslprep/workflows/asl/reference.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,103 @@ def init_enhance_and_skullstrip_asl_wf(
422422
]) # fmt: skip
423423

424424
return workflow
425+
426+
427+
def init_synthstrip_aslref_wf(
428+
disable_n4=False,
429+
name='synthstrip_aslref_wf',
430+
):
431+
"""Enhance and run brain extraction on an ASL image.
432+
433+
This workflow uses SynthStrip to extract the brain from an ASLRef image.
434+
435+
Parameters
436+
----------
437+
disable_n4 : :obj:`bool`
438+
If True, N4 bias field correction will be disabled.
439+
name : str
440+
Name of workflow (default: ``synthstrip_aslref_wf``)
441+
442+
Inputs
443+
------
444+
in_file : str
445+
ASLRef image
446+
447+
Outputs
448+
-------
449+
bias_corrected_file : str
450+
the ``in_file`` after N4 bias field correction.
451+
If ``disable_n4`` is True, this will be the same as ``in_file``.
452+
skull_stripped_file : str
453+
the ``bias_corrected_file`` after skull-stripping
454+
mask_file : str
455+
mask of the skull-stripped input file
456+
"""
457+
from nipype.interfaces import afni
458+
from nipype.interfaces import utility as niu
459+
from nipype.pipeline import engine as pe
460+
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
461+
from niworkflows.interfaces.fixes import FixN4BiasFieldCorrection as N4BiasFieldCorrection
462+
from niworkflows.interfaces.header import CopyXForm
463+
from niworkflows.interfaces.nibabel import ApplyMask
464+
465+
from aslprep.workflows.segmentation import init_synthstrip_wf
466+
467+
workflow = Workflow(name=name)
468+
inputnode = pe.Node(niu.IdentityInterface(fields=['in_file', 'pre_mask']), name='inputnode')
469+
outputnode = pe.Node(
470+
niu.IdentityInterface(fields=['mask_file', 'skull_stripped_file', 'bias_corrected_file']),
471+
name='outputnode',
472+
)
473+
474+
n4_buffer = pe.Node(niu.IdentityInterface(fields=['bias_corrected_file']), name='n4_buffer')
475+
476+
if not disable_n4:
477+
# Run N4 normally, force num_threads=1 for stability (images are small, no need for >1)
478+
n4_correct = pe.Node(
479+
N4BiasFieldCorrection(dimension=3, copy_header=True, bspline_fitting_distance=200),
480+
shrink_factor=2,
481+
name='n4_correct',
482+
n_procs=1,
483+
)
484+
n4_correct.inputs.rescale_intensities = True
485+
486+
workflow.connect([
487+
(inputnode, n4_correct, [('in_file', 'input_image')]),
488+
(n4_correct, n4_buffer, [('output_image', 'bias_corrected_file')]),
489+
]) # fmt:skip
490+
else:
491+
workflow.connect([(inputnode, n4_buffer, [('in_file', 'bias_corrected_file')])])
492+
493+
# Use AFNI's unifize for T2 contrast & fix header
494+
unifize = pe.Node(
495+
afni.Unifize(
496+
t2=True,
497+
outputtype='NIFTI_GZ',
498+
# Default -clfrac is 0.1, 0.4 was too conservative
499+
# -rbt because I'm a Jedi AFNI Master (see 3dUnifize's documentation)
500+
args='-clfrac 0.2 -rbt 18.3 65.0 90.0',
501+
out_file='uni.nii.gz',
502+
),
503+
name='unifize',
504+
)
505+
fixhdr_unifize = pe.Node(CopyXForm(), name='fixhdr_unifize', mem_gb=0.1)
506+
507+
synthstrip_wf = init_synthstrip_wf(name='synthstrip_wf')
508+
509+
# Compute masked brain
510+
apply_mask = pe.Node(ApplyMask(), name='apply_mask')
511+
512+
workflow.connect([
513+
(inputnode, fixhdr_unifize, [('in_file', 'hdr_file')]),
514+
(inputnode, synthstrip_wf, [('in_file', 'inputnode.original_image')]),
515+
(inputnode, unifize, [('in_file', 'in_file')]),
516+
(unifize, fixhdr_unifize, [('out_file', 'in_file')]),
517+
(synthstrip_wf, apply_mask, [('outputnode.brain_mask', 'in_mask')]),
518+
(fixhdr_unifize, apply_mask, [('out_file', 'in_file')]),
519+
(apply_mask, outputnode, [('out_file', 'skull_stripped_file')]),
520+
(n4_buffer, outputnode, [('bias_corrected_file', 'bias_corrected_file')]),
521+
(synthstrip_wf, outputnode, [('outputnode.brain_mask', 'mask_file')]),
522+
]) # fmt: skip
523+
524+
return workflow

0 commit comments

Comments
 (0)