Skip to content

Commit 6e2778a

Browse files
authored
Merge pull request #283 from nipreps/enh/mcribs-surface-processing
ENH: Add M-CRIB-S surface processing
2 parents d068bff + 27ab2cc commit 6e2778a

File tree

10 files changed

+468
-134
lines changed

10 files changed

+468
-134
lines changed

nibabies/cli/parser.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,12 @@ def _slice_time_ref(value, parser):
664664
action="store_true",
665665
help="Force traditional FreeSurfer surface reconstruction.",
666666
)
667+
g_baby.add_argument(
668+
"--surface-recon-method",
669+
choices=("infantfs", "freesurfer", "mcribs"),
670+
default="infantfs",
671+
help="Method to use for surface reconstruction",
672+
)
667673
return parser
668674

669675

@@ -674,6 +680,14 @@ def parse_args(args=None, namespace=None):
674680
parser = _build_parser()
675681
opts = parser.parse_args(args, namespace)
676682

683+
# Deprecations
684+
if opts.force_reconall:
685+
config.loggers.cli.warning(
686+
"--force-reconall is deprecated and will be removed in a future release."
687+
"To run traditional `recon-all`, use `--surface-recon-method freesurfer` instead."
688+
)
689+
opts.surface_recon_method = "freesurfer"
690+
677691
if opts.config_file:
678692
skip = {} if opts.reports_only else {"execution": ("run_uuid",)}
679693
config.load(opts.config_file, skip=skip)
@@ -745,14 +759,21 @@ def parse_args(args=None, namespace=None):
745759

746760
if config.execution.fs_subjects_dir is None:
747761
if output_layout == "bids":
748-
config.execution.fs_subjects_dir = output_dir / "sourcedata" / "infant-freesurfer"
762+
config.execution.fs_subjects_dir = output_dir / "sourcedata" / "freesurfer"
749763
elif output_layout == "legacy":
750-
config.execution.fs_subjects_dir = output_dir / "infant-freesurfer"
764+
config.execution.fs_subjects_dir = output_dir / "freesurfer"
751765
if config.execution.nibabies_dir is None:
752766
if output_layout == "bids":
753767
config.execution.nibabies_dir = output_dir
754768
elif output_layout == "legacy":
755769
config.execution.nibabies_dir = output_dir / "nibabies"
770+
if config.workflow.surface_recon_method == "mcribs":
771+
if output_layout == "bids":
772+
config.execution.mcribs_dir = output_dir / "sourcedata" / "mcribs"
773+
elif output_layout == "legacy":
774+
config.execution.mcribs_dir = output_dir / "mcribs"
775+
# Ensure the directory is created
776+
config.execution.mcribs_dir.mkdir(exist_ok=True, parents=True)
756777

757778
# Wipe out existing work_dir
758779
if opts.clean_workdir and work_dir.exists():

nibabies/config.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ class execution(_Config):
394394
"""Output verbosity."""
395395
low_mem = None
396396
"""Utilize uncompressed NIfTIs and other tricks to minimize memory allocation."""
397+
mcribs_dir = None
398+
"""M-CRIB-S processing and output directory."""
397399
md_only_boilerplate = False
398400
"""Do not convert boilerplate from MarkDown to LaTex and HTML."""
399401
nibabies_dir = None
@@ -439,6 +441,7 @@ class execution(_Config):
439441
"fs_subjects_dir",
440442
"layout",
441443
"log_dir",
444+
"mcribs_dir",
442445
"nibabies_dir",
443446
"output_dir",
444447
"segmentation_atlases_dir",
@@ -543,8 +546,6 @@ class workflow(_Config):
543546
"""Remove the mean from fieldmaps."""
544547
force_syn = None
545548
"""Run *fieldmap-less* susceptibility-derived distortions estimation."""
546-
force_reconall = False
547-
"""Force traditional FreeSurfer surface reconstruction instead of infant version."""
548549
hires = None
549550
"""Run FreeSurfer ``recon-all`` with the ``-hires`` flag."""
550551
ignore = None
@@ -578,6 +579,8 @@ class workflow(_Config):
578579
spaces = None
579580
"""Keeps the :py:class:`~niworkflows.utils.spaces.SpatialReferences`
580581
instance keeping standard and nonstandard spaces."""
582+
surface_recon_method = "infantfs"
583+
"""Method to use for surface reconstruction."""
581584
topup_max_vols = 5
582585
"""Maximum number of volumes to use with TOPUP, per-series (EPI or BOLD)."""
583586
use_aroma = None

nibabies/data/boilerplate.bib

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,3 +392,16 @@ @article{infantfs
392392
title = {Infant {FreeSurfer}: An automated segmentation and surface extraction pipeline for T1-weighted neuroimaging data of infants 0{\textendash}2 years},
393393
journal = {{NeuroImage}}
394394
}
395+
396+
@article{mcribs,
397+
doi = {10.1038/s41598-020-61326-2},
398+
url = {https://doi.org/10.1038%2Fs41598-020-61326-2},
399+
year = 2020,
400+
month = {mar},
401+
publisher = {Springer Science and Business Media {LLC}},
402+
volume = {10},
403+
number = {1},
404+
author = {Chris L. Adamson and Bonnie Alexander and Gareth Ball and Richard Beare and Jeanie L. Y. Cheong and Alicia J. Spittle and Lex W. Doyle and Peter J. Anderson and Marc L. Seal and Deanne K. Thompson},
405+
title = {Parcellation of the neonatal cortex using Surface-based Melbourne Children's Regional Infant Brain atlases (M-{CRIB}-S)},
406+
journal = {Scientific Reports}
407+
}

nibabies/interfaces/mcribs.py

Lines changed: 105 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@ class MCRIBReconAllInputSpec(CommandLineInputSpec):
3636
)
3737
t2w_file = File(
3838
exists=True,
39-
required=True,
4039
copyfile=True,
4140
desc='T2w (Isotropic + N4 corrected)',
4241
)
4342
segmentation_file = File(
43+
exists=True,
4444
desc='Segmentation file (skips tissue segmentation)',
4545
)
46+
mask_file = File(exists=True, desc='T2w mask')
4647

4748
# MCRIBS options
4849
conform = traits.Bool(
@@ -54,39 +55,31 @@ class MCRIBReconAllInputSpec(CommandLineInputSpec):
5455
desc='Perform tissue type segmentation',
5556
)
5657
surfrecon = traits.Bool(
57-
True,
58-
usedefault=True,
5958
argstr='--surfrecon',
6059
desc='Reconstruct surfaces',
6160
)
6261
surfrecon_method = traits.Enum(
6362
'Deformable',
6463
argstr='--surfreconmethod %s',
65-
usedefault=True,
64+
requires=['surfrecon'],
6665
desc='Surface reconstruction method',
6766
)
6867
join_thresh = traits.Float(
69-
1.0,
7068
argstr='--deformablejointhresh %f',
71-
usedefault=True,
69+
requires=['surfrecon'],
7270
desc='Join threshold parameter for Deformable',
7371
)
7472
fast_collision = traits.Bool(
75-
True,
7673
argstr='--deformablefastcollision',
77-
usedefault=True,
74+
requires=['surfrecon'],
7875
desc='Use Deformable fast collision test',
7976
)
8077
autorecon_after_surf = traits.Bool(
81-
True,
8278
argstr='--autoreconaftersurf',
83-
usedefault=True,
8479
desc='Do all steps after surface reconstruction',
8580
)
8681
segstats = traits.Bool(
87-
True,
8882
argstr='--segstats',
89-
usedefault=True,
9083
desc='Compute statistics on segmented volumes',
9184
)
9285
nthreads = traits.Int(
@@ -97,6 +90,7 @@ class MCRIBReconAllInputSpec(CommandLineInputSpec):
9790

9891
class MCRIBReconAllOutputSpec(TraitedSpec):
9992
mcribs_dir = Directory(desc='MCRIBS output directory')
93+
subjects_dir = Directory(desc='FreeSurfer output directory')
10094

10195

10296
class MCRIBReconAll(CommandLine):
@@ -111,10 +105,17 @@ def cmdline(self):
111105
# Avoid processing if valid
112106
if self.inputs.outdir:
113107
sid = self.inputs.subject_id
114-
logf = Path(self.inputs.outdir) / sid / 'logs' / f'{sid}.log'
115-
if logf.exists():
116-
logtxt = logf.read_text().splitlines()[-3:]
117-
self._no_run = 'Finished without error' in logtxt
108+
# Check MIRTK surface recon deformable
109+
if self.inputs.surfrecon:
110+
surfrecon_dir = Path(self.inputs.outdir) / sid / 'SurfReconDeformable' / sid
111+
if self._verify_surfrecon_outputs(surfrecon_dir, error=False):
112+
self._no_run = True
113+
# Check FS directory population
114+
elif self.inputs.autorecon_after_surf:
115+
fs_dir = Path(self.inputs.outdir) / sid / 'freesurfer' / sid
116+
if self._verify_autorecon_outputs(fs_dir, error=False):
117+
self._no_run = True
118+
118119
if self._no_run:
119120
return "echo MCRIBSReconAll: nothing to do"
120121
return cmd
@@ -147,21 +148,22 @@ def _setup_directory_structure(self, mcribs_dir: Path) -> None:
147148
root.mkdir(**mkdir_kw)
148149

149150
# T2w operations
150-
t2w = root / 'RawT2' / f'{sid}.nii.gz'
151-
t2w.parent.mkdir(**mkdir_kw)
152-
if not t2w.exists():
153-
shutil.copy(self.inputs.t2w_file, str(t2w))
154-
155-
if not self.inputs.conform:
156-
t2wiso = root / 'RawT2RadiologicalIsotropic' / f'{sid}.nii.gz'
157-
t2wiso.parent.mkdir(**mkdir_kw)
158-
if not t2wiso.exists():
159-
t2wiso.symlink_to(f'../RawT2/{sid}.nii.gz')
160-
161-
n4 = root / 'TissueSegDrawEM' / sid / 'N4' / f'{sid}.nii.gz'
162-
n4.parent.mkdir(**mkdir_kw)
163-
if not n4.exists():
164-
n4.symlink_to(f'../../../RawT2/{sid}.nii.gz')
151+
if self.inputs.t2w_file:
152+
t2w = root / 'RawT2' / f'{sid}.nii.gz'
153+
t2w.parent.mkdir(**mkdir_kw)
154+
if not t2w.exists():
155+
shutil.copy(self.inputs.t2w_file, str(t2w))
156+
157+
if not self.inputs.conform:
158+
t2wiso = root / 'RawT2RadiologicalIsotropic' / f'{sid}.nii.gz'
159+
t2wiso.parent.mkdir(**mkdir_kw)
160+
if not t2wiso.exists():
161+
t2wiso.symlink_to(f'../RawT2/{sid}.nii.gz')
162+
163+
n4 = root / 'TissueSegDrawEM' / sid / 'N4' / f'{sid}.nii.gz'
164+
n4.parent.mkdir(**mkdir_kw)
165+
if not n4.exists():
166+
n4.symlink_to(f'../../../RawT2/{sid}.nii.gz')
165167

166168
# Segmentation
167169
if self.inputs.segmentation_file:
@@ -184,6 +186,11 @@ def _setup_directory_structure(self, mcribs_dir: Path) -> None:
184186
if not surfrec.exists():
185187
surfrec.symlink_to(f'../../../RawT2/{sid}.nii.gz')
186188

189+
if self.inputs.mask_file:
190+
surfrec_mask = surfrec.parent / 'brain-mask.nii.gz'
191+
if not surfrec_mask.exists():
192+
shutil.copy(self.inputs.mask_file, str(surfrec_mask))
193+
187194
if self.inputs.surfrecon:
188195
# Create FreeSurfer layout to safeguard against cd-ing into missing directories
189196
for d in ('surf', 'mri', 'label', 'scripts', 'stats'):
@@ -196,21 +203,82 @@ def _run_interface(self, runtime):
196203
# if users wish to preserve their runs
197204
mcribs_dir = self.inputs.outdir or Path(runtime.cwd) / 'mcribs'
198205
self._mcribs_dir = Path(mcribs_dir)
199-
self._setup_directory_structure(self._mcribs_dir)
206+
if self.inputs.surfrecon:
207+
assert self.inputs.t2w_file, "Missing T2w input"
208+
self._setup_directory_structure(self._mcribs_dir)
200209
# overwrite CWD to be in MCRIB subject's directory
201210
runtime.cwd = str(self._mcribs_dir / self.inputs.subject_id)
202211
return super()._run_interface(runtime)
203212

204213
def _list_outputs(self):
205214
outputs = self._outputs().get()
206-
outputs['mcribs_dir'] = str(self._mcribs_dir)
207-
208-
# Copy freesurfer directory into FS subjects dir
209215
sid = self.inputs.subject_id
210-
mcribs_fs = self._mcribs_dir / sid / 'freesurfer' / sid
211-
if mcribs_fs.exists() and self.inputs.subjects_dir:
216+
if self.inputs.surfrecon:
217+
# verify surface reconstruction was successful
218+
surfrecon_dir = self._mcribs_dir / sid / 'SurfReconDeformable' / sid
219+
self._verify_surfrecon_outputs(surfrecon_dir, error=True)
220+
221+
outputs['mcribs_dir'] = str(self._mcribs_dir)
222+
if self.inputs.autorecon_after_surf and self.inputs.subjects_dir:
223+
mcribs_fs = self._mcribs_dir / sid / 'freesurfer' / sid
224+
self._verify_autorecon_outputs(mcribs_fs, error=True)
212225
dst = Path(self.inputs.subjects_dir) / self.inputs.subject_id
213226
if not dst.exists():
214227
shutil.copytree(mcribs_fs, dst)
228+
outputs['subjects_dir'] = self.inputs.subjects_dir
215229

216230
return outputs
231+
232+
@staticmethod
233+
def _verify_surfrecon_outputs(surfrecon_dir: Path, error: bool) -> bool:
234+
"""
235+
Sanity check to ensure the surface reconstruction was successful.
236+
237+
MCRIBReconAll does not return a failing exit code if a step failed, which leads
238+
this interface to be marked as completed without error in such cases.
239+
"""
240+
# fmt:off
241+
surfrecon_files = {
242+
'meshes': (
243+
'pial-lh-reordered.vtp',
244+
'pial-rh-reordered.vtp',
245+
'white-rh.vtp',
246+
'white-lh.vtp',
247+
)
248+
}
249+
# fmt:on
250+
for d, fls in surfrecon_files.items():
251+
for fl in fls:
252+
if not (surfrecon_dir / d / fl).exists():
253+
if error:
254+
raise FileNotFoundError(f"SurfReconDeformable missing: {fl}")
255+
return False
256+
return True
257+
258+
@staticmethod
259+
def _verify_autorecon_outputs(fs_dir: Path, error: bool) -> bool:
260+
"""
261+
Sanity check to ensure the necessary FreeSurfer files have been created.
262+
263+
MCRIBReconAll does not return a failing exit code if a step failed, which leads
264+
this interface to be marked as completed without error in such cases.
265+
"""
266+
# fmt:off
267+
fs_files = {
268+
'mri': ('T2.mgz', 'aseg.presurf.mgz', 'ribbon.mgz', 'brain.mgz'),
269+
'label': ('lh.cortex.label', 'rh.cortex.label'),
270+
'stats': ('aseg.stats', 'brainvol.stats', 'lh.aparc.stats', 'rh.curv.stats'),
271+
'surf': (
272+
'lh.pial', 'rh.pial',
273+
'lh.white', 'rh.white',
274+
'lh.curv', 'rh.curv',
275+
'lh.thickness', 'rh.thickness'),
276+
}
277+
# fmt:on
278+
for d, fls in fs_files.items():
279+
for fl in fls:
280+
if not (fs_dir / d / fl).exists():
281+
if error:
282+
raise FileNotFoundError(f"FreeSurfer directory missing: {fl}")
283+
return False
284+
return True

0 commit comments

Comments
 (0)