diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 94b90a11186a7c6ef1c11515731b73b64355675a..3d4d505204e049c862039cbce1b1bc2921687e6d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,14 +1,17 @@
-# Keeping this commented out until CI is working 
-#
-# test:2.7:
-#   image: python2.7
-#   script:
-#     - cat requirements.txt | xargs -n 1 pip install
-#     - python setup.py test
+test:2.7:
+  image: fsleyes-py27
+  script:
+    - cat requirements.txt | xargs -n 1 pip install
+    - pip install scipy
+    - pip install coverage
+    - su -s /bin/bash -c "xvfb-run python setup.py test" nobody
+    - coverage report -m
 
-
-# test:3.5:
-#   image: python3.5
-#   script:
-#     - cat requirements.txt | xargs -n 1 pip install
-#     - python setup.py test
+test:3.6:
+  image: fsleyes-py36
+  script:
+    - cat requirements.txt | xargs -n 1 pip install
+    - pip install scipy
+    - pip install coverage
+    - su -s /bin/bash -c "xvfb-run python setup.py test" nobody
+    - coverage report -m
diff --git a/doc/contributing.rst b/doc/contributing.rst
index 81c6fb5d046992f63b3358481b25eafb85e8c52a..3673975f3eb8a0c96125c9fe18c8c061ee7f2b19 100644
--- a/doc/contributing.rst
+++ b/doc/contributing.rst
@@ -16,16 +16,17 @@ Development model
   developers are free to choose their own development workflow in their own
   repositories.
 
-- A separate branch is created for each release. Hotfixes may be added to
-  these release branches.
-
 - Merge requests will not be accepted unless:
-   
+
  - All existing tests pass (or have been updated as needed).
  - New tests have been written to cover newly added features.
  - Code coverage is as close to 100% as possible.
  - Coding conventions are adhered to (unless there is good reason not to).
 
+- A separate branch is created for each release. Hotfixes may be added to
+  these release branches. Hotfixes should be merged into the master branch,
+  and then cherry-picked onto the release branch(es).
+
 
 Version number
 --------------
@@ -40,13 +41,16 @@ numbers::
 
 - The ``patch`` number is incremented on bugfixes and minor
   (backwards-compatible) changes.
-   
+
 - The ``minor`` number is incremented on feature additions and/or
   backwards-compatible changes.
 
 - The ``major`` number is incremented on major feature additions, and
   backwards-incompatible changes.
 
+Additionally, a single letter (``a``, ``b``, ``c``, etc) may be appended
+to the version number, indicating a hotfix release.
+
 
 Testing
 -------
@@ -78,7 +82,7 @@ Configure your text editor to use:
 - `flake8 <http://flake8.pycqa.org/en/latest/>`_: This checks your code for
   adherence to the `PEP8 <https://www.python.org/dev/peps/pep-0008/>`_ coding
   standard.
-   
+
 - `pylint <https://www.pylint.org/>`_: This checks that your code follows
   other good conventions.
 
diff --git a/fsl/data/atlases.py b/fsl/data/atlases.py
index 9f6b9fe8df68f665233dadd9fac579521f8ea80f..2a10b1214c89b092e91d7ec4728408e9f08f3c86 100644
--- a/fsl/data/atlases.py
+++ b/fsl/data/atlases.py
@@ -308,6 +308,58 @@ class AtlasRegistry(notifier.Notifier):
         fslsettings.write('fsl.data.atlases', atlases)
 
 
+class AtlasLabel(object):
+    """The ``AtlasLabel`` class is used by the :class:`AtlasDescription` class
+    as a container object used for storing atlas label information.
+
+    An ``AtlasLabel`` instance contains the following attributes:
+
+    ========= ==============================================================
+    ``name``  Region name
+    ``index`` For probabilistic atlases, the volume index into the 4D atlas
+              image that corresponds to this region. For label atlases, the
+              value of voxels that are in this region. For summary images of
+              probabilistic atlases, add 1 to this value to get the
+              corresponding voxel values.
+    ``x``     X coordinate of the region in world space
+    ``y``     Y coordinate of the region in world space
+    ``z``     Z coordinate of the region in world space
+    ========= ==============================================================
+
+    .. note:: The ``x``, ``y`` and ``z`` label coordinates are pre-calculated
+              centre-of-gravity coordinates, as listed in the atlas xml file.
+              They are in the coordinate system defined by the transformation
+              matrix for the first image in the ``images`` list of the atlas
+              XML file (typically MNI152 space).
+    """
+
+    def __init__(self, name, index, x, y, z):
+        self.name  = name
+        self.index = index
+        self.x     = x
+        self.y     = y
+        self.z     = z
+
+
+    def __eq__(self, other):
+        """Compares the ``index`` of this ``AtlasLabel`` with another.
+        """
+        return self.index == other.index
+
+
+    def __neq__(self, other):
+        """Compares the ``index`` of this ``AtlasLabel`` with another.
+        """
+        return self.index != other.index
+
+
+    def __lt__(self, other):
+        """Compares this ``AtlasLabel`` with another by their ``index``
+        attribute.
+        """
+        return self.index < other.index
+
+
 class AtlasDescription(object):
     """An ``AtlasDescription`` instance parses and stores the information
     stored in the FSL XML file that describes a single FSL atlas.  An XML
@@ -393,30 +445,9 @@ class AtlasDescription(object):
                       ``numpy`` arrays), one for each image in ``images``,
                       defining the voxel to world coordinate transformations.
 
-    ``labels``        A list of ``AtlasLabel`` objects, describing each
+    ``labels``        A list of :class`AtlasLabel` objects, describing each
                       region / label in the atlas.
     ================= ======================================================
-
-    Each ``AtlasLabel`` instance in the ``labels`` list contains the
-    following attributes:
-
-    ========= ==============================================================
-    ``name``  Region name
-    ``index`` For probabilistic atlases, the volume index into the 4D atlas
-              image that corresponds to this region. For label atlases, the
-              value of voxels that are in this region. For summary images of
-              probabilistic atlases, add 1 to this value to get the
-              corresponding voxel values.
-    ``x``     X coordinate of the region in world space
-    ``y``     Y coordinate of the region in world space
-    ``z``     Z coordinate of the region in world space
-    ========= ==============================================================
-
-    .. note:: The ``x``, ``y`` and ``z`` label coordinates are pre-calculated
-              centre-of-gravity coordinates, as listed in the atlas xml file.
-              They are in the coordinate system defined by the transformation
-              matrix for the first image in the ``images`` list.(typically
-              MNI152 space).
     """
 
 
@@ -471,11 +502,6 @@ class AtlasDescription(object):
             self.pixdims      .append(i.pixdim[:3])
             self.xforms       .append(i.voxToWorldMat)
 
-        # A container object used for
-        # storing atlas label information
-        class AtlasLabel(object):
-            pass
-
         labels      = data.findall('label')
         self.labels = []
 
@@ -488,14 +514,14 @@ class AtlasDescription(object):
 
         for i, label in enumerate(labels):
 
-            al        = AtlasLabel()
-            al.name   = label.text
-            al.index  = int(  label.attrib['index'])
-            al.x      = float(label.attrib['x'])
-            al.y      = float(label.attrib['y'])
-            al.z      = float(label.attrib['z'])
+            name  = label.text
+            index = int(  label.attrib['index'])
+            x     = float(label.attrib['x'])
+            y     = float(label.attrib['y'])
+            z     = float(label.attrib['z'])
+            al    = AtlasLabel(name, index, x, y, z)
 
-            coords[i] = (al.x, al.y, al.z)
+            coords[i] = (x, y, z)
 
             self.labels.append(al)
 
@@ -507,7 +533,6 @@ class AtlasDescription(object):
         # Update the coordinates
         # in our label objects
         for i, label in enumerate(self.labels):
-
             label.x, label.y, label.z = coords[i]
 
 
diff --git a/fsl/data/image.py b/fsl/data/image.py
index f44c3bec5539e84f24a8ee9285fd713499b687aa..1166cd53c4b1cce2246ac04301f1acce781bc0da 100644
--- a/fsl/data/image.py
+++ b/fsl/data/image.py
@@ -34,6 +34,7 @@ and file names:
 
 import                      os
 import os.path           as op
+import                      string
 import                      logging
 
 import                      six
@@ -290,6 +291,33 @@ class Nifti(notifier.Notifier):
         return origShape, shape, pixdims
 
 
+    def strval(self, key):
+        """Returns the specified NIFTI header field, converted to a python
+        string, correctly null-terminated, and with non-printable characters
+        removed.
+
+        This method is used to sanitise some NIFTI header fields. The default
+        Python behaviour for converting a sequence of bytes to a string is to
+        strip all termination characters (bytes with value of ``0x00``) from
+        the end of the sequence.
+
+        This default behaviour does not handle the case where a sequence of
+        bytes which did contain a long string is subsequently overwritten with
+        a shorter string - the short string will be terminated, but that
+        termination character will be followed by the remainder of the
+        original string.
+        """
+
+        val = self.header[key]
+
+        try:    val = bytes(val).partition(b'\0')[0]
+        except: val = bytes(val)
+
+        val = val.decode('ascii')
+
+        return ''.join([c for c in val if c in string.printable]).strip()
+
+
     @property
     def niftiVersion(self):
         """Returns the NIFTI file version:
@@ -662,7 +690,8 @@ class Image(Nifti):
                  loadData=True,
                  calcRange=True,
                  indexed=False,
-                 threaded=False):
+                 threaded=False,
+                 **kwargs):
         """Create an ``Image`` object with the given image data or file name.
 
         :arg image:     A string containing the name of an image file to load,
@@ -706,6 +735,9 @@ class Image(Nifti):
         :arg threaded:  If ``True``, the :class:`.ImageWrapper` will use a
                         separate thread for data range calculation. Defaults
                         to ``False``. Ignored if ``loadData`` is ``True``.
+
+        All other arguments are passed through to the ``nibabel.load`` function
+        (if it is called).
         """
 
         nibImage   = None
@@ -735,7 +767,7 @@ class Image(Nifti):
             # Otherwise we let nibabel
             # manage the file reference(s)
             else:
-                nibImage  = nib.load(image)
+                nibImage  = nib.load(image, **kwargs)
 
             dataSource = image
 
diff --git a/fsl/utils/platform.py b/fsl/utils/platform.py
index 71cdd829425ada0d346005d9fa9b7d403457f795..0bd260a3481925d5e3e792d5b2c5d39a82d5f440 100644
--- a/fsl/utils/platform.py
+++ b/fsl/utils/platform.py
@@ -84,27 +84,26 @@ def isWidgetAlive(widget):
     """Returns ``True`` if the given ``wx.Window`` object is "alive" (i.e.
     has not been destroyed), ``False`` otherwise. Works in both wxPython
     and wxPython/Phoenix.
+
+    .. warning:: Don't try to test whether a ``wx.MenuItem`` has been
+                 destroyed, as it will probably result in segmentation
+                 faults. Check the parent ``wx.Menu`` instead.
     """
 
     import wx
 
-    if platform.wxFlavour == WX_PHOENIX:
-        return bool(widget)
 
-    elif platform.wxFlavour == WX_PYTHON:
-        try:
-            # GetId seems to be available on all wx
-            # objects, despite not being documented.
-            #
-            # I was originally calling IsEnabled,
-            # but this causes segfaults if called
-            # on a wx.MenuItem from within an
-            # event handler on that menu item!
-            widget.GetId()
-            return True
-
-        except wx.PyDeadObjectError:
-            return False
+    if platform.wxFlavour == platform.WX_PHOENIX:
+        excType = RuntimeError
+    elif platform.wxFlavour == platform.WX_PYTHON:
+        excType = wx.PyDeadObjectError
+
+    try:
+        widget.GetParent()
+        return True
+
+    except excType:
+        return False
 
 
 class Platform(notifier.Notifier):
@@ -146,6 +145,7 @@ class Platform(notifier.Notifier):
         self.isWidgetAlive = isWidgetAlive
 
         self.__inSSHSession = False
+        self.__inVNCSession = False
         self.__glVersion    = None
         self.__glRenderer   = None
         self.__glIsSoftware = None
@@ -163,17 +163,14 @@ class Platform(notifier.Notifier):
         except ImportError:
             self.__canHaveGui = False
 
+        # If one of the SSH_/VNC environment
+        # variables is set, then we're probably
+        # running over SSH/VNC.
+        sshVars = ['SSH_CLIENT', 'SSH_TTY']
+        vncVars = ['VNCDESKTOP', 'X2GO_SESSION', 'NXSESSIONID']
 
-        # If one of the SSH_ environment
-        # variables is set, and we're
-        # not running in a VNC session,
-        # then we're probably running
-        # over SSH.
-        inSSH = 'SSH_CLIENT' in os.environ or \
-                'SSH_TTY'    in os.environ
-        inVNC = 'VNCDESKTOP' in os.environ
-
-        self.__inSSHSession = inSSH and not inVNC
+        self.__inSSHSession = any(s in os.environ for s in sshVars)
+        self.__inVNCSession = any(v in os.environ for v in vncVars)
 
 
     @property
@@ -220,6 +217,19 @@ class Platform(notifier.Notifier):
         return self.__inSSHSession
 
 
+    @property
+    def inVNCSession(self):
+        """``True`` if this application is running over a VNC (or similar)
+        session, ``False`` otherwise. Currently, the following remote desktop
+        environments are detected:
+
+          - VNC
+          - x2go
+          - NoMachine
+        """
+        return self.__inVNCSession
+
+
     @property
     def wxPlatform(self):
         """One of :data:`WX_UNKNOWN`, :data:`WX_MAC_COCOA`,
@@ -353,9 +363,6 @@ class Platform(notifier.Notifier):
         # necessary.
         self.__glIsSoftware = any((
             'software' in value,
-            'mesa'     in value,
-            'gallium'  in value,
-            'llvmpipe' in value,
             'chromium' in value,
         ))
 
diff --git a/fsl/utils/settings.py b/fsl/utils/settings.py
index 5fd3207c827698d2cf3ade58446867be35c19f48..491198adb5f86729e93e9b7d80859b6e8047f35a 100644
--- a/fsl/utils/settings.py
+++ b/fsl/utils/settings.py
@@ -373,9 +373,9 @@ class Settings(object):
             with open(configFile, 'rb') as f:
                 return pickle.load(f)
         except:
-            log.warning('Unable to load stored {} configuration file '
-                        '{}'.format(self.__configID, configFile),
-                        exc_info=True)
+            log.debug('Unable to load stored {} configuration file '
+                      '{}'.format(self.__configID, configFile),
+                      exc_info=True)
             return {}
 
 
diff --git a/requirements.txt b/requirements.txt
index 83de75e266e952ae485cb15e388b0863a408a996..dd852d16ed620cb5d8943efe4fb353c716e49865 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,4 +2,4 @@ six>=1.10.0,<2.0
 numpy>=1.11.1,<2.0
 nibabel>=2.1,<3.0
 indexed_gzip>=0.3.3,<0.4
-wxPython>=3.0.2.0,<=4.0
+wxPython>=3.0.2.0,<=4.0.0a2
diff --git a/setup.py b/setup.py
index c793739fa383707ced42d2984d0953777a1325d7..459dc01e149a193f1d1dee7ee04b1857f304877a 100644
--- a/setup.py
+++ b/setup.py
@@ -102,11 +102,11 @@ setup(
     install_requires=install_requires,
 
     setup_requires=['pytest-runner'],
-    tests_require=['pytest',
-                   'mock',
+    tests_require=['mock',
                    'pytest-cov',
                    'pytest-html',
-                   'pytest-runner'],
+                   'pytest-runner',
+                   'pytest'],
     test_suite='tests',
 
     cmdclass={'doc' : doc},
diff --git a/tests/test_callfsl.py b/tests/test_callfsl.py
index 28481b33c33267042a2828e84c1c7a802d7b85ad..e137160e46e341d0d9744aaafa3b8bdfe8e3885a 100644
--- a/tests/test_callfsl.py
+++ b/tests/test_callfsl.py
@@ -10,7 +10,9 @@ import os.path    as op
 import subprocess as sp
 
 import numpy      as np
+import nibabel    as nib
 
+import mock
 import pytest
 
 import fsl.utils.callfsl                  as callfsl
@@ -24,6 +26,17 @@ def setup_module():
         raise Exception('FSLDIR is not set - callfsl tests cannot be run')
 
 
+# mock subprocess.check_output command
+# which expects 'fslstats -m filename'
+# or 'fslinfo ...'
+def mock_check_output(args):
+    if args[0].endswith('fslinfo'):
+        return 'info'
+
+    img = nib.load(args[-2])
+    return str(img.get_data().mean())
+
+
 def test_callfsl():
 
     with tests.testdir() as testdir:
@@ -35,12 +48,16 @@ def test_callfsl():
 
         # Pass a single string
         cmd    = 'fslstats {} -m'.format(fname)
-        result = callfsl.callFSL(cmd)
-        assert np.isclose(float(result), img.mean())
 
-        # Or pass a list of args
-        result = callfsl.callFSL(*cmd.split())
-        assert np.isclose(float(result), img.mean())
+        with mock.patch('fsl.utils.callfsl.sp.check_output',
+                        mock_check_output):
+            result = callfsl.callFSL(cmd)
+
+            assert np.isclose(float(result), img.mean())
+
+            # Or pass a list of args
+            result = callfsl.callFSL(*cmd.split())
+            assert np.isclose(float(result), img.mean())
 
         # Bad commands
         badcmds = ['fslblob', 'fslstats notafile']
@@ -51,7 +68,9 @@ def test_callfsl():
 
         # No FSL - should crash
         cmd = 'fslinfo {}'.format(fname)
-        callfsl.callFSL(cmd)
+        with mock.patch('fsl.utils.callfsl.sp.check_output',
+                        mock_check_output):
+            callfsl.callFSL(cmd)
         fslplatform.fsldir = None
         with pytest.raises(Exception):
             callfsl.callFSL(cmd)
diff --git a/tests/test_image.py b/tests/test_image.py
index 19f7eea86db037a40e71d267cecf358792590762..22568f92620f97f7117d628af3217ac4e1acd6d7 100644
--- a/tests/test_image.py
+++ b/tests/test_image.py
@@ -56,7 +56,7 @@ def make_image(filename=None,
     xform = np.eye(4)
     for i, p in enumerate(pixdims):
         xform[i, i] = p
-    
+
     data  = np.array(np.random.random(dims) * 100, dtype=dtype)
 
     if   imgtype == 0: img = nib.AnalyzeImage(data, xform, hdr)
@@ -68,7 +68,7 @@ def make_image(filename=None,
         if op.splitext(filename)[1] == '':
             if imgtype == 0: filename = '{}.img'.format(filename)
             else:            filename = '{}.nii'.format(filename)
- 
+
         nib.save(img, filename)
 
     return img
@@ -95,7 +95,7 @@ def test_load():
                 'ambiguous.img',
                 'ambiguous.img.gz',
                 'notnifti.nii.gz']
-    
+
 
     shouldPass = ['compressed',
                   'compressed.nii.gz',
@@ -121,12 +121,12 @@ def test_load():
     testdir = tempfile.mkdtemp()
 
     for f in toCreate:
-        
+
         if f.startswith('notnifti'):
             make_dummy_file(op.join(testdir, f))
         else:
             make_random_image(op.join(testdir, f))
-            
+
     # Not raising an error means the test passes
     try:
         for fname in shouldPass:
@@ -164,7 +164,7 @@ def test_create():
     assert img.niftiVersion == 1
 
     for imgType in [0, 1, 2]:
-        
+
         nimg = make_image(imgtype=imgType, pixdims=(5, 6, 7))
         nhdr = nimg.header
 
@@ -189,13 +189,13 @@ def test_create():
             img = fslimage.Image(nimg)
             assert img.niftiVersion == imgtype
             assert np.all(np.isclose(img.pixdim, (2, 3, 4)))
-            
+
         finally:
             shutil.rmtree(testdir)
 
 
 def test_bad_create():
-    
+
     class BadThing(object):
         pass
 
@@ -204,7 +204,7 @@ def test_bad_create():
         fslimage.Image(
             np.random.random((10, 10, 10)),
             header=BadThing())
-        
+
     # Bad data
     with pytest.raises(Exception):
         fslimage.Image(BadThing())
@@ -221,18 +221,18 @@ def test_bad_create():
     with pytest.raises(Exception):
         fslimage.Image(np.random.random(10, 10, 10),
                        xform=np.eye(3))
-        
+
     with pytest.raises(Exception):
         fslimage.Image(np.random.random(10, 10, 10),
                        xform=np.eye(5))
 
 
-def  test_Image_atts_analyze(): _test_Image_atts(0) 
+def  test_Image_atts_analyze(): _test_Image_atts(0)
 def  test_Image_atts_nifti1():  _test_Image_atts(1)
 def  test_Image_atts_nifti2():  _test_Image_atts(2)
 def _test_Image_atts(imgtype):
     """Test that basic Nifti/Image attributes are correct. """
-    
+
     testdir     = tempfile.mkdtemp()
     allowedExts = fslimage.ALLOWED_EXTENSIONS
     fileGroups  = fslimage.FILE_GROUPS
@@ -259,17 +259,17 @@ def _test_Image_atts(imgtype):
     tests = it.product(dims, pixdims, dtypes)
     tests = list(tests)
     paths = ['test{:03d}'.format(i) for i in range(len(tests))]
-                       
+
     for path, atts in zip(paths, tests):
 
         dims, pixdims, dtype = atts
 
         ndims   = len(dims)
-        pixdims = pixdims[:ndims] 
+        pixdims = pixdims[:ndims]
 
         path = op.abspath(op.join(testdir, path))
         make_image(path, imgtype, dims, pixdims, dtype)
- 
+
     try:
 
         for path, atts in zip(paths, tests):
@@ -295,12 +295,12 @@ def _test_Image_atts(imgtype):
                                                   fileGroups=fileGroups)
     finally:
         shutil.rmtree(testdir)
-        
-def  test_Image_atts2_analyze(): _test_Image_atts2(0) 
+
+def  test_Image_atts2_analyze(): _test_Image_atts2(0)
 def  test_Image_atts2_nifti1():  _test_Image_atts2(1)
 def  test_Image_atts2_nifti2():  _test_Image_atts2(2)
 def _test_Image_atts2(imgtype):
-    
+
     # See fsl.utils.constants for the meanings of these codes
     xyzUnits  = [0, 1, 2, 3]
     timeUnits = [8, 16, 24, 32, 40, 48]
@@ -392,7 +392,7 @@ def test_addExt():
 
     for path in toCreate:
         path = op.abspath(op.join(testdir, path))
-        make_random_image(path) 
+        make_random_image(path)
 
     try:
         for path, mustExist, expected in tests:
@@ -456,14 +456,14 @@ def test_defaultExt():
         os.environ['FSLOUTPUTTYPE'] = o
 
         assert fslimage.defaultExt() == e
-        
+
 
 def  test_Image_orientation_analyze_neuro(): _test_Image_orientation(0, 'neuro')
 def  test_Image_orientation_analyze_radio(): _test_Image_orientation(0, 'radio')
 def  test_Image_orientation_nifti1_neuro():  _test_Image_orientation(1, 'neuro')
 def  test_Image_orientation_nifti1_radio():  _test_Image_orientation(1, 'radio')
 def  test_Image_orientation_nifti2_neuro():  _test_Image_orientation(2, 'neuro')
-def  test_Image_orientation_nifti2_radio():  _test_Image_orientation(2, 'radio') 
+def  test_Image_orientation_nifti2_radio():  _test_Image_orientation(2, 'radio')
 def _test_Image_orientation(imgtype, voxorient):
     """Test the Nifti.isNeurological and Nifti.getOrientation methods. """
 
@@ -493,7 +493,7 @@ def _test_Image_orientation(imgtype, voxorient):
         expectvox0Orientation = constants.ORIENT_R2L
         expectvox1Orientation = constants.ORIENT_P2A
         expectvox2Orientation = constants.ORIENT_I2S
-        
+
     elif voxorient == 'neuro':
         expectNeuroTest       = True
         expectvox0Orientation = constants.ORIENT_L2R
@@ -534,7 +534,7 @@ def  test_Image_sqforms_nifti1_nosqform(): _test_Image_sqforms(1, 1, 0)
 def  test_Image_sqforms_nifti2_normal():   _test_Image_sqforms(2, 1, 1)
 def  test_Image_sqforms_nifti2_nosform():  _test_Image_sqforms(2, 0, 1)
 def  test_Image_sqforms_nifti2_noqform():  _test_Image_sqforms(2, 1, 0)
-def  test_Image_sqforms_nifti2_nosqform(): _test_Image_sqforms(2, 0, 0) 
+def  test_Image_sqforms_nifti2_nosqform(): _test_Image_sqforms(2, 0, 0)
 def _test_Image_sqforms(imgtype, sformcode, qformcode):
     """Test the Nifti.getXFormCode method, and the voxToWorldMat/worldToVoxMat
     attributes for NIFTI images with the given sform/qform code combination.
@@ -587,7 +587,7 @@ def _test_Image_sqforms(imgtype, sformcode, qformcode):
 
     with pytest.raises(ValueError):
         image.getXFormCode('badcode')
-    
+
     try:
         assert np.all(np.isclose(image.voxToWorldMat,  expAffine))
         assert np.all(np.isclose(image.worldToVoxMat,  invExpAffine))
@@ -601,10 +601,10 @@ def _test_Image_sqforms(imgtype, sformcode, qformcode):
         shutil.rmtree(testdir)
 
 
-def  test_Image_changeXform_analyze():         _test_Image_changeXform(0) 
+def  test_Image_changeXform_analyze():         _test_Image_changeXform(0)
 def  test_Image_changeXform_nifti1():          _test_Image_changeXform(1)
 def  test_Image_changeXform_nifti1_nosqform(): _test_Image_changeXform(1, 0, 0)
-def  test_Image_changeXform_nifti2():          _test_Image_changeXform(2) 
+def  test_Image_changeXform_nifti2():          _test_Image_changeXform(2)
 def _test_Image_changeXform(imgtype, sformcode=None, qformcode=None):
     """Test changing the Nifti.voxToWorldMat attribute. """
 
@@ -641,7 +641,7 @@ def _test_Image_changeXform(imgtype, sformcode=None, qformcode=None):
     if imgtype > 0:
         expSformCode = image.get_sform(coded=True)[1]
         expQformCode = image.get_qform(coded=True)[1]
-        
+
         if sformcode == 0:
             expSformCode = constants.NIFTI_XFORM_ALIGNED_ANAT
     else:
@@ -654,7 +654,7 @@ def _test_Image_changeXform(imgtype, sformcode=None, qformcode=None):
         assert img.saveState
 
         if imgtype == 0:
-            # ANALYZE affine is not editable 
+            # ANALYZE affine is not editable
             with pytest.raises(Exception):
                 img.voxToWorldMat = newXform
             return
@@ -679,7 +679,7 @@ def _test_Image_changeXform(imgtype, sformcode=None, qformcode=None):
 
 def  test_Image_changeData_analyze(seed): _test_Image_changeData(0)
 def  test_Image_changeData_nifti1(seed):  _test_Image_changeData(1)
-def  test_Image_changeData_nifti2(seed):  _test_Image_changeData(2) 
+def  test_Image_changeData_nifti2(seed):  _test_Image_changeData(2)
 def _test_Image_changeData(imgtype):
     """Test that changing image data triggers notification, and also causes
     the dataRange attribute to be updated.
@@ -687,13 +687,13 @@ def _test_Image_changeData(imgtype):
 
     testdir   = tempfile.mkdtemp()
     imagefile = op.join(testdir, 'image')
-    
+
     make_image(imagefile, imgtype)
 
     img = fslimage.Image(imagefile)
 
     notified = {}
-    
+
     def randvox():
         return (np.random.randint(0, img.shape[0]),
                 np.random.randint(0, img.shape[1]),
@@ -773,7 +773,7 @@ def _test_Image_2D(imgtype):
     # which happens when you create
     # an XY slice with fslroi. This
     # should still be read in as a
-    # 3D image. 
+    # 3D image.
     testdims = [(10, 20),
                 (10, 20, 1),
                 (10, 1,  20),
@@ -787,9 +787,9 @@ def _test_Image_2D(imgtype):
         for shape in testdims:
 
             pixdim = [2] * len(shape)
-                
+
             imagefile = op.join(testdir, 'image')
-            
+
             make_image(imagefile, imgtype, shape, pixdim)
 
             image = fslimage.Image(imagefile)
@@ -831,9 +831,9 @@ def test_Image_voxelsToScaledVoxels():
         if itype > 0 and pixdims[0] > 0:
             xf[0, 0] = -pixdims[0]
             xf[0, 3] =  pixdims[0] * (dims[0] - 1)
-            
+
         return xf
-    
+
     for imgType, dim, pixdim in it.product(imgTypes, dims, pixdims):
         nimg = make_image(imgtype=imgType, dims=dim, pixdims=pixdim)
         img  = fslimage.Image(nimg)
@@ -842,7 +842,7 @@ def test_Image_voxelsToScaledVoxels():
         result   = img.voxelsToScaledVoxels()
 
         assert np.all(np.isclose(result, expected))
-        
+
 
 def test_Image_sameSpace():
 
@@ -853,7 +853,7 @@ def test_Image_sameSpace():
     pixdims = [(2, 2, 2, 1),
                (2, 3, 4, 1)]
 
-    for (imgType, 
+    for (imgType,
          dim1,
          dim2,
          pixdim1,
@@ -885,8 +885,8 @@ def _test_Image_save(imgtype):
             if rvox not in rvoxes:
                 rvoxes.append(rvox)
         return rvoxes
-        
-    
+
+
     testdir   = tempfile.mkdtemp()
     if imgtype == 0:
         filename  = op.join(testdir, 'blob.img')
@@ -904,11 +904,13 @@ def _test_Image_save(imgtype):
     try:
         make_image(filename, imgtype)
 
-        img = fslimage.Image(filename)
+        # Using mmap can cause a "Bus error"
+        # under docker. No idea why.
+        img = fslimage.Image(filename, mmap=False)
 
         randvoxes = randvoxes(5)
         randvals  = [np.random.random() for i in range(5)]
-        
+
         for (x, y, z), v in zip(randvoxes, randvals):
             img[x, y, z] = v
 
@@ -937,7 +939,7 @@ def _test_Image_save(imgtype):
 
             # Load the image back in
             img2 = fslimage.Image(img.dataSource)
-            
+
             assert img.saveState
             assert img.dataSource == expDataSource
 
@@ -945,8 +947,8 @@ def _test_Image_save(imgtype):
                 assert np.all(np.isclose(img.voxToWorldMat, xform))
 
             for (x, y, z), v in zip(randvoxes, randvals):
-                assert np.isclose(img[x, y, z], v) 
-            
+                assert np.isclose(img[x, y, z], v)
+
 
     finally:
         shutil.rmtree(testdir)