Commit f011b82f authored by Paul McCarthy's avatar Paul McCarthy 🚵
Browse files

further experimentation

parent 0634907a
......@@ -8,25 +8,31 @@
them to be called from Python.
"""
from .bet import (bet,) # noqa
from .eddy import (eddy_cuda, # noqa
topup)
from .flirt import (flirt, # noqa
invxfm,
applyxfm,
concatxfm,
mcflirt)
from .fnirt import (fnirt, # noqa
applywarp,
invwarp,
convertwarp)
from .fslmaths import (fslmaths,) # noqa
from .fugue import (fugue, # noqa
sigloss)
from .melodic import (melodic, # noqa
fsl_regfilt)
from .misc import (fslreorient2std, # noqa
fslroi,
slicer,
cluster)
from .wrapperutils import (applyArgStyle, # noqa
required,
fileOrImage,
fileOrArray,
RETURN,
SHOW_IF_TRUE,
HIDE_IF_TRUE)
from .bet import (bet,) # noqa
from .eddy import (eddy_cuda, # noqa
topup)
from .flirt import (flirt, # noqa
invxfm,
applyxfm,
concatxfm,
mcflirt)
from .fnirt import (fnirt, # noqa
applywarp,
invwarp,
convertwarp)
from .fslmaths import (fslmaths,) # noqa
from .fugue import (fugue, # noqa
sigloss)
from .melodic import (melodic, # noqa
fsl_regfilt)
from .misc import (fslreorient2std, # noqa
fslroi,
slicer,
cluster)
......@@ -11,6 +11,10 @@ import fsl.utils.run as run
from . import wrapperutils as wutils
def pybet(): # ??
pass
@wutils.fileOrImage('input', 'output')
def bet(input, output, **kwargs):
"""Delete non-brain tissue from an image of the whole head.
......@@ -20,7 +24,6 @@ def bet(input, output, **kwargs):
:arg mask:
:arg robust:
:arg fracintensity:
:arg seg:
Refer to the ``bet`` command-line help for details on all arguments.
"""
......
......@@ -20,60 +20,34 @@
import fsl.utils.run as run
import fsl.utils.assertions as asrt
import fsl.data.image as fslimage
from . import wrapperutils as wutils
def flirt(src, ref, out=None, omat=None, dof=None, cost=None, wmseg=None,
init=None, schedule=None, echospacing=None, pedir=None,
fieldmap=None, fieldmapmask=None, bbrslope=None, bbrtype=None,
interp=None, refweight=None, applyisoxfm=None, usesqform=False,
nosearch=False, verbose=0):
@wutils.required('src', 'ref')
@wutils.fileOrImage('src', 'ref', 'out', 'wmseg', 'fieldmap', 'fieldmapmask')
@wutils.fileOrArray('init', 'omat', 'wmcoords', 'wmnorms')
def flirt(src, ref, **kwargs):
"""FLIRT (FMRIB's Linear Image Registration Tool)."""
asrt.assertIsNifti(src, ref)
asrt.assertFileExists(src, ref)
cmd = "flirt -in {0} -ref {1}".format(src, ref)
if out is not None:
asrt.assertIsNifti(out)
cmd += " -out {0}".format(out)
if omat is not None:
cmd += " -omat {0}".format(omat)
if dof is not None:
cmd += " -dof {0}".format(dof)
if cost is not None:
cmd += " -cost {0}".format(cost)
if wmseg is not None:
asrt.assertIsNifti(wmseg)
cmd += " -wmseg {0}".format(wmseg)
if init is not None:
cmd += " -init {0}".format(init)
if schedule is not None:
cmd += " -schedule {0}".format(schedule)
if echospacing is not None:
cmd += " -echospacing {0}".format(echospacing)
if pedir is not None:
cmd += " -pedir {0}".format(pedir)
if fieldmap is not None:
cmd += " -fieldmap {0}".format(fieldmap)
if fieldmapmask is not None:
cmd += " -fieldmapmask {0}".format(fieldmapmask)
if bbrslope is not None:
cmd += " -bbrslope {0}".format(bbrslope)
if bbrtype is not None:
cmd += " -bbrtype {0}".format(bbrtype)
if interp is not None:
cmd += " -interp {0}".format(interp)
if refweight is not None:
asrt.assertIsNifti(refweight)
cmd += " -refweight {0}".format(refweight)
if applyisoxfm is not None:
cmd += " -applyisoxfm {0}".format(applyisoxfm)
if verbose is not None:
cmd += " -verbose {0}".format(verbose)
if usesqform:
cmd += " -usesqform"
if nosearch:
cmd += " -nosearch"
valmap = {
'usesqform' : wutils.SHOW_IF_TRUE,
'displayinit' : wutils.SHOW_IF_TRUE,
'noresample' : wutils.SHOW_IF_TRUE,
'forcescaling' : wutils.SHOW_IF_TRUE,
'applyxfm' : wutils.SHOW_IF_TRUE,
'nosearch' : wutils.SHOW_IF_TRUE,
'noclamp' : wutils.SHOW_IF_TRUE,
'noresampblur' : wutils.SHOW_IF_TRUE,
'2D' : wutils.SHOW_IF_TRUE,
'v' : wutils.SHOW_IF_TRUE,
'version' : wutils.SHOW_IF_TRUE,
'help' : wutils.SHOW_IF_TRUE,
}
cmd = ['flirt', '-in', src, '-ref', ref]
cmd += wutils.applyArgStyle('-', valmap=valmap, **kwargs)
return run.runfsl(cmd)
......@@ -94,13 +68,25 @@ def applyxfm(src, ref, mat, out, interp='spline'):
cmd = "flirt -init {0} -in {1} -ref {2} -applyxfm -out {3} -interp {4}"
return run.runfsl(cmd.format(mat, src, ref, out, interp))
@wutils.required( 'inmat1', 'inmat2', 'outmat')
@wutils.fileOrArray('inmat1', 'inmat2', 'outmat')
def concatxfm(inmat1, inmat2, outmat):
"""Tool to concatenate two FSL transformation matrices."""
print('inmat1', inmat1)
print('inmat2', inmat2)
print('outmat', outmat)
asrt.assertFileExists(inmat1, inmat2)
cmd = "convert_xfm -omat {0} -concat {1} {2}"
return run.runfsl(cmd.format(outmat, inmat2, inmat1))
cmd = ['convert_xfm',
'-omat',
outmat,
'-concat',
inmat2,
inmat1]
return run.runfsl(cmd)
def mcflirt(infile, outfile, reffile=None, spline_final=True, plots=True,
......
#!/usr/bin/env python
#
# wrapperutils.py -
# wrapperutils.py - Functions and decorators used by the FSL wrapper
# functions.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module contains functions and decorators used by the FSL wrapper
functions.
.. autosummary::
:nosignatures:
applyArgStyle
required
fileOrImage
fileOrArray
"""
import os.path as op
import os
import sys
import inspect
import tempfile
import warnings
......@@ -16,50 +29,133 @@ import collections
import six
import nibabel as nib
import numpy as np
import fsl.utils.tempdir as tempdir
import fsl.data.image as fslimage
class _BooleanFlag(object):
def __init__(self, show):
self.show = show
def __eq__(self, other):
return type(other) == type(self) and self.show == other.show
def _update_wrapper(wrapper, wrapped, *args, **kwargs):
"""Replacement for the built-in ``functools.update_wrapper``. This
implementation ensures that the wrapper function has an attribute
called ``__wrapped__``, which refers to the ``wrapped`` function.
This behaviour is only required in Python versions < 3.4.
"""
wrapper = functools.update_wrapper(wrapper, wrapped, *args, **kwargs)
# Python >= 3.4 does things right
if sys.version_info[0] * 10 + sys.version_info[1] < 3.4:
wrapper.__wrapped__ = wrapped
return wrapper
SHOW_IF_TRUE = _BooleanFlag(True)
HIDE_IF_TRUE = _BooleanFlag(False)
def _unwrap(func):
"""Replacement for the built-in ``inspect.unwrap`` function, which
is not present in Python versions prior to 3.4.
"""
# Python >= 3.4 has an inspect.unwrap function
if sys.version_info[0] * 10 + sys.version_info[1] < 3.4:
return inspect.unwrap(func)
# Otherwise we follow the __wrapped__ chain ourselves
if hasattr(func, '__wrapped__'):
return _unwrap(func.__wrapped__)
return func
SHOW_IF_TRUE = object()
"""Constant to be used in the ``valmap`` passed to the :func:`applyArgStyle`
function.
When a ``SHOW_IF_TRUE`` argument is ``True``, it is added to the generated
command line arguments.
"""
HIDE_IF_TRUE = object()
"""Constant to be used in the ``valmap`` passed to the :func:`applyArgStyle`
function.
When a ``HIDE_IF_TRUE`` argument is ``True``, it is suppressed from the
generated command line arguments.
"""
def applyArgStyle(style, argmap=None, valmap=None, **kwargs):
"""Turns the given ``kwargs`` into command line options. This function
is intended to be used to automatically generate command line options
from arguments passed into a Python function.
:arg style: Controls how the ``kwargs`` are converted into command-line
options - must be one of the following:
- `'-'`: ``-name val``
- `'--'`: ``--name val``
- `'-='`: ``-name=val``
- `'--='`: ``--name=val``
:arg argmap: Dictionary of ``{kwarg-name : cli-name}`` mappings. This can
be used if you want to use different argument names in your
Python function for the command-line options.
:arg valmap: Dictionary of ``{cli-name : value}`` mappings. This can be
used to define specific semantics for some command-line
options. Acceptable values for ``value`` are as follows
- :data:`SHOW_IF_TRUE` - if the argument is present, and
``True`` in ``kwargs``, the command line option
will be added (without any arguments).
- :data:`HIDE_IF_TRUE` - if the argument is present, and
``False`` in ``kwargs``, the command line option
will be added (without any arguments).
- Any other constant value. If the argument is present
in ``kwargs``, its command-line option will be
added, with the constant value as its argument.
The argument for any options not specified in the ``valmap``
will be converted into strings.
def fmtarg(arg, style):
:arg kwargs: Arguments to be converted into command-line options.
:returns: A list containing the generated command-line options.
"""
if style not in ('-', '--', '-=', '--='):
raise ValueError('Invalid style: {}'.format(style))
if argmap is None: argmap = {}
if valmap is None: valmap = {}
def fmtarg(arg):
if style in ('-', '-='): arg = '-{}'.format(arg)
elif style in ('--', '--='): arg = '--{}'.format(arg)
return arg
def fmtval(val, style=None):
def fmtval(val):
if isinstance(val, collections.Sequence) and \
not isinstance(val, six.string_types):
return ' '.join([str(v) for v in val])
else:
return str(val)
if style not in ('-', '--', '-=', '--='):
raise ValueError('Invalid style: {}'.format(style))
args = []
for k, v in kwargs.items():
k = argmap.get(k, k)
mapv = valmap.get(k, fmtval(v, style))
k = fmtarg(k, style)
mapv = valmap.get(k, fmtval(v))
k = fmtarg(k)
if (mapv is SHOW_IF_TRUE and v) or \
(mapv is HIDE_IF_TRUE and not v):
args.append(k)
if mapv in (SHOW_IF_TRUE, HIDE_IF_TRUE):
if v == mapv.show:
args.append(k)
elif '=' in style:
args.append('{}={}'.format(k, mapv))
else:
......@@ -72,110 +168,216 @@ def required(*reqargs):
"""Decorator which makes sure that all specified keyword arguments are
present before calling the decorated function.
"""
def decorator(func):
def wrapper(**kwargs):
def wrapper(*args, **kwargs):
kwargs = kwargs.copy()
kwargs.update(argsToKwargs(func, args))
for reqarg in reqargs:
assert reqarg in kwargs
return func(**kwargs)
return wrapper
return _update_wrapper(wrapper, func)
return decorator
def argsToKwargs(func, args):
"""Given a function, and a sequence of positional arguments destined
for that function, converts the positional arguments into a dict
of keyword arguments. Used by the :class:`_FileOrImage` class.
of keyword arguments. Used by the :class:`_FileOrThing` class.
"""
# getfullargspec is the only way to get the names
# of positional arguments in Python 2.x. It is
# deprecated in python 3.5, but not in python 3.6.
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=DeprecationWarning)
spec = inspect.getfullargspec(func)
func = _unwrap(func)
# getargspec is the only way to get the names
# of positional arguments in Python 2.x.
if sys.version_info[0] < 3:
argnames = inspect.getargspec(func).args
# getargspec is deprecated in python 3.x
else:
# getfullargspec is deprecated in
# python 3.5, but not in python 3.6.
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=DeprecationWarning)
argnames = inspect.getfullargspec(func).args
kwargs = collections.OrderedDict()
for name, val in zip(spec.args, args):
for name, val in zip(argnames, args):
kwargs[name] = val
return kwargs
RETURN = object()
"""
"""Constant used by the :class:`_FileOrThing` class to indicate that an output
file should be loaded into memory and returned as a Python object.
"""
class _FileOrImage(object):
"""
class _FileOrThing(object):
"""Decorator which ensures that certain arguments which are passed into the
decorated function are always passed as file names. Both positional and
keyword arguments can be specified.
The ``_FileOrThing`` class is not intended to be used directly - see the
:func:`fileOrImage` and :func:`fileOrArray` decorator functions for more
details.
These decorators are intended for functions which wrap a command-line tool,
i.e. where some inputs/outputs need to be specified as file names.
**Inputs**
Any arguments which are not of type ``Thing`` are passed through to the
decorated function unmodified. Arguments which are of type ``Thing`` are
saved to a temporary file, and the name of that file is passed to the
function.
Inputs:
- In-memory nibabel images loaded from a file. The image is replaced with
its file name.
**Outputs**
- In-memory nibabel images. The image is saved to a temporary file, and
replaced with the temporary file's name. The file is deleted after the
function has returned.
Outputs:
- File name: The file name is passed straight through to the function.
- ``RETURN``: A temporary file name is passed to the function. After the
function has completed, the image is loaded into memory and the
temporary file is deleted. The image is returned from the function
call.
If an argument is given the special :data:`RETURN` value, it is assumed
to be an output argument. In this case, it is replaced with a temporary
file name then, after the function has completed, that file is loaded
into memory, and the value returned (along with the function's output,
and any other arguments with a value of ``RETURN``).
**Return value**
Functions decorated with a ``_FileOrThing`` decorator will always return a
tuple, where the first element is the function's actual return value. The
remainder of the tuple will contain any arguments that were given the
special ``RETURN`` value. ``None`` is returned for any ``RETURN``
arguments corresponded to output files that were not generated by the
function.
**Example**
As an example of using the ``fileOrArray`` decorator on a function
which concatenates two files containing affine transformations, and
saves the output to a file::
# if atob, btoc, or output are passed
# in as arrays, they are converted to
# file names.
@fileOrArray('atob', 'btoc', 'output')
def concat(atob, btoc, output=None):
# inputs are guaranteed to be files
atob = np.loadtxt(atob)
btoc = np.loadtxt(atoc)
atoc = np.dot(btoc, atob)
if output is not None:
np.savetxt(output, atoc)
Because we have decorated the ``concat`` function with :func:`fileToArray`,
it can be called with either file names, or Numpy arrays::
# All arguments are passed through
# unmodified - the output will be
# saved to a file called atoc.mat
concat('atob.txt', 'btoc.txt', 'atoc.mat')
# The output is returned as a numpy
# array (in a tuple with the concat
# function's return value)
atoc = concat('atob.txt', 'btoc.txt', RETURN)[1]
# The inputs are saved to temporary
# files, and those file names are
# passed to the concat function.
atoc = concat(np.diag([2, 2, 2, 0]), np.diag([3, 3, 3, 3]), RETURN)[1]
"""
def __init__(self, *imgargs):
"""
def __init__(self, prepareThing, loadThing, *things):
"""Initialise a ``_FileOrThing`` decorator.
:arg prepareThing: Function which
:arg loadThing: Function which is called for arguments that
were set to :data:`RETURN`.
:arg things:
"""
self.__imgargs = imgargs
self.__prepareThing = prepareThing
self.__loadThing = loadThing
self.__things = things
def __call__(self, func):
"""
"""
return functools.partial(self.__wrapper, func)
"""Creates and returns the real decorator function. """
self.__func = func
self.__isFileOrThing = False
if hasattr(func, '__self__'):
self.__isFileOrThing = isinstance(func.__self__, _FileOrThing)
wrapper = functools.partial(self.__wrapper, func)
return _update_wrapper(wrapper, func)
def __wrapper(self, func, *args, **kwargs):
"""
"""Function which wraps ``func``, ensuring that any arguments of
type ``Thing`` are saved to temporary files, and any arguments
with the value :data:`RETURN` are loaded and returned.
"""
func = self.__func
isFileOrThing = self.__isFileOrThing
kwargs = kwargs.copy()
kwargs.update(argsToKwargs(func, args))
# Create a tempdir to store any temporary
# input/output images, but don't change
# input/output things, but don't change
# into it, as file paths passed to the
# function may be relative.
with tempdir.tempdir(changeto=False) as td:
kwargs, infiles, outfiles = self.__prepareArgs(td, kwargs)
kwargs, infiles, outfiles = self.__prepareThings(td, kwargs)
# Call the function
result = func(**kwargs)
result = func(**kwargs)
# Load the output images that
# Load the output things that
# were specified as RETURN
outimgs = []
outthings = []
for of in outfiles:
# output file didn't get created
if not op.exists(of):
oi = None
ot = None
# load the file, and create
# an in-memory copy (the file
# is going to get deleted)
# load the thing
else:
oi = nib.load(of)
oi = nib.nifti1.Nifti1Image(oi.get_data(), None, oi.header)
ot = self.__loadThing(of)
outimgs.append(oi)
outthings.append(ot)
return tuple([result] + outimgs)
if isFileOrThing:
things = result[1:]
result = result[0]
return tuple([result] + things + outthings)
else:
return tuple([result] + outthings)
def __prepareArgs(self, workdir, kwargs):
def __prepareThings(self, workdir, kwargs):
"""
"""