diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index dae80e9d6fc3df091bef68599230e7cfa58d1b96..f47a711bc6f641f6aebcebbeda39ecb597f551f1 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,16 +2,16 @@ This document contains the ``fslpy`` release history in reverse chronological
 order.
 
 
-3.0.0 (Under development)
--------------------------
+3.0.0 (Sunday 29th March 2020)
+------------------------------
 
 
 Added
 ^^^^^
 
 
-* New wrapper functions for the FSL :func:`.prelude` and :func:`applyxfm4D`
-  commands.
+* New wrapper functions for the FSL :class:`.fslstats`, :func:`.prelude` and
+  :func:`applyxfm4D` commands.
 * New ``firstDot`` option to the :func:`.path.getExt`,
   :func:`.path.removeExt`, and :func:`.path.splitExt`, functions, offering
   rudimentary support for double-barrelled filenames.
@@ -19,6 +19,7 @@ Added
   affine, which is applied to the input image before the deformation field.
 * New :class:`.SubmitParams` class, providing a higer level interface for
   cluster submission.
+* New :meth:`.FileTree.load_json` and  :meth:`.FileTree.save_json` methods.
 
 
 Changed
@@ -26,6 +27,10 @@ Changed
 
 
 * ``fslpy`` now requires a minimum Python version of 3.7.
+* The default value for the ``partial_fill`` option to :meth:`.FileTree.read`
+  has been changed to ``False``. Accordingly, the :class:`.FileTreeQuery`
+  calls the :meth:`.FileTree.partial_fill` method on the ``FileTree`` it is
+  given.
 * The :func:`.gifti.relatedFiles` function now supports files with
   BIDS-style naming conventions.
 * The :func:`.run.run` and :func:`.run.runfsl` functions now pass through any
@@ -54,6 +59,8 @@ Changed
 * The :func:`.fileOrText` decorator has been updated to work with input
   values - file paths must be passed in as ``pathlib.Path`` objects, so they
   can be differentiated from input values.
+* Loaded :class:`.Image` objects returned by :mod:`fsl.wrappers` functions
+  are now named according to the wrapper function argument name.
 
 
 Fixed
@@ -69,6 +76,24 @@ Fixed
   a single set of coordinates.
 
 
+Removed
+^^^^^^^
+
+
+* Removed the deprecated ``.StatisticAtlas.proportions``,
+  ``.StatisticAtlas.coordProportions``, and
+  ``.StatisticAtlas.maskProportions`` methods.
+* Removed the deprecated ``indexed`` option to :meth:`.Image.__init__`.
+* Removed the deprecated ``.Image.resample`` method.
+* Removed the deprecated ``.image.loadIndexedImageFile`` function.
+* Removed the deprecatd ``.FileTreeQuery.short_names`` and
+  ``.Match.short_name`` properties.
+* Removed the deprecated ``.idle.inIdle``, ``.idle.cancelIdle``,
+  ``.idle.idleReset``, ``.idle.getIdleTimeout``, and
+  ``.idle.setIdleTimeout`` functions.
+* Removed the deprecated ``resample.calculateMatrix`` function.
+
+
 2.8.4 (Monday 2nd March 2020)
 -----------------------------
 
diff --git a/fsl/data/atlases.py b/fsl/data/atlases.py
index 28ca1825a5b3804ca4e6e5cbabad9084a1470fb4..fa30529fb12cba90cd8d97bb5f8cc27de900331f 100644
--- a/fsl/data/atlases.py
+++ b/fsl/data/atlases.py
@@ -58,7 +58,6 @@ import fsl.utils.image.resample as resample
 import fsl.transform.affine     as affine
 import fsl.utils.notifier       as notifier
 import fsl.utils.settings       as fslsettings
-import fsl.utils.deprecated     as deprecated
 
 
 log = logging.getLogger(__name__)
@@ -1085,24 +1084,6 @@ class StatisticAtlas(Atlas):
         return avgvals
 
 
-    @deprecated.deprecated('2.6.0', '3.0.0', 'Use values instead')
-    def proportions(self, *args, **kwargs):
-        """Deprecated - use :meth:`values` instead. """
-        return self.values(*args, **kwargs)
-
-
-    @deprecated.deprecated('2.6.0', '3.0.0', 'Use coordValues instead')
-    def coordProportions(self, *args, **kwargs):
-        """Deprecated - use :meth:`coordValues` instead. """
-        return self.coordValues(*args, **kwargs)
-
-
-    @deprecated.deprecated('2.6.0', '3.0.0', 'Use maskValues instead')
-    def maskProportions(self, *args, **kwargs):
-        """Deprecated - use :meth:`maskValues` instead. """
-        return self.maskValues(*args, **kwargs)
-
-
 class ProbabilisticAtlas(StatisticAtlas):
     """A 4D atlas which contains one volume for each region. Each volume
     contains probabiliy values for one region, between 0 and 100.
diff --git a/fsl/data/image.py b/fsl/data/image.py
index 97fe2d72259864a9890f571ea91671e43a56659b..8cc5c62087b25ebe9f463dcc7e95b1f57f3fe484 100644
--- a/fsl/data/image.py
+++ b/fsl/data/image.py
@@ -52,7 +52,6 @@ import fsl.transform.affine  as affine
 import fsl.utils.notifier    as notifier
 import fsl.utils.memoize     as memoize
 import fsl.utils.path        as fslpath
-import fsl.utils.deprecated  as deprecated
 import fsl.utils.bids        as fslbids
 import fsl.data.constants    as constants
 import fsl.data.imagewrapper as imagewrapper
@@ -993,7 +992,6 @@ class Image(Nifti):
                  xform=None,
                  loadData=True,
                  calcRange=True,
-                 indexed=False,
                  threaded=False,
                  dataSource=None,
                  loadMeta=False,
@@ -1002,7 +1000,7 @@ class Image(Nifti):
 
         :arg image:      A string containing the name of an image file to load,
                          or a :mod:`numpy` array, or a :mod:`nibabel` image
-                         object.
+                         object, or an ``Image``object.
 
         :arg name:       A name for the image.
 
@@ -1035,9 +1033,6 @@ class Image(Nifti):
                          incrementally updated as more data is read from memory
                          or disk.
 
-        :arg indexed:    Deprecated. Has no effect, and will be removed in
-                         ``fslpy`` 3.0.
-
         :arg threaded:   If ``True``, the :class:`.ImageWrapper` will use a
                          separate thread for data range calculation. Defaults
                          to ``False``. Ignored if ``loadData`` is ``True``.
@@ -1059,12 +1054,6 @@ class Image(Nifti):
         nibImage = None
         saved    = False
 
-        if indexed is not False:
-            warnings.warn('The indexed argument is deprecated '
-                          'and has no effect',
-                          category=DeprecationWarning,
-                          stacklevel=2)
-
         if loadData:
             threaded = False
 
@@ -1463,14 +1452,6 @@ class Image(Nifti):
         self.notify(topic='saveState')
 
 
-    @deprecated.deprecated('2.2.0', '3.0.0',
-                           'Use fsl.utils.image.resample instead.')
-    def resample(self, *args, **kwargs):
-        """Deprecated - use :func:`.image.resample` instead. """
-        from fsl.utils.image.resample import resample
-        return resample(self, *args, **kwargs)
-
-
     def __getitem__(self, sliceobj):
         """Access the image data with the specified ``sliceobj``.
 
@@ -1661,9 +1642,3 @@ def defaultExt():
     outputType = os.environ.get('FSLOUTPUTTYPE', 'NIFTI_GZ')
 
     return options.get(outputType, '.nii.gz')
-
-
-@deprecated.deprecated('2.0.0', '3.0.0', 'Use nibabel.load instead.')
-def loadIndexedImageFile(filename):
-    """Deprecated - this call is equivalent to calling ``nibabel.load``. """
-    return nib.load(filename)
diff --git a/fsl/utils/filetree/query.py b/fsl/utils/filetree/query.py
index d371bcdf37a7d31d7ed333e602ba50f992477ade..a83982083515261bb7cefd063fe697e03d5ed698 100644
--- a/fsl/utils/filetree/query.py
+++ b/fsl/utils/filetree/query.py
@@ -31,8 +31,7 @@ from typing import Dict, List, Tuple
 
 import numpy as np
 
-from fsl.utils.deprecated import deprecated
-from .                    import FileTree
+from . import FileTree
 
 
 log = logging.getLogger(__name__)
@@ -207,15 +206,6 @@ class FileTreeQuery(object):
         return list(self.__templatevars.keys())
 
 
-    @property
-    @deprecated('2.6.0', '3.0.0', 'Use templates instead')
-    def short_names(self) -> List[str]:
-        """Returns a list containing all templates of the ``FileTree`` that
-        are present in the directory.
-        """
-        return self.templates
-
-
     def query(self, template, asarray=False, **variables):
         """Search for files of the given ``template``, which match
         the specified ``variables``. All hits are returned for variables
@@ -292,12 +282,6 @@ class Match(object):
         return self.__filename
 
 
-    @property
-    @deprecated('2.6.0', '3.0.0', 'Use template instead')
-    def short_name(self):
-        return self.template
-
-
     @property
     def template(self):
         return self.__template
diff --git a/fsl/utils/idle.py b/fsl/utils/idle.py
index d2c93aeb08e0928df4e0dcceb84adf32dc98550f..bef726633b40f8977a3a77a429d77917f7c398b0 100644
--- a/fsl/utils/idle.py
+++ b/fsl/utils/idle.py
@@ -85,8 +85,6 @@ from   collections import abc
 try:                import queue
 except ImportError: import Queue as queue
 
-from fsl.utils.deprecated import deprecated
-
 
 log = logging.getLogger(__name__)
 
@@ -597,36 +595,6 @@ def idleWhen(*args, **kwargs):
     idleLoop.idleWhen(*args, **kwargs)
 
 
-@deprecated('2.7.0', '3.0.0', 'Use idleLoop.inIdle instead')
-def inIdle(taskName):
-    """Deprecated - use ``idleLoop.inIdle`` instead. """
-    return idleLoop.inIdle(taskName)
-
-
-@deprecated('2.7.0', '3.0.0', 'Use idleLoop.cancelIdle instead')
-def cancelIdle(taskName):
-    """Deprecated - use ``idleLoop.cancelIdle`` instead. """
-    return idleLoop.cancelIdle(taskName)
-
-
-@deprecated('2.7.0', '3.0.0', 'Use idleLoop.reset instead')
-def idleReset():
-    """Deprecated - use ``idleLoop.reset`` instead. """
-    return idleLoop.reset()
-
-
-@deprecated('2.7.0', '3.0.0', 'Use idleLoop.callRate instead')
-def getIdleTimeout():
-    """Deprecated - use ``idleLoop.callRate`` instead. """
-    return idleLoop.callRate
-
-
-@deprecated('2.7.0', '3.0.0', 'Use idleLoop.callRate instead')
-def setIdleTimeout(timeout=None):
-    """Deprecated - use ``idleLoop.callRate`` instead. """
-    idleLoop.callRate = timeout
-
-
 def block(secs, delta=0.01, until=None):
     """Blocks for the specified number of seconds, yielding to the main ``wx``
     loop.
diff --git a/fsl/utils/image/resample.py b/fsl/utils/image/resample.py
index 371e87b602c60b777bc6e8587aec32b185f08e1d..f5918fad29f18ff0912968c56293ed67ac44c0ae 100644
--- a/fsl/utils/image/resample.py
+++ b/fsl/utils/image/resample.py
@@ -18,7 +18,6 @@ import numpy                as np
 import scipy.ndimage        as ndimage
 
 import fsl.transform.affine as affine
-import fsl.utils.deprecated as deprecated
 
 
 def resampleToPixdims(image, newPixdims, **kwargs):
@@ -282,13 +281,3 @@ def applySmoothing(data, matrix, newShape):
     sigma[ratio >= 1.1] *= 0.425
 
     return ndimage.gaussian_filter(data, sigma)
-
-
-@deprecated.deprecated('2.8.0', '3.0.0',
-                       'Use fsl.transform.affine.rescale instead')
-def calculateMatrix(oldShape, newShape, origin):
-    """Deprecated - use :func:`.affine.rescale` instead. """
-    xform = affine.rescale(oldShape, newShape, origin)
-    if np.all(np.isclose(xform, np.eye(len(oldShape) + 1))):
-        return None
-    return xform[:-1, :]
diff --git a/fsl/utils/run.py b/fsl/utils/run.py
index 70915cf2dea6d0adcc68cd733283382c634153d1..caacbf9761bc7c9da7e198449915ee848ce6c04b 100644
--- a/fsl/utils/run.py
+++ b/fsl/utils/run.py
@@ -180,13 +180,17 @@ def run(*args, **kwargs):
     returnStderr   = kwargs.pop('stderr',   False)
     returnExitcode = kwargs.pop('exitcode', False)
     submit         = kwargs.pop('submit',   {})
-    log            = kwargs.pop('log',      {})
-    tee            = log   .get('tee',      False)
-    logStdout      = log   .get('stdout',   None)
-    logStderr      = log   .get('stderr',   None)
-    logCmd         = log   .get('cmd',      None)
+    log            = kwargs.pop('log',      None)
     args           = prepareArgs(args)
 
+    if log is None:
+        log = {}
+
+    tee       = log.get('tee',    False)
+    logStdout = log.get('stdout', None)
+    logStderr = log.get('stderr', None)
+    logCmd    = log.get('cmd',    None)
+
     if not bool(submit):
         submit = None
 
diff --git a/fsl/utils/transform.py b/fsl/utils/transform.py
deleted file mode 100644
index b3d51afb21f0e5a5a861458d4fde9e2e62daebc2..0000000000000000000000000000000000000000
--- a/fsl/utils/transform.py
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env python
-#
-# transforms.py - Deprecated
-#
-# Author: Paul McCarthy <pauldmccarthy@gmail.com>
-#
-"""The ``fsl.utils.transform`` module is deprecated - use the
-:mod:`fsl.transform` module instead.
-"""
-
-
-import fsl.utils.deprecated as     deprecated
-from   fsl.transform.affine import *                     # noqa
-from   fsl.transform.flirt  import (flirtMatrixToSform,  # noqa
-                                    sformToFlirtMatrix)
-
-
-deprecated.warn('fsl.utils.transform',
-                vin='2.4.0',
-                rin='3.0.0',
-                msg='Use the fsl.transform module instead')
diff --git a/fsl/version.py b/fsl/version.py
index 5078f11fae8655123c981a23615b0b47a641fd2f..34d5447e181f21be4dfed288ad18931acda71e1b 100644
--- a/fsl/version.py
+++ b/fsl/version.py
@@ -47,7 +47,7 @@ import            re
 import            string
 
 
-__version__ = '2.9.0.dev0'
+__version__ = '3.1.0.dev0'
 """Current version number, as a string. """
 
 
diff --git a/fsl/wrappers/__init__.py b/fsl/wrappers/__init__.py
index 5e1f61e89dda468017033e20b526712d70dda4c4..87f410cb9c5475ab4542e96c074d0c04327763e1 100644
--- a/fsl/wrappers/__init__.py
+++ b/fsl/wrappers/__init__.py
@@ -37,12 +37,15 @@ instead. Aliases may also be used to provide a more readable interface (e.g.
 the :func:`.bet` function uses ``mask`` instead of ``m``).
 
 
-One exception to the above is :class:`.fslmaths`, which provides a more
-object-oriented interface::
+Two exceptions to the above are :class:`.fslmaths` and :class:`.fslstats`,
+which provide a more object-oriented interface::
+
+    from fsl.wrappers import fslmaths, fslstats
 
-    from fsl.wrappers import fslmaths
     fslmaths('image.nii').mas('mask.nii').bin().run('output.nii')
 
+    imean, imin, imax = fslstats('image.nii').k('mask.nii').m.R.run()
+
 
 Wrapper functions for commands which accept NIfTI image or numeric text files
 will for the most part accept either in-memory ``nibabel`` images/Numpy arrays
@@ -95,6 +98,7 @@ from .fnirt        import (fnirt,           # noqa
                            invwarp,
                            convertwarp)
 from .fslmaths     import (fslmaths,)       # noqa
+from .fslstats     import (fslstats,)       # noqa
 from .fugue        import (fugue,           # noqa
                            prelude,
                            sigloss)
diff --git a/fsl/wrappers/fslstats.py b/fsl/wrappers/fslstats.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4ad2e2ddca49a83664283245bcbc8ec499cca3c
--- /dev/null
+++ b/fsl/wrappers/fslstats.py
@@ -0,0 +1,255 @@
+#!/usr/bin/env python
+#
+# fslstats.py - Wrapper for fslstats
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""This module provides the :class:`fslstats` class, which acts as a wrapper
+for the ``fslstats`` command-line tool.
+
+
+.. warning:: This wrapper function will only work with FSL 6.0.2 or newer.
+"""
+
+
+import              io
+import functools as ft
+import numpy     as np
+
+import fsl.data.image      as fslimage
+from . import wrapperutils as wutils
+
+
+class fslstats(object):
+    """The ``fslstats`` class is a wrapper around the ``fslstats`` command-line
+    tool. It provides an object-oriented interface - options are specified by
+    chaining method calls and attribute accesses together.
+
+
+    .. warning:: This wrapper function will only work with FSL 6.0.2 or newer,
+                 due to bugs in ``fslstats`` output formatting that are
+                 present in older versions.
+
+
+    Any ``fslstats`` command-line option which does not require any arguments
+    (e.g. ``-r``) can be set by accessing an attribute on a ``fslstats``
+    object, e.g.::
+
+        stats = fslstats('image.nii.gz')
+        stats.r
+
+
+    ``fslstats`` command-line options which do require additional arguments
+    (e.g. ``-k``) can be set by calling a method on an ``fslstats`` object,
+    e.g.::
+
+        stats = fslstats('image.nii.gz')
+        stats.k('mask.nii.gz')
+
+
+    The ``fslstats`` command can be executed via the :meth:`run` method.
+    Normally, the results will be returned as a scalar floating point number,
+    or a ``numpy`` array. Pre-options will affect the structure of the return
+    value - see :meth:`__init__` for details.
+
+
+    Attribute and method calls can be chained together, so a complete
+    ``fslstats`` call can be performed in a single line, e.g.::
+
+        imgmin, imgmax = fslstats('image.nii.gz').k('mask.nii.gz').r.run()
+    """
+
+    OPTIONS = {
+        'robust_minmax'   : 'r',
+        'minmax'          : 'R',
+        'mean_entropy'    : 'e',
+        'mean_entropy_nz' : 'E',
+        'volume'          : 'v',
+        'volume_nz'       : 'V',
+        'mean'            : 'm',
+        'mean_nz'         : 'M',
+        'stddev'          : 's',
+        'stddev_nz'       : 'S',
+        'smallest_roi'    : 'w',
+        'max_vox'         : 'x',
+        'min_vox'         : 'X',
+        'cog_mm'          : 'c',
+        'cog_vox'         : 'C',
+        'abs'             : 'a',
+        'zero_naninf'     : 'n',
+    }
+    """This dict contains options which do not require any additional
+    arguments. They are set via attribute access on the ``fslstats``
+    object.
+    """
+
+
+    ARG_OPTIONS = {
+        'lower_threshold' : 'l',
+        'upper_threshold' : 'u',
+        'percentile'      : 'p',
+        'percentile_nz'   : 'P',
+        'mask'            : 'k',
+        'diff'            : 'd',
+        'hist'            : 'h',
+        'hist_bounded'    : 'H',
+    }
+    """This dict contains options which require additional arguments.
+    They are set via method calls on the ``fslstats`` object (with the
+    additional arguments passed into the method call).
+    """
+
+
+    # add {shortopt : shortopt} mappings
+    # for all options to simplify code in
+    # the  fslstats class
+    OPTIONS    .update({v : v for v in OPTIONS    .values()})
+    ARG_OPTIONS.update({v : v for v in ARG_OPTIONS.values()})
+
+
+    def __init__(self,
+                 input,
+                 t=False,
+                 K=None,
+                 sep_volumes=False,
+                 index_mask=None):
+        """Create a ``fslstats`` object.
+
+        If one of the ``t`` or ``K`` pre-options is set, e.g.::
+
+            fslstats('image_4d.nii.gz', t=True)
+
+        or::
+
+            fslstats('image_4d.nii.gz', K='mask.nii.gz')
+
+        then :meth:`run` will return a 2D ``numpy`` array of shape ``(nvols,
+        nvals)`` if ``t`` is set, or ``(nlabels, nvals)`` if ``K`` is set.
+
+        If both of the ``t`` and ``K`` pre-options are set, e.g.::
+
+            fslstats('image_4d.nii.gz', t=True, K='mask.nii.gz')
+
+        then the result will be a 3D numpy array of shape ``(nvols, nlabels,
+        nvals)``.
+
+        If neither ``t`` or ``K`` are set, then the result will be a scalar,
+        or a 1D ``numpy`` array.
+
+        :arg input:       Input image - either a file name, or an
+                          :class:`.Image` object, or a ``nibabel.Nifti1Image``
+                          object.
+        :arg t:           Produce separate results for each 3D volume in the
+                          input image.
+        :arg K:           Produce separate results for each sub-mask within
+                          the provided mask image.
+        :arg sep_volumes: Alias for ``t``.
+        :arg index_mask:  Alias for ``K``.
+        """
+
+        if t is None: t = sep_volumes
+        if K is None: K = index_mask
+
+        self.__input   = input
+        self.__options = []
+
+        # pre-options must be supplied
+        # before input image
+        if t:             self.__options.append( '-t')
+        if K is not None: self.__options.extend(('-K', K))
+
+        self.__options.append(input)
+
+
+    def __getattr__(self, name):
+        """Intercepts attribute accesses and accumulates ``fslstats`` command-line
+        flags accordingly.
+        """
+
+        # options which take no args
+        # are called as attributes
+        if name in fslstats.OPTIONS:
+            flag = fslstats.OPTIONS[name]
+            args = False
+
+        # options which take args
+        # are called as methods
+        elif name in fslstats.ARG_OPTIONS:
+            flag = fslstats.ARG_OPTIONS[name]
+            args = True
+        else:
+            raise AttributeError(name)
+
+        addFlag = ft.partial(self.__addFlag, flag)
+
+        if args: return addFlag
+        else:    return addFlag()
+
+
+    def __addFlag(self, flag, *args):
+        """Used by :meth:`__getattr__`. Add the given flag and any arguments to
+        the accumulated list of command-line options.
+        """
+        self.__options.extend(('-' + flag,) + args)
+        return self
+
+
+    def run(self, raw=False):
+        """Run the ``fslstats`` command-line tool. See :meth:`__init__` for a
+        description of the return value.
+
+        :arg raw: Defaults to ``False``. If ``True``, the raw standard output
+                  and error is returned, instead of a scalar/numpy array.
+
+        :returns: Result of ``fslstats`` as a scalar or ``numpy`` array.
+        """
+
+        # The parsing logic below will not work
+        # with versions of fslstats prior to fsl
+        # 6.0.2, due to a quirk in the output
+        # formatting of older versions.
+
+        # The default behaviour of run/runfsl
+        # is to tee the command output streams
+        # to the calling process streams. We
+        # can disable this via log=None.
+        result = self.__run('fslstats', *self.__options, log=None)
+
+        if raw:
+            return result.stdout
+
+        result  = np.genfromtxt(io.StringIO(result.stdout[0].strip()))
+        sepvols = '-t' in self.__options
+        lblmask = '-K' in self.__options
+
+        # One line of output for each volume and
+        # for each label (with volume the slowest
+        # changing). Reshape to 3D.
+        if sepvols and lblmask:
+
+            # We need know the number of volumes
+            # (or the number of labels) in order
+            # to know how to shape the results.
+            img = fslimage.Image(self.__input, loadData=False)
+
+            if img.ndim >= 4: nvols = img.shape[3]
+            else:             nvols = 1
+
+            # reshape the result into
+            # (nvals, nvols, nlbls)
+            nlbls  = int(len(result) / nvols)
+            result = result.reshape((nvols, nlbls, -1)).squeeze()
+
+        # Scalar - use numpy indexing weirdness
+        # to get our single value out.
+        elif result.size == 1:
+            result = result[()]
+
+        return result
+
+
+    @wutils.fileOrImage()
+    @wutils.fslwrapper
+    def __run(self, *cmd):
+        """Run the given ``fslmaths`` command. """
+        return [str(c) for c in cmd]
diff --git a/fsl/wrappers/wrapperutils.py b/fsl/wrappers/wrapperutils.py
index f325bca18d5f5c38990b0db57c8066746acf392a..150ef0ae979d520cfd68404ba91238af6a66eb09 100644
--- a/fsl/wrappers/wrapperutils.py
+++ b/fsl/wrappers/wrapperutils.py
@@ -403,18 +403,18 @@ def namedPositionals(func, args):
 
 
 LOAD = object()
-"""Constant used by the :class:`_FileOrThing` class to indicate that an output
+"""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 _FileOrThing(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
+    The ``FileOrThing`` class is not intended to be used directly - see the
     :func:`fileOrImage` and :func:`fileOrArray` decorator functions for more
     details.
 
@@ -445,7 +445,7 @@ class _FileOrThing(object):
     **Return value**
 
 
-    Functions decorated with a ``_FileOrThing`` decorator will always return a
+    Functions decorated with a ``FileOrThing`` decorator will always return a
     ``dict``-like object, where the function's actual return value is
     accessible via an attribute called ``stdout``. All output arguments with a
     value of ``LOAD`` will be present as dictionary entries, with the keyword
@@ -460,7 +460,7 @@ class _FileOrThing(object):
 
     The above description holds in all situations, except when an argument
     called ``submit`` is passed, and is set to a value which evaluates to
-    ``True``. In this case, the ``_FileOrThing`` decorator will pass all
+    ``True``. In this case, the ``FileOrThing`` decorator will pass all
     arguments straight through to the decorated function, and will return its
     return value unchanged.
 
@@ -534,27 +534,27 @@ class _FileOrThing(object):
     **Using with other decorators**
 
 
-    ``_FileOrThing`` decorators can be chained with other ``_FileOrThing``
-    decorators, and other decorators.  When multiple ``_FileOrThing``
+    ``FileOrThing`` decorators can be chained with other ``FileOrThing``
+    decorators, and other decorators.  When multiple ``FileOrThing``
     decorators are used on a single function, the outputs from each decorator
     are merged together into a single dict-like object.
 
 
-    ``_FileOrThing`` decorators can be used with any other decorators
+    ``FileOrThing`` decorators can be used with any other decorators
     **as long as** they do not manipulate the return value, and as long as
-    the ``_FileOrThing`` decorators are adjacent to each other.
+    the ``FileOrThing`` decorators are adjacent to each other.
     """
 
 
-    class _Results(dict):
+    class Results(dict):
         """A custom ``dict`` type used to return outputs from a function
-        decorated with ``_FileOrThing``. All outputs are stored as dictionary
+        decorated with ``FileOrThing``. All outputs are stored as dictionary
         items, with the argument name as key, and the output object (the
         "thing") as value.
 
         Where possible (i.e. for outputs named with a valid Python
         identifier), the outputs are also made accessible as attributes of
-        the ``_Results`` object.
+        the ``Results`` object.
 
         The decorated function's actual return value is accessible via the
         :meth:`stdout` property.
@@ -562,7 +562,7 @@ class _FileOrThing(object):
 
 
         def __init__(self, stdout):
-            """Create a ``_Results`` dict.
+            """Create a ``Results`` dict.
 
             :arg stdout: Return value of the decorated function (typically a
                          tuple containing the standard output and error of the
@@ -593,7 +593,7 @@ class _FileOrThing(object):
                  removeExt,
                  *args,
                  **kwargs):
-        """Initialise a ``_FileOrThing`` decorator.
+        """Initialise a ``FileOrThing`` decorator.
 
         :arg func:      The function to be decorated.
 
@@ -604,8 +604,10 @@ class _FileOrThing(object):
                         arguments that were set to :data:`LOAD`.
 
         :arg load:      Function which is called to load items for arguments
-                        that were set to :data:`LOAD`. Must accept a file path
-                        as its sole argument.
+                        that were set to :data:`LOAD`. Must accept the
+                        following arguments:
+                         - the name of the argument
+                         - path to the file to be loaded
 
         :arg removeExt: Function which can remove a file extension from a file
                         path.
@@ -618,7 +620,7 @@ class _FileOrThing(object):
 
         All other positional arguments are interpreted as the names of the
         arguments to the function which will be handled by this
-        ``_FileOrThing`` decorator. If not provided, *all* arguments passed to
+        ``FileOrThing`` decorator. If not provided, *all* arguments passed to
         the function will be handled.
 
 
@@ -673,8 +675,8 @@ class _FileOrThing(object):
                                      'or LOAD with submit=True!')
             return func(*args, **kwargs)
 
-        # If this _FileOrThing is being called
-        # by another _FileOrThing don't create
+        # If this FileOrThing is being called
+        # by another FileOrThing don't create
         # another working directory. We do this
         # sneakily, by setting an attribute on
         # the wrapped function which stores the
@@ -734,8 +736,8 @@ class _FileOrThing(object):
         passed to the ``prepOut`` function specified at :meth:`__init__`.
         All other arguments are passed through the ``prepIn`` function.
 
-        :arg parent:  ``True`` if this ``_FileOrThing`` is the first in a
-                      chain of ``_FileOrThing`` decorators.
+        :arg parent:  ``True`` if this ``FileOrThing`` is the first in a
+                      chain of ``FileOrThing`` decorators.
 
         :arg workdir: Directory in which all temporary files should be stored.
 
@@ -760,7 +762,7 @@ class _FileOrThing(object):
                         - A dictionary of ``{ name : filename }`` mappings,
                           for all arguments with a value of ``LOAD``.
 
-                        - A dictionary   ``{ filepat : replstr }`` paths, for
+                        - A dictionary of ``{ filepat : replstr }`` paths, for
                           all output-prefix arguments with a value of ``LOAD``.
         """
 
@@ -779,7 +781,7 @@ class _FileOrThing(object):
 
         # Prefixed outputs are only
         # managed by the parent
-        # _FileOrthing in a chain of
+        # FileOrthing in a chain of
         # FoT decorators.
         if not parent:
             prefix = None
@@ -894,11 +896,11 @@ class _FileOrThing(object):
 
     def __generateResult(
             self, workdir, result, outprefix, outfiles, prefixes):
-        """Loads function outputs and returns a :class:`_Results` object.
+        """Loads function outputs and returns a :class:`Results` object.
 
         Called by :meth:`__call__` after the decorated function has been
         called. Figures out what files should be loaded, and loads them into
-        a ``_Results`` object.
+        a ``Results`` object.
 
         :arg workdir:   Directory which contains the function outputs.
         :arg result:    Function return value.
@@ -909,23 +911,23 @@ class _FileOrThing(object):
         :arg prefixes:  Dictionary containing output-prefix patterns to be
                         loaded (see :meth:`__prepareArgs`).
 
-        :returns:       A ``_Results`` object containing all loaded outputs.
+        :returns:       A ``Results`` object containing all loaded outputs.
         """
 
-        # make a _Results object to store
+        # make a Results object to store
         # the output. If we are decorating
-        # another _FileOrThing, the
+        # another FileOrThing, the
         # results will get merged together
-        # into a single _Results dict.
-        if not isinstance(result, _FileOrThing._Results):
-            result = _FileOrThing._Results(result)
+        # into a single Results dict.
+        if not isinstance(result, FileOrThing.Results):
+            result = FileOrThing.Results(result)
 
         # Load the LOADed outputs
         for oname, ofile in outfiles.items():
 
             log.debug('Loading output %s: %s', oname, ofile)
 
-            if op.exists(ofile): oval = self.__load(ofile)
+            if op.exists(ofile): oval = self.__load(oname, ofile)
             else:                oval = None
 
             result[oname] = oval
@@ -953,21 +955,23 @@ class _FileOrThing(object):
                 log.debug('Loading prefixed output %s [%s]: %s',
                           prefPat, prefName, prefixed)
 
+                noext   = self.__removeExt(prefixed)
+                prefPat = prefPat.replace('\\', '\\\\')
+                noext   = re.sub('^' + prefPat, prefName, noext)
+                withext = re.sub('^' + prefPat, prefName, prefixed)
+
                 # if the load function returns
                 # None, this file is probably
                 # not of the correct type.
-                fval = self.__load(fullpath)
+                fval = self.__load(noext, fullpath)
                 if fval is not None:
-                    noext = self.__removeExt(prefixed)
-                    prefPat  = prefPat.replace('\\', '\\\\')
-                    noext = re.sub('^' + prefPat, prefName, noext)
+
                     # If there is already an item in result with the
                     # name (stripped of prefix), then instead store
                     # the result with the full prefixed name
                     if noext not in result:
                         result[noext] = fval
                     else:
-                        withext = re.sub('^' + prefPat, prefName, prefixed)
                         result[withext] = fval
                     break
 
@@ -1014,7 +1018,7 @@ def fileOrImage(*args, **kwargs):
     def prepOut(workdir, name, val):
         return op.join(workdir, '{}.nii.gz'.format(name))
 
-    def load(path):
+    def load(name, path):
 
         if not fslimage.looksLikeImage(path):
             return None
@@ -1027,7 +1031,8 @@ def fileOrImage(*args, **kwargs):
         # if any arguments were fsl images,
         # that takes precedence.
         if fslimage.Image in intypes:
-            return fslimage.Image(data, header=img.header)
+            return fslimage.Image(data, header=img.header, name=name)
+
         # but if all inputs were file names,
         # nibabel takes precedence
         elif nib.nifti1.Nifti1Image in intypes or len(intypes) == 0:
@@ -1039,13 +1044,13 @@ def fileOrImage(*args, **kwargs):
             raise RuntimeError('Cannot handle type: {}'.format(intypes))
 
     def decorator(func):
-        fot = _FileOrThing(func,
-                           prepIn,
-                           prepOut,
-                           load,
-                           fslimage.removeExt,
-                           *args,
-                           **kwargs)
+        fot = FileOrThing(func,
+                          prepIn,
+                          prepOut,
+                          load,
+                          fslimage.removeExt,
+                          *args,
+                          **kwargs)
 
         def wrapper(*args, **kwargs):
             result = fot(*args, **kwargs)
@@ -1076,18 +1081,18 @@ def fileOrArray(*args, **kwargs):
     def prepOut(workdir, name, val):
         return op.join(workdir, '{}.txt'.format(name))
 
-    def load(path):
+    def load(_, path):
         try:              return np.loadtxt(path)
         except Exception: return None
 
     def decorator(func):
-        fot = _FileOrThing(func,
-                           prepIn,
-                           prepOut,
-                           load,
-                           fslpath.removeExt,
-                           *args,
-                           **kwargs)
+        fot = FileOrThing(func,
+                          prepIn,
+                          prepOut,
+                          load,
+                          fslpath.removeExt,
+                          *args,
+                          **kwargs)
 
         def wrapper(*args, **kwargs):
             return fot(*args, **kwargs)
@@ -1141,20 +1146,20 @@ def fileOrText(*args, **kwargs):
     def prepOut(workdir, name, val):
         return op.join(workdir, '{}.txt'.format(name))
 
-    def load(path):
+    def load(_, path):
         try:
             with open(path, "r") as f:
                 return f.read()
         except Exception: return None
 
     def decorator(func):
-        fot = _FileOrThing(func,
-                           prepIn,
-                           prepOut,
-                           load,
-                           fslpath.removeExt,
-                           *args,
-                           **kwargs)
+        fot = FileOrThing(func,
+                          prepIn,
+                          prepOut,
+                          load,
+                          fslpath.removeExt,
+                          *args,
+                          **kwargs)
 
         def wrapper(*args, **kwargs):
             return fot(*args, **kwargs)
diff --git a/tests/test_scripts/test_resample_image.py b/tests/test_scripts/test_resample_image.py
index ed7e944b57e2e4196bf45c10efe341ada0243c84..01063067d0836359999ec205b7e23a3d7dbcb518 100644
--- a/tests/test_scripts/test_resample_image.py
+++ b/tests/test_scripts/test_resample_image.py
@@ -8,7 +8,7 @@ import pytest
 import fsl.scripts.resample_image as resample_image
 
 
-import  fsl.utils.transform as transform
+import fsl.transform.affine as affine
 from fsl.utils.tempdir import tempdir
 from fsl.data.image    import Image
 
@@ -22,22 +22,22 @@ def test_resample_image_shape():
         resample_image.main('image resampled -s 20,20,20'.split())
         res = Image('resampled')
 
-        expv2w = transform.concat(
+        expv2w = affine.concat(
             img.voxToWorldMat,
-            transform.scaleOffsetXform([0.5, 0.5, 0.5], 0))
+            affine.scaleOffsetXform([0.5, 0.5, 0.5], 0))
 
         assert np.all(np.isclose(res.shape, (20, 20, 20)))
         assert np.all(np.isclose(res.pixdim, (0.5, 0.5, 0.5)))
         assert np.all(np.isclose(res.voxToWorldMat, expv2w))
         assert np.all(np.isclose(
-            np.array(transform.axisBounds(res.shape, res.voxToWorldMat)) - 0.25,
-                     transform.axisBounds(img.shape, img.voxToWorldMat)))
+            np.array(affine.axisBounds(res.shape, res.voxToWorldMat)) - 0.25,
+                     affine.axisBounds(img.shape, img.voxToWorldMat)))
 
         resample_image.main('image resampled -s 20,20,20 -o corner'.split())
         res = Image('resampled')
         assert np.all(np.isclose(
-            transform.axisBounds(res.shape, res.voxToWorldMat),
-            transform.axisBounds(img.shape, img.voxToWorldMat)))
+            affine.axisBounds(res.shape, res.voxToWorldMat),
+            affine.axisBounds(img.shape, img.voxToWorldMat)))
 
 
 def test_resample_image_shape_4D():
@@ -65,9 +65,9 @@ def test_resample_image_dim():
         resample_image.main('image resampled -d 0.5,0.5,0.5'.split())
 
         res = Image('resampled')
-        expv2w = transform.concat(
+        expv2w = affine.concat(
             img.voxToWorldMat,
-            transform.scaleOffsetXform([0.5, 0.5, 0.5], 0))
+            affine.scaleOffsetXform([0.5, 0.5, 0.5], 0))
 
         assert np.all(np.isclose(res.shape, (20, 20, 20)))
         assert np.all(np.isclose(res.pixdim, (0.5, 0.5, 0.5)))
diff --git a/tests/test_wrappers/__init__.py b/tests/test_wrappers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/test_wrappers/test_fslstats.py b/tests/test_wrappers/test_fslstats.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d20b4accaf7416a42d02c136d43129028f06fde
--- /dev/null
+++ b/tests/test_wrappers/test_fslstats.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+
+
+import os
+import os.path as op
+import sys
+import contextlib
+
+import pytest
+
+import numpy as np
+
+import fsl.utils.run as run
+import fsl.utils.tempdir as tempdir
+import fsl.wrappers  as fw
+
+from .. import mockFSLDIR as mockFSLDIR_base, make_random_image
+
+
+mock_fslstats = """
+#!{}
+
+shape = {{outshape}}
+
+import sys
+import numpy as np
+
+data = np.random.randint(1, 10, shape)
+
+if len(shape) == 1:
+    data = data.reshape(1, -1)
+
+np.savetxt(sys.stdout, data, fmt='%i')
+""".format(sys.executable).strip()
+
+
+@contextlib.contextmanager
+def mockFSLDIR(shape):
+    with mockFSLDIR_base() as fd:
+        fname  = op.join(fd, 'bin', 'fslstats')
+        script = mock_fslstats.format(outshape=shape)
+        with open(fname, 'wt') as f:
+            f.write(script)
+        os.chmod(fname, 0o755)
+        yield fd
+
+
+def test_fslstats_cmdline():
+    with tempdir.tempdir(), run.dryrun(), mockFSLDIR(1) as fsldir:
+
+        make_random_image('image')
+        cmd = op.join(fsldir, 'bin', 'fslstats')
+
+        result   = fw.fslstats('image').m.r.mask('mask').k('mask').r.run(True)
+        expected = cmd + ' image -m -r -k mask -k mask -r'
+        assert result[0] == expected
+
+        result   = fw.fslstats('image', t=True, K='mask').m.R.u(123).s.volume.run(True)
+        expected = cmd + ' -t -K mask image -m -R -u 123 -s -v'
+        assert result[0] == expected
+
+        result   = fw.fslstats('image', K='mask').n.V.p(1).run(True)
+        expected = cmd + ' -K mask image -n -V -p 1'
+        assert result[0] == expected
+
+        result   = fw.fslstats('image', t=True).H(10, 1, 99).d('diff').run(True)
+        expected = cmd + ' -t image -H 10 1 99 -d diff'
+        assert result[0] == expected
+
+        # unknown option
+        with pytest.raises(AttributeError):
+            fw.fslstats('image').Q
+
+
+def test_fslstats_result():
+    with tempdir.tempdir():
+
+        with mockFSLDIR('(1,)') as fsldir:
+            result = fw.fslstats('image').run()
+            assert np.isscalar(result)
+
+        with mockFSLDIR('(2,)') as fsldir:
+            result = fw.fslstats('image').run()
+            assert result.shape == (2,)
+
+        # 3 mask lbls, 2 values
+        with mockFSLDIR('(3, 2)') as fsldir:
+            result = fw.fslstats('image', K='mask').run()
+            assert result.shape == (3, 2)
+
+        # 5 vols, 2 values
+        with mockFSLDIR('(5, 2)') as fsldir:
+            result = fw.fslstats('image', t=True).run()
+            assert result.shape == (5, 2)
+
+        # 5 vols, 3 mask lbls, 2 values
+        with mockFSLDIR('(15, 2)') as fsldir:
+            make_random_image('image', (10, 10, 10, 5))
+            result = fw.fslstats('image', K='mask', t=True).run()
+            assert result.shape == (5, 3, 2)
+
+        # -t/-K with a 3D image
+        with mockFSLDIR('(4,)') as fsldir:
+            make_random_image('image', (10, 10, 10))
+            result = fw.fslstats('image', K='mask', t=True).run()
+            assert result.shape == (4,)
+
+            result = fw.fslstats('image', t=True).run()
+            assert result.shape == (4,)
+
+            result = fw.fslstats('image', K='mask').run()
+            assert result.shape == (4,)
diff --git a/tests/test_wrappers.py b/tests/test_wrappers/test_wrappers.py
similarity index 99%
rename from tests/test_wrappers.py
rename to tests/test_wrappers/test_wrappers.py
index 1759672a94c96c77cbe7946d86431d63b4cd0e83..695bedfb30e585a6f2dd0c966a2165b298f99d77 100644
--- a/tests/test_wrappers.py
+++ b/tests/test_wrappers/test_wrappers.py
@@ -17,7 +17,7 @@ import fsl.utils.assertions               as asrt
 import fsl.utils.run                      as run
 from fsl.utils.tempdir import tempdir
 
-from . import mockFSLDIR, make_random_image
+from .. import mockFSLDIR, make_random_image
 
 
 def checkResult(cmd, base, args, stripdir=None):
diff --git a/tests/test_wrapperutils.py b/tests/test_wrappers/test_wrapperutils.py
similarity index 99%
rename from tests/test_wrapperutils.py
rename to tests/test_wrappers/test_wrapperutils.py
index c943ae5cf4d02f174793d2e25d8ddce779569d0c..e928057ff858a37b030f149782cf33400fe54e02 100644
--- a/tests/test_wrapperutils.py
+++ b/tests/test_wrappers/test_wrapperutils.py
@@ -27,8 +27,8 @@ import fsl.data.image            as fslimage
 import fsl.wrappers.wrapperutils as wutils
 
 
-from . import mockFSLDIR, cleardir, checkdir, testdir
-from .test_run import mock_submit
+from .. import mockFSLDIR, cleardir, checkdir, testdir
+from ..test_run import mock_submit
 
 
 def test_applyArgStyle():