Skip to content

Commit a95c8bd

Browse files
authored
Merge pull request #436 from mgxd/fix/mcribs-clipping
ENH: Minimize clipping prior to surface reconstruction with MCRIBS
2 parents ffc69a8 + c2cd283 commit a95c8bd

File tree

4 files changed

+150
-82
lines changed

4 files changed

+150
-82
lines changed

nibabies/interfaces/mcribs.py

Lines changed: 49 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class MCRIBReconAllInputSpec(CommandLineInputSpec):
3232
t1w_file = File(
3333
exists=True,
3434
copyfile=True,
35-
desc='T1w to be used for deformable (must be registered to T2w image)',
35+
desc='T1w to be used for deformable (must be in register with T2w image)',
3636
)
3737
t2w_file = File(
3838
exists=True,
@@ -47,14 +47,20 @@ class MCRIBReconAllInputSpec(CommandLineInputSpec):
4747

4848
# MCRIBS options
4949
conform = traits.Bool(
50+
False,
51+
usedefault=True,
5052
argstr='--conform',
5153
desc='Reorients to radiological, axial slice orientation. Resamples to isotropic voxels',
5254
)
5355
tissueseg = traits.Bool(
56+
False,
57+
usedefault=True,
5458
argstr='--tissueseg',
5559
desc='Perform tissue type segmentation',
5660
)
5761
surfrecon = traits.Bool(
62+
False,
63+
usedefault=True,
5864
argstr='--surfrecon',
5965
desc='Reconstruct surfaces',
6066
)
@@ -75,6 +81,8 @@ class MCRIBReconAllInputSpec(CommandLineInputSpec):
7581
desc='Use Deformable fast collision test',
7682
)
7783
autorecon_after_surf = traits.Bool(
84+
False,
85+
usedefault=True,
7886
argstr='--autoreconaftersurf',
7987
desc='Do all steps after surface reconstruction',
8088
)
@@ -99,25 +107,51 @@ class MCRIBReconAll(CommandLine):
99107
output_spec = MCRIBReconAllOutputSpec
100108
_no_run = False
101109

110+
_expected_files = {
111+
'surfrecon': {
112+
'meshes': (
113+
'pial-lh-reordered.vtp',
114+
'pial-rh-reordered.vtp',
115+
'white-rh.vtp',
116+
'white-lh.vtp',
117+
)
118+
},
119+
'autorecon': {
120+
'mri': ('T2.mgz', 'aseg.presurf.mgz', 'ribbon.mgz', 'brain.mgz'),
121+
'label': ('lh.cortex.label', 'rh.cortex.label'),
122+
'stats': ('aseg.stats', 'brainvol.stats', 'lh.aparc.stats', 'rh.curv.stats'),
123+
'surf': (
124+
'lh.pial', 'rh.pial',
125+
'lh.white', 'rh.white',
126+
'lh.curv', 'rh.curv',
127+
'lh.thickness', 'rh.thickness'),
128+
}
129+
} # fmt:skip
130+
102131
@property
103132
def cmdline(self):
104133
cmd = super().cmdline
105-
# Avoid processing if valid
134+
if self.inputs.surfrecon and not self.inputs.t2w_file:
135+
raise AttributeError('Missing `t2w_file` input.')
136+
137+
# If an output directory is provided, check if we can skip the run
106138
if self.inputs.outdir:
107139
sid = self.inputs.subject_id
108140
# Check MIRTK surface recon deformable
109141
if self.inputs.surfrecon:
110142
surfrecon_dir = Path(self.inputs.outdir) / sid / 'SurfReconDeformable' / sid
111-
if self._verify_surfrecon_outputs(surfrecon_dir, error=False):
143+
if self._verify_outputs('surfrecon', surfrecon_dir):
112144
self._no_run = True
113145
# Check FS directory population
114-
elif self.inputs.autorecon_after_surf:
146+
if self.inputs.autorecon_after_surf:
115147
fs_dir = Path(self.inputs.outdir) / sid / 'freesurfer' / sid
116-
if self._verify_autorecon_outputs(fs_dir, error=False):
148+
if self._verify_outputs('autorecon', fs_dir):
117149
self._no_run = True
150+
else:
151+
self._no_run = False
118152

119153
if self._no_run:
120-
return 'echo MCRIBSReconAll: nothing to do'
154+
return 'echo MCRIBReconAll: nothing to do'
121155
return cmd
122156

123157
def _setup_directory_structure(self, mcribs_dir: Path) -> None:
@@ -204,8 +238,6 @@ def _run_interface(self, runtime):
204238
mcribs_dir = self.inputs.outdir or Path(runtime.cwd) / 'mcribs'
205239
self._mcribs_dir = Path(mcribs_dir)
206240
if self.inputs.surfrecon:
207-
if not self.inputs.t2w_file:
208-
raise AttributeError('Missing T2w input')
209241
self._setup_directory_structure(self._mcribs_dir)
210242
# overwrite CWD to be in MCRIB subject's directory
211243
runtime.cwd = str(self._mcribs_dir / self.inputs.subject_id)
@@ -217,11 +249,11 @@ def _list_outputs(self):
217249
if self.inputs.surfrecon:
218250
# verify surface reconstruction was successful
219251
surfrecon_dir = self._mcribs_dir / sid / 'SurfReconDeformable' / sid
220-
self._verify_surfrecon_outputs(surfrecon_dir, error=True)
252+
self._verify_outputs('surfrecon', surfrecon_dir, error=True)
221253

222254
mcribs_fs = self._mcribs_dir / sid / 'freesurfer' / sid
223255
if self.inputs.autorecon_after_surf:
224-
self._verify_autorecon_outputs(mcribs_fs, error=True)
256+
self._verify_outputs('autorecon', mcribs_fs, error=True)
225257

226258
outputs['mcribs_dir'] = str(self._mcribs_dir)
227259
if self.inputs.autorecon_after_surf and self.inputs.subjects_dir:
@@ -241,56 +273,19 @@ def _list_outputs(self):
241273

242274
return outputs
243275

244-
@staticmethod
245-
def _verify_surfrecon_outputs(surfrecon_dir: Path, error: bool) -> bool:
276+
def _verify_outputs(self, step: str, root: Path, error: bool = False) -> bool:
246277
"""
247-
Sanity check to ensure the surface reconstruction was successful.
278+
Method to check to ensure the expected files are present successful.
248279
249280
MCRIBReconAll does not return a failing exit code if a step failed, which leads
250281
this interface to be marked as completed without error in such cases.
251282
"""
252-
# fmt:off
253-
surfrecon_files = {
254-
'meshes': (
255-
'pial-lh-reordered.vtp',
256-
'pial-rh-reordered.vtp',
257-
'white-rh.vtp',
258-
'white-lh.vtp',
259-
)
260-
}
261-
# fmt:on
262-
for d, fls in surfrecon_files.items():
263-
for fl in fls:
264-
if not (surfrecon_dir / d / fl).exists():
265-
if error:
266-
raise FileNotFoundError(f'SurfReconDeformable missing: {fl}')
267-
return False
268-
return True
269-
270-
@staticmethod
271-
def _verify_autorecon_outputs(fs_dir: Path, error: bool) -> bool:
272-
"""
273-
Sanity check to ensure the necessary FreeSurfer files have been created.
274283

275-
MCRIBReconAll does not return a failing exit code if a step failed, which leads
276-
this interface to be marked as completed without error in such cases.
277-
"""
278-
# fmt:off
279-
fs_files = {
280-
'mri': ('T2.mgz', 'aseg.presurf.mgz', 'ribbon.mgz', 'brain.mgz'),
281-
'label': ('lh.cortex.label', 'rh.cortex.label'),
282-
'stats': ('aseg.stats', 'brainvol.stats', 'lh.aparc.stats', 'rh.curv.stats'),
283-
'surf': (
284-
'lh.pial', 'rh.pial',
285-
'lh.white', 'rh.white',
286-
'lh.curv', 'rh.curv',
287-
'lh.thickness', 'rh.thickness'),
288-
}
289-
# fmt:on
290-
for d, fls in fs_files.items():
291-
for fl in fls:
292-
if not (fs_dir / d / fl).exists():
284+
expected = self._expected_files[step]
285+
for d, files in expected.items():
286+
for fl in files:
287+
if not (root / d / fl).exists():
293288
if error:
294-
raise FileNotFoundError(f'FreeSurfer directory missing: {fl}')
289+
raise FileNotFoundError(f'{step.capitalize()} missing: {fl}')
295290
return False
296291
return True
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import shutil
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
from nibabies.interfaces.mcribs import MCRIBReconAll
7+
8+
SUBJECT_ID = 'X'
9+
10+
11+
@pytest.fixture
12+
def mcribs_directory(tmp_path):
13+
def make_tree(path, tree):
14+
for d, fls in tree.items():
15+
(path / d).mkdir(exist_ok=True)
16+
for f in fls:
17+
(path / d / f).touch()
18+
19+
root = tmp_path / 'mcribs'
20+
surfrecon = root / SUBJECT_ID / 'SurfReconDeformable' / SUBJECT_ID
21+
surfrecon.mkdir(parents=True, exist_ok=True)
22+
make_tree(surfrecon, MCRIBReconAll._expected_files['surfrecon'])
23+
autorecon = root / SUBJECT_ID / 'freesurfer' / SUBJECT_ID
24+
autorecon.mkdir(parents=True, exist_ok=True)
25+
make_tree(autorecon, MCRIBReconAll._expected_files['autorecon'])
26+
27+
yield root
28+
29+
shutil.rmtree(root)
30+
31+
32+
def test_MCRIBReconAll(mcribs_directory):
33+
t2w = Path('T2w.nii.gz')
34+
t2w.touch()
35+
36+
surfrecon = MCRIBReconAll(
37+
subject_id=SUBJECT_ID,
38+
surfrecon=True,
39+
surfrecon_method='Deformable',
40+
join_thresh=1.0,
41+
fast_collision=True,
42+
)
43+
44+
# Requires T2w input
45+
with pytest.raises(AttributeError):
46+
surfrecon.cmdline # noqa
47+
48+
surfrecon.inputs.t2w_file = t2w
49+
# Since no existing directory is found, will run fresh
50+
assert 'MCRIBReconAll --deformablefastcollision --deformablejointhresh' in surfrecon.cmdline
51+
52+
# But should not need to run again
53+
surfrecon.inputs.outdir = mcribs_directory
54+
assert surfrecon.cmdline == 'echo MCRIBReconAll: nothing to do'
55+
56+
t2w.unlink()
57+
58+
autorecon = MCRIBReconAll(
59+
subject_id=SUBJECT_ID,
60+
autorecon_after_surf=True,
61+
)
62+
# No need for T2w here
63+
assert autorecon.cmdline == 'MCRIBReconAll --autoreconaftersurf X'
64+
autorecon.inputs.outdir = mcribs_directory
65+
assert autorecon.cmdline == 'echo MCRIBReconAll: nothing to do'

nibabies/workflows/anatomical/fit.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,6 @@ def init_infant_anat_fit_wf(
10301030
surface_recon_wf = init_mcribs_surface_recon_wf(
10311031
omp_nthreads=omp_nthreads,
10321032
use_aseg=bool(anat_aseg),
1033-
use_mask=True,
10341033
precomputed=precomputed,
10351034
mcribs_dir=str(config.execution.mcribs_dir),
10361035
)
@@ -1040,10 +1039,8 @@ def init_infant_anat_fit_wf(
10401039
('subject_id', 'inputnode.subject_id'),
10411040
('subjects_dir', 'inputnode.subjects_dir'),
10421041
]),
1043-
(t2w_buffer, surface_recon_wf, [
1044-
('t2w_preproc', 'inputnode.t2w'),
1045-
('t2w_mask', 'inputnode.in_mask'),
1046-
]),
1042+
(t2w_validate, surface_recon_wf, [('out_file', 'inputnode.t2w')]),
1043+
(t2w_buffer, surface_recon_wf, [('t2w_mask', 'inputnode.in_mask'),]),
10471044
(aseg_buffer, surface_recon_wf, [
10481045
('anat_aseg', 'inputnode.in_aseg'),
10491046
]),
@@ -1950,7 +1947,6 @@ def init_infant_single_anat_fit_wf(
19501947
surface_recon_wf = init_mcribs_surface_recon_wf(
19511948
omp_nthreads=omp_nthreads,
19521949
use_aseg=bool(anat_aseg),
1953-
use_mask=True,
19541950
precomputed=precomputed,
19551951
mcribs_dir=str(config.execution.mcribs_dir),
19561952
)
@@ -1960,10 +1956,8 @@ def init_infant_single_anat_fit_wf(
19601956
('subject_id', 'inputnode.subject_id'),
19611957
('subjects_dir', 'inputnode.subjects_dir'),
19621958
]),
1963-
(anat_buffer, surface_recon_wf, [
1964-
('anat_preproc', 'inputnode.t2w'),
1965-
('anat_mask', 'inputnode.in_mask'),
1966-
]),
1959+
(anat_validate, surface_recon_wf, [('out_file', 'inputnode.t2w')]),
1960+
(anat_buffer, surface_recon_wf, [('anat_mask', 'inputnode.in_mask')]),
19671961
(aseg_buffer, surface_recon_wf, [
19681962
('anat_aseg', 'inputnode.in_aseg'),
19691963
]),

nibabies/workflows/anatomical/surfaces.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from nipype.interfaces import freesurfer as fs
77
from nipype.interfaces import io as nio
88
from nipype.interfaces import utility as niu
9+
from nipype.interfaces.ants import N4BiasFieldCorrection
910
from nipype.pipeline import engine as pe
1011
from niworkflows.engine.workflows import LiterateWorkflow
1112
from niworkflows.interfaces.freesurfer import (
@@ -14,6 +15,7 @@
1415
from niworkflows.interfaces.freesurfer import (
1516
PatchedRobustRegister as RobustRegister,
1617
)
18+
from niworkflows.interfaces.morphology import BinaryDilation
1719
from niworkflows.interfaces.patches import FreeSurferSource
1820
from smriprep.interfaces.freesurfer import MakeMidthickness
1921
from smriprep.interfaces.workbench import SurfaceResample
@@ -42,7 +44,6 @@ def init_mcribs_surface_recon_wf(
4244
*,
4345
omp_nthreads: int,
4446
use_aseg: bool,
45-
use_mask: bool,
4647
precomputed: dict,
4748
mcribs_dir: str | None = None,
4849
name: str = 'mcribs_surface_recon_wf',
@@ -119,7 +120,27 @@ def init_mcribs_surface_recon_wf(
119120
fs_to_mcribs = pe.Node(MapLabels(mappings=fs2mcribs), name='fs_to_mcribs')
120121

121122
t2w_las = pe.Node(ReorientImage(target_orientation='LAS'), name='t2w_las')
122-
seg_las = t2w_las.clone(name='seg_las')
123+
seg_las = pe.Node(ReorientImage(target_orientation='LAS'), name='seg_las')
124+
125+
# dilated mask and use in recon-neonatal-cortex
126+
mask_dil = pe.Node(BinaryDilation(radius=3), name='mask_dil')
127+
mask_las = pe.Node(ReorientImage(target_orientation='LAS'), name='mask_las')
128+
129+
# N4BiasCorrection occurs in MCRIBTissueSegMCRIBS (which is skipped)
130+
# Run it (with mask to rescale intensities) prior injection
131+
n4_mcribs = pe.Node(
132+
N4BiasFieldCorrection(
133+
dimension=3,
134+
bspline_fitting_distance=200,
135+
save_bias=True,
136+
copy_header=True,
137+
n_iterations=[50] * 5,
138+
convergence_threshold=1e-7,
139+
rescale_intensities=True,
140+
shrink_factor=4,
141+
),
142+
name='n4_mcribs',
143+
)
123144

124145
mcribs_recon = pe.Node(
125146
MCRIBReconAll(
@@ -128,43 +149,36 @@ def init_mcribs_surface_recon_wf(
128149
join_thresh=1.0,
129150
fast_collision=True,
130151
nthreads=omp_nthreads,
152+
outdir=mcribs_dir,
131153
),
132154
name='mcribs_recon',
133155
mem_gb=5,
134156
)
135-
if mcribs_dir:
136-
mcribs_recon.inputs.outdir = mcribs_dir
137-
mcribs_recon.config = {'execution': {'remove_unnecessary_outputs': False}}
138-
139-
if use_mask:
140-
# If available, dilated mask and use in recon-neonatal-cortex
141-
from niworkflows.interfaces.morphology import BinaryDilation
142-
143-
mask_dil = pe.Node(BinaryDilation(radius=3), name='mask_dil')
144-
mask_las = t2w_las.clone(name='mask_las')
145-
workflow.connect([
146-
(inputnode, mask_dil, [('in_mask', 'in_mask')]),
147-
(mask_dil, mask_las, [('out_mask', 'in_file')]),
148-
(mask_las, mcribs_recon, [('out_file', 'mask_file')]),
149-
]) # fmt:skip
157+
mcribs_recon.config = {'execution': {'remove_unnecessary_outputs': False}}
150158

151159
mcribs_postrecon = pe.Node(
152160
MCRIBReconAll(autorecon_after_surf=True, nthreads=omp_nthreads),
153161
name='mcribs_postrecon',
154162
mem_gb=5,
155163
)
164+
mcribs_postrecon.config = {'execution': {'remove_unnecessary_outputs': False}}
156165

157166
fssource = pe.Node(FreeSurferSource(), name='fssource', run_without_submitting=True)
158167
midthickness_wf = init_make_midthickness_wf(omp_nthreads=omp_nthreads)
159168

160169
workflow.connect([
161170
(inputnode, t2w_las, [('t2w', 'in_file')]),
162171
(inputnode, fs_to_mcribs, [('in_aseg', 'in_file')]),
172+
(inputnode, mask_dil, [('in_mask', 'in_mask')]),
173+
(mask_dil, mask_las, [('out_mask', 'in_file')]),
174+
(mask_las, mcribs_recon, [('out_file', 'mask_file')]),
163175
(fs_to_mcribs, seg_las, [('out_file', 'in_file')]),
164176
(inputnode, mcribs_recon, [
165177
('subjects_dir', 'subjects_dir'),
166178
('subject_id', 'subject_id')]),
167-
(t2w_las, mcribs_recon, [('out_file', 't2w_file')]),
179+
(t2w_las, n4_mcribs, [('out_file', 'input_image')]),
180+
(mask_las, n4_mcribs, [('out_file', 'mask_image')]),
181+
(n4_mcribs, mcribs_recon, [('output_image', 't2w_file')]),
168182
(seg_las, mcribs_recon, [('out_file', 'segmentation_file')]),
169183
(inputnode, mcribs_postrecon, [
170184
('subjects_dir', 'subjects_dir'),

0 commit comments

Comments
 (0)