Skip to content
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

[RTM] Add recon-all preprocessing #347

Merged
merged 30 commits into from
Feb 17, 2017
Merged

Conversation

effigies
Copy link
Member

@effigies effigies commented Jan 26, 2017

The goal of this PR is to get recon-all capabilities into FMRIPREP. Using its outputs in other steps in the pipeline will be addressed in other PRs.

Goals from #101:

#101 issues for a separate PR (for easy reference later):

  • Use FreeSurfer WM/Ventricle parcellations in aCompCor
  • Use bbregister to register functionals to T1

reconall = pe.Node(
freesurfer.ReconAll(
args='-parallel -openmp {:d}'.format(cpu_count())),
name='Reconstruction2')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having an issue here:

traits.trait_errors.TraitError: The trait 'subjects_dir' of a ReconAllInputSpec instance is an existing
directory name, but the path '/scratch/workflow_enumerator/01/t1w_preprocessing/Reconstruction' does not exist.

But... it's still running Reconstruction2, and I'm getting outputs. Is this a nipype bug?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, actually it's a segfault.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The erroring bit:

#-----------------------------------------
#@# Parcellation Stats 2 lh Thu Jan 26 17:41:39 UTC 2017
/scratch/workflow_enumerator/01/t1w_preprocessing/Reconstruction/recon_all/scripts

 mris_anatomical_stats -th3 -mgz -cortex ../label/lh.cortex.label -f ../stats/lh.aparc.a2009s.stats -b -a ../label/lh.aparc.a2009s.annot -c ../label/aparc.annot.a2009s.ctab recon_all lh white

#-----------------------------------------
#@# Parcellation Stats 2 rh Thu Jan 26 17:41:39 UTC 2017
/scratch/workflow_enumerator/01/t1w_preprocessing/Reconstruction/recon_all/scripts

 mris_anatomical_stats -th3 -mgz -cortex ../label/rh.cortex.label -f ../stats/rh.aparc.a2009s.stats -b -a ../label/rh.aparc.a2009s.annot -c ../label/aparc.annot.a2009s.ctab recon_all rh white

Waiting for PID 509 of (509 512) to complete...
/opt/freesurfer/bin/reconbatchjobs: line 81:   509 Segmentation fault      (core dumped) exec $JOB >> $LOG 2>&1
Waiting for PID 512 of (509 512) to complete...
/opt/freesurfer/bin/reconbatchjobs: line 81:   512 Segmentation fault      (core dumped) exec $JOB >> $LOG 2>&1
PIDs (509 512) completed and logs appended.
Linux 336cda4cfeae 4.4.0-59-generic #80-Ubuntu SMP Fri Jan 6 17:47:47 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

I've seen mris_anatomical_stats segfault issues before. I suspect it's because of the huge chunk of skull left by AFNI skull stripping (which is probably because of the low-res test data).

Short term: we'll kill the mris_anatomical_stats outputs, at least for testing. The outputs aren't used downstream in recon-all.

Medium term: I'll try to get a minimal reproduction (and better logs) for the FreeSurfer folks. Even if it fails, it shouldn't be segfaulting.

Somewhat relevant: There's no way to actually run these on CircleCI. I'll hook up the --no-freesurfer option for the tests, but we should figure out a way to test and validate FreeSurfer bits (at least) before releases.

autorecon1 = pe.Node(
freesurfer.ReconAll(
directive='autorecon1',
args='-noskullstrip -parallel -openmp {:d}'.format(cpu_count())),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the value from --nthreads

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also set the number of expected threads for the scheduler (See https://github.com/poldracklab/fmriprep/blob/master/fmriprep/workflows/anatomical.py#L253)

@effigies
Copy link
Member Author

Using https://openfmri.org/dataset/ds000005/ with --participant_label sub-01 to test FreeSurfer.

Could someone have a look at this error?

170131-17:48:08,337 niworkflows INFO:
	 Generating report for ReconAll (subject recon_all)
170131-17:48:08,338 niworkflows INFO:
	 Generating visual report
170131-17:48:47,327 niworkflows INFO:
	 Successfully created report (/scratch/workflow_enumerator/sub-01/t1w_preprocessing/Reconstruction2/reconall.svg)
170131-17:48:47,404 workflow INFO:
	 [Job finished] jobname: Reconstruction2 jobid: 95
170131-17:48:47,514 workflow INFO:
	 Executing: ReconAll_Report ID: 96
170131-17:48:47,531 workflow INFO:
	 Executing node ReconAll_Report in dir: /scratch/workflow_enumerator/sub-01/t1w_preprocessing/ReconAll_Report
170131-17:48:47,586 workflow INFO:
	 [Job finished] jobname: ReconAll_Report jobid: 96
Traceback (most recent call last):
  File "/usr/local/miniconda/bin/fmriprep", line 9, in <module>
    load_entry_point('fmriprep', 'console_scripts', 'fmriprep')()
  File "/root/src/fmriprep/fmriprep/run_workflow.py", line 87, in main
    create_workflow(opts)
  File "/root/src/fmriprep/fmriprep/run_workflow.py", line 185, in create_workflow
    run_reports(settings['output_dir'])
  File "/root/src/fmriprep/fmriprep/viz/reports.py", line 190, in run_reports
    report = Report(root, config, out_dir, out_filename)
  File "/root/src/fmriprep/fmriprep/viz/reports.py", line 89, in __init__
    self._load_config(config)
  File "/root/src/fmriprep/fmriprep/viz/reports.py", line 104, in _load_config
    self.index()
  File "/root/src/fmriprep/fmriprep/viz/reports.py", line 125, in index
    self.index_error_dir(error_dir)
  File "/root/src/fmriprep/fmriprep/viz/reports.py", line 147, in index_error_dir
    crash_data = loadcrash(os.path.join(root, f))
  File "/src/nipype/nipype/utils/filemanip.py", line 502, in loadcrash
    return loadpkl(infile)
  File "/src/nipype/nipype/utils/filemanip.py", line 527, in loadpkl
    unpkl = pickle.load(pkl_file)
  File "/usr/local/miniconda/lib/python3.5/site-packages/traits/has_traits.py", line 1441, in __setstate__
    self.trait_set( trait_change_notify = trait_change_notify, **state )
  File "/usr/local/miniconda/lib/python3.5/site-packages/traits/has_traits.py", line 1544, in trait_set
    setattr( self, name, value )
  File "/src/nipype/nipype/interfaces/base.py", line 2047, in validate
    value = super(MultiPath, self).validate(object, name, newvalue)
  File "/usr/local/miniconda/lib/python3.5/site-packages/traits/trait_types.py", line 2337, in validate
    return TraitListObject( self, object, name, value )
  File "/usr/local/miniconda/lib/python3.5/site-packages/traits/trait_handlers.py", line 2314, in __init__
    raise excp
  File "/usr/local/miniconda/lib/python3.5/site-packages/traits/trait_handlers.py", line 2306, in __init__
    value = [ validate( object, name, val ) for val in value ]
  File "/usr/local/miniconda/lib/python3.5/site-packages/traits/trait_handlers.py", line 2306, in <listcomp>
    value = [ validate( object, name, val ) for val in value ]
  File "/src/nipype/nipype/interfaces/traits_extension.py", line 89, in validate
    self.error(object, name, value)
  File "/usr/local/miniconda/lib/python3.5/site-packages/traits/trait_handlers.py", line 173, in error
    value )
traits.trait_errors.TraitError: Each element of the 'in_file' trait of a DerivativesDataSinkInputSpec instance must be an existing file name, but a value of '/scratch/workflow_enumerator/01/t1w_preprocessing/Reconstruction2/report.html' <class 'str'> was specified.

It produces the expected output file, but I can't figure out why it seems to think there should exist a /scratch/workflow_enumerator/01/t1w_preprocessing/Reconstruction2/report.html file, given that the enumerator uses sub-01.

Going to try running again in an absolutely clean scratch directory, but that's going to take a while, so if you see something right away, cool.

@effigies
Copy link
Member Author

effigies commented Feb 1, 2017

Okay, so I'm having two major issues. The first is the above, where for whatever reason the reporting works fine, but throws an error.

The second is I can't run -parcstats, -parcstats2, -parcstats3, -segstats, -wmparc or -balabels, given an ANTS-skullstripped brainmask without segfaulting.

Though that may be because ANTS skullstripping seems a bit over-zealous.
ants_mask

@effigies
Copy link
Member Author

effigies commented Feb 7, 2017

Report: sub-01.html

BIDS directory after running:

$ tree -L 4 /data/bids/ds000005_R2.0.1 | head -n 27
/data/bids/ds000005_R2.0.1
├── CHANGES
├── dataset_description.json
├── derivatives
│   └── freesurfer
│       └── sub-01
│           ├── label
│           ├── mri
│           ├── scripts
│           ├── stats
│           ├── surf
│           ├── tmp
│           ├── touch
│           └── trash
├── participants.tsv
├── README
├── sub-01
│   ├── anat
│   │   ├── sub-01_inplaneT2.nii.gz
│   │   └── sub-01_T1w.nii.gz
│   └── func
│       ├── sub-01_task-mixedgamblestask_run-01_bold.nii.gz
│       ├── sub-01_task-mixedgamblestask_run-01_events.tsv
│       ├── sub-01_task-mixedgamblestask_run-02_bold.nii.gz
│       ├── sub-01_task-mixedgamblestask_run-02_events.tsv
│       ├── sub-01_task-mixedgamblestask_run-03_bold.nii.gz
│       └── sub-01_task-mixedgamblestask_run-03_events.tsv
...

@effigies
Copy link
Member Author

effigies commented Feb 7, 2017

At this point, we have reconstruction, saving derivatives, and reporting.

At present, I'm accepting whatever external skullstripping is used, ANTs or AFNI. If we want to only swap out the FreeSurfer skullstripping if ANTs is specified, we could do that.

The following features are coded but not tested:

  • Multiple T1w images (haven't yet dug up a dataset to test this on)
  • T2 images (no <1.2mm T2s to test on)
    • Also, at present accepting inplaneT2 (because those were available in the above dataset), but I think we should actually be looking for images labeled T2w?
  • -hires (Waiting in a Sherlock queue to run on BEAST data)

Talking with @chrisfilo, perhaps the T2 stuff should be stripped out for now, and another PR can pull them in when we can get some proper testing. The same could go for multiple T1s or -hires, if we just want to get basic support in as quickly as possible.

The whole thing depends on nipy/nipype#1790, which serves as a decent test of that PR.

@chrisgorgo
Copy link
Contributor

chrisgorgo commented Feb 7, 2017 via email

@chrisgorgo
Copy link
Contributor

Are you planning to map the EPI timeseries to fsaverage in a separate PR?

@effigies
Copy link
Member Author

effigies commented Feb 7, 2017

Yes, since it's a separate process to recon-all, that seemed to make more sense to me.

@effigies effigies force-pushed the recon_all branch 3 times, most recently from 7c4179f to eb37f93 Compare February 8, 2017 20:43
Copy link
Member Author

@effigies effigies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-emptive comments for reviewers.

def detect_inputs(t1w_list, default_flags=''):
from nipype.utils.filemanip import filename_to_list
import nibabel as nib
t1w_list = filename_to_list(t1w_list)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If t1w is undefined, this function will have issues. However since it's a mandatory input to the workflow, it also should cause general failure, so I haven't specifically handled that case.

@@ -23,6 +23,11 @@
from fmriprep.utils.misc import collect_bids_data, 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]+))?'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this changes from only matching sub-... at the start of string to also matching after a /.

autorecon1_flags.append('-hires')
reconall_flags.append('-hires')
return (t1w_outs, ' '.join(autorecon1_flags),
' '.join(reconall_flags))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach doesn't permit type checking the flags that are passed. -noskullstrip and -hires aren't ReconAll options, but this will eventually also produce -T2 and -T2pial options, which are supported directly by nipype.

Not sure if this will bother anybody.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't -hires need to be entered directly before a corresponding -i with a T1 volume for it to work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I can tell from reading the Csh source, it looks like it's set globally. Do you have a source suggesting this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though, it does look like it should come with an experts file, which we should probably produce. I guess just go with their mris_inflate -n 15?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you check in the logs that the right resolution is used internally?
BTW it actually runs without an experts file (at least in my experience).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it does run without the experts file.

Not sure what to look for in the log, but (from a currently running test):

>>> nib.load('/local-scratch/cjmarkie/12650555/workflow_enumerator/sub-387/t1w_preprocessing/Reconstruction/sub-387/mri/T1.mgz').header.get_zooms()
(0.89840001, 0.89840001, 0.89840001)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pinged the FreeSurfer list about -hires best practices. My guess is that 15 max iterations should work for anything -- if something works well with a max of 10, it should stop by 10, but we should give smaller voxels a chance. 760d9de has the update I'd make if we want to go with that strategy. That seems like it's probably a good idea, since we'll want to support people with smaller voxels than we currently have access to.

match = BIDS_NAME.search(in_file)
params = match.groupdict() if match is not None else {}
return tuple(map(params.get, ['subject_id', 'ses_id', 'task_id',
'acq_id', 'rec_id', 'run_id']))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like this be more generally useful? I could put it in a better home if we might want to reuse.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be useful for pybids. I've opened an issue bids-standard/pybids#31

autorecon1.interface._can_resume = False
autorecon1.interface.num_threads = nthreads
if 'FREESURFER_SUBJECTS' in os.environ:
autorecon1.inputs.subjects_dir = os.getenv('FREESURFER_SUBJECTS')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't get used in my docker runs; do we even care about supporting existing FREESURFER_SUBJECTS directories, or is the derivatives folder the correct BIDS-y way to do this? A command-line option is always a possibility, as well.

mask = nib.load(skullstripped)
bmask = new_img_like(mask, mask.get_data() > 0)
resampled_mask = resample_to_img(bmask, img, 'nearest')
masked_image = new_img_like(img, img.get_data() * resampled_mask.get_data())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned this to Chris, but for anybody else who's reviewing, just note that the choice of binarizing before resampling will be slightly more prone to declaring voxels "not brain" than after. This is because, at the boundary, an interpolated value will always be > 0, while a nearest neighbor in a resampled binary map may be 0.

Just in case anybody wants to object.

masked_image = new_img_like(img, img.get_data() * resampled_mask.get_data())
masked_image.to_filename(bm_auto)

copyfile(bm_auto, bm, copy=True, use_hardlink=True)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardlinks should be fine, right? Seems a waste to copy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brain masks are so small and hard links are not supported on all systems. Will this throw an exception if hard links are not supported?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the fallback behavior would be to copy.

input_names=['dirname', 'basename'],
output_names=['path']),
name='SubjectDir',
run_without_submitting=True)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to get the full subject directory? It's 11 lines for os.path.join.

base_directory=settings['output_dir'],
container='derivatives'),
name='FSSubjOut',
run_without_submitting=True)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used DataSink instead of DerivativesDataSink because the latter doesn't like directories.

@effigies
Copy link
Member Author

Derivatives properly copying. Rebased against master, waiting on tests.

Any more comments?

run_without_submitting=True)
# Copy fsaverage subject into derivatives/freesurfer
setattr(subject_out.inputs, 'freesurfer.@fsaverage',
'/opt/freesurfer/subjects/fsaverage')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working on bbregister/mri_vol2surf, I'm realizing we don't want this inside t1w_preprocessing -- or really anything below workflow_enumerator -- as then it would be re-run for every subject.

Does it make sense to make something like:

class BIDSFreeSurferDir(BaseInterface):
    inputs = (freesurfer_home, derivatives_dir)
    outputs = subjects_dir

    def _run_interface(self, runtime):
         # Create subjects directory
         # Copy fsaverage

And then pass subjects_dir through to workflows that need access to it? We would then be recon-all'ing directly in derivatives/freesurfer/sub-{subj_id}/, which seems okay to me.

WDYT, @chrisfilo? This can wait, but I think we'll have to deal with it in the next PR (where bbregister will need to know about the common subjects directory to do the registration) if we don't, now.

@effigies effigies force-pushed the recon_all branch 3 times, most recently from da03c28 to 127651b Compare February 15, 2017 19:22
@effigies
Copy link
Member Author

Okay, so I've tested the following cases on Sherlock:

  1. New derivatives directory - copies fsaverage, creates new subject
  2. Existing fsaverage, new subject - leaves fsaverage untouched, creates new subject
  3. Existing fsaverage and subject - leaves fsaverage untouched, runs ReconAll without error (also not doing anything)
  4. Existing fsaverage and subject, deleted nipype scratch directory - leaves fsaverage untouched, runs ReconAll without error (overwriting brainmasks)

Pushed a commit to address this last case and not overwrite brainmasks. I believe we agreed that existing recon directories should be treated as "correct", and if someone wants to start over, they can delete the recon directory.

@chrisgorgo
Copy link
Contributor

chrisgorgo commented Feb 16, 2017 via email

@effigies
Copy link
Member Author

Alright. No errors, and existing brainmasks were left alone in case 4. This is ready for final review and merge.

@chrisgorgo
Copy link
Contributor

Excellent work!

@chrisgorgo chrisgorgo merged commit 324c544 into nipreps:master Feb 17, 2017
@effigies effigies deleted the recon_all branch February 17, 2017 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants