Skip to content

Commit c695f4c

Browse files
authored
Merge pull request #50 from josephmje/enh/skullstrip
ENH: b0 reference and skullstrip workflow
2 parents 5bd7fcf + 51da317 commit c695f4c

File tree

11 files changed

+687
-107
lines changed

11 files changed

+687
-107
lines changed

.circleci/config.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,24 @@ jobs:
251251
key: THP002-anat-v00-{{ .Branch }}-{{ .Revision }}-{{ epoch }}
252252
paths:
253253
- /tmp/THP002/work
254+
- run:
255+
name: Run full diffusion workflow on THP002
256+
no_output_timeout: 2h
257+
command: |
258+
mkdir -p /tmp/THP002/work /tmp/THP002/derivatives
259+
sudo setfacl -d -m group:$(id -gn):rwx /tmp/THP002/derivatives && \
260+
sudo setfacl -m group:$(id -gn):rwx /tmp/THP002/derivatives
261+
sudo setfacl -d -m group:$(id -gn):rwx /tmp/THP002/work && \
262+
sudo setfacl -m group:$(id -gn):rwx /tmp/THP002/work
263+
docker run -e FS_LICENSE=$FS_LICENSE --rm \
264+
-v /tmp/data/THP002:/data \
265+
-v /tmp/THP002/derivatives:/out \
266+
-v /tmp/fslicense/license.txt:/tmp/fslicense/license.txt:ro \
267+
-v /tmp/config/nipype.cfg:/home/dmriprep/.nipype/nipype.cfg \
268+
-v /tmp/THP002/work:/work \
269+
--user $(id -u):$(id -g) \
270+
nipreps/dmriprep:latest /data /out participant -vv --sloppy \
271+
--notrack --skip-bids-validation -w /work --omp-nthreads 2 --nprocs 2
254272
- store_artifacts:
255273
path: /tmp/THP002/derivatives/dmriprep
256274
- run:

dmriprep/config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
33
"""Settings."""
44

5+
DEFAULT_MEMORY_MIN_GB = 0.01
56
NONSTANDARD_REFERENCES = ['anat', 'T1w', 'dwi', 'fsnative']

dmriprep/config/reports-spec.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ sections:
2626
caption: Surfaces (white and pial) reconstructed with FreeSurfer (<code>recon-all</code>)
2727
overlaid on the participant's T1w template.
2828
subtitle: Surface reconstruction
29+
- name: Diffusion
30+
ordering: session,acquisition,run
31+
reportlets:
32+
- bids: {datatype: dwi, desc: validation, suffix: dwi}
33+
- bids: {datatype: dwi, desc: brain, suffix: mask}
34+
caption: Average b=0 that serves for reference in early preprocessing steps.
35+
descriptions: The reference b=0 is obtained as the voxel-wise median across
36+
all b=0 found in the dataset, after accounting for signal drift.
37+
The red contour shows the brain mask calculated using this reference b=0.
38+
subtitle: Reference b=0 and brain mask
2939
- name: About
3040
reportlets:
3141
- bids: {datatype: anat, desc: about, suffix: T1w}

dmriprep/data/tests/dwi_mask.nii.gz

Whitespace-only changes.

dmriprep/interfaces/images.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Image tools interfaces."""
2+
import numpy as np
3+
import nibabel as nb
4+
from nipype.utils.filemanip import fname_presuffix
5+
from nipype import logging
6+
from nipype.interfaces.base import (
7+
traits, TraitedSpec, BaseInterfaceInputSpec, SimpleInterface, File
8+
)
9+
10+
LOGGER = logging.getLogger('nipype.interface')
11+
12+
13+
class _ExtractB0InputSpec(BaseInterfaceInputSpec):
14+
in_file = File(exists=True, mandatory=True, desc='dwi file')
15+
b0_ixs = traits.List(traits.Int, mandatory=True,
16+
desc='Index of b0s')
17+
18+
19+
class _ExtractB0OutputSpec(TraitedSpec):
20+
out_file = File(exists=True, desc='b0 file')
21+
22+
23+
class ExtractB0(SimpleInterface):
24+
"""
25+
Extract all b=0 volumes from a dwi series.
26+
27+
Example
28+
-------
29+
>>> os.chdir(tmpdir)
30+
>>> extract_b0 = ExtractB0()
31+
>>> extract_b0.inputs.in_file = str(data_dir / 'dwi.nii.gz')
32+
>>> extract_b0.inputs.b0_ixs = [0, 1, 2]
33+
>>> res = extract_b0.run() # doctest: +SKIP
34+
35+
"""
36+
37+
input_spec = _ExtractB0InputSpec
38+
output_spec = _ExtractB0OutputSpec
39+
40+
def _run_interface(self, runtime):
41+
self._results['out_file'] = extract_b0(
42+
self.inputs.in_file,
43+
self.inputs.b0_ixs,
44+
newpath=runtime.cwd)
45+
return runtime
46+
47+
48+
def extract_b0(in_file, b0_ixs, newpath=None):
49+
"""Extract the *b0* volumes from a DWI dataset."""
50+
out_file = fname_presuffix(
51+
in_file, suffix='_b0', newpath=newpath)
52+
53+
img = nb.load(in_file)
54+
data = img.get_fdata(dtype='float32')
55+
56+
b0 = data[..., b0_ixs]
57+
58+
hdr = img.header.copy()
59+
hdr.set_data_shape(b0.shape)
60+
hdr.set_xyzt_units('mm')
61+
hdr.set_data_dtype(np.float32)
62+
nb.Nifti1Image(b0, img.affine, hdr).to_filename(out_file)
63+
return out_file
64+
65+
66+
class _RescaleB0InputSpec(BaseInterfaceInputSpec):
67+
in_file = File(exists=True, mandatory=True, desc='b0s file')
68+
mask_file = File(exists=True, mandatory=True, desc='mask file')
69+
70+
71+
class _RescaleB0OutputSpec(TraitedSpec):
72+
out_ref = File(exists=True, desc='One average b0 file')
73+
out_b0s = File(exists=True, desc='series of rescaled b0 volumes')
74+
75+
76+
class RescaleB0(SimpleInterface):
77+
"""
78+
Rescale the b0 volumes to deal with average signal decay over time.
79+
80+
Example
81+
-------
82+
>>> os.chdir(tmpdir)
83+
>>> rescale_b0 = RescaleB0()
84+
>>> rescale_b0.inputs.in_file = str(data_dir / 'dwi.nii.gz')
85+
>>> rescale_b0.inputs.mask_file = str(data_dir / 'dwi_mask.nii.gz')
86+
>>> res = rescale_b0.run() # doctest: +SKIP
87+
88+
"""
89+
90+
input_spec = _RescaleB0InputSpec
91+
output_spec = _RescaleB0OutputSpec
92+
93+
def _run_interface(self, runtime):
94+
self._results['out_b0s'] = rescale_b0(
95+
self.inputs.in_file,
96+
self.inputs.mask_file,
97+
newpath=runtime.cwd
98+
)
99+
self._results['out_ref'] = median(
100+
self._results['out_b0s'],
101+
newpath=runtime.cwd
102+
)
103+
return runtime
104+
105+
106+
def rescale_b0(in_file, mask_file, newpath=None):
107+
"""Rescale the input volumes using the median signal intensity."""
108+
out_file = fname_presuffix(
109+
in_file, suffix='_rescaled_b0', newpath=newpath)
110+
111+
img = nb.load(in_file)
112+
if img.dataobj.ndim == 3:
113+
return in_file
114+
115+
data = img.get_fdata(dtype='float32')
116+
mask_img = nb.load(mask_file)
117+
mask_data = mask_img.get_fdata(dtype='float32')
118+
119+
median_signal = np.median(data[mask_data > 0, ...], axis=0)
120+
rescaled_data = 1000 * data / median_signal
121+
hdr = img.header.copy()
122+
nb.Nifti1Image(rescaled_data, img.affine, hdr).to_filename(out_file)
123+
return out_file
124+
125+
126+
def median(in_file, newpath=None):
127+
"""Average a 4D dataset across the last dimension using median."""
128+
out_file = fname_presuffix(
129+
in_file, suffix='_b0ref', newpath=newpath)
130+
131+
img = nb.load(in_file)
132+
if img.dataobj.ndim == 3:
133+
return in_file
134+
if img.shape[-1] == 1:
135+
nb.squeeze_image(img).to_filename(out_file)
136+
return out_file
137+
138+
median_data = np.median(img.get_fdata(dtype='float32'),
139+
axis=-1)
140+
141+
hdr = img.header.copy()
142+
hdr.set_xyzt_units('mm')
143+
hdr.set_data_dtype(np.float32)
144+
nb.Nifti1Image(median_data, img.affine, hdr).to_filename(out_file)
145+
return out_file

dmriprep/interfaces/reports.py

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,6 @@
2323
\t</ul>
2424
"""
2525

26-
DWI_TEMPLATE = """\t\t<h3 class="elem-title">Summary</h3>
27-
\t\t<ul class="elem-desc">
28-
\t\t\t<li>Susceptibility distortion correction: {sdc}</li>
29-
\t\t\t<li>Registration: {registration}</li>
30-
\t\t</ul>
31-
"""
32-
3326
ABOUT_TEMPLATE = """\t<ul>
3427
\t\t<li>dMRIPrep version: {version}</li>
3528
\t\t<li>dMRIPrep command: <code>{command}</code></li>
@@ -128,39 +121,3 @@ def _generate_segment(self):
128121
return ABOUT_TEMPLATE.format(version=self.inputs.version,
129122
command=self.inputs.command,
130123
date=time.strftime("%Y-%m-%d %H:%M:%S %z"))
131-
132-
133-
class DiffusionSummaryInputSpec(BaseInterfaceInputSpec):
134-
distortion_correction = traits.Str(desc='Susceptibility distortion correction method',
135-
mandatory=True)
136-
pe_direction = traits.Enum(None, 'i', 'i-', 'j', 'j-', mandatory=True,
137-
desc='Phase-encoding direction detected')
138-
registration = traits.Enum('FSL', 'FreeSurfer', mandatory=True,
139-
desc='Diffusion/anatomical registration method')
140-
fallback = traits.Bool(desc='Boundary-based registration rejected')
141-
registration_dof = traits.Enum(6, 9, 12, desc='Registration degrees of freedom',
142-
mandatory=True)
143-
144-
145-
class DiffusionSummary(SummaryInterface):
146-
input_spec = DiffusionSummaryInputSpec
147-
148-
def _generate_segment(self):
149-
dof = self.inputs.registration_dof
150-
reg = {
151-
'FSL': [
152-
'FSL <code>flirt</code> with boundary-based registration'
153-
' (BBR) metric - %d dof' % dof,
154-
'FSL <code>flirt</code> rigid registration - 6 dof'],
155-
'FreeSurfer': [
156-
'FreeSurfer <code>bbregister</code> '
157-
'(boundary-based registration, BBR) - %d dof' % dof,
158-
'FreeSurfer <code>mri_coreg</code> - %d dof' % dof],
159-
}[self.inputs.registration][self.inputs.fallback]
160-
if self.inputs.pe_direction is None:
161-
pedir = 'MISSING - Assuming Anterior-Posterior'
162-
else:
163-
pedir = {'i': 'Left-Right', 'j': 'Anterior-Posterior'}[self.inputs.pe_direction[0]]
164-
165-
return DWI_TEMPLATE.format(
166-
pedir=pedir, sdc=self.inputs.distortion_correction, registration=reg)

dmriprep/workflows/base.py

Lines changed: 38 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
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-
dMRIPrep base processing workflows.
5-
6-
.. autofunction:: init_dmriprep_wf
7-
.. autofunction:: init_single_subject_wf
8-
9-
"""
10-
1+
"""dMRIPrep base processing workflows."""
112
import sys
123
import os
134
from packaging.version import Version
@@ -29,7 +20,7 @@
2920
from ..interfaces.reports import SubjectSummary, AboutSummary
3021
from ..utils.bids import collect_data
3122
from ..__about__ import __version__
32-
# from .dwi import init_dwi_preproc_wf
23+
from .dwi import init_dwi_preproc_wf
3324

3425

3526
def init_dmriprep_wf(
@@ -435,59 +426,42 @@ def init_single_subject_wf(
435426
if anat_only:
436427
return workflow
437428

438-
# for dwi_file in subject_data['dwi']:
439-
# dwi_preproc_wf = init_dwi_preproc_wf(
440-
# aroma_melodic_dim=aroma_melodic_dim,
441-
# bold2t1w_dof=bold2t1w_dof,
442-
# bold_file=bold_file,
443-
# cifti_output=cifti_output,
444-
# debug=debug,
445-
# dummy_scans=dummy_scans,
446-
# err_on_aroma_warn=err_on_aroma_warn,
447-
# fmap_bspline=fmap_bspline,
448-
# fmap_demean=fmap_demean,
449-
# force_syn=force_syn,
450-
# freesurfer=freesurfer,
451-
# ignore=ignore,
452-
# layout=layout,
453-
# low_mem=low_mem,
454-
# medial_surface_nan=medial_surface_nan,
455-
# num_bold=len(subject_data['bold']),
456-
# omp_nthreads=omp_nthreads,
457-
# output_dir=output_dir,
458-
# output_spaces=output_spaces,
459-
# reportlets_dir=reportlets_dir,
460-
# regressors_all_comps=regressors_all_comps,
461-
# regressors_fd_th=regressors_fd_th,
462-
# regressors_dvars_th=regressors_dvars_th,
463-
# t2s_coreg=t2s_coreg,
464-
# use_aroma=use_aroma,
465-
# use_syn=use_syn,
466-
# )
467-
468-
# workflow.connect([
469-
# (anat_preproc_wf, dwi_preproc_wf,
470-
# [(('outputnode.t1_preproc', _pop), 'inputnode.t1_preproc'),
471-
# ('outputnode.t1_brain', 'inputnode.t1_brain'),
472-
# ('outputnode.t1_mask', 'inputnode.t1_mask'),
473-
# ('outputnode.t1_seg', 'inputnode.t1_seg'),
474-
# ('outputnode.t1_aseg', 'inputnode.t1_aseg'),
475-
# ('outputnode.t1_aparc', 'inputnode.t1_aparc'),
476-
# ('outputnode.t1_tpms', 'inputnode.t1_tpms'),
477-
# ('outputnode.template', 'inputnode.template'),
478-
# ('outputnode.forward_transform', 'inputnode.anat2std_xfm'),
479-
# ('outputnode.reverse_transform', 'inputnode.std2anat_xfm'),
480-
# ('outputnode.joint_template', 'inputnode.joint_template'),
481-
# ('outputnode.joint_forward_transform', 'inputnode.joint_anat2std_xfm'),
482-
# ('outputnode.joint_reverse_transform', 'inputnode.joint_std2anat_xfm'),
483-
# # Undefined if --no-freesurfer, but this is safe
484-
# ('outputnode.subjects_dir', 'inputnode.subjects_dir'),
485-
# ('outputnode.subject_id', 'inputnode.subject_id'),
486-
# ('outputnode.t1_2_fsnative_forward_transform',
487-
# 'inputnode.t1_2_fsnative_forward_transform'),
488-
# ('outputnode.t1_2_fsnative_reverse_transform',
489-
# 'inputnode.t1_2_fsnative_reverse_transform')]),
490-
# ])
429+
for dwi_file in subject_data['dwi']:
430+
dwi_preproc_wf = init_dwi_preproc_wf(
431+
dwi_file=dwi_file,
432+
debug=debug,
433+
force_syn=force_syn,
434+
ignore=ignore,
435+
layout=layout,
436+
low_mem=low_mem,
437+
num_dwi=len(subject_data['dwi']),
438+
omp_nthreads=omp_nthreads,
439+
output_dir=output_dir,
440+
reportlets_dir=reportlets_dir,
441+
use_syn=use_syn,
442+
)
443+
444+
workflow.connect([
445+
(anat_preproc_wf, dwi_preproc_wf,
446+
[(('outputnode.t1w_preproc', _pop), 'inputnode.t1w_preproc'),
447+
('outputnode.t1w_brain', 'inputnode.t1w_brain'),
448+
('outputnode.t1w_mask', 'inputnode.t1w_mask'),
449+
('outputnode.t1w_dseg', 'inputnode.t1w_dseg'),
450+
('outputnode.t1w_aseg', 'inputnode.t1w_aseg'),
451+
('outputnode.t1w_aparc', 'inputnode.t1w_aparc'),
452+
('outputnode.t1w_tpms', 'inputnode.t1w_tpms'),
453+
('outputnode.template', 'inputnode.template'),
454+
('outputnode.anat2std_xfm', 'inputnode.anat2std_xfm'),
455+
('outputnode.std2anat_xfm', 'inputnode.std2anat_xfm'),
456+
('outputnode.joint_template', 'inputnode.joint_template'),
457+
('outputnode.joint_anat2std_xfm', 'inputnode.joint_anat2std_xfm'),
458+
('outputnode.joint_std2anat_xfm', 'inputnode.joint_std2anat_xfm'),
459+
# Undefined if --fs-no-reconall, but this is safe
460+
('outputnode.subjects_dir', 'inputnode.subjects_dir'),
461+
('outputnode.subject_id', 'inputnode.subject_id'),
462+
('outputnode.t1w2fsnative_xfm', 'inputnode.t1w2fsnative_xfm'),
463+
('outputnode.fsnative2t1w_xfm', 'inputnode.fsnative2t1w_xfm')]),
464+
])
491465

492466
return workflow
493467

dmriprep/workflows/dwi/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Pre-processing dMRI workflows."""
2+
3+
from .base import init_dwi_preproc_wf
4+
from .util import init_dwi_reference_wf
5+
6+
__all__ = [
7+
'init_dwi_preproc_wf',
8+
'init_dwi_reference_wf',
9+
]

0 commit comments

Comments
 (0)