Skip to content

Commit 4609eb9

Browse files
authored
Merge pull request #280 from mgxd/enh/mcribs-recon-all
ENH: Testing MCRIBS integration
2 parents 201662a + fd68a88 commit 4609eb9

File tree

6 files changed

+392
-2
lines changed

6 files changed

+392
-2
lines changed

Dockerfile

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ RUN apt-get update && \
66
COPY . /src/nibabies
77
RUN python -m build /src/nibabies
88

9-
# Ubuntu 20.04 LTS
9+
# Python to support legacy MCRIBS
10+
FROM python:3.6.15-slim as pyenv
11+
RUN pip install --no-cache-dir numpy nibabel scipy pandas numexpr contextlib2 \
12+
&& cp /usr/lib/x86_64-linux-gnu/libffi.so.7* /usr/local/lib
13+
14+
# Ubuntu 22.04 LTS
1015
FROM ubuntu:jammy-20221130
1116
ENV DEBIAN_FRONTEND="noninteractive" \
1217
LANG="en_US.UTF-8" \
@@ -264,6 +269,23 @@ RUN conda install -y -n base \
264269
&& rm -rf ~/.conda ~/.cache/pip/*; sync \
265270
&& ldconfig
266271

272+
# MCRIBS
273+
COPY --from=nipreps/mcribs@sha256:6c7a8dedd61d0ead8c7c4a57ab158928c1c1d787d87dae33ab7ee43226fb1e0f /opt/MCRIBS/ /opt/MCRIBS
274+
RUN apt-get update && apt-get install -y --no-install-recommends \
275+
libboost-dev \
276+
libeigen3-dev \
277+
libflann-dev \
278+
libgl1-mesa-dev \
279+
libglu1-mesa-dev \
280+
libssl-dev \
281+
libxt-dev \
282+
zlib1g-dev \
283+
&& rm -rf /var/lib/apt/lists/*
284+
ENV PATH="/opt/MCRIBS/bin:/opt/MCRIBS/MIRTK/MIRTK-install/bin:/opt/MCRIBS/MIRTK/MIRTK-install/lib/tools:${PATH}" \
285+
LD_LIBRARY_PATH="/opt/MCRIBS/lib:/opt/MCRIBS/ITK/ITK-install/lib:/opt/MCRIBS/VTK/VTK-install/lib:/opt/MCRIBS/MIRTK/MIRTK-install/lib:${LD_LIBRARY_PATH}" \
286+
MCRIBS_HOME="/opt/MCRIBS" \
287+
PYTHONPATH="/opt/MCRIBS/lib/python:$PYTHONPATH"
288+
267289
# Precaching atlases
268290
COPY scripts/fetch_templates.py fetch_templates.py
269291
RUN ${CONDA_PYTHON} fetch_templates.py && \
@@ -290,4 +312,6 @@ LABEL org.label-schema.build-date=$BUILD_DATE \
290312
org.label-schema.version=$VERSION \
291313
org.label-schema.schema-version="1.0"
292314

315+
COPY --from=pyenv /usr/local/lib/ /usr/local/lib/
316+
ENV LD_LIBRARY_PATH="/usr/local/lib:${LD_LIBRARY_PATH}"
293317
ENTRYPOINT ["/opt/conda/bin/nibabies"]

nibabies/cli/mcribs.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import os
2+
from argparse import ArgumentParser
3+
4+
import nipype.pipeline.engine as pe
5+
from niworkflows.interfaces.nibabel import MapLabels
6+
7+
from nibabies.interfaces.mcribs import MCRIBReconAll
8+
9+
10+
def _parser():
11+
parser = ArgumentParser(description="Test script for MCRIBS surfaces")
12+
parser.add_argument('subject', help='Subject ID')
13+
parser.add_argument('t2w', type=os.path.abspath, help='Input T2w (radioisotropic)')
14+
parser.add_argument(
15+
'segmentation', type=os.path.abspath, help='Input anatomical segmentation in T2w space'
16+
)
17+
parser.add_argument(
18+
'--outdir', type=os.path.abspath, help='Output directory to persist MCRIBS output'
19+
)
20+
parser.add_argument('--nthreads', type=int, help='Number of threads to parallelize tasks')
21+
return parser
22+
23+
24+
def main(argv: list = None):
25+
pargs = _parser().parse_args(argv)
26+
27+
t2w_file = _check_file(pargs.t2w)
28+
seg_file = _check_file(pargs.segmentation)
29+
30+
aseg2mcrib = {
31+
2: 51,
32+
3: 21,
33+
4: 49,
34+
5: 0,
35+
7: 17,
36+
8: 17,
37+
10: 43,
38+
11: 41,
39+
12: 47,
40+
13: 47,
41+
14: 0,
42+
15: 0,
43+
16: 19,
44+
17: 1,
45+
18: 3,
46+
26: 41,
47+
28: 45,
48+
31: 49,
49+
41: 52,
50+
42: 20,
51+
43: 50,
52+
44: 0,
53+
46: 18,
54+
47: 18,
55+
49: 42,
56+
50: 40,
57+
51: 46,
58+
52: 46,
59+
53: 2,
60+
54: 4,
61+
58: 40,
62+
60: 44,
63+
63: 50,
64+
253: 48,
65+
}
66+
map_labels = pe.Node(MapLabels(in_file=seg_file, mappings=aseg2mcrib), name='map_labels')
67+
68+
recon = pe.Node(
69+
MCRIBReconAll(subject_id=pargs.subject, t2w_file=t2w_file), name='mcribs_recon'
70+
)
71+
if pargs.outdir:
72+
recon.inputs.outdir = pargs.outdir
73+
if pargs.nthreads:
74+
recon.inputs.nthreads = pargs.nthreads
75+
76+
wf = pe.Workflow(f'MRA_{pargs.subject}')
77+
wf.connect(map_labels, 'out_file', recon, 'segmentation_file')
78+
wf.run()
79+
80+
81+
def _check_file(fl: str) -> str:
82+
import nibabel as nb
83+
import numpy as np
84+
85+
img = nb.load(fl)
86+
if len(img.shape) != 3:
87+
raise ValueError('Image {fl} is not 3 dimensional.')
88+
89+
voxdims = img.header['pixdim'][1:4]
90+
if not np.allclose(voxdims, voxdims[1]):
91+
raise ValueError(f'Image {fl} is not isotropic: {voxdims}.')
92+
93+
ornt = nb.io_orientation(img.affine)
94+
axcodes = nb.orientations.ornt2axcodes(ornt)
95+
if ''.join(axcodes) != 'LAS':
96+
las = nb.orientations.axcodes2ornt('LAS')
97+
transform = nb.orientations.ornt_transform(ornt, las)
98+
reornt = img.as_reoriented(transform)
99+
outfl = os.path.abspath(f'LASornt_{os.path.basename(fl)}')
100+
print(f'Creating reorientated image {outfl}')
101+
reornt.to_filename(outfl)
102+
return outfl
103+
return fl
104+
105+
106+
if __name__ == '__main__':
107+
main()

nibabies/interfaces/mcribs.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import shutil
2+
from pathlib import Path
3+
4+
from nipype.interfaces.base import (
5+
CommandLine,
6+
CommandLineInputSpec,
7+
Directory,
8+
File,
9+
TraitedSpec,
10+
traits,
11+
)
12+
13+
14+
class MCRIBReconAllInputSpec(CommandLineInputSpec):
15+
# Input structure massaging
16+
outdir = Directory(
17+
exists=True,
18+
hash_files=False,
19+
desc='Path to save output, or path of existing MCRIBS output',
20+
)
21+
subjects_dir = Directory(
22+
exists=True,
23+
hash_files=False,
24+
desc='Path to FreeSurfer subjects directory',
25+
)
26+
subject_id = traits.Str(
27+
required=True,
28+
argstr='%s',
29+
position=-1,
30+
desc='Subject ID',
31+
)
32+
t1w_file = File(
33+
exists=True,
34+
copyfile=True,
35+
desc='T1w to be used for deformable (must be registered to T2w image)',
36+
)
37+
t2w_file = File(
38+
exists=True,
39+
required=True,
40+
copyfile=True,
41+
desc='T2w (Isotropic + N4 corrected)',
42+
)
43+
segmentation_file = File(
44+
desc='Segmentation file (skips tissue segmentation)',
45+
)
46+
47+
# MCRIBS options
48+
conform = traits.Bool(
49+
argstr='--conform',
50+
desc='Reorients to radiological, axial slice orientation. Resamples to isotropic voxels',
51+
)
52+
tissueseg = traits.Bool(
53+
argstr='--tissueseg',
54+
desc='Perform tissue type segmentation',
55+
)
56+
surfrecon = traits.Bool(
57+
True,
58+
usedefault=True,
59+
argstr='--surfrecon',
60+
desc='Reconstruct surfaces',
61+
)
62+
surfrecon_method = traits.Enum(
63+
'Deformable',
64+
argstr='--surfreconmethod %s',
65+
usedefault=True,
66+
desc='Surface reconstruction method',
67+
)
68+
join_thresh = traits.Float(
69+
1.0,
70+
argstr='--deformablejointhresh %f',
71+
usedefault=True,
72+
desc='Join threshold parameter for Deformable',
73+
)
74+
fast_collision = traits.Bool(
75+
True,
76+
argstr='--deformablefastcollision',
77+
usedefault=True,
78+
desc='Use Deformable fast collision test',
79+
)
80+
autorecon_after_surf = traits.Bool(
81+
True,
82+
argstr='--autoreconaftersurf',
83+
usedefault=True,
84+
desc='Do all steps after surface reconstruction',
85+
)
86+
segstats = traits.Bool(
87+
True,
88+
argstr='--segstats',
89+
usedefault=True,
90+
desc='Compute statistics on segmented volumes',
91+
)
92+
nthreads = traits.Int(
93+
argstr='-nthreads %d',
94+
desc='Number of threads for multithreading applications',
95+
)
96+
97+
98+
class MCRIBReconAllOutputSpec(TraitedSpec):
99+
mcribs_dir = Directory(desc='MCRIBS output directory')
100+
101+
102+
class MCRIBReconAll(CommandLine):
103+
_cmd = 'MCRIBReconAll'
104+
input_spec = MCRIBReconAllInputSpec
105+
output_spec = MCRIBReconAllOutputSpec
106+
_no_run = False
107+
108+
@property
109+
def cmdline(self):
110+
cmd = super().cmdline
111+
# Avoid processing if valid
112+
if self.inputs.outdir:
113+
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
118+
if self._no_run:
119+
return "echo MCRIBSReconAll: nothing to do"
120+
return cmd
121+
122+
def _setup_directory_structure(self, mcribs_dir: Path) -> None:
123+
'''
124+
Create the required structure for skipping steps.
125+
126+
The directory tree
127+
------------------
128+
129+
<subject_id>/
130+
├── RawT2
131+
│ └── <subject_id>.nii.gz
132+
├── SurfReconDeformable
133+
│ └── <subject_id>
134+
│ └── temp
135+
│ └── t2w-image.nii.gz
136+
├── TissueSeg
137+
│ ├── <subject_id>_all_labels.nii.gz
138+
│ └── <subject_id>_all_labels_manedit.nii.gz
139+
└── TissueSegDrawEM
140+
└── <subject_id>
141+
└── N4
142+
└── <subject_id>.nii.gz
143+
'''
144+
sid = self.inputs.subject_id
145+
mkdir_kw = {'parents': True, 'exist_ok': True}
146+
root = mcribs_dir / sid
147+
root.mkdir(**mkdir_kw)
148+
149+
# 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')
165+
166+
# Segmentation
167+
if self.inputs.segmentation_file:
168+
# TissueSeg directive disabled
169+
tisseg = root / 'TissueSeg' / f'{sid}_all_labels.nii.gz'
170+
tisseg.parent.mkdir(**mkdir_kw)
171+
if not tisseg.exists():
172+
shutil.copy(self.inputs.segmentation_file, str(tisseg))
173+
manedit = tisseg.parent / f'{sid}_all_labels_manedit.nii.gz'
174+
if not manedit.exists():
175+
manedit.symlink_to(tisseg.name)
176+
177+
if self.inputs.surfrecon:
178+
t2wseg = root / 'TissueSeg' / f'{sid}_t2w_restore.nii.gz'
179+
if not t2wseg.exists():
180+
t2wseg.symlink_to(f'../RawT2/{sid}.nii.gz')
181+
182+
surfrec = root / 'SurfReconDeformable' / sid / 'temp' / 't2w-image.nii.gz'
183+
surfrec.parent.mkdir(**mkdir_kw)
184+
if not surfrec.exists():
185+
surfrec.symlink_to(f'../../../RawT2/{sid}.nii.gz')
186+
# TODO?: T1w -> <subject_id>/RawT1RadiologicalIsotropic/<subjectid>.nii.gz
187+
return
188+
189+
def _run_interface(self, runtime):
190+
# if users wish to preserve their runs
191+
mcribs_dir = self.inputs.outdir or Path(runtime.cwd) / 'mcribs'
192+
self._mcribs_dir = Path(mcribs_dir)
193+
self._setup_directory_structure(self._mcribs_dir)
194+
# overwrite CWD to be in MCRIB subject's directory
195+
runtime.cwd = str(self._mcribs_dir / self.inputs.subject_id)
196+
return super()._run_interface(runtime)
197+
198+
def _list_outputs(self):
199+
outputs = self._outputs().get()
200+
outputs['mcribs_dir'] = str(self._mcribs_dir)
201+
202+
# Copy freesurfer directory into FS subjects dir
203+
sid = self.inputs.subject_id
204+
mcribs_fs = self._mcribs_dir / sid / 'freesurfer' / sid
205+
if mcribs_fs.exists() and self.inputs.subjects_dir:
206+
dst = Path(self.inputs.subjects_dir) / self.inputs.subject_id
207+
if not dst.exists():
208+
shutil.copytree(mcribs_fs, dst)
209+
210+
return outputs

nibabies/workflows/anatomical/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def init_infant_anat_wf(
147147
"surfaces",
148148
"morphometrics",
149149
"anat_aseg",
150+
"anat_mcrib",
150151
"anat_aparc",
151152
"template",
152153
]

0 commit comments

Comments
 (0)