diff --git a/fsl/fslview/glimagedata.py b/fsl/fslview/glimagedata.py
new file mode 100644
index 0000000000000000000000000000000000000000..177eb10b6c026acc034b77c757a13d8fcbb51ae2
--- /dev/null
+++ b/fsl/fslview/glimagedata.py
@@ -0,0 +1,296 @@
+#!/usr/bin/env python
+#
+# glimagedata.py - Create OpenGL data to render 2D slices of a 3D image.
+
+# A GLImageData object encapsulates the OpenGL information necessary
+# to render 2D slices of a 3D image.
+# 
+# A slice from one image is rendered using four buffers and two textures.
+
+# The first buffer, the 'geometry buffer' simply contains the 3D
+# coordinates (single precision floating point) of four vertices, which
+# define the geometry of a single voxel (using triangle strips).
+
+# The remaining buffers contain the X, Y, and Z coordinates of the voxels
+# in the slice to be displayed. These coordinates are stored as single
+# precision floating points, and used both to position a voxel, and to
+# look up its value in the 3D data texture (see below). 
+
+# The image data itself is stored as a 3D texture, with each voxel value
+# stored as a single unsigned byte in the range 0-255.  
+
+# Finally, a 1D texture is used is used to store a lookup table containing
+# an RGBA8 colour map, to colour each voxel according to its value.
+#
+# All of these things are created when a GLImageData object is
+# instantiated. They are available as attributes of the object:
+#
+#  - dataBuffer
+#  - xBuffer
+#  - yBuffer
+#  - zBuffer
+#  - geomBuffer
+#  - colourBuffer
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import numpy as np
+
+import OpenGL.GL         as gl
+import OpenGL.arrays.vbo as vbo
+
+# This extension provides the GL_R8 constant,
+# which is built into modern versions of OpenGL.
+import OpenGL.GL.ARB.texture_rg as arbrg
+
+class GLImageData(object):
+
+    def __init__(self, image, xax, yax, imageDisplay=None):
+        """
+        Initialise the OpenGL data buffers required to render the given image.
+        Parameters:
+        
+          - image:        A fsl.data.fslimage.Image object.
+        
+          - xax:          The image axis which maps to the screen x axis.
+        
+          - yax:          The image axis which maps to the screen y axis.
+        
+          - imageDisplay: Optional. A fsl.data.fslimage.ImageDisplay object
+                          which describes how the image is to be displayed.
+                          If not provided, the default image.display instance
+                          is used (see fsl.data.fslimage.ImageDisplay for
+                          details).
+        """
+
+        self.image = image
+        self.xax   = xax
+        self.yax   = yax
+
+        if imageDisplay is not None: self.display = imageDisplay
+        else:                        self.display = image.display
+
+        # Maximum number of colours used to draw image data.
+        # Keep this to a power of two, as some GL implementations
+        # will complain/misbehave if it isn't.
+        self.colourResolution = 256
+
+        self.initGLImageData()
+
+
+    def initGLImageData(self):
+        """
+        Creates and initialises the OpenGL data for the fslimage.Image
+        object that was passed to the GLImageData constructor.
+        """
+
+        image  = self.image
+        xax    = self.xax
+        yax    = self.yax
+
+        # The geometry buffer defines the geometry of
+        # a single voxel, rendered as a triangle strip.
+        geomData = np.zeros((4, 3), dtype=np.float32)
+        geomData[:, [xax, yax]] = [[-0.5, -0.5],
+                                   [ 0.5, -0.5],
+                                   [-0.5,  0.5],
+                                   [ 0.5,  0.5]] 
+        
+        geomData   = geomData.ravel('C')
+        geomBuffer = vbo.VBO(geomData, gl.GL_STATIC_DRAW)
+        
+        # x/y/z coordinates are stored as VBO arrays
+        voxData = []
+        for dim in image.shape:
+            data = np.arange(0, dim, dtype=np.float32)
+            voxData.append(data)        
+        
+        # the screen x coordinate data has to be repeated (ydim)
+        # times - we are drawing row-wise, and opengl does not
+        # allow us to loop over a VBO in a single instance
+        # rendering call
+        voxData[xax] = np.tile(voxData[xax], image.shape[yax])
+        
+        xBuffer = vbo.VBO(voxData[0], gl.GL_STATIC_DRAW)
+        yBuffer = vbo.VBO(voxData[1], gl.GL_STATIC_DRAW)
+        zBuffer = vbo.VBO(voxData[2], gl.GL_STATIC_DRAW)
+
+        # The colour buffer, containing a map of
+        # colours (stored on the GPU as a 1D texture)
+        # This is initialised in the updateColourBuffer
+        # method
+        colourBuffer = gl.glGenTextures(1) 
+
+        self.dataBuffer   = self.initImageBuffer()
+        self.voxXBuffer   = xBuffer
+        self.voxYBuffer   = yBuffer
+        self.voxZBuffer   = zBuffer
+        self.geomBuffer   = geomBuffer
+        self.colourBuffer = colourBuffer
+
+        # Add listeners to this image so the view can be
+        # updated when its display properties are changed
+        self.configDisplayListeners()
+
+        # Create the colour buffer for the given image
+        self.updateColourBuffer() 
+
+        
+    def initImageBuffer(self):
+        """
+        Initialises the OpenGL buffer used to store the data for the given
+        image. The buffer is stored as an attribute of the image and, if it
+        has already been created (e.g. by another SliceCanvas object), the
+        existing buffer is returned. 
+        """
+
+        image = self.image
+
+        texShape = 2 ** (np.ceil(np.log2(image.shape)))
+        pad      = [(0, l - s) for (l, s) in zip(texShape, image.shape)]
+        self.imageTexShape = texShape 
+
+        try:    imageBuffer = image.getAttribute('glImageBuffer')
+        except: imageBuffer = None
+
+        if imageBuffer is not None:
+            return imageBuffer
+
+        # The image data is normalised to lie
+        # between 0 and 255, and cast to uint8
+        imageData = np.array(image.data, dtype=np.float32)
+        imageData = 255.0 * (imageData       - imageData.min()) / \
+                            (imageData.max() - imageData.min())
+
+        # and each dimension is padded so it has a
+        # power-of-two length. Ugh. This is a horrible,
+        # but as far as I'm aware, necessary hack.  At
+        # least it's necessary using the OpenGL 2.1
+        # API on OSX mavericks. It massively increases
+        # image load time, too.
+        imageData = np.pad(imageData, pad, 'constant', constant_values=0)
+        imageData = np.array(imageData, dtype=np.uint8)
+
+        # Then flattened, with fortran dimension ordering,
+        # so the data, as stored on the GPU, has its first
+        # dimension as the fastest changing.
+        imageData = imageData.ravel(order='F')
+
+        # Image data is stored on the GPU as a 3D texture
+        imageBuffer = gl.glGenTextures(1)
+        gl.glBindTexture(gl.GL_TEXTURE_3D, imageBuffer)
+        gl.glTexParameteri(gl.GL_TEXTURE_3D,
+                           gl.GL_TEXTURE_MAG_FILTER,
+                           gl.GL_NEAREST)
+        gl.glTexParameteri(gl.GL_TEXTURE_3D,
+                           gl.GL_TEXTURE_MIN_FILTER,
+                           gl.GL_NEAREST)
+        gl.glTexParameteri(gl.GL_TEXTURE_3D,
+                           gl.GL_TEXTURE_WRAP_S,
+                           gl.GL_CLAMP_TO_EDGE)
+        gl.glTexParameteri(gl.GL_TEXTURE_3D,
+                           gl.GL_TEXTURE_WRAP_T,
+                           gl.GL_CLAMP_TO_EDGE)
+        gl.glTexParameteri(gl.GL_TEXTURE_3D,
+                           gl.GL_TEXTURE_WRAP_R,
+                           gl.GL_CLAMP_TO_EDGE)         
+        
+        gl.glTexImage3D(gl.GL_TEXTURE_3D,
+                        0,
+                        arbrg.GL_R8,
+                        texShape[0],
+                        texShape[1],
+                        texShape[2],
+                        0,
+                        gl.GL_RED,
+                        gl.GL_UNSIGNED_BYTE,
+                        imageData)
+
+        # And added as an attribute of the image, so
+        # other things which want to render the image
+        # don't need to duplicate all of that data.
+        image.setAttribute('glImageBuffer', imageBuffer)
+
+        return imageBuffer
+
+        
+    def updateColourBuffer(self):
+        """
+        Regenerates the colour buffer used to colour image voxels.
+        """
+
+        display      = self.display
+        colourBuffer = self.colourBuffer
+
+        # Here we are creating a range of values to be passed
+        # to the matplotlib.colors.Colormap instance of the
+        # image display. We scale this range such that data
+        # values which lie outside the configured display range
+        # will map to values below 0.0 or above 1.0. It is
+        # assumed that the Colormap instance is configured to
+        # generate appropriate colours for these out-of-range
+        # values.
+        
+        normalRange = np.linspace(0.0, 1.0, self.colourResolution)
+        normalStep  = 1.0 / (self.colourResolution - 1) 
+
+        normMin = (display.displayMin - display.dataMin) / \
+                  (display.dataMax    - display.dataMin)
+        normMax = (display.displayMax - display.dataMin) / \
+                  (display.dataMax    - display.dataMin)
+
+        newStep  = normalStep / (normMax - normMin)
+        newRange = (normalRange - normMin) * (newStep / normalStep)
+
+        # Create [self.colourResolution] rgb values,
+        # spanning the entire range of the image
+        # colour map
+        colourmap = display.cmap(newRange)
+        
+        # The colour data is stored on
+        # the GPU as 8 bit rgba tuples
+        colourmap = np.floor(colourmap * 255)
+        colourmap = np.array(colourmap, dtype=np.uint8)
+        colourmap = colourmap.ravel(order='C')
+
+        # GL texture creation stuff
+        gl.glBindTexture(gl.GL_TEXTURE_1D, colourBuffer)
+        gl.glTexParameteri(gl.GL_TEXTURE_1D,
+                           gl.GL_TEXTURE_MAG_FILTER,
+                           gl.GL_NEAREST)
+        gl.glTexParameteri(gl.GL_TEXTURE_1D,
+                           gl.GL_TEXTURE_MIN_FILTER,
+                           gl.GL_NEAREST)
+        gl.glTexParameteri(gl.GL_TEXTURE_1D,
+                           gl.GL_TEXTURE_WRAP_S,
+                           gl.GL_CLAMP_TO_EDGE) 
+        
+        gl.glTexImage1D(gl.GL_TEXTURE_1D,
+                        0,
+                        gl.GL_RGBA8,
+                        self.colourResolution,
+                        0,
+                        gl.GL_RGBA,
+                        gl.GL_UNSIGNED_BYTE,
+                        colourmap)
+
+
+    def configDisplayListeners(self):
+        """
+        Adds a bunch of listeners to the fslimage.ImageDisplay object which
+        defines how the given image is to be displayed. This is done so we
+        can update the colour texture when image display properties are
+        changed. 
+        """
+        
+        def colourUpdateNeeded(*a):
+            self.updateColourBuffer()
+
+        display = self.display
+        lnrName = 'GlImageData_{}'.format(id(self))
+
+        display.addListener('displayMin', lnrName, colourUpdateNeeded)
+        display.addListener('displayMax', lnrName, colourUpdateNeeded)
+        display.addListener('rangeClip',  lnrName, colourUpdateNeeded)
+        display.addListener('cmap',       lnrName, colourUpdateNeeded)
diff --git a/fsl/fslview/slicecanvas.py b/fsl/fslview/slicecanvas.py
index 1cb2d23e24ee5ddcf1cdf70bf8d6c95102bdd354..1c15f308fe99f6f8df144f45ecf741187cffbd49 100644
--- a/fsl/fslview/slicecanvas.py
+++ b/fsl/fslview/slicecanvas.py
@@ -6,298 +6,21 @@
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
 
-import numpy             as np
+import numpy       as np
 
-import                      wx
-import wx.glcanvas       as wxgl
+import                wx
+import wx.glcanvas as wxgl
 
-import OpenGL.GL         as gl
-import OpenGL.arrays.vbo as vbo
+import OpenGL.GL   as gl
 
 # Under OS X, I don't think I can request an OpenGL 3.2 core profile
 # using wx - I'm stuck with OpenGL 2.1 I'm using these ARB extensions
 # for functionality which is standard in 3.2.
 import OpenGL.GL.ARB.instanced_arrays as arbia
 import OpenGL.GL.ARB.draw_instanced   as arbdi
-import OpenGL.GL.ARB.texture_rg       as arbrg
-
-import fsl.data.fslimage as fslimage
-
-
-class GLImageData(object):
-    """
-    A GLImageData object encapsulates the OpenGL information necessary
-    to render 2D slices of a 3D image.
-    
-    A slice from one image is rendered using four buffers and two textures.
-
-    The first buffer, the 'geometry buffer' simply contains the 3D
-    coordinates (single precision floating point) of four vertices, which
-    define the geometry of a single voxel (using triangle strips).
-
-    The remaining buffers contain the X, Y, and Z coordinates of the voxels
-    in the slice to be displayed. These coordinates are stored as single
-    precision floating points, and used both to position a voxel, and to
-    look up its value in the 3D data texture (see below). 
-
-    The image data itself is stored as a 3D texture, with each voxel value
-    stored as a single unsigned byte in the range 0-255.  
-
-    Finally, a 1D texture is used is used to store a lookup table containing
-    an RGBA8 colour map, to colour each voxel according to its value.
-    """
-
-    def __init__(self, image, canvas):
-        """
-        Initialise the OpenGL data buffers required to render the given image.
-        Parameters:
-          - image:  A fsl.data.fslimage.Image object.
-          - canvas: The SliceCanvas object which is rendering the image.
-        """
-
-        self.image  = image
-        self.canvas = canvas
-
-        # Maximum number of colours used to draw image data.
-        # Keep this to a power of two, as some GL implementations
-        # will complain/misbehave if it isn't.
-        self.colourResolution = 256
-
-        self.initGLImageData()
-
-
-    def initGLImageData(self):
-        """
-        Creates and initialises the OpenGL data for the fslimage.Image
-        object that was passed to the GLImageData constructor.
-        """
-
-        image  = self.image
-        canvas = self.canvas
-
-        # The geometry buffer defines the geometry of
-        # a single voxel, rendered as a triangle strip.
-        geomData = np.zeros((4, 3), dtype=np.float32)
-        geomData[:, [canvas.xax, canvas.yax]] = [[-0.5, -0.5],
-                                                 [ 0.5, -0.5],
-                                                 [-0.5,  0.5],
-                                                 [ 0.5,  0.5]] 
-        
-        geomData   = geomData.ravel('C')
-        geomBuffer = vbo.VBO(geomData, gl.GL_STATIC_DRAW)
-        
-        # x/y/z coordinates are stored as VBO arrays
-        voxData = []
-        for dim in image.shape:
-            data = np.arange(0, dim, dtype=np.float32)
-            voxData.append(data)        
-        
-        # the screen x coordinate data has to be repeated (ydim)
-        # times - we are drawing row-wise, and opengl does not
-        # allow us to loop over a VBO in a single instance
-        # rendering call
-        voxData[canvas.xax] = np.tile(voxData[    canvas.xax],
-                                      image.shape[canvas.yax])
-        
-        xBuffer = vbo.VBO(voxData[0], gl.GL_STATIC_DRAW)
-        yBuffer = vbo.VBO(voxData[1], gl.GL_STATIC_DRAW)
-        zBuffer = vbo.VBO(voxData[2], gl.GL_STATIC_DRAW)
-
-        # The colour buffer, containing a map of
-        # colours (stored on the GPU as a 1D texture)
-        # This is initialised in the updateColourBuffer
-        # method
-        colourBuffer = gl.glGenTextures(1) 
-
-        self.dataBuffer   = self.initImageBuffer()
-        self.voxXBuffer   = xBuffer
-        self.voxYBuffer   = yBuffer
-        self.voxZBuffer   = zBuffer
-        self.geomBuffer   = geomBuffer
-        self.colourBuffer = colourBuffer
-
-        # Add listeners to this image so the view can be
-        # updated when its display properties are changed
-        self.configDisplayListeners()
-
-        # Create the colour buffer for the given image
-        self.updateColourBuffer() 
-
-        
-    def initImageBuffer(self):
-        """
-        Initialises the OpenGL buffer used to store the data for the given
-        image. The buffer is stored as an attribute of the image and, if it
-        has already been created (e.g. by another SliceCanvas object), the
-        existing buffer is returned. 
-        """
-
-        image = self.image
-
-        texShape = 2 ** (np.ceil(np.log2(image.shape)))
-        pad      = [(0, l - s) for (l, s) in zip(texShape, image.shape)]
-        self.imageTexShape = texShape 
-
-        try:    imageBuffer = image.getAttribute('glBuffers')
-        except: imageBuffer = None
-
-        if imageBuffer is not None:
-            return imageBuffer
-
-        # The image data is normalised to lie
-        # between 0 and 255, and cast to uint8
-        imageData = np.array(image.data, dtype=np.float32)
-        imageData = 255.0 * (imageData       - imageData.min()) / \
-                            (imageData.max() - imageData.min())
-
-        # and each dimension is padded so it has a
-        # power-of-two length. Ugh. This is a horrible,
-        # but as far as I'm aware necessary hack.  At
-        # least it's necessary using the OpenGL 2.1
-        # API on OSX mavericks. It massively increases
-        # image load time, too.
-        imageData = np.pad(imageData, pad, 'constant', constant_values=0)
-        imageData = np.array(imageData, dtype=np.uint8)
-
-        # Then flattened, with fortran dimension ordering,
-        # so the data, as stored on the GPU, has its first
-        # dimension as the fastest changing.
-        imageData = imageData.ravel(order='F')
-
-        # Image data is stored on the GPU as a 3D texture
-        imageBuffer = gl.glGenTextures(1)
-        gl.glBindTexture(gl.GL_TEXTURE_3D, imageBuffer)
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_MAG_FILTER,
-                           gl.GL_NEAREST)
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_MIN_FILTER,
-                           gl.GL_NEAREST)
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_WRAP_S,
-                           gl.GL_CLAMP_TO_EDGE)
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_WRAP_T,
-                           gl.GL_CLAMP_TO_EDGE)
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_WRAP_R,
-                           gl.GL_CLAMP_TO_EDGE)         
-        
-        gl.glTexImage3D(gl.GL_TEXTURE_3D,
-                        0,
-                        arbrg.GL_R8,
-                        texShape[0],
-                        texShape[1],
-                        texShape[2],
-                        0,
-                        gl.GL_RED,
-                        gl.GL_UNSIGNED_BYTE,
-                        imageData)
-
-        # And added as an attribute of the image, so
-        # other things which want to render the image
-        # don't need to recreate all of those buffers.
-        image.setAttribute('glBuffers', imageBuffer)
-
-        return imageBuffer
-
-        
-    def updateColourBuffer(self):
-        """
-        Regenerates the colour buffer used to colour image voxels.
-        """
-
-        display      = self.image.display
-        colourBuffer = self.colourBuffer
-
-        # Here we are creating a range of values to be passed
-        # to the matplotlib.colors.Colormap instance of the
-        # image display. We scale this range such that data
-        # values which lie outside the configured display range
-        # will map to values below 0.0 or above 1.0. It is
-        # assumed that the Colormap instance is configured to
-        # generate appropriate colours for these out-of-range
-        # values.
-        
-        normalRange = np.linspace(0.0, 1.0, self.colourResolution)
-        normalStep  = 1.0 / (self.colourResolution - 1) 
-
-        normMin = (display.displayMin - display.dataMin) / \
-                  (display.dataMax    - display.dataMin)
-        normMax = (display.displayMax - display.dataMin) / \
-                  (display.dataMax    - display.dataMin)
-
-        newStep  = normalStep / (normMax - normMin)
-        newRange = (normalRange - normMin) * (newStep / normalStep)
-
-        # Create [self.colourResolution] rgb values,
-        # spanning the entire range of the image
-        # colour map
-        colourmap = display.cmap(newRange)
-        
-        # The colour data is stored on
-        # the GPU as 8 bit rgba tuples
-        colourmap = np.floor(colourmap * 255)
-        colourmap = np.array(colourmap, dtype=np.uint8)
-        colourmap = colourmap.ravel(order='C')
-
-        # GL texture creation stuff
-        gl.glBindTexture(gl.GL_TEXTURE_1D, colourBuffer)
-        gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                           gl.GL_TEXTURE_MAG_FILTER,
-                           gl.GL_NEAREST)
-        gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                           gl.GL_TEXTURE_MIN_FILTER,
-                           gl.GL_NEAREST)
-        gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                           gl.GL_TEXTURE_WRAP_S,
-                           gl.GL_CLAMP_TO_EDGE) 
-        
-        gl.glTexImage1D(gl.GL_TEXTURE_1D,
-                        0,
-                        gl.GL_RGBA8,
-                        self.colourResolution,
-                        0,
-                        gl.GL_RGBA,
-                        gl.GL_UNSIGNED_BYTE,
-                        colourmap)
-
-
-    def configDisplayListeners(self):
-        """
-        Adds a bunch of listeners to the fslimage.ImageDisplay object
-        (accessible as an attribute, called 'display', of the given image),
-        which defines how the given image is to be displayed. This is done
-        so we can refresh the image view when image display properties are
-        changed. 
-        """
-
-        def refreshNeeded(*a):
-            """
-            The view just needs to be refreshed (e.g. the alpha property
-            has changed).
-            """
-            self.canvas.Refresh()
-
-        def colourUpdateNeeded(*a):
-            """
-            The colour map for this image needs to be recreated (e.g. the
-            colour map has been changed).
-            """
-            self.updateColourBuffer()
-            self.canvas.Refresh()
-
-        display           = self.image.display
-        lnrName           = 'SliceCanvas_{{}}_{}'.format(id(self))
-        refreshProps      = ['alpha', 'enabled']
-        colourUpdateProps = ['displayMin', 'displayMax', 'rangeClip', 'cmap']
-
-        for prop in refreshProps:
-            display.addListener(prop, lnrName.format(prop), refreshNeeded)
-
-        for prop in colourUpdateProps:
-            display.addListener(prop, lnrName.format(prop), colourUpdateNeeded)
 
+import fsl.data.fslimage       as fslimage
+import fsl.fslview.glimagedata as glimagedata
 
 
 # The vertex shader positions and colours a single vertex.
@@ -577,15 +300,26 @@ class SliceCanvas(wxgl.GLCanvas):
         self.ypos = self.ypos
         self.zpos = self.zpos
 
-        # Create a GLImageData object
-        # for any new images
+        # Create a GLImageData object for any new images,
+        # and attach a listener to their display properties
+        # so we know when to refresh the canvas.
         for image in self.imageList:
             try:
                 glData = image.getAttribute(self.name)
             except:
-                glData = GLImageData(image, self)
+                glData = glimagedata.GLImageData(image, self.xax, self.yax)
                 image.setAttribute(self.name, glData)
 
+                def refresh(*a):
+                    self.Refresh()
+
+                image.display.addListener('enabled',    self.name, refresh)
+                image.display.addListener('alpha',      self.name, refresh)
+                image.display.addListener('displayMin', self.name, refresh)
+                image.display.addListener('displayMax', self.name, refresh)
+                image.display.addListener('rangeClip',  self.name, refresh)
+                image.display.addListener('cmap',       self.name, refresh)
+
         self.Refresh()