Skip to content

Commit 569f6dd

Browse files
committed
RF: MCRIBReconAll interface
- Remove defaults from inputs. It now is required to explicitly pass in processing steps. - Add helper methods to verify outputs of crucial checkpoints (after neonatal cortex reconstruction, FS file generation) - Save the brain mask under the proper intermediate filename.
1 parent bcbf519 commit 569f6dd

File tree

1 file changed

+99
-39
lines changed

1 file changed

+99
-39
lines changed

nibabies/interfaces/mcribs.py

Lines changed: 99 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +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(desc='T2w mask')
46+
mask_file = File(exists=True, desc='T2w mask')
4747

4848
# MCRIBS options
4949
conform = traits.Bool(
@@ -55,39 +55,31 @@ class MCRIBReconAllInputSpec(CommandLineInputSpec):
5555
desc='Perform tissue type segmentation',
5656
)
5757
surfrecon = traits.Bool(
58-
True,
59-
usedefault=True,
6058
argstr='--surfrecon',
6159
desc='Reconstruct surfaces',
6260
)
6361
surfrecon_method = traits.Enum(
6462
'Deformable',
6563
argstr='--surfreconmethod %s',
66-
usedefault=True,
64+
requires=['surfrecon'],
6765
desc='Surface reconstruction method',
6866
)
6967
join_thresh = traits.Float(
70-
1.0,
7168
argstr='--deformablejointhresh %f',
72-
usedefault=True,
69+
requires=['surfrecon'],
7370
desc='Join threshold parameter for Deformable',
7471
)
7572
fast_collision = traits.Bool(
76-
True,
7773
argstr='--deformablefastcollision',
78-
usedefault=True,
74+
requires=['surfrecon'],
7975
desc='Use Deformable fast collision test',
8076
)
8177
autorecon_after_surf = traits.Bool(
82-
True,
8378
argstr='--autoreconaftersurf',
84-
usedefault=True,
8579
desc='Do all steps after surface reconstruction',
8680
)
8781
segstats = traits.Bool(
88-
True,
8982
argstr='--segstats',
90-
usedefault=True,
9183
desc='Compute statistics on segmented volumes',
9284
)
9385
nthreads = traits.Int(
@@ -113,10 +105,17 @@ def cmdline(self):
113105
# Avoid processing if valid
114106
if self.inputs.outdir:
115107
sid = self.inputs.subject_id
116-
logf = Path(self.inputs.outdir) / sid / 'logs' / f'{sid}.log'
117-
if logf.exists():
118-
logtxt = logf.read_text().splitlines()[-3:]
119-
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+
120119
if self._no_run:
121120
return "echo MCRIBSReconAll: nothing to do"
122121
return cmd
@@ -149,21 +148,22 @@ def _setup_directory_structure(self, mcribs_dir: Path) -> None:
149148
root.mkdir(**mkdir_kw)
150149

151150
# T2w operations
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')
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')
167167

168168
# Segmentation
169169
if self.inputs.segmentation_file:
@@ -187,7 +187,7 @@ def _setup_directory_structure(self, mcribs_dir: Path) -> None:
187187
surfrec.symlink_to(f'../../../RawT2/{sid}.nii.gz')
188188

189189
if self.inputs.mask_file:
190-
surfrec_mask = surfrec.parent / 'mask-image.nii.gz'
190+
surfrec_mask = surfrec.parent / 'brain-mask.nii.gz'
191191
if not surfrec_mask.exists():
192192
shutil.copy(self.inputs.mask_file, str(surfrec_mask))
193193

@@ -203,22 +203,82 @@ def _run_interface(self, runtime):
203203
# if users wish to preserve their runs
204204
mcribs_dir = self.inputs.outdir or Path(runtime.cwd) / 'mcribs'
205205
self._mcribs_dir = Path(mcribs_dir)
206-
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)
207209
# overwrite CWD to be in MCRIB subject's directory
208210
runtime.cwd = str(self._mcribs_dir / self.inputs.subject_id)
209211
return super()._run_interface(runtime)
210212

211213
def _list_outputs(self):
212214
outputs = self._outputs().get()
213-
outputs['mcribs_dir'] = str(self._mcribs_dir)
214-
215-
# Copy freesurfer directory into FS subjects dir
216215
sid = self.inputs.subject_id
217-
mcribs_fs = self._mcribs_dir / sid / 'freesurfer' / sid
218-
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)
219225
dst = Path(self.inputs.subjects_dir) / self.inputs.subject_id
220226
if not dst.exists():
221227
shutil.copytree(mcribs_fs, dst)
222228
outputs['subjects_dir'] = self.inputs.subjects_dir
223229

224230
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)