Skip to content
Snippets Groups Projects
Commit f011b82f authored by Paul McCarthy's avatar Paul McCarthy :mountain_bicyclist:
Browse files

further experimentation

parent 0634907a
No related branches found
No related tags found
No related merge requests found
...@@ -8,25 +8,31 @@ ...@@ -8,25 +8,31 @@
them to be called from Python. them to be called from Python.
""" """
from .wrapperutils import (applyArgStyle, # noqa
from .bet import (bet,) # noqa required,
from .eddy import (eddy_cuda, # noqa fileOrImage,
topup) fileOrArray,
from .flirt import (flirt, # noqa RETURN,
invxfm, SHOW_IF_TRUE,
applyxfm, HIDE_IF_TRUE)
concatxfm, from .bet import (bet,) # noqa
mcflirt) from .eddy import (eddy_cuda, # noqa
from .fnirt import (fnirt, # noqa topup)
applywarp, from .flirt import (flirt, # noqa
invwarp, invxfm,
convertwarp) applyxfm,
from .fslmaths import (fslmaths,) # noqa concatxfm,
from .fugue import (fugue, # noqa mcflirt)
sigloss) from .fnirt import (fnirt, # noqa
from .melodic import (melodic, # noqa applywarp,
fsl_regfilt) invwarp,
from .misc import (fslreorient2std, # noqa convertwarp)
fslroi, from .fslmaths import (fslmaths,) # noqa
slicer, from .fugue import (fugue, # noqa
cluster) 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 ...@@ -11,6 +11,10 @@ import fsl.utils.run as run
from . import wrapperutils as wutils from . import wrapperutils as wutils
def pybet(): # ??
pass
@wutils.fileOrImage('input', 'output') @wutils.fileOrImage('input', 'output')
def bet(input, output, **kwargs): def bet(input, output, **kwargs):
"""Delete non-brain tissue from an image of the whole head. """Delete non-brain tissue from an image of the whole head.
...@@ -20,7 +24,6 @@ def bet(input, output, **kwargs): ...@@ -20,7 +24,6 @@ def bet(input, output, **kwargs):
:arg mask: :arg mask:
:arg robust: :arg robust:
:arg fracintensity: :arg fracintensity:
:arg seg:
Refer to the ``bet`` command-line help for details on all arguments. Refer to the ``bet`` command-line help for details on all arguments.
""" """
......
...@@ -20,60 +20,34 @@ ...@@ -20,60 +20,34 @@
import fsl.utils.run as run import fsl.utils.run as run
import fsl.utils.assertions as asrt import fsl.utils.assertions as asrt
import fsl.data.image as fslimage 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, @wutils.required('src', 'ref')
init=None, schedule=None, echospacing=None, pedir=None, @wutils.fileOrImage('src', 'ref', 'out', 'wmseg', 'fieldmap', 'fieldmapmask')
fieldmap=None, fieldmapmask=None, bbrslope=None, bbrtype=None, @wutils.fileOrArray('init', 'omat', 'wmcoords', 'wmnorms')
interp=None, refweight=None, applyisoxfm=None, usesqform=False, def flirt(src, ref, **kwargs):
nosearch=False, verbose=0):
"""FLIRT (FMRIB's Linear Image Registration Tool).""" """FLIRT (FMRIB's Linear Image Registration Tool)."""
asrt.assertIsNifti(src, ref) asrt.assertIsNifti(src, ref)
asrt.assertFileExists(src, ref)
cmd = "flirt -in {0} -ref {1}".format(src, ref) valmap = {
'usesqform' : wutils.SHOW_IF_TRUE,
if out is not None: 'displayinit' : wutils.SHOW_IF_TRUE,
asrt.assertIsNifti(out) 'noresample' : wutils.SHOW_IF_TRUE,
cmd += " -out {0}".format(out) 'forcescaling' : wutils.SHOW_IF_TRUE,
if omat is not None: 'applyxfm' : wutils.SHOW_IF_TRUE,
cmd += " -omat {0}".format(omat) 'nosearch' : wutils.SHOW_IF_TRUE,
if dof is not None: 'noclamp' : wutils.SHOW_IF_TRUE,
cmd += " -dof {0}".format(dof) 'noresampblur' : wutils.SHOW_IF_TRUE,
if cost is not None: '2D' : wutils.SHOW_IF_TRUE,
cmd += " -cost {0}".format(cost) 'v' : wutils.SHOW_IF_TRUE,
if wmseg is not None: 'version' : wutils.SHOW_IF_TRUE,
asrt.assertIsNifti(wmseg) 'help' : wutils.SHOW_IF_TRUE,
cmd += " -wmseg {0}".format(wmseg) }
if init is not None:
cmd += " -init {0}".format(init) cmd = ['flirt', '-in', src, '-ref', ref]
if schedule is not None: cmd += wutils.applyArgStyle('-', valmap=valmap, **kwargs)
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"
return run.runfsl(cmd) return run.runfsl(cmd)
...@@ -94,13 +68,25 @@ def applyxfm(src, ref, mat, out, interp='spline'): ...@@ -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}" cmd = "flirt -init {0} -in {1} -ref {2} -applyxfm -out {3} -interp {4}"
return run.runfsl(cmd.format(mat, src, ref, out, interp)) 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): def concatxfm(inmat1, inmat2, outmat):
"""Tool to concatenate two FSL transformation matrices.""" """Tool to concatenate two FSL transformation matrices."""
print('inmat1', inmat1)
print('inmat2', inmat2)
print('outmat', outmat)
asrt.assertFileExists(inmat1, inmat2) asrt.assertFileExists(inmat1, inmat2)
cmd = "convert_xfm -omat {0} -concat {1} {2}" cmd = ['convert_xfm',
return run.runfsl(cmd.format(outmat, inmat2, inmat1)) '-omat',
outmat,
'-concat',
inmat2,
inmat1]
return run.runfsl(cmd)
def mcflirt(infile, outfile, reffile=None, spline_final=True, plots=True, def mcflirt(infile, outfile, reffile=None, spline_final=True, plots=True,
......
#!/usr/bin/env python #!/usr/bin/env python
# #
# wrapperutils.py - # wrapperutils.py - Functions and decorators used by the FSL wrapper
# functions.
# #
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # 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.path as op
import os import os
import sys
import inspect import inspect
import tempfile import tempfile
import warnings import warnings
...@@ -16,50 +29,133 @@ import collections ...@@ -16,50 +29,133 @@ import collections
import six import six
import nibabel as nib import nibabel as nib
import numpy as np
import fsl.utils.tempdir as tempdir import fsl.utils.tempdir as tempdir
import fsl.data.image as fslimage import fsl.data.image as fslimage
class _BooleanFlag(object): def _update_wrapper(wrapper, wrapped, *args, **kwargs):
def __init__(self, show): """Replacement for the built-in ``functools.update_wrapper``. This
self.show = show implementation ensures that the wrapper function has an attribute
def __eq__(self, other): called ``__wrapped__``, which refers to the ``wrapped`` function.
return type(other) == type(self) and self.show == other.show
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) def _unwrap(func):
HIDE_IF_TRUE = _BooleanFlag(False) """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): 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) if style in ('-', '-='): arg = '-{}'.format(arg)
elif style in ('--', '--='): arg = '--{}'.format(arg) elif style in ('--', '--='): arg = '--{}'.format(arg)
return arg return arg
def fmtval(val, style=None): def fmtval(val):
if isinstance(val, collections.Sequence) and \ if isinstance(val, collections.Sequence) and \
not isinstance(val, six.string_types): not isinstance(val, six.string_types):
return ' '.join([str(v) for v in val]) return ' '.join([str(v) for v in val])
else: else:
return str(val) return str(val)
if style not in ('-', '--', '-=', '--='):
raise ValueError('Invalid style: {}'.format(style))
args = [] args = []
for k, v in kwargs.items(): for k, v in kwargs.items():
k = argmap.get(k, k) k = argmap.get(k, k)
mapv = valmap.get(k, fmtval(v, style)) mapv = valmap.get(k, fmtval(v))
k = fmtarg(k, style) 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: elif '=' in style:
args.append('{}={}'.format(k, mapv)) args.append('{}={}'.format(k, mapv))
else: else:
...@@ -72,110 +168,216 @@ def required(*reqargs): ...@@ -72,110 +168,216 @@ def required(*reqargs):
"""Decorator which makes sure that all specified keyword arguments are """Decorator which makes sure that all specified keyword arguments are
present before calling the decorated function. present before calling the decorated function.
""" """
def decorator(func): def decorator(func):
def wrapper(**kwargs): def wrapper(*args, **kwargs):
kwargs = kwargs.copy()
kwargs.update(argsToKwargs(func, args))
for reqarg in reqargs: for reqarg in reqargs:
assert reqarg in kwargs assert reqarg in kwargs
return func(**kwargs) return func(**kwargs)
return wrapper return _update_wrapper(wrapper, func)
return decorator return decorator
def argsToKwargs(func, args): def argsToKwargs(func, args):
"""Given a function, and a sequence of positional arguments destined """Given a function, and a sequence of positional arguments destined
for that function, converts the positional arguments into a dict 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 func = _unwrap(func)
# deprecated in python 3.5, but not in python 3.6.
with warnings.catch_warnings(): # getargspec is the only way to get the names
warnings.filterwarnings('ignore', category=DeprecationWarning) # of positional arguments in Python 2.x.
spec = inspect.getfullargspec(func) 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() kwargs = collections.OrderedDict()
for name, val in zip(spec.args, args): for name, val in zip(argnames, args):
kwargs[name] = val kwargs[name] = val
return kwargs return kwargs
RETURN = object() 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: **Outputs**
- In-memory nibabel images loaded from a file. The image is replaced with
its file name.
- 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: If an argument is given the special :data:`RETURN` value, it is assumed
- File name: The file name is passed straight through to the function. to be an output argument. In this case, it is replaced with a temporary
- ``RETURN``: A temporary file name is passed to the function. After the file name then, after the function has completed, that file is loaded
function has completed, the image is loaded into memory and the into memory, and the value returned (along with the function's output,
temporary file is deleted. The image is returned from the function and any other arguments with a value of ``RETURN``).
call.
**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): def __call__(self, func):
""" """Creates and returns the real decorator function. """
"""
return functools.partial(self.__wrapper, func) 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): 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)) kwargs.update(argsToKwargs(func, args))
# Create a tempdir to store any temporary # 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 # into it, as file paths passed to the
# function may be relative. # function may be relative.
with tempdir.tempdir(changeto=False) as td: with tempdir.tempdir(changeto=False) as td:
kwargs, infiles, outfiles = self.__prepareArgs(td, kwargs) kwargs, infiles, outfiles = self.__prepareThings(td, kwargs)
# Call the function # Call the function
result = func(**kwargs) result = func(**kwargs)
# Load the output images that # Load the output things that
# were specified as RETURN # were specified as RETURN
outimgs = [] outthings = []
for of in outfiles: for of in outfiles:
# output file didn't get created # output file didn't get created
if not op.exists(of): if not op.exists(of):
oi = None ot = None
# load the file, and create # load the thing
# an in-memory copy (the file
# is going to get deleted)
else: else:
oi = nib.load(of) ot = self.__loadThing(of)
oi = nib.nifti1.Nifti1Image(oi.get_data(), None, oi.header)
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):
""" """
""" """
...@@ -183,48 +385,110 @@ class _FileOrImage(object): ...@@ -183,48 +385,110 @@ class _FileOrImage(object):
infiles = [] infiles = []
outfiles = [] outfiles = []
for imgarg in self.__imgargs: for tname in self.__things:
img = kwargs.get(imgarg, None) tval = kwargs.get(tname, None)
# Not specified, nothing to do if tval is None:
if img is None:
continue continue
# This is an input image which has tval, infile, outfile = self.__prepareThing(workdir, tname, tval)
# been specified as an in-memory
# nibabel image. if the image has if infile is not None: infiles .append(infile)
# a backing file, replace the image if outfile is not None: outfiles.append(outfile)
# object with the file name.
# Otherwise, save the image out to kwargs[tname] = tval
# a temporary file, and replace the
# image with the file name.
if isinstance(img, nib.nifti1.Nifti1Image):
imgfile = img.get_filename()
# in-memory image - we have
# to save it out to a file
if imgfile is None:
hd, imgfile = tempfile.mkstemp(fslimage.defaultExt())
os.close(hd)
img.to_filename(imgfile)
infiles.append(imgfile)
# replace the image with its
# file name
kwargs[img] = imgfile
# This is an output image, and the
# caller has requested that it be
# returned from the function call
# as an in-memory image.
if img == RETURN:
kwargs[imgarg] = '{}.nii.gz'.format(imgarg)
outfiles.append(imgarg)
return kwargs, infiles, outfiles return kwargs, infiles, outfiles
fileOrImage = _FileOrImage def fileOrImage(*imgargs):
"""Decorator which can be used to ensure that any NIfTI images are saved
to file, and output images can be loaded and returned as ``nibabel``
image objects.
"""
def prepareArg(workdir, name, val):
newval = val
infile = None
outfile = None
# This is an input image which has
# been specified as an in-memory
# nibabel image. if the image has
# a backing file, replace the image
# object with the file name.
# Otherwise, save the image out to
# a temporary file, and replace the
# image with the file name.
if isinstance(val, nib.nifti1.Nifti1Image):
imgfile = val.get_filename()
# in-memory image - we have
# to save it out to a file
if imgfile is None:
hd, imgfile = tempfile.mkstemp(fslimage.defaultExt())
os.close(hd)
val.to_filename(imgfile)
infile = imgfile
# replace the image with its
# file name
newval = imgfile
# This is an output image, and the
# caller has requested that it be
# returned from the function call
# as an in-memory image.
elif val == RETURN:
newval = op.join(workdir, '{}.nii.gz'.format(name))
outfile = newval
return newval, infile, outfile
def loadImage(path):
# create an independent in-memory
# copy of the image file
img = nib.load(path)
return nib.nifti1.Nifti1Image(img.get_data(), None, img.header)
return _FileOrThing(prepareArg, loadImage, *imgargs)
def fileOrArray(*arrargs):
"""Decorator which can be used to ensure that any Numpy arrays are saved
to text files, and output files can be loaded and returned as Numpy arrays.
"""
def prepareArg(workdir, name, val):
newval = val
infile = None
outfile = None
# Input has been provided as a numpy
# array - save it to a file, and
# replace the argument with the file
# name
if isinstance(val, np.ndarray):
hd, arrfile = tempfile.mkstemp('.txt')
os.close(hd)
np.savetxt(arrfile, val, fmt='%0.18f')
newval = arrfile
# This is an output, and the caller has
# requested that it be returned from the
# function call as an in-memory array.
elif val == RETURN:
newval = op.join(workdir, '{}.txt'.format(name))
outfile = newval
return newval, infile, outfile
return _FileOrThing(prepareArg, np.loadtxt, *arrargs)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment