# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
#
# Copyright 2021 The NiPreps Developers <nipreps@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# We support and encourage derived works from this project, please read
# about our expectations at
#
# https://www.nipreps.org/community/licensing/
#
"""Nipype's recon-all replacement."""
import os
from looseversion import LooseVersion
from nipype import logging
from nipype.interfaces import freesurfer as fs
from nipype.interfaces.base import File, InputMultiObject, isdefined, traits
from nipype.utils.filemanip import check_depends
from niworkflows.interfaces import freesurfer as nwfs
iflogger = logging.getLogger('nipype.interface')
class _ReconAllInputSpec(fs.preprocess.ReconAllInputSpec):
directive = traits.Enum(
'all',
'autorecon1',
# autorecon2 variants
'autorecon2',
'autorecon2-volonly',
'autorecon2-perhemi',
'autorecon2-inflate1',
'autorecon2-cp',
'autorecon2-wm',
# autorecon3 variants
'autorecon3',
'autorecon3-T2pial',
# Mix of autorecon2 and autorecon3 steps
'autorecon-pial',
'autorecon-hemi',
# Not "multi-stage flags"
'localGI',
'qcache',
argstr='-%s',
desc='process directive',
usedefault=True,
xor=['steps'],
position=0,
)
steps = InputMultiObject(
traits.Enum(
# autorecon1
'motioncor',
'talairach',
'nuintensitycor',
'normalization',
'skullstrip',
# autorecon2-volonly
'gcareg',
'canorm',
'careg',
'careginv', # 5.3
'rmneck', # 5.3
'skull-lta', # 5.3
'calabel',
'normalization2',
'maskbfs',
'segmentation',
# autorecon2 per-hemi
'tessellate',
'smooth1',
'inflate1',
'qsphere',
'fix',
'white',
'smooth2',
'inflate2',
'curvHK', # 6.0
'curvstats',
# autorecon3 per-hemi
'sphere',
'surfreg',
'jacobian_white',
'avgcurv',
'cortparc',
'pial',
'pctsurfcon',
'parcstats',
'cortparc2',
'parcstats2',
'cortparc3',
'parcstats3',
'label-exvivo-ec',
# autorecon vol
'cortribbon',
'hyporelabel', # 6.0
'segstats',
'aparc2aseg',
'apas2aseg', # 6.0
'wmparc',
'balabels',
),
desc='specific process directives',
xor=['directive'],
position=0,
)
hemi = traits.Enum('lh', 'rh', desc='hemisphere to process', argstr='-%s-only')
[docs]
class ReconAll(fs.ReconAll):
input_spec = _ReconAllInputSpec
@property
def cmdline(self):
cmd = super(fs.ReconAll, self).cmdline
# Adds '-expert' flag if expert flags are passed
# Mutually exclusive with 'expert' input parameter
cmd += self._prep_expert_file()
if not self._is_resuming():
return cmd
subjects_dir = self.inputs.subjects_dir
if not isdefined(subjects_dir):
subjects_dir = self._gen_subjects_dir()
# Check only relevant steps
directive = self.inputs.directive
if not isdefined(directive):
steps = []
if isdefined(self.inputs.steps):
steps = [step for step in self._steps if step[0] in self.inputs.steps]
elif directive == 'autorecon1':
steps = self._autorecon1_steps
elif directive == 'autorecon2-volonly':
steps = self._autorecon2_volonly_steps
elif directive == 'autorecon2-perhemi':
steps = self._autorecon2_perhemi_steps
elif directive.startswith('autorecon2'):
if isdefined(self.inputs.hemi):
if self.inputs.hemi == 'lh':
steps = self._autorecon2_volonly_steps + self._autorecon2_lh_steps
else:
steps = self._autorecon2_volonly_steps + self._autorecon2_rh_steps
else:
steps = self._autorecon2_steps
elif directive == 'autorecon-hemi':
if self.inputs.hemi == 'lh':
steps = self._autorecon_lh_steps
else:
steps = self._autorecon_rh_steps
elif directive == 'autorecon3':
steps = self._autorecon3_steps
else:
steps = self._steps
no_run = True
flags = []
for step, outfiles, infiles in steps:
flag = f'-{step}'
noflag = f'-no{step}'
if noflag in cmd:
continue
elif flag in cmd:
no_run = False
continue
# FreeSurfer changed the meaning and order of -apas2aseg without
# updating the recon table on the wiki. Hack it until fixed in nipype.
if step == 'apas2aseg' and fs.Info.looseversion() >= LooseVersion('7.3.0'):
infiles = []
subj_dir = os.path.join(subjects_dir, self.inputs.subject_id)
if check_depends(
[os.path.join(subj_dir, f) for f in outfiles],
[os.path.join(subj_dir, f) for f in infiles],
):
flags.append(noflag)
else:
if isdefined(self.inputs.steps):
flags.append(flag)
no_run = False
if no_run and not self.force_run:
iflogger.info('recon-all complete : Not running')
return 'echo recon-all: nothing to do'
cmd += ' ' + ' '.join(flags)
iflogger.info('resume recon-all : %s', cmd)
return cmd
def _format_arg(self, name, trait_spec, value):
# Nipype disables this if -autorecon-hemi is passed
# We need to use it either way to prevent undesired behavior
if name == 'hemi':
return trait_spec.argstr % value
return super()._format_arg(name, trait_spec, value)
class _MRIsConvertDataInputSpec(fs.utils.MRIsConvertInputSpec):
in_file = File(
exists=True,
position=-2,
genfile=True,
argstr='%s',
desc='Surface file',
)
_xor = ('annot_file', 'parcstats_file', 'label_file', 'scalarcurv_file', 'functional_file')
annot_file = File(
exists=True,
argstr='--annot %s',
mandatory=True,
xor=_xor,
desc='input is annotation or gifti label data',
)
parcstats_file = File(
exists=True,
argstr='--parcstats %s',
mandatory=True,
xor=_xor,
desc='infile is name of text file containing label/val pairs',
)
label_file = File(
exists=True,
argstr='--label %s',
mandatory=True,
xor=_xor,
desc='infile is .label file, label is name of this label',
)
scalarcurv_file = File(
exists=True,
argstr='-c %s',
mandatory=True,
xor=_xor,
desc='input is scalar curv overlay file (must still specify surface)',
)
functional_file = File(
exists=True,
argstr='-f %s',
mandatory=True,
xor=_xor,
desc='input is functional time-series or other multi-frame data (must specify surface)',
)
[docs]
class MRIsConvertData(fs.utils.MRIsConvert):
"""Convert surface data files (label, curvature, functional, etc)
Wraps mris_convert to automatically select the correct ?h.white surface if
passed a file from the subject's surf/ directory
"""
input_spec = _MRIsConvertDataInputSpec
def _gen_filename(self, name):
if name == 'in_file':
if isdefined(self.inputs.in_file):
return self.inputs.in_file
# Find file we're trying to convert
fname = None
for opt in ('annot', 'parcstats', 'label', 'scalarcurv', 'functional'):
input_file = getattr(self.inputs, f'{opt}_file')
if isdefined(input_file):
fname = input_file
break
if fname is None:
raise ValueError('Missing file to derive filename from.')
# $SUB/lh.curv -> $SUB/lh.white, etc
dirname, basename = os.path.split(fname)
hemi = basename.split('.', 1)[0]
if hemi not in ('lh', 'rh'):
return None
self.inputs.in_file = os.path.join(dirname, f'{hemi}.white')
return self.inputs.in_file
return super()._gen_filename(name)
class MakeMidthicknessInputSpec(nwfs._MakeMidthicknessInputSpec, fs.base.FSTraitedSpecOpenMP):
pass
[docs]
class MakeMidthickness(nwfs.MakeMidthickness, fs.base.FSCommandOpenMP):
"""Patched MakeMidthickness interface
Ensures output filenames are specified with hemisphere labels, when appropriate.
This may not cover all use-cases in MRIsExpand, but we're just making midthickness
files.
This interface also sets the OMP_NUM_THREADS environment variable to floor(1.5x) the
number of threads requested by the user, as tests indicate that cores are underutilized
by a factor of 2/3.
>>> from smriprep.interfaces.freesurfer import MakeMidthickness
>>> mris_expand = MakeMidthickness(thickness=True, distance=0.5)
>>> mris_expand.inputs.in_file = 'lh.white'
>>> mris_expand.cmdline
'mris_expand -thickness lh.white 0.5 lh.expanded'
>>> mris_expand.inputs.out_name = 'graymid'
>>> mris_expand.cmdline
'mris_expand -thickness lh.white 0.5 lh.graymid'
Explicit hemisphere labels should still be respected:
>>> mris_expand.inputs.out_name = 'rh.graymid'
>>> mris_expand.cmdline
'mris_expand -thickness lh.white 0.5 rh.graymid'
"""
input_spec = MakeMidthicknessInputSpec
def _format_arg(self, name, trait_spec, value):
# FreeSurfer at some point changed whether it would add the hemi label onto the
# surface. Therefore we'll do it ourselves.
if name == 'out_name':
value = self._associated_file(self.inputs.in_file, value)
return super()._format_arg(name, trait_spec, value)
def _num_threads_update(self):
if self.inputs.num_threads:
self.inputs.environ.update({'OMP_NUM_THREADS': str(self.inputs.num_threads * 3 // 2)})