Skip to content

[RTM] Add recon-all preprocessing #347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Feb 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1df5d78
Add CLI option to disable freesurfer analyses
effigies Jan 20, 2017
f2b36aa
Initial ReconAll wire-up
effigies Jan 26, 2017
90d13f9
Use all available processors for ReconAll
effigies Jan 26, 2017
f5e8221
Turn FreeSurfer off in CircleCI tests
effigies Jan 27, 2017
19123e5
Require -noskullstrip in second recon-all
effigies Jan 27, 2017
195fda6
Add 'freesurfer' to mock settings
effigies Jan 30, 2017
bc94ae8
Use more standard nipype options
effigies Jan 30, 2017
87d1731
Do not symlink skullstripped file
effigies Jan 30, 2017
50138ae
Add source_file to recon_report
effigies Jan 30, 2017
9343495
Ignore more segfaulting sections, update out_report name
effigies Jan 31, 2017
0e03a9b
Resample skullstripped image to T1.mgz space
effigies Feb 3, 2017
6280127
Segfaults fixed with T1-sized brainmask
effigies Feb 4, 2017
067acaa
Check T1 dimensions
effigies Feb 5, 2017
05b06d7
Pull out BIDS label matching
effigies Feb 7, 2017
d45effc
Copy FreeSurfer recon results into bids_root/derivatives/freesurfer
effigies Feb 7, 2017
e9b15fa
Add Surface Reconstruction entry to report
effigies Feb 7, 2017
2cb691c
Add parallel option
effigies Feb 8, 2017
cba1e40
Use output_dir, not bids_root for freesurfer derivative
effigies Feb 8, 2017
cdcbcdf
DOC: Rename ds005->workflows, fix doc build
effigies Feb 10, 2017
a7d02e3
Start recon-all documentation
effigies Feb 10, 2017
d07e0a9
Update ignore files
effigies Feb 10, 2017
5eff2f5
Bookkeeping
effigies Feb 10, 2017
36f2933
Use stripped down beast/sub-387 sample report (fix typo)
effigies Feb 11, 2017
0df1930
Regenerate reconall.svg with inkscape to clean up metadata
effigies Feb 13, 2017
d378ea7
Copy fsaverage into derivatives/freesurfer
effigies Feb 13, 2017
51ec1f1
Update pins
effigies Feb 13, 2017
464f238
Add BIDSFreeSurferDir interface
effigies Feb 14, 2017
38032ef
RF: Create subjects_dir at top of workflow, pipe through
effigies Feb 14, 2017
2ab235b
Overwrite (or leave untouched) existing fsaverage
effigies Feb 15, 2017
90c4677
Do not overwrite brainmasks, if they exist
effigies Feb 16, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ coverage.xml

# Sphinx documentation
docs/_build/
build/

# PyBuilder
target/
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,5 @@ the ``fmriprep`` distribution.
All trademarks referenced herein are property of their respective
holders.

Copyright (c) 2015-2016, the fmriprep developers and the CRN.
Copyright (c) 2015-2017, the fmriprep developers and the CRN.
All rights reserved.
4 changes: 2 additions & 2 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ dependencies:
test:
override:
- docker run -ti --rm --entrypoint="python" poldracklab/fmriprep:latest -m unittest discover test
- docker run -ti --rm -v $HOME/nipype.cfg:/root/.nipype/nipype.cfg:ro -v $HOME/data:/data:ro -v $HOME/ds054/scratch:/scratch -v $HOME/ds054/out:/out poldracklab/fmriprep:latest /data/ds054 /out/ participant --debug -w /scratch:
- docker run -ti --rm -v $HOME/nipype.cfg:/root/.nipype/nipype.cfg:ro -v $HOME/data:/data:ro -v $HOME/ds054/scratch:/scratch -v $HOME/ds054/out:/out poldracklab/fmriprep:latest /data/ds054 /out/ participant --no-freesurfer --debug -w /scratch:
timeout: 4800
- docker run -ti --rm -v $HOME/nipype.cfg:/root/.nipype/nipype.cfg:ro -v $HOME/data:/data:ro -v $HOME/ds005/scratch:/scratch -v $HOME/ds005/out:/out poldracklab/fmriprep:latest /data/ds005 /out/ participant --debug -w /scratch:
- docker run -ti --rm -v $HOME/nipype.cfg:/root/.nipype/nipype.cfg:ro -v $HOME/data:/data:ro -v $HOME/ds005/scratch:/scratch -v $HOME/ds005/out:/out poldracklab/fmriprep:latest /data/ds005 /out/ participant --no-freesurfer --debug -w /scratch:
timeout: 4800
- find ~/ds054/scratch -not -name "*.svg" -not -name "*.html" -not -name "*.svg" -not -name "*.rst" -type f -delete
- find ~/ds005/scratch -not -name "*.svg" -not -name "*.html" -not -name "*.svg" -not -name "*.rst" -type f -delete
Expand Down
4 changes: 4 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.png
*.dot
etc/
args.txt
3,414 changes: 3,414 additions & 0 deletions docs/_static/reconall.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
246,263 changes: 7,549 additions & 238,714 deletions docs/_static/sample_report.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The :abbr:`CRN (Center for Reproducible Neuroscience)` developers team:
* Ross Blair
* Shoshana Berleant
* Oscar Esteban
* Christopher J. Markiewicz
* Russell A. Poldrack

Poldrack Lab, Psychology Department, Stanford University.
Expand Down
8 changes: 5 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,11 @@ def fake_set_up():
fake_set_up()

ds005_wf = wf_ds005_type({'func': 'fake data'}, {'ants_nthreads': 1,
'nthreads': 1,
'output_dir': 'x',
'biggest_epi_file_size_gb': 1,
'skip_native': True})
'skip_native': True,
'freesurfer': True})

sub_wfs = {name.split('.')[0] for name in ds005_wf.list_node_names()} # get only first-level nodes/workflows
ds005_workflows = {name: ds005_wf.get_node(name) for name in sub_wfs}
Expand All @@ -341,5 +343,5 @@ def fake_set_up():
args = subprocess.Popen([os.path.abspath(os.path.join('../fmriprep',
'run_workflow.py')),
'-h'],
stdout=subprocess.PIPE).communicate()[0]
fp.write(args)
stdout=subprocess.PIPE).communicate()[0].decode()
fp.write(args)
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ Contents

installation
usage
ds005
workflows
contributors
2 changes: 1 addition & 1 deletion docs/links.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
.. _BIDS: http://bids.neuroimaging.io
.. _Usage: usage.html
.. _Installation: installation.html
.. _workflows: ds005.html
.. _workflows: workflows.html
22 changes: 18 additions & 4 deletions docs/ds005.rst → docs/workflows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ Workflows
Basic workflow (no fieldmaps)
=============================

``fmriprep``'s basic pipeline is used on datasets for which there are only t1ws and at least one functional (EPI) file, but no SBRefs or fieldmaps.
To force using this pipeline on datasets that do include fieldmaps and SBRefs use the `--ignore fieldmaps` flag.
``fmriprep``'s basic pipeline is used on datasets for which there are only t1ws
and at least one functional (EPI) file, but no SBRefs or fieldmaps.
To force using this pipeline on datasets that do include fieldmaps and SBRefs
use the ``--ignore fieldmaps`` flag.

What It Does
------------
Expand All @@ -20,15 +22,15 @@ High-level view of the basic pipeline:
BIDSDatasource
~~~~~~~~~~~~~~

This node reads the BIDS_-formatted t1 data.
This node reads the BIDS_-formatted T1 data.

t1w_preprocessing
~~~~~~~~~~~~~~~~~

.. image:: t1w_preprocessing.dot.png
:scale: 100%

The t1w_preprocessing sub-workflow finds the skull stripping mask and the
The ``t1w_preprocessing`` sub-workflow finds the skull stripping mask and the
white matter/gray matter/cerebrospinal fluid segments and finds a non-linear
warp to the MNI space.

Expand All @@ -47,6 +49,15 @@ warp to the MNI space.

Animation showing T1 to MNI normalization (ANTs)

Additionally, FreeSurfer surfaces are reconstructed from T1-weighted structural
image(s), using the ANTs-extracted brain mask.
This feature may be disabled with the ``--no-freesurfer`` flag.

.. figure:: _static/reconall.svg
:scale: 100%

Surface reconstruction (FreeSurfer)

EPI_HMC
~~~~~~~

Expand Down Expand Up @@ -139,6 +150,9 @@ Derivatives related to EPI files are in the ``func`` subfolder:
- ``*bold_space-MNI152NLin2009cAsym_preproc.nii.gz`` Same as above, but in MNI space
- ``*bold_target-T1w_affine.txt`` The ITK-formatted affine to transform the EPI into T1w space (the inverse of ``anat/*T1w_target-meanBOLD_affine.txt``)

If FreeSurfer reconstruction is performed, the reconstructed subject is placed in
``derivatives/freesurfer/sub-{subj_id}``.

Images
------

Expand Down
2 changes: 1 addition & 1 deletion fmriprep/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
'pybids>=0.0.1',
'nitime',
'niworkflows',
'nipype>=0.13.0rc1'
'nipype'
]

LINKS_REQUIRES = []
Expand Down
3 changes: 2 additions & 1 deletion fmriprep/interfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
from fmriprep.interfaces.bids import ReadSidecarJSON, DerivativesDataSink, BIDSDataGrabber
from fmriprep.interfaces.bids import ReadSidecarJSON, DerivativesDataSink, \
BIDSDataGrabber, BIDSFreeSurferDir
from fmriprep.interfaces.images import ImageDataSink
from fmriprep.interfaces.utils import FormatHMCParam, IntraModalMerge
101 changes: 67 additions & 34 deletions fmriprep/interfaces/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,39 @@

import os
import os.path as op
import pkg_resources as pkgr
import re
import simplejson as json
from shutil import copy
from shutil import copy, copytree, rmtree

from nipype import logging
from nipype.interfaces.base import (
traits, isdefined, TraitedSpec, BaseInterface, BaseInterfaceInputSpec,
File, InputMultiPath, OutputMultiPath, Str
File, Directory, InputMultiPath, OutputMultiPath, Str
)
from builtins import str, bytes

from fmriprep.utils.misc import collect_bids_data, make_folder
from fmriprep.utils.misc import make_folder

LOGGER = logging.getLogger('interface')
BIDS_NAME = re.compile(
'^(.*\/)?(?P<subject_id>sub-[a-zA-Z0-9]+)(_(?P<ses_id>ses-[a-zA-Z0-9]+))?'
'(_(?P<task_id>task-[a-zA-Z0-9]+))?(_(?P<acq_id>acq-[a-zA-Z0-9]+))?'
'(_(?P<rec_id>rec-[a-zA-Z0-9]+))?(_(?P<run_id>run-[a-zA-Z0-9]+))?')


class FileNotFoundError(IOError):
pass


class SimpleInterface(BaseInterface):
def __init__(self, **inputs):
super(SimpleInterface, self).__init__(**inputs)
self._results = {}

def _list_outputs(self):
return self._results


class BIDSDataGrabberInputSpec(BaseInterfaceInputSpec):
subject_data = traits.Dict((str, bytes), traits.Any)
subject_id = Str()
Expand All @@ -44,14 +57,10 @@ class BIDSDataGrabberOutputSpec(TraitedSpec):
t1w = OutputMultiPath(desc='output T1w images')


class BIDSDataGrabber(BaseInterface):
class BIDSDataGrabber(SimpleInterface):
input_spec = BIDSDataGrabberInputSpec
output_spec = BIDSDataGrabberOutputSpec

def __init__(self, **inputs):
self._results = {'out_dict': {}}
super(BIDSDataGrabber, self).__init__(**inputs)

def _run_interface(self, runtime):
bids_dict = self.inputs.subject_data

Expand All @@ -73,12 +82,8 @@ def _run_interface(self, runtime):
LOGGER.warn('No \'{}\' images found for sub-{}'.format(
imtype, self.inputs.subject_id))


return runtime

def _list_outputs(self):
return self._results


class DerivativesDataSinkInputSpec(BaseInterfaceInputSpec):
base_directory = traits.Directory(
Expand All @@ -92,28 +97,23 @@ class DerivativesDataSinkInputSpec(BaseInterfaceInputSpec):
class DerivativesDataSinkOutputSpec(TraitedSpec):
out_file = OutputMultiPath(File(exists=True, desc='written file path'))

class DerivativesDataSink(BaseInterface):
class DerivativesDataSink(SimpleInterface):
input_spec = DerivativesDataSinkInputSpec
output_spec = DerivativesDataSinkOutputSpec
out_path_base = "derivatives"
_always_run = True

def __init__(self, out_path_base=None, **inputs):
self._results = {'out_file': []}
super(DerivativesDataSink, self).__init__(**inputs)
self._results['out_file'] = []
if out_path_base:
self.out_path_base = out_path_base
super(DerivativesDataSink, self).__init__(**inputs)

def _run_interface(self, runtime):
fname, _ = _splitext(self.inputs.source_file)
_, ext = _splitext(self.inputs.in_file[0])

m = re.search(
'^(?P<subject_id>sub-[a-zA-Z0-9]+)(_(?P<ses_id>ses-[a-zA-Z0-9]+))?'
'(_(?P<task_id>task-[a-zA-Z0-9]+))?(_(?P<acq_id>acq-[a-zA-Z0-9]+))?'
'(_(?P<rec_id>rec-[a-zA-Z0-9]+))?(_(?P<run_id>run-[a-zA-Z0-9]+))?',
fname
)
m = BIDS_NAME.search(fname)

# TODO this quick and dirty modality detection needs to be implemented
# correctly
Expand Down Expand Up @@ -142,7 +142,6 @@ def _run_interface(self, runtime):
if len(self.inputs.in_file) > 1 and not isdefined(self.inputs.extra_values):
formatstr = '{bname}_{suffix}{i:04d}{ext}'


for i, fname in enumerate(self.inputs.in_file):
out_file = formatstr.format(
bname=base_fname,
Expand All @@ -156,9 +155,6 @@ def _run_interface(self, runtime):

return runtime

def _list_outputs(self):
return self._results


class ReadSidecarJSONInputSpec(BaseInterfaceInputSpec):
in_file = File(exists=True, mandatory=True, desc='the input nifti file')
Expand All @@ -173,7 +169,7 @@ class ReadSidecarJSONOutputSpec(TraitedSpec):
run_id = traits.Str()
out_dict = traits.Dict()

class ReadSidecarJSON(BaseInterface):
class ReadSidecarJSON(SimpleInterface):
"""
An utility to find and read JSON sidecar files of a BIDS tree
"""
Expand All @@ -183,13 +179,6 @@ class ReadSidecarJSON(BaseInterface):
input_spec = ReadSidecarJSONInputSpec
output_spec = ReadSidecarJSONOutputSpec

def __init__(self, **inputs):
self._results = {}
super(ReadSidecarJSON, self).__init__(**inputs)

def _list_outputs(self):
return self._results

def _run_interface(self, runtime):
metadata = get_metadata_for_nifti(self.inputs.in_file)
output_keys = [key for key in list(self.output_spec().get().keys()) if key.endswith('_id')]
Expand All @@ -209,6 +198,50 @@ def _run_interface(self, runtime):
return runtime


class BIDSFreeSurferDirInputSpec(BaseInterfaceInputSpec):
derivatives = Directory(exists=True, mandatory=True,
desc='BIDS derivatives directory')
freesurfer_home = Directory(exists=True, mandatory=True,
desc='FreeSurfer installation directory')
subjects_dir = traits.Str('freesurfer', usedefault=True,
desc='Name of FreeSurfer subjects directory')
overwrite_fsaverage = traits.Bool(False, usedefault=True,
desc='Overwrite fsaverage, if present')


class BIDSFreeSurferDirOutputSpec(TraitedSpec):
subjects_dir = traits.Directory(exists=True,
desc='FreeSurfer subjects directory')


class BIDSFreeSurferDir(SimpleInterface):
""" Create a FreeSurfer subjects directory in a BIDS derivatives directory
and copy fsaverage from the local FreeSurfer distribution.

Output subjects_dir = ``{derivatives}/{subjects_dir}``, and may be passed to
ReconAll and other FreeSurfer interfaces.
"""
input_spec = BIDSFreeSurferDirInputSpec
output_spec = BIDSFreeSurferDirOutputSpec

def _run_interface(self, runtime):
subjects_dir = os.path.join(self.inputs.derivatives,
self.inputs.subjects_dir)
make_folder(subjects_dir)
self._results['subjects_dir'] = subjects_dir

source = os.path.join(self.inputs.freesurfer_home, 'subjects',
'fsaverage')
dest = os.path.join(subjects_dir, 'fsaverage')
# Finesse is overrated. Either leave it alone or completely clobber it.
if os.path.exists(dest) and self.inputs.overwrite_fsaverage:
rmtree(dest)
if not os.path.exists(dest):
copytree(source, dest)

return runtime


def get_metadata_for_nifti(in_file):
"""Fetchs metadata for a given nifi file"""
in_file = op.abspath(in_file)
Expand Down
8 changes: 7 additions & 1 deletion fmriprep/run_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ def main():
help="don't use ANTs-based skull-stripping (use AFNI instead, fast)")
g_ants.set_defaults(skull_strip_ants=True)

# FreeSurfer options
g_fs = parser.add_argument_group('settings for FreeSurfer preprocessing')
g_fs.add_argument('--no-freesurfer', action='store_false', dest='freesurfer',
help='disable FreeSurfer preprocessing')

opts = parser.parse_args()
create_workflow(opts)

Expand All @@ -102,7 +107,8 @@ def create_workflow(opts):
'output_dir': op.abspath(opts.output_dir),
'work_dir': op.abspath(opts.work_dir),
'ignore': opts.ignore,
'skip_native': opts.skip_native
'skip_native': opts.skip_native,
'freesurfer': opts.freesurfer,
}

# set up logger
Expand Down
8 changes: 7 additions & 1 deletion fmriprep/viz/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
"file_pattern": "anat/.*t1_2_mni",
"title": "T1 to MNI",
"description": "Corrected anatomical T1 image registered into MNI space"
},
{
"name": "t1w_preprocessing/reconall",
"file_pattern": "anat/.*reconall",
"title": "Surface Reconstruction",
"description": "White and pial surfaces overlaid on T1 image"
}

]
Expand Down Expand Up @@ -57,7 +63,7 @@
{
"name": "epi/tcompcor",
"file_pattern": "func/.*_tcompcor",
"title": "tCompCor hich variance map",
"title": "tCompCor high variance map",
"description": "Top 5% most variable voxels within heavily eroded brain mask."
},
{
Expand Down
Loading