From 8ce305993639f91c272ff87ca0225550fb040ad7 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Wed, 7 Jan 2015 12:17:16 +0000
Subject: [PATCH] Moved file extension stuff back out of image module, and into
 a new module called imageio, which also contains functions for loading/saving
 images. Skeleton for saveImage function.

---
 fsl/data/image.py              | 337 ++++--------------------------
 fsl/data/imageio.py            | 367 +++++++++++++++++++++++++++++++++
 fsl/tools/bet.py               |   7 +-
 fsl/tools/fslview_parseargs.py |   3 +-
 4 files changed, 410 insertions(+), 304 deletions(-)
 create mode 100644 fsl/data/imageio.py

diff --git a/fsl/data/image.py b/fsl/data/image.py
index c2c60329a..7e0a42e99 100644
--- a/fsl/data/image.py
+++ b/fsl/data/image.py
@@ -1,28 +1,33 @@
 #!/usr/bin/env python
-#
+# 
 # image.py - Classes for representing 3D/4D images and collections of said
 # images.
 #
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
-"""Classes for representing 3D/4D images and collections of said images."""
+"""Classes for representing 3D/4D images and collections of said images.
+
+See the :mod:`fsl.data.imageio` module for image loading/saving 
+functionality.
+
+"""
 
-import os
 import logging
-import tempfile
 import collections
-import subprocess as sp
-import os.path    as op
+import os.path as op
 
-import numpy      as np
-import nibabel    as nib
+import numpy   as np
+import nibabel as nib
 
 import props
-import fsl.utils.transform  as transform
+
+import fsl.utils.transform as transform
+import fsl.data.imageio    as iio
 
 
 log = logging.getLogger(__name__)
 
+
 # Constants which represent the orientation
 # of an axis, in either voxel or world space.
 ORIENT_UNKNOWN = -1
@@ -33,6 +38,7 @@ ORIENT_A2P     = 3
 ORIENT_I2S     = 4
 ORIENT_S2I     = 5
 
+
 # Constants from the NIFTI1 specification that define
 # the 'space' in which an image is assumed to be.
 NIFTI_XFORM_UNKNOWN      = 0
@@ -42,228 +48,6 @@ NIFTI_XFORM_TALAIRACH    = 3
 NIFTI_XFORM_MNI_152      = 4
 
 
-# TODO The wx.FileDialog does not    
-# seem to handle wildcards with      
-# multiple suffixes (e.g. '.nii.gz'),
-# so i'm just providing '*.gz'for now
-ALLOWED_EXTENSIONS = ['.nii', '.img', '.hdr', '.gz']
-"""The file extensions which we understand. This list is used as the default
-if if the ``allowedExts`` parameter is not passed to any of the functions in
-this module.
-"""
-
-EXTENSION_DESCRIPTIONS = ['NIFTI1 images',
-                          'ANALYZE75 images',
-                          'NIFTI1/ANALYZE75 headers',
-                          'Compressed images']
-"""Descriptions for each of the extensions in :data:`ALLOWED_EXTENSIONS`. """
-
-
-DEFAULT_EXTENSION  = '.nii.gz'
-"""The default file extension (TODO read this from ``$FSLOUTPUTTYPE``)."""
-
-
-def makeWildcard(allowedExts=None):
-    """Returns a wildcard string for use in a file dialog, to limit
-    the acceptable file types.
-    
-    :arg allowedExts: A list of strings containing the allowed file
-                      extensions.
-    """
-    
-    if allowedExts is None:
-        allowedExts  = ALLOWED_EXTENSIONS
-        descs        = EXTENSION_DESCRIPTIONS
-    else:
-        descs        = allowedExts
-
-    exts = ['*{}'.format(ext) for ext in allowedExts]
-
-    allDesc = 'All supported files'
-    allExts = ';'.join(exts)
-
-    wcParts = ['|'.join((desc, ext)) for (desc, ext) in zip(descs, exts)]
-    wcParts = ['|'.join((allDesc, allExts))] + wcParts
-
-    return '|'.join(wcParts)
-
-
-def isSupported(filename, allowedExts=None):
-    """
-    Returns ``True`` if the given file has a supported extension, ``False``
-    otherwise.
-
-    :arg filename:    The file name to test.
-    
-    :arg allowedExts: A list of strings containing the allowed file
-                      extensions.
-    """
-
-    if allowedExts is None: allowedExts = ALLOWED_EXTENSIONS
-
-    return any(map(lambda ext: filename.endswith(ext, allowedExts)))
-
-
-def removeExtension(filename, allowedExts=None):
-    """
-    Removes the extension from the given file name. Raises a :exc:`ValueError`
-    if the file has an unsupported extension.
-
-    :arg filename:    The file name to strip.
-    
-    :arg allowedExts: A list of strings containing the allowed file
-                      extensions.    
-    """
-
-    if allowedExts is None: allowedExts = ALLOWED_EXTENSIONS
-
-    # figure out the extension of the given file
-    extMatches = map(lambda ext: filename.endswith(ext), allowedExts)
-
-    # the file does not have a supported extension
-    if not any(extMatches):
-        raise ValueError('Unsupported file type')
-
-    # figure out the length of the matched extension
-    extIdx = extMatches.index(True)
-    extLen = len(allowedExts[extIdx])
-
-    # and trim it from the file name
-    return filename[:-extLen]
-
-
-def addExtension(
-        prefix,
-        mustExist=False,
-        allowedExts=None,
-        defaultExt=None):
-    """Adds a file extension to the given file ``prefix``.
-
-    If ``mustExist`` is False (the default), and the file does not already
-    have a supported extension, the default extension is appended and the new
-    file name returned. If the prefix already has a supported extension,
-    it is returned unchanged.
-
-    If ``mustExist`` is ``True``, the function checks to see if any files
-    exist that have the given prefix, and a supported file extension.  A
-    :exc:`ValueError` is raised if:
-
-       - No files exist with the given prefix and a supported extension.
-       - More than one file exists with the given prefix, and a supported
-         extension.
-
-    Otherwise the full file name is returned.
-
-    :arg prefix:      The file name refix to modify.
-    :arg mustExist:   Whether the file must exist or not.
-    :arg allowedExts: List of allowed file extensions.
-    :arg defaultExt:  Default file extension to use.
-    """
-
-    if allowedExts is None: allowedExts = ALLOWED_EXTENSIONS
-    if defaultExt  is None: defaultExt  = DEFAULT_EXTENSION
-
-    if not mustExist:
-
-        # the provided file name already
-        # ends with a supported extension 
-        if any(map(lambda ext: prefix.endswith(ext), allowedExts)):
-            return prefix
-
-        return prefix + defaultExt
-
-    # If the provided prefix already ends with a
-    # supported extension , check to see that it exists
-    if any(map(lambda ext: prefix.endswith(ext), allowedExts)):
-        extended = [prefix]
-        
-    # Otherwise, make a bunch of file names, one per
-    # supported extension, and test to see if exactly
-    # one of them exists.
-    else:
-        extended = map(lambda ext: prefix + ext, allowedExts)
-
-    exists = map(op.isfile, extended)
-
-    # Could not find any supported file
-    # with the specified prefix
-    if not any(exists):
-        raise ValueError(
-            'Could not find a supported file with prefix {}'.format(prefix))
-
-    # Ambiguity! More than one supported
-    # file with the specified prefix
-    if len(filter(bool, exists)) > 1:
-        raise ValueError('More than one file with prefix {}'.format(prefix))
-
-    # Return the full file name of the
-    # supported file that was found
-    extIdx = exists.index(True)
-    return extended[extIdx]
-
-
-def _loadImageFile(filename):
-    """Given the name of an image file, loads it using nibabel.
-
-    If the file is large, and is gzipped, it is decompressed to a temporary
-    location, so that it can be memory-mapped.  A tuple is returned,
-    consisting of the nibabel image object, and the name of the file that it
-    was loaded from (either the passed-in file name, or the name of the
-    temporary decompressed file).
-    """
-
-    # If we have a GUI, we can display a dialog
-    # message. Otherwise we print a log message
-    haveGui = False
-    try:
-        import wx
-        if wx.GetApp() is not None: 
-            haveGui = True
-    except:
-        pass
-
-    realFilename = filename
-    mbytes = op.getsize(filename) / 1048576.0
-
-    # The mbytes limit is arbitrary
-    if filename.endswith('.nii.gz') and mbytes > 512:
-
-        unzipped, filename = tempfile.mkstemp(suffix='.nii')
-
-        unzipped = os.fdopen(unzipped)
-
-        msg = '{} is a large file ({} MB) - decompressing ' \
-              'to {}, to allow memory mapping...'.format(realFilename,
-                                                         mbytes,
-                                                         filename)
-
-        if not haveGui:
-            log.info(msg)
-        else:
-            busyDlg = wx.BusyInfo(msg, wx.GetTopLevelWindows()[0])
-
-        gzip = ['gzip', '-d', '-c', realFilename]
-        log.debug('Running {} > {}'.format(' '.join(gzip), filename))
-
-        # If the gzip call fails, revert to loading from the gzipped file
-        try:
-            sp.call(gzip, stdout=unzipped)
-            unzipped.close()
-
-        except OSError as e:
-            log.warn('gzip call failed ({}) - cannot memory '
-                     'map file: {}'.format(e, realFilename),
-                     exc_info=True)
-            unzipped.close()
-            os.remove(filename)
-            filename = realFilename
-
-        if haveGui:
-            busyDlg.Destroy()
-
-    return nib.load(filename), filename
-
-
 class Image(props.HasProperties):
     """Class which represents a 3D/4D image. Internally, the image is
     loaded/stored using :mod:`nibabel`.
@@ -279,20 +63,20 @@ class Image(props.HasProperties):
 
     :ivar shape:          A list/tuple containing the number of voxels
                           along each image dimension.
-    
+
     :ivar pixdim:         A list/tuple containing the size of one voxel
                           along each image dimension.
-    
+
     :ivar voxToWorldMat:  A 4*4 array specifying the affine transformation
                           for transforming voxel coordinates into real world
                           coordinates.
 
     :ivar worldToVoxMat:  A 4*4 array specifying the affine transformation
                           for transforming real world coordinates into voxel
-                          coordinates. 
+                          coordinates.
 
     :ivar imageFile:      The name of the file that the image was loaded from.
-    
+
     :ivar tempFile:       The name of the temporary file which was created (in
                           the event that the image was large and was gzipped -
                           see :func:`_loadImageFile`).
@@ -306,7 +90,7 @@ class Image(props.HasProperties):
         default='volume')
     """This property defines the type of image data."""
 
-    
+
     name = props.String()
     """The name of this image."""
 
@@ -322,7 +106,7 @@ class Image(props.HasProperties):
     as stored in memory, is saved to disk, ``False`` otherwise.
     """
 
-    
+
     def __init__(self, image, xform=None, name=None):
         """Initialise an Image object with the given image data or file name.
 
@@ -333,7 +117,7 @@ class Image(props.HasProperties):
         # The image parameter may be the name of an image file
         if isinstance(image, basestring):
             
-            nibImage, filename = _loadImageFile(addExtension(image))
+            nibImage, filename = iio.loadImage(iio.addExt(image))
             self.nibImage      = nibImage
             self.imageFile     = image
 
@@ -341,10 +125,10 @@ class Image(props.HasProperties):
             # the provided file name, that means that the
             # image was opened from a temporary file
             if filename != image:
-                self.name     = removeExtension(op.basename(self.imageFile))
+                self.name     = iio.removeExt(op.basename(self.imageFile))
                 self.tempFile = nibImage.get_filename()
             else:
-                self.name     = removeExtension(op.basename(self.imageFile))
+                self.name     = iio.removeExt(op.basename(self.imageFile))
 
             self.saved = True
                 
@@ -402,7 +186,7 @@ class Image(props.HasProperties):
         
         :arg volume:  If this is a 4D image, the volume index.
         """
-
+        
         if self.is4DImage() and volume is None:
             raise ValueError('Volume must be specified for 4D images')
         
@@ -426,10 +210,13 @@ class Image(props.HasProperties):
 
 
     def save(self):
+        """Convenience method to save any changes made to the :attr:`data` of 
+        this :class:`Image` instance.
+
+        See the :func:`fsl.data.imageio.save` function.
         """
-        """
-        pass
-        
+        return iio.saveImage(self)
+    
 
     def __hash__(self):
         """Returns a number which uniquely idenfities this :class:`Image`
@@ -440,7 +227,9 @@ class Image(props.HasProperties):
 
     def __str__(self):
         """Return a string representation of this :class:`Image`."""
-        return '{}("{}")'.format(self.__class__.__name__, self.imageFile)
+        return '{}({}, {})'.format(self.__class__.__name__,
+                                   self.name,
+                                   self.imageFile)
 
         
     def __repr__(self):
@@ -560,6 +349,7 @@ class ImageList(props.HasProperties):
     as if it were a list itself.
     """
 
+    
     def _validateImage(self, atts, images):
         """Returns ``True`` if all objects in the given ``images`` list are
         :class:`Image` objects, ``False`` otherwise.
@@ -576,69 +366,16 @@ class ImageList(props.HasProperties):
         :class:`Image` objects."""
         
         if images is None: images = []
-
         self.images.extend(images)
 
-        # set the _lastDir attribute,
-        # used by the addImages method
-        if len(images) == 0: self._lastDir = os.getcwd()
-        else:                self._lastDir = op.dirname(images[-1].imageFile)
-
 
     def addImages(self, fromDir=None, addToEnd=True):
         """Convenience method for interactively adding images to this
         :class:`ImageList`.
 
-        If the :mod:`wx` package is available, pops up a file dialog
-        prompting the user to select one or more images to append to the
-        image list.
-
-        :param str fromDir:   Directory in which the file dialog should start.
-                              If ``None``, the most recently visited directory
-                              (via this method) is used, or a directory from
-                              an already loaded image, or the current working
-                              directory.
-
-        :param bool addToEnd: If True (the default), the new images are added
-                              to the end of the list. Otherwise, they are added
-                              to the beginning of the list.
-
-        Returns: True if images were successfully added, False if no images
-        were added.
-        
-        :raise ImportError:  if :mod:`wx` is not present.
-        :raise RuntimeError: if a :class:`wx.App` has not been created.
-
+        See the :func:`fsl.data.imageio.addImages` function.
         """
-        import wx
-
-        app = wx.GetApp()
-
-        if app is None:
-            raise RuntimeError('A wx.App has not been created')
-
-        saveLastDir = False
-        if fromDir is None:
-            fromDir = self._lastDir
-            saveLastDir = True
-
-        dlg = wx.FileDialog(app.GetTopWindow(),
-                            message='Open image file',
-                            defaultDir=fromDir,
-                            wildcard=makeWildcard(),
-                            style=wx.FD_OPEN | wx.FD_MULTIPLE)
-
-        if dlg.ShowModal() != wx.ID_OK: return False
-
-        paths         = dlg.GetPaths()
-        images        = map(Image, paths)
-
-        if saveLastDir: self._lastDir = op.dirname(paths[-1])
-
-        if addToEnd: self.extend(      images)
-        else:        self.insertAll(0, images)
-
-        return True
+        return iio.addImages(self, fromDir, addToEnd)
 
 
     # Wrappers around the images list property, allowing this
diff --git a/fsl/data/imageio.py b/fsl/data/imageio.py
new file mode 100644
index 000000000..75a3bd652
--- /dev/null
+++ b/fsl/data/imageio.py
@@ -0,0 +1,367 @@
+#!/usr/bin/env python
+#
+# imageio.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+log = logging.getLogger(__name__)
+
+import                   os 
+import os.path        as op
+import subprocess     as sp
+import                   tempfile
+
+import nibabel        as nib
+
+import image          as fslimage
+
+
+# TODO The wx.FileDialog does not    
+# seem to handle wildcards with      
+# multiple suffixes (e.g. '.nii.gz'),
+# so i'm just providing '*.gz'for now
+ALLOWED_EXTENSIONS = ['.nii', '.img', '.hdr', '.gz', '.nii.gz', '.img.gz']
+"""The file extensions which we understand. This list is used as the default
+if if the ``allowedExts`` parameter is not passed to any of the functions in
+this module.
+"""
+
+EXTENSION_DESCRIPTIONS = ['NIFTI1 images',
+                          'ANALYZE75 images',
+                          'NIFTI1/ANALYZE75 headers',
+                          'Compressed images',
+                          'Compressed images',
+                          'Compressed images']
+"""Descriptions for each of the extensions in :data:`ALLOWED_EXTENSIONS`. """
+
+
+DEFAULT_EXTENSION  = '.nii.gz'
+"""The default file extension (TODO read this from ``$FSLOUTPUTTYPE``)."""
+
+
+def makeWildcard(allowedExts=None):
+    """Returns a wildcard string for use in a file dialog, to limit
+    the acceptable file types.
+    
+    :arg allowedExts: A list of strings containing the allowed file
+                      extensions.
+    """
+    
+    if allowedExts is None:
+        allowedExts  = ALLOWED_EXTENSIONS
+        descs        = EXTENSION_DESCRIPTIONS
+    else:
+        descs        = allowedExts
+
+    exts = ['*{}'.format(ext) for ext in allowedExts]
+
+    allDesc = 'All supported files'
+    allExts = ';'.join(exts)
+
+    wcParts = ['|'.join((desc, ext)) for (desc, ext) in zip(descs, exts)]
+    wcParts = ['|'.join((allDesc, allExts))] + wcParts
+
+    return '|'.join(wcParts)
+
+
+def isSupported(filename, allowedExts=None):
+    """
+    Returns ``True`` if the given file has a supported extension, ``False``
+    otherwise.
+
+    :arg filename:    The file name to test.
+    
+    :arg allowedExts: A list of strings containing the allowed file
+                      extensions.
+    """
+
+    if allowedExts is None: allowedExts = ALLOWED_EXTENSIONS
+
+    return any(map(lambda ext: filename.endswith(ext, allowedExts)))
+
+
+def removeExt(filename, allowedExts=None):
+    """
+    Removes the extension from the given file name. Returns the filename
+    unmodified if it does not have a supported extension.
+
+    :arg filename:    The file name to strip.
+    
+    :arg allowedExts: A list of strings containing the allowed file
+                      extensions.    
+    """
+
+    if allowedExts is None: allowedExts = ALLOWED_EXTENSIONS
+
+    # figure out the extension of the given file
+    extMatches = map(lambda ext: filename.endswith(ext), allowedExts)
+
+    # the file does not have a supported extension
+    if not any(extMatches):
+        return filename
+
+    # figure out the length of the matched extension
+    extIdx = extMatches.index(True)
+    extLen = len(allowedExts[extIdx])
+
+    # and trim it from the file name
+    return filename[:-extLen]
+
+
+def addExt(
+        prefix,
+        mustExist=True,
+        allowedExts=None,
+        defaultExt=None):
+    """Adds a file extension to the given file ``prefix``.
+
+    If ``mustExist`` is False, and the file does not already have a 
+    supported extension, the default extension is appended and the new
+    file name returned. If the prefix already has a supported extension,
+    it is returned unchanged.
+
+    If ``mustExist`` is ``True`` (the default), the function checks to see 
+    if any files exist that have the given prefix, and a supported file 
+    extension.  A :exc:`ValueError` is raised if:
+
+       - No files exist with the given prefix and a supported extension.
+       - More than one file exists with the given prefix, and a supported
+         extension.
+
+    Otherwise the full file name is returned.
+
+    :arg prefix:      The file name refix to modify.
+    :arg mustExist:   Whether the file must exist or not.
+    :arg allowedExts: List of allowed file extensions.
+    :arg defaultExt:  Default file extension to use.
+    """
+
+    if allowedExts is None: allowedExts = ALLOWED_EXTENSIONS
+    if defaultExt  is None: defaultExt  = DEFAULT_EXTENSION
+
+    if not mustExist:
+
+        # the provided file name already
+        # ends with a supported extension 
+        if any(map(lambda ext: prefix.endswith(ext), allowedExts)):
+            return prefix
+
+        return prefix + defaultExt
+
+    # If the provided prefix already ends with a
+    # supported extension , check to see that it exists
+    if any(map(lambda ext: prefix.endswith(ext), allowedExts)):
+        extended = [prefix]
+        
+    # Otherwise, make a bunch of file names, one per
+    # supported extension, and test to see if exactly
+    # one of them exists.
+    else:
+        extended = map(lambda ext: prefix + ext, allowedExts)
+
+    exists = map(op.isfile, extended)
+
+    # Could not find any supported file
+    # with the specified prefix
+    if not any(exists):
+        raise ValueError(
+            'Could not find a supported file with prefix {}'.format(prefix))
+
+    # Ambiguity! More than one supported
+    # file with the specified prefix
+    if len(filter(bool, exists)) > 1:
+        raise ValueError('More than one file with prefix {}'.format(prefix))
+
+    # Return the full file name of the
+    # supported file that was found
+    extIdx = exists.index(True)
+    return extended[extIdx]
+
+
+def loadImage(filename):
+    """Given the name of an image file, loads it using nibabel.
+
+    If the file is large, and is gzipped, it is decompressed to a temporary
+    location, so that it can be memory-mapped.  A tuple is returned,
+    consisting of the nibabel image object, and the name of the file that it
+    was loaded from (either the passed-in file name, or the name of the
+    temporary decompressed file).
+    """
+
+    # If we have a GUI, we can display a dialog
+    # message. Otherwise we print a log message
+    haveGui = False
+    try:
+        import wx
+        if wx.GetApp() is not None: 
+            haveGui = True
+    except:
+        pass
+
+    realFilename = filename
+    mbytes = op.getsize(filename) / 1048576.0
+
+    # The mbytes limit is arbitrary
+    if filename.endswith('.nii.gz') and mbytes > 512:
+
+        unzipped, filename = tempfile.mkstemp(suffix='.nii')
+
+        unzipped = os.fdopen(unzipped)
+
+        msg = '{} is a large file ({} MB) - decompressing ' \
+              'to {}, to allow memory mapping...'.format(realFilename,
+                                                         mbytes,
+                                                         filename)
+
+        if not haveGui:
+            log.info(msg)
+        else:
+            busyDlg = wx.BusyInfo(msg, wx.GetTopLevelWindows()[0])
+
+        gzip = ['gzip', '-d', '-c', realFilename]
+        log.debug('Running {} > {}'.format(' '.join(gzip), filename))
+
+        # If the gzip call fails, revert to loading from the gzipped file
+        try:
+            sp.call(gzip, stdout=unzipped)
+            unzipped.close()
+
+        except OSError as e:
+            log.warn('gzip call failed ({}) - cannot memory '
+                     'map file: {}'.format(e, realFilename),
+                     exc_info=True)
+            unzipped.close()
+            os.remove(filename)
+            filename = realFilename
+
+        if haveGui:
+            busyDlg.Destroy()
+
+    return nib.load(filename), filename
+
+
+def saveImage(image, imageList=None, fromDir=None):
+    """Convenience method for interactively saving changes to an image.
+
+    If the :mod:`wx` package is available, a dialog is popped up, prompting
+    the user to select a destination. Or, if the image has been loaded 
+    from a file, the user is prompted to confirm that they want to overwrite  
+    the image.
+
+
+    :param image:         The :class:`~fsl.data.image.Image` instance to
+                          be saved.
+
+
+    :param imageList:     The :class:`~fsl.data.image.ImageList` instance
+                          which contains the given image.
+
+
+    :param str fromDir:   Directory in which the file dialog should start.
+                          If ``None``, the most recently visited directory
+                          (via this method) is used, or the directory from
+                          the given image, or the current working directory.
+
+    :raise ImportError:  if :mod:`wx` is not present.
+    :raise RuntimeError: if a :class:`wx.App` has not been created.
+    """
+
+    if image.saved:
+        return
+    
+    import wx
+
+    app = wx.GetApp()
+
+    if app is None:
+        raise RuntimeError('A wx.App has not been created') 
+
+    lastDir = getattr(saveImage, 'lastDir', None)
+
+    if lastDir is None:
+        if image.imageFile is None: lastDir = os.cwd()
+        else:                       lastDir = op.dirname(image.imageFile)
+
+    if image.imageFile is None: filename = os.cwd()
+    else:                       filename = op.basename(image.imageFile) 
+
+    saveLastDir = False
+    if fromDir is None:
+        fromDir = lastDir
+        saveLastDir = True
+
+    dlg = wx.FileDialog(app.GetTopWindow(),
+                        message='Save image file',
+                        defaultDir=fromDir,
+                        defaultFile=filename, 
+                        wildcard=makeWildcard(),
+                        style=wx.FD_SAVE)
+
+    if dlg.ShowModal() != wx.ID_OK: return False
+
+    if saveLastDir: saveImage.lastDir = lastDir
+
+
+
+def addImages(imageList, fromDir=None, addToEnd=True):
+    """Convenience method for interactively adding images to an
+    :class:`fsl.data.image.ImageList`.
+    If the :mod:`wx` package is available, pops up a file dialog
+    prompting the user to select one or more images to append to the
+    image list.
+
+    :param str fromDir:   Directory in which the file dialog should start.
+                          If ``None``, the most recently visited directory
+                          (via this method) is used, or a directory from
+                          an already loaded image, or the current working
+                          directory.
+
+    :param bool addToEnd: If True (the default), the new images are added
+                          to the end of the list. Otherwise, they are added
+                          to the beginning of the list.
+
+    Returns: True if images were successfully added, False if no images
+    were added.
+    
+    :raise ImportError:  if :mod:`wx` is not present.
+    :raise RuntimeError: if a :class:`wx.App` has not been created.
+    """
+    import wx
+
+    app = wx.GetApp()
+
+    if app is None:
+        raise RuntimeError('A wx.App has not been created')
+
+    lastDir = getattr(addImages, 'lastDir', None)
+
+    if lastDir is None:
+        if len(imageList) > 0 and imageList[-1].imageFile is not None:
+            lastDir = op.dirname(imageList[-1].imageFile)
+        else:
+            lastDir = os.cwd()
+
+    saveLastDir = False
+    if fromDir is None:
+        fromDir = lastDir
+        saveLastDir = True
+
+    dlg = wx.FileDialog(app.GetTopWindow(),
+                        message='Open image file',
+                        defaultDir=fromDir,
+                        wildcard=makeWildcard(),
+                        style=wx.FD_OPEN | wx.FD_MULTIPLE)
+
+    if dlg.ShowModal() != wx.ID_OK: return False
+
+    paths         = dlg.GetPaths()
+    images        = map(fslimage.Image, paths)
+
+    if saveLastDir: addImages.lastDir = op.dirname(paths[-1])
+
+    if addToEnd: imageList.extend(      images)
+    else:        imageList.insertAll(0, images)
+
+    return True
diff --git a/fsl/tools/bet.py b/fsl/tools/bet.py
index 85af2b83b..797ba782c 100644
--- a/fsl/tools/bet.py
+++ b/fsl/tools/bet.py
@@ -10,6 +10,7 @@ from collections import OrderedDict
 import props
 
 import fsl.data.image               as fslimage
+import fsl.data.imageio             as iio
 import fsl.utils.transform          as transform
 import fsl.fslview.displaycontext   as displaycontext
 
@@ -33,12 +34,12 @@ class Options(props.HasProperties):
 
     inputImage           = props.FilePath(
         exists=True,
-        suffixes=fslimage.ALLOWED_EXTENSIONS,
+        suffixes=iio.ALLOWED_EXTENSIONS,
         required=True)
     outputImage          = props.FilePath(required=True)
     t2Image              = props.FilePath(
         exists=True,
-        suffixes=fslimage.ALLOWED_EXTENSIONS,
+        suffixes=iio.ALLOWED_EXTENSIONS,
         required=lambda i: i.runChoice == '-A2')
 
     runChoice            = props.Choice(runChoices)
@@ -65,7 +66,7 @@ class Options(props.HasProperties):
         """
 
         if not valid: return
-        value = fslimage.removeExtension(value)
+        value = iio.removeExt(value)
         self.outputImage = value + '_brain'
 
         
diff --git a/fsl/tools/fslview_parseargs.py b/fsl/tools/fslview_parseargs.py
index 893e97f3b..6c4b6e475 100644
--- a/fsl/tools/fslview_parseargs.py
+++ b/fsl/tools/fslview_parseargs.py
@@ -18,6 +18,7 @@ import argparse
 
 import props
 import fsl.data.image             as fslimage
+import fsl.data.imageio           as iio
 import fsl.utils.transform        as transform 
 import fsl.fslview.displaycontext as displaycontext
 
@@ -233,7 +234,7 @@ def parseArgs(mainParser, argv, name, desc, toolOptsDesc='[options]'):
 
         # an -i with something that is
         # not a file following it
-        if not op.isfile(imgFile):
+        if not op.isfile(iio.addExt(imgFile, True)):
             print_help()
             sys.argv(1)            
 
-- 
GitLab