diff --git a/doc/fsl.fsleyes.editor.rst b/doc/fsl.fsleyes.editor.rst
index 62d946c8269071f6d1b530b2751156c19d9df691..f38c93d8e293d9798ee6698bfc06a8d7f1aeddb4 100644
--- a/doc/fsl.fsleyes.editor.rst
+++ b/doc/fsl.fsleyes.editor.rst
@@ -1,17 +1,6 @@
 fsl.fsleyes.editor package
 ==========================
 
-Submodules
-----------
-
-.. toctree::
-
-   fsl.fsleyes.editor.editor
-   fsl.fsleyes.editor.selection
-
-Module contents
----------------
-
 .. automodule:: fsl.fsleyes.editor
     :members:
     :undoc-members:
diff --git a/doc/images/editor.png b/doc/images/editor.png
new file mode 100644
index 0000000000000000000000000000000000000000..0d6216b498267cdc093f26e4d9b1d7fc83a3e53f
Binary files /dev/null and b/doc/images/editor.png differ
diff --git a/fsl/fsleyes/editor/editor.py b/fsl/fsleyes/editor/editor.py
index ee0b92d867428c4718cc0afec139edceeb52610d..f3ff2c9133c1ccacce369c42ea9e4a81391a9cda 100644
--- a/fsl/fsleyes/editor/editor.py
+++ b/fsl/fsleyes/editor/editor.py
@@ -1,12 +1,15 @@
 #!/usr/bin/env python
 #
-# editor.py -
+# editor.py - The Editor class.
 #
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
+"""This module provides the :class:`Editor` class, which provides
+functionality to edit the data in an :class:`.Image` overlay.
+"""
+
 
 import logging
-log = logging.getLogger(__name__)
 
 import collections
 
@@ -18,36 +21,55 @@ import selection
 import fsl.data.image as fslimage
 
 
-class ValueChange(object):
-    def __init__(self, overlay, volume, offset, oldVals, newVals):
-        self.overlay = overlay
-        self.volume  = volume
-        self.offset  = offset
-        self.oldVals = oldVals
-        self.newVals = newVals
+log = logging.getLogger(__name__)
 
 
-class SelectionChange(object):
-    def __init__(self, overlay, offset, oldSelection, newSelection):
-        self.overlay      = overlay
-        self.offset       = offset
-        self.oldSelection = oldSelection
-        self.newSelection = newSelection
+class Editor(props.HasProperties):
+    """The ``Editor`` class provides functionality to edit the data of an
+    :class:`.Image` overlay.
 
+    An ``Editor`` instance uses a :class:`.Selection` object which allows
+    voxel selections to be made, and keeps track of all changes to the
+    selection and image.
 
-class Editor(props.HasProperties):
+
+    .. autosummary::
+       :nosignatures:
+    
+       getSelection
+       fillSelection
+       createMaskFromSelection
+       createROIFromSelection
+
+    Undo/redo and change groups.
+
+    .. autosummary::
+       :nosignatures:
+
+       undo
+       redo
+       startChangeGroup
+       endChangeGroup
+    """
 
     canUndo = props.Boolean(default=False)
+    """
+    """
+
+    
     canRedo = props.Boolean(default=False)
+    """
+    """
 
+    
     def __init__(self, overlayList, displayCtx):
 
-        self._name           = '{}_{}'.format(self.__class__.__name__,
-                                              id(self))
-        self._overlayList    = overlayList
-        self._displayCtx     = displayCtx
-        self._selection      = None
-        self._currentOverlay = None
+        self.__name           = '{}_{}'.format(self.__class__.__name__,
+                                               id(self))
+        self.__overlayList    = overlayList
+        self.__displayCtx     = displayCtx
+        self.__selection      = None
+        self.__currentOverlay = None
  
         # A list of state objects, providing
         # records of what has been done. The
@@ -57,18 +79,18 @@ class Editor(props.HasProperties):
         # everything after the doneIndex
         # represents states which have been
         # undone.
-        self._doneList  = []
-        self._doneIndex = -1
-        self._inGroup   = False
+        self.__doneList  = []
+        self.__doneIndex = -1
+        self.__inGroup   = False
 
-        self._displayCtx .addListener('selectedOverlay',
-                                      self._name,
-                                      self._selectedOverlayChanged)
-        self._overlayList.addListener('overlays',
-                                      self._name,
-                                      self._selectedOverlayChanged) 
+        self.__displayCtx .addListener('selectedOverlay',
+                                       self.__name,
+                                       self.__selectedOverlayChanged)
+        self.__overlayList.addListener('overlays',
+                                       self.__name,
+                                       self.__selectedOverlayChanged) 
 
-        self._selectedOverlayChanged()
+        self.__selectedOverlayChanged()
 
         log.memory('{}.init ({})'.format(type(self).__name__, id(self)))
 
@@ -78,76 +100,33 @@ class Editor(props.HasProperties):
 
         
     def destroy(self):
-        self._displayCtx .removeListener('selectedOverlay', self._name)
-        self._overlayList.removeListener('overlays',        self._name)
+        self.__displayCtx .removeListener('selectedOverlay', self.__name)
+        self.__overlayList.removeListener('overlays',        self.__name)
         
-        if self._selection is not None:
-            self._selection.removeListener('selection', self._name)
-
-        self._overlayList    = None
-        self._displayCtx     = None
-        self._selection      = None
-        self._currentOverlay = None
-        self._doneList       = None
-
-
-    def _selectedOverlayChanged(self, *a):
-        overlay = self._displayCtx.getSelectedOverlay()
-
-        if self._currentOverlay == overlay:
-            return
-
-        if overlay is None:
-            self._currentOverlay = None
-            self._selection      = None
-            return
-
-        display = self._displayCtx.getDisplay(overlay)
-
-        if not isinstance(overlay, fslimage.Image) or \
-           display.overlayType != 'volume':
-            self._currentOverlay = None
-            self._selection      = None
-            return
-
-        if self._selection is not None:
-            oldSel = self._selection.transferSelection(
-                overlay, display)
-        else:
-            oldSel = None
-                        
-        self._currentOverlay = overlay
-        self._selection      = selection.Selection(overlay,
-                                                   display,
-                                                   oldSel)
-
-        self._selection.addListener('selection',
-                                    self._name,
-                                    self._selectionChanged)
-
-
-    def _selectionChanged(self, *a):
+        if self.__selection is not None:
+            self.__selection.removeListener('selection', self.__name)
 
-        old, new, offset = self._selection.getLastChange()
-        
-        change = SelectionChange(self._currentOverlay, offset, old, new)
-        self._changeMade(change)
+        self.__overlayList    = None
+        self.__displayCtx     = None
+        self.__selection      = None
+        self.__currentOverlay = None
+        self.__doneList       = None
 
 
     def getSelection(self):
-        return self._selection
+        return self.__selection
 
 
     def fillSelection(self, newVals):
 
-        overlay = self._currentOverlay
+        overlay = self.__currentOverlay
 
         if overlay is None:
             return
         
-        opts = self._displayCtx.getOpts(overlay)
+        opts = self.__displayCtx.getOpts(overlay)
 
-        selectBlock, offset = self._selection.getBoundedSelection()
+        selectBlock, offset = self.__selection.getBoundedSelection()
 
         if not isinstance(newVals, collections.Sequence):
             nv = np.zeros(selectBlock.shape, dtype=np.float32)
@@ -174,99 +153,183 @@ class Editor(props.HasProperties):
         oldVals = np.array(oldVals)
         
         change = ValueChange(overlay, opts.volume, offset, oldVals, newVals)
-        self._applyChange(change)
-        self._changeMade( change)
+        self.__applyChange(change)
+        self.__changeMade( change)
+
+            
+    def createMaskFromSelection(self):
+
+        overlay = self.__currentOverlay
+        if overlay is None:
+            return
+
+        overlayIdx = self.__overlayList.index(overlay)
+        mask       = np.array(self.__selection.selection, dtype=np.uint8)
+        header     = overlay.nibImage.get_header()
+        name       = '{}_mask'.format(overlay.name)
+
+        roiImage = fslimage.Image(mask, name=name, header=header)
+        self.__overlayList.insert(overlayIdx + 1, roiImage) 
+
+
+    def createROIFromSelection(self):
+
+        overlay = self.__currentOverlay
+        if overlay is None:
+            return
+
+        overlayIdx = self.__overlayList.index(overlay) 
+        opts       = self.__displayCtx.getDisplay(overlay)
+        
+        roi       = np.zeros(overlay.shape[:3], dtype=overlay.data.dtype)
+        selection = self.__selection.selection > 0
+
+        if   len(overlay.shape) == 3:
+            roi[selection] = overlay.data[selection]
+        elif len(overlay.shape) == 4:
+            roi[selection] = overlay.data[:, :, :, opts.volume][selection]
+        else:
+            raise RuntimeError('Only 3D and 4D images are currently supported')
+
+        header = overlay.nibImage.get_header()
+        name   = '{}_roi'.format(overlay.name)
+
+        roiImage = fslimage.Image(roi, name=name, header=header)
+        self.__overlayList.insert(overlayIdx + 1, roiImage)
 
         
     def startChangeGroup(self):
-        del self._doneList[self._doneIndex + 1:]
+        del self.__doneList[self.__doneIndex + 1:]
         
-        self._inGroup    = True
-        self._doneIndex += 1
-        self._doneList.append([])
+        self.__inGroup    = True
+        self.__doneIndex += 1
+        self.__doneList.append([])
 
         log.debug('Starting change group - merging subsequent '
-                  'changes at index {} of {}'.format(self._doneIndex,
-                                                     len(self._doneList)))
+                  'changes at index {} of {}'.format(self.__doneIndex,
+                                                     len(self.__doneList)))
 
         
     def endChangeGroup(self):
-        self._inGroup = False
+        self.__inGroup = False
         log.debug('Ending change group at {} of {}'.format(
-            self._doneIndex, len(self._doneList))) 
-
-        
-    def _changeMade(self, change):
-
-        if self._inGroup:
-            self._doneList[self._doneIndex].append(change)
-        else:
-            del self._doneList[self._doneIndex + 1:]
-            self._doneList.append(change)
-            self._doneIndex += 1
-            
-        self.canUndo = True
-        self.canRedo = False
-
-        log.debug('New change ({} of {})'.format(self._doneIndex,
-                                                 len(self._doneList)))
+            self.__doneIndex, len(self.__doneList))) 
 
 
     def undo(self):
-        if self._doneIndex == -1:
+        if self.__doneIndex == -1:
             return
 
-        log.debug('Undo change {} of {}'.format(self._doneIndex,
-                                                len(self._doneList)))        
+        log.debug('Undo change {} of {}'.format(self.__doneIndex,
+                                                len(self.__doneList)))        
 
-        change = self._doneList[self._doneIndex]
+        change = self.__doneList[self.__doneIndex]
 
         if not isinstance(change, collections.Sequence):
             change = [change]
 
         for c in reversed(change):
-            self._revertChange(c)
+            self.__revertChange(c)
 
-        self._doneIndex -= 1
+        self.__doneIndex -= 1
 
-        self._inGroup = False
+        self.__inGroup = False
         self.canRedo  = True
-        if self._doneIndex == -1:
+        if self.__doneIndex == -1:
             self.canUndo = False
         
 
     def redo(self):
-        if self._doneIndex == len(self._doneList) - 1:
+        if self.__doneIndex == len(self.__doneList) - 1:
             return
 
-        log.debug('Redo change {} of {}'.format(self._doneIndex + 1,
-                                                len(self._doneList))) 
+        log.debug('Redo change {} of {}'.format(self.__doneIndex + 1,
+                                                len(self.__doneList))) 
 
-        change = self._doneList[self._doneIndex + 1]
+        change = self.__doneList[self.__doneIndex + 1]
         
         if not isinstance(change, collections.Sequence):
             change = [change] 
 
         for c in change:
-            self._applyChange(c)
+            self.__applyChange(c)
 
-        self._doneIndex += 1
+        self.__doneIndex += 1
 
-        self._inGroup = False
+        self.__inGroup = False
         self.canUndo  = True
-        if self._doneIndex == len(self._doneList) - 1:
+        if self.__doneIndex == len(self.__doneList) - 1:
             self.canRedo = False
 
 
-    def _applyChange(self, change):
+    def __selectedOverlayChanged(self, *a):
+        overlay = self.__displayCtx.getSelectedOverlay()
+
+        if self.__currentOverlay == overlay:
+            return
+
+        if overlay is None:
+            self.__currentOverlay = None
+            self.__selection      = None
+            return
+
+        display = self.__displayCtx.getDisplay(overlay)
+
+        if not isinstance(overlay, fslimage.Image) or \
+           display.overlayType != 'volume':
+            self.__currentOverlay = None
+            self.__selection      = None
+            return
+
+        if self.__selection is not None:
+            oldSel = self.__selection.transferSelection(
+                overlay, display)
+        else:
+            oldSel = None
+                        
+        self.__currentOverlay = overlay
+        self.__selection      = selection.Selection(overlay,
+                                                    display,
+                                                    oldSel)
+
+        self.__selection.addListener('selection',
+                                     self.__name,
+                                     self.__selectionChanged)
+
+
+    def __selectionChanged(self, *a):
+
+        old, new, offset = self.__selection.getLastChange()
+        
+        change = SelectionChange(self.__currentOverlay, offset, old, new)
+        self.__changeMade(change)
+
+        
+    def __changeMade(self, change):
+
+        if self.__inGroup:
+            self.__doneList[self.__doneIndex].append(change)
+        else:
+            del self.__doneList[self.__doneIndex + 1:]
+            self.__doneList.append(change)
+            self.__doneIndex += 1
+            
+        self.canUndo = True
+        self.canRedo = False
+
+        log.debug('New change ({} of {})'.format(self.__doneIndex,
+                                                 len(self.__doneList)))
+
+
+    def __applyChange(self, change):
 
         overlay = change.overlay
-        opts    = self._displayCtx.getOpts(overlay)
+        opts    = self.__displayCtx.getOpts(overlay)
 
         if overlay.is4DImage(): volume = opts.volume
         else:                   volume = None
         
-        self._displayCtx.selectOverlay(overlay)
+        self.__displayCtx.selectOverlay(overlay)
 
         if isinstance(change, ValueChange):
             log.debug('Changing image data - offset '
@@ -275,17 +338,17 @@ class Editor(props.HasProperties):
             change.overlay.applyChange(change.offset, change.newVals, volume)
             
         elif isinstance(change, SelectionChange):
-            self._selection.disableListener('selection', self._name)
-            self._selection.setSelection(change.newSelection, change.offset)
-            self._selection.enableListener('selection', self._name)
+            self.__selection.disableListener('selection', self.__name)
+            self.__selection.setSelection(change.newSelection, change.offset)
+            self.__selection.enableListener('selection', self.__name)
 
         
-    def _revertChange(self, change):
+    def __revertChange(self, change):
 
         overlay = change.overlay
-        opts    = self._displayCtx.getOpts(overlay)
+        opts    = self.__displayCtx.getOpts(overlay)
         
-        self._displayCtx.selectOverlay(overlay)
+        self.__displayCtx.selectOverlay(overlay)
 
         if overlay.is4DImage(): volume = opts.volume
         else:                   volume = None 
@@ -297,47 +360,51 @@ class Editor(props.HasProperties):
             change.overlay.applyChange(change.offset, change.oldVals, volume)
             
         elif isinstance(change, SelectionChange):
-            self._selection.disableListener('selection', self._name)
-            self._selection.setSelection(change.oldSelection, change.offset)
-            self._selection.enableListener('selection', self._name)
+            self.__selection.disableListener('selection', self.__name)
+            self.__selection.setSelection(change.oldSelection, change.offset)
+            self.__selection.enableListener('selection', self.__name)
 
 
-    def createMaskFromSelection(self):
-
-        overlay = self._currentOverlay
-        if overlay is None:
-            return
-
-        overlayIdx = self._overlayList.index(overlay)
-        mask       = np.array(self._selection.selection, dtype=np.uint8)
-        header     = overlay.nibImage.get_header()
-        name       = '{}_mask'.format(overlay.name)
-
-        roiImage = fslimage.Image(mask, name=name, header=header)
-        self._overlayList.insert(overlayIdx + 1, roiImage) 
-
-
-    def createROIFromSelection(self):
-
-        overlay = self._currentOverlay
-        if overlay is None:
-            return
+class ValueChange(object):
+    """Represents a change which has been made to the data for an
+    :class:`.Image` instance. Stores the location, the old values,
+    and the new values.
+    """
 
-        overlayIdx = self._overlayList.index(overlay) 
-        opts       = self._displayCtx.getDisplay(overlay)
+    
+    def __init__(self, overlay, volume, offset, oldVals, newVals):
+        """Create a ``ValueChange``.
+
+        :arg overlay: The :class:`.Image` instance.
+        :arg volume:  Volume index, if ``overlay`` is 4D.
+        :arg offset:  Location (voxel coordinates) of the change.
+        :arg oldVals: A ``numpy`` array containing the old image values.
+        :arg newVals: A ``numpy`` array containing the new image values.
+        """
         
-        roi       = np.zeros(overlay.shape[:3], dtype=overlay.data.dtype)
-        selection = self._selection.selection > 0
+        self.overlay = overlay
+        self.volume  = volume
+        self.offset  = offset
+        self.oldVals = oldVals
+        self.newVals = newVals
 
-        if   len(overlay.shape) == 3:
-            roi[selection] = overlay.data[selection]
-        elif len(overlay.shape) == 4:
-            roi[selection] = overlay.data[:, :, :, opts.volume][selection]
-        else:
-            raise RuntimeError('Only 3D and 4D images are currently supported')
 
-        header = overlay.nibImage.get_header()
-        name   = '{}_roi'.format(overlay.name)
+class SelectionChange(object):
+    """Represents a change which has been made to a :class:`.Selection`
+    instance. Stores the location, the old selection, and the new selection.
+    """
 
-        roiImage = fslimage.Image(roi, name=name, header=header)
-        self._overlayList.insert(overlayIdx + 1, roiImage)
+    
+    def __init__(self, overlay, offset, oldSelection, newSelection):
+        """Create a ``SelectionChange``.
+        
+        :arg overlay:      The :class:`.Image` instance.
+        :arg offset:       Location (voxel coordinates) of the change.
+        :arg oldSelection: A ``numpy`` array containing the old selection.
+        :arg newSelection: A ``numpy`` array containing the new selection.
+        """
+        
+        self.overlay      = overlay
+        self.offset       = offset
+        self.oldSelection = oldSelection
+        self.newSelection = newSelection
diff --git a/fsl/fsleyes/editor/selection.py b/fsl/fsleyes/editor/selection.py
index 2dea7bcc3c49931ff563b667339f61c1574151a7..5b4f0fd63911f2ef3bd80242d91b06ecd9c2c846 100644
--- a/fsl/fsleyes/editor/selection.py
+++ b/fsl/fsleyes/editor/selection.py
@@ -1,11 +1,11 @@
 #!/usr/bin/env python
 #
-# selection.py - Provides the Selection class, which represents a
-# selection on a 3D image.
+# selection.py - The Selection class.
 #
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
-"""This module provides the :class:`Selection` class.
+"""This module provides the :class:`Selection` class, which represents a
+selection of voxels in a 3D :class:`.Image`.
 """
 
 import logging
@@ -22,190 +22,255 @@ log = logging.getLogger(__name__)
 
 
 class Selection(props.HasProperties):
-
-
-    selection = props.Object()
+    """The ``Selection`` class represents a selection of voxels in a 3D
+    :class:`.Image`. The selection is stored as a ``numpy`` mask array,
+    the same shape as the image. Methods are available to query and update
+    the selection.
 
     
-    def __init__(self, image, display, selection=None):
-        self._image                = image
-        self._display              = display
-        self._opts                 = display.getDisplayOpts()
-        self._lastChangeOffset     = None
-        self._lastChangeOldBlock   = None
-        self._lastChangeNewBlock   = None
+    Changes to a ``Selection`` can be made through *blocks*, which are 3D
+    cuboid regions. The following methods allow a block to be
+    selected/deselected, where the block is specified by a voxel coordinate,
+    and a block size:
 
-        if selection is None:
-            selection = np.zeros(image.shape[:3], dtype=np.uint8)
-        elif selection.shape != image.shape[:3] or \
-             selection.dtype != np.uint8:
-            raise ValueError('Incompatible selection array: {} ({})'.format(
-                selection.shape,
-                selection.dtype))
+    .. autosummary::
+       :nosignatures:
 
-        self.selection = selection
+       selectBlock
+       deselectBlock
 
-        log.memory('{}.init ({})'.format(type(self).__name__, id(self)))
 
+    The following methods offer more fine grained control over selection
+    blocks - with these methods, you pass in a block that you have created
+    yourself, and an offset into the selection, specifying its location:
 
-    def __del__(self):
-        log.memory('{}.del ({})'.format(type(self).__name__, id(self)))
+    .. autosummary::
+       :nosignatures:
+
+       setSelection
+       replaceSelection
+       addToSelection
+       removeFromSelection
 
     
-    def getSelectionSize(self):
-        return self.selection.sum()
+    A third approach to making a selection is provided by the
+    :meth:`selectByValue` method, which allows a selection to be made
+    in a manner similar to a *bucket fill* technique found in any image
+    editor.
 
+    A ``Selection`` object keeps track of the most recent change made
+    through any of the above methods. The most recent change can be retrieved
+    through the :meth:`getLastChange` method.
 
-    def getBoundedSelection(self):
-        
-        xs, ys, zs = np.where(self.selection > 0)
 
-        xlo = xs.min()
-        ylo = ys.min()
-        zlo = zs.min()
-        xhi = xs.max() + 1
-        yhi = ys.max() + 1
-        zhi = zs.max() + 1
-
-        selection = self.selection[xlo:xhi, ylo:yhi, zlo:zhi]
+    Finally, the ``Selection`` class offers a few other methods for
+    convenience:
+    
+    .. autosummary::
+       :nosignatures:
 
-        return selection, (xlo, ylo, zlo)
+       getSelectionSize
+       clearSelection
+       getBoundedSelection
+       getIndices
+       generateBlock
 
+    """
     
-    def transferSelection(self, destImg, destDisplay):
 
-        srcImg   = self._image
-        srcOpts  = self._opts
-        destOpts = destDisplay.getDisplayOpts()
+    selection = props.Object()
+    """The ``numpy`` mask array containing the current selection is stored
+    in a :class:`props.Object`, so that listeners can register to be notified
+    whenever it changes.
 
-        if srcOpts.transform not in ('id', 'pixdim'):
-            raise RuntimeError('Unsupported transform for {}: {}'.format(
-                srcImg, srcOpts.transform))
-        if destOpts.transform not in ('id', 'pixdim'):
-            raise RuntimeError('Unsupported transform for {}: {}'.format(
-                destImg, destOpts.transform))
+    .. warning:: Do not modify the selection directly through this attribute -
+                 use the ``Selection`` instance methods
+                 (e.g. :meth:`setSelection`) instead.  If you modify the
+                 selection directly through this attribute, the
+                 :meth:`getLastChange` method will break.
+    """
 
-        srcShape  = srcImg .shape[:3]
-        destShape = destImg.shape[:3]
-        
-        if   srcOpts .transform == 'id':     srcDims  = (1.0, 1.0, 1.0)
-        elif srcOpts .transform == 'pixdim': srcDims  = srcImg.pixdim[:3]
-        if   destOpts.transform == 'id':     destDims = (1.0, 1.0, 1.0)
-        elif destOpts.transform == 'pixdim': destDims = destImg.pixdim[:3]
+    
+    def __init__(self, image, display, selection=None):
+        """Create a ``Selection`` instance.
 
-        srcXferShape  = np.zeros(3, dtype=np.float32)
-        destXferShape = np.zeros(3, dtype=np.float32)
+        :arg image:     The :class:`.Image` instance  associated with this
+                        ``Selection``.
+        
+        :arg display:   The :class:`.Display` instance for the ``image``.
+        
+        :arg selection: Selection array. If not provided, one is created.
+                        Must be a ``numpy.uint8`` array with the same shape
+                        as ``image``. This array is *not* copied.
+        """
+    
+        self.__image              = image
+        self.__display            = display
+        self.__opts               = display.getDisplayOpts()
+        self.__lastChangeOffset   = None
+        self.__lastChangeOldBlock = None
+        self.__lastChangeNewBlock = None
 
-        # Figure out the shape, of the area
-        # to be copied, in source image voxels,
-        # and in destination image voxels
-        for i in range(3):
+        if selection is None:
+            selection = np.zeros(image.shape[:3], dtype=np.uint8)
+            
+        elif selection.shape != image.shape[:3] or \
+             selection.dtype != np.uint8:
+            raise ValueError('Incompatible selection array: {} ({})'.format(
+                selection.shape, selection.dtype))
 
-            srcSize   = float(srcShape[ i] * srcDims[ i])
-            destSize  = float(destShape[i] * destDims[i])
-            xferSize  = min(srcSize, destSize)
+        self.selection = selection
 
-            srcXferShape[ i] = int(round(xferSize / srcDims[ i]))
-            destXferShape[i] = int(round(xferSize / destDims[i]))
+        log.memory('{}.init ({})'.format(type(self).__name__, id(self)))
 
-        xferred = np.zeros(destShape, dtype=np.uint8)
 
-        srcx,  srcy,  srcz  = map(int, srcXferShape)
-        destx, desty, destz = map(int, destXferShape)
+    def __del__(self):
+        """Prints a log message."""
+        log.memory('{}.del ({})'.format(type(self).__name__, id(self)))
 
-        zoomFactor = destXferShape / srcXferShape
-        zoomed = ndiint.zoom(
-            self.selection[:srcx, :srcy, :srcz],
-            zoomFactor,
-            order=0)
 
-        if zoomed.shape == (destx, desty, destz):
-            xferred[:destx, :desty, :destz] = zoomed
-
-        return xferred
 
+    def selectBlock(self, voxel, blockSize, axes=(0, 1, 2)):
+        """Selects the block (sets all voxels to 1) specified by the given
+        voxel and block size.
 
-    def _updateSelectionBlock(self, block, offset):
+        :arg voxel:     Starting voxel coordinates of the block.
+        :arg blockSize: Size of the block along each axis.
+        :arg axes:      Limit the block to the specified axes.
 
-        block = np.array(block, dtype=np.uint8)
+        """
+        self.addToSelection(*self.generateBlock(voxel,
+                                                blockSize,
+                                                self.selection.shape,
+                                                axes))
 
-        if block.size == 0:
-            return
+        
+    def deselectBlock(self, voxel, blockSize, axes=(0, 1, 2)):
+        """De-selects the block (sets all voxels to 0) specified by the given
+        voxel and block size.
 
-        if offset is None:
-            offset = (0, 0, 0)
+        :arg voxel:     Starting voxel coordinates of the block.
+        :arg blockSize: Size of the block along each axis.
+        :arg axes:      Limit the block to the specified axes.        
+        """ 
+        self.removeFromSelection(*self.generateBlock(voxel,
+                                                     blockSize,
+                                                     self.selection.shape,
+                                                     axes)) 
 
-        xlo, ylo, zlo = offset
+        
+    def setSelection(self, block, offset):
+        """Copies the given ``block`` into the selection, starting at
+        ``offset``.
 
-        xhi = xlo + block.shape[0]
-        yhi = ylo + block.shape[1]
-        zhi = zlo + block.shape[2]
+        :arg block:  A ``numpy.uint8`` array containing a selection.
+        :arg offset: Voxel coordinates specifying the block location.
+        """
+        self.__updateSelectionBlock(block, offset) 
+        
+    
+    def replaceSelection(self, block, offset):
+        """Clears the entire selection, then copies the given ``block``
+        into the selection, starting at ``offset``.
+        
+        :arg block:  A ``numpy.uint8`` array containing a selection.
+        :arg offset: Voxel coordinates specifying the block location. 
+        """
+        self.clearSelection()
+        self.__updateSelectionBlock(block, offset)
 
-        self._lastChangeOffset   = offset
-        self._lastChangeOldBlock = np.array(self.selection[xlo:xhi,
-                                                           ylo:yhi,
-                                                           zlo:zhi])
-        self._lastChangeNewBlock = np.array(block)
+        
+    def addToSelection(self, block, offset):
+        """Adds the selection (via a boolean OR operation) in the given
+        ``block`` to the current selection, starting at ``offset``.
 
-        log.debug('Updating selection ({}) block [{}:{}, {}:{}, {}:{}]'.format(
-            id(self), xlo, xhi, ylo, yhi, zlo, zhi))
+        :arg block:  A ``numpy.uint8`` array containing a selection.
+        :arg offset: Voxel coordinates specifying the block location. 
+        """
+        existing = self.__getSelectionBlock(block.shape, offset)
+        block    = np.logical_or(block, existing)
+        
+        self.__updateSelectionBlock(block, offset)
 
-        self.selection[xlo:xhi, ylo:yhi, zlo:zhi] = block
-        self.notify('selection') 
 
+    def removeFromSelection(self, block, offset):
+        """Clears all voxels in the selection where the values in ``block``
+        are non-zero.
         
-    def _getSelectionBlock(self, size, offset):
-        
-        xlo, ylo, zlo = offset
-        xhi, yhi, zhi = size
-
-        xhi = xlo + size[0]
-        yhi = ylo + size[1]
-        zhi = zlo + size[2]
+        :arg block:  A ``numpy.uint8`` array containing a selection.
+        :arg offset: Voxel coordinates specifying the block location. 
+        """
+        existing             = self.__getSelectionBlock(block.shape, offset)
+        existing[block != 0] = False
+        self.__updateSelectionBlock(existing, offset)
 
-        return np.array(self.selection[xlo:xhi, ylo:yhi, zlo:zhi])
+    
+    def getSelectionSize(self):
+        """Returns the number of voxels that are currently selected. """
+        return self.selection.sum()
 
 
-    def replaceSelection(self, block, offset):
-        self.clearSelection()
-        self._updateSelectionBlock(block, offset)
+    def getBoundedSelection(self):
+        """Extracts the smallest region from the :attr:`selection` which
+        contains all selected voxels.
 
+        Returns a tuple containing the region, as a ``numpy.uint8`` array, and
+        the coordinates specifying its location in the full :attr:`selection`
+        array.
+        """
         
-    def setSelection(self, block, offset):
-        self._updateSelectionBlock(block, offset) 
+        xs, ys, zs = np.where(self.selection > 0)
 
-        
-    def addToSelection(self, block, offset):
-        existing = self._getSelectionBlock(block.shape, offset)
-        block    = np.logical_or(block, existing)
-        self._updateSelectionBlock(block, offset)
+        xlo = xs.min()
+        ylo = ys.min()
+        zlo = zs.min()
+        xhi = xs.max() + 1
+        yhi = ys.max() + 1
+        zhi = zs.max() + 1
 
+        selection = self.selection[xlo:xhi, ylo:yhi, zlo:zhi]
 
-    def removeFromSelection(self, block, offset):
-        existing             = self._getSelectionBlock(block.shape, offset)
-        existing[block != 0] = False
-        self._updateSelectionBlock(existing, offset)
+        return selection, (xlo, ylo, zlo)
 
         
     def clearSelection(self):
+        """Clears (sets to 0) the entire selection. """
 
         log.debug('Clearing selection ({})'.format(id(self)))
         
-        self._lastChangeOffset     = [0, 0, 0]
-        self._lastChangeOldBlock   = np.array(self.selection)
-        self.selection[:]          = False
-        self._lastChangeNewBlock   = np.array(self.selection)
+        self.__lastChangeOffset     = [0, 0, 0]
+        self.__lastChangeOldBlock   = np.array(self.selection)
+        self.selection[:]           = False
+        self.__lastChangeNewBlock   = np.array(self.selection)
 
         self.notify('selection')
 
 
     def getLastChange(self):
-        return (self._lastChangeOldBlock,
-                self._lastChangeNewBlock,
-                self._lastChangeOffset)
+        """Returns the most recent change made to this ``Selection``.
+
+        A tuple is returned, containing the following:
+
+         - A ``numpy.uint8`` array containing the old block value
+         - A ``numpy.uint8`` array containing the new block value
+         - Voxel coordinates denoting the block location in the full
+           :attr:`selection` array.
+        """
+        return (self.__lastChangeOldBlock,
+                self.__lastChangeNewBlock,
+                self.__lastChangeOffset)
 
 
     def getIndices(self, restrict=None):
+        """Returns a :math:`N \\times 3` array which contains the
+        coordinates of all voxels that are currently selected.
+
+        If the ``restrict`` argument is not provided, the entire
+        selection image is searched.
+
+        :arg restrict: A ``slice`` object specifying a sub-set of the
+                       full selection to consider.
+        """
 
         if restrict is None: selection = self.selection
         else:                selection = self.selection[restrict]
@@ -223,60 +288,38 @@ class Selection(props.HasProperties):
 
         return result
 
-
-    @classmethod
-    def generateBlock(cls, voxel, blockSize, shape, axes=(0, 1, 2)):
-
-        if blockSize == 1:
-            return np.array([True], dtype=np.uint8).reshape(1, 1, 1), voxel
-
-        blockLo = [v - int(np.floor((blockSize - 1) / 2.0)) for v in voxel]
-        blockHi = [v + int(np.ceil(( blockSize - 1) / 2.0)) for v in voxel]
-
-        for i in range(3):
-            if i not in axes:
-                blockLo[i] = voxel[i]
-                blockHi[i] = voxel[i] + 1
-            else:
-                blockLo[i] = max(blockLo[i],     0)
-                blockHi[i] = min(blockHi[i] + 1, shape[i])
-
-            if blockHi[i] <= blockLo[i]:
-                return np.ones((0, 0, 0), dtype=np.uint8), voxel
-
-        block = np.ones((blockHi[0] - blockLo[0],
-                         blockHi[1] - blockLo[1],
-                         blockHi[2] - blockLo[2]), dtype=np.uint8)
-
-        offset = blockLo
-
-        return block, offset
-
-
-    def selectBlock(self, voxel, blockSize, axes=(0, 1, 2)):
-        self.addToSelection(*self.generateBlock(voxel,
-                                                blockSize,
-                                                self.selection.shape,
-                                                axes))
-
-        
-    def deselectBlock(self, voxel, blockSize, axes=(0, 1, 2)):
-        self.removeFromSelection(*self.generateBlock(voxel,
-                                                     blockSize,
-                                                     self.selection.shape,
-                                                     axes)) 
-
     
     def selectByValue(self,
                       seedLoc,
                       precision=None,
                       searchRadius=None,
                       local=False):
-
-        if   len(self._image.shape) == 3:
-            data = self._image.data
-        elif len(self._image.shape) == 4:
-            data = self._image.data[:, :, :, self._opts.volume]
+        """A *bucket fill* style selection routine. Given a seed location,
+        finds all voxels which have a value similar to that of that location.
+        The current selection is replaced with all voxels that were found.
+
+        :arg seedLoc:      Voxel coordinates specifying the seed location
+
+        :arg precision:    Voxels which have a value that is less than
+                           ``precision`` from the seed location value will
+                           be selected.
+
+        :arg searchRadius: May be either a single value, or a sequence of
+                           three values - one for each axis. If provided,
+                           the search is limited to an ellipse, centred on
+                           the seed location, with the specified
+                           ``searchRadius`` (in voxels). If not provided,
+                           the search will cover the entire image space.
+
+        :arg local:        If ``True``, a voxel will only be selected if it
+                           is adjacent to an already selected voxel (using
+                           8-neighbour connectivity).
+        """
+
+        if   len(self.__image.shape) == 3:
+            data = self.__image.data
+        elif len(self.__image.shape) == 4:
+            data = self.__image.data[:, :, :, self.__opts.volume]
         else:
             raise RuntimeError('Only 3D and 4D images are currently supported')
 
@@ -361,10 +404,186 @@ class Selection(props.HasProperties):
         #
         # If local is not True, any same or similar 
         # values are part of the selection
-        # 
         if local:
             hits, _   = ndimeas.label(hits)
             seedLabel = hits[seedLoc[0], seedLoc[1], seedLoc[2]]
             hits      = hits == seedLabel
 
         self.replaceSelection(hits, searchOffset)
+
+    
+    def transferSelection(self, destImg, destDisplay):
+        """Re-samples the current selection into the destination image
+        space. 
+
+        Each ``Selection`` instance is in terms of a specific :class:`.Image`
+        instance, which has a specific dimensionality. In order to apply
+        a ``Selection`` which is in terms of one ``Image``, the selection
+        array needs to be re-sampled.
+        
+        .. todo:: This method will need to be re-written with the introduction
+                  of **GedMode**.
+
+        :arg destImg:     The :class:`.Image` that the selection is to be
+                          transferred to.
+
+        :arg destDisplay: The :class:`.Display` instance associated with
+                          ``destImg``.
+
+        :returns: a new ``numpy.uint8`` array, suitable for creating a new
+                 ``Selection`` object for use with the given ``destImg``.
+        """
+
+        srcImg   = self.__image
+        srcOpts  = self.__opts
+        destOpts = destDisplay.getDisplayOpts()
+
+        if srcOpts.transform not in ('id', 'pixdim'):
+            raise RuntimeError('Unsupported transform for {}: {}'.format(
+                srcImg, srcOpts.transform))
+        if destOpts.transform not in ('id', 'pixdim'):
+            raise RuntimeError('Unsupported transform for {}: {}'.format(
+                destImg, destOpts.transform))
+
+        srcShape  = srcImg .shape[:3]
+        destShape = destImg.shape[:3]
+        
+        if   srcOpts .transform == 'id':     srcDims  = (1.0, 1.0, 1.0)
+        elif srcOpts .transform == 'pixdim': srcDims  = srcImg.pixdim[:3]
+        if   destOpts.transform == 'id':     destDims = (1.0, 1.0, 1.0)
+        elif destOpts.transform == 'pixdim': destDims = destImg.pixdim[:3]
+
+        srcXferShape  = np.zeros(3, dtype=np.float32)
+        destXferShape = np.zeros(3, dtype=np.float32)
+
+        # Figure out the shape, of the area
+        # to be copied, in source image voxels,
+        # and in destination image voxels
+        for i in range(3):
+
+            srcSize   = float(srcShape[ i] * srcDims[ i])
+            destSize  = float(destShape[i] * destDims[i])
+            xferSize  = min(srcSize, destSize)
+
+            srcXferShape[ i] = int(round(xferSize / srcDims[ i]))
+            destXferShape[i] = int(round(xferSize / destDims[i]))
+
+        xferred = np.zeros(destShape, dtype=np.uint8)
+
+        srcx,  srcy,  srcz  = map(int, srcXferShape)
+        destx, desty, destz = map(int, destXferShape)
+
+        zoomFactor = destXferShape / srcXferShape
+        zoomed = ndiint.zoom(
+            self.selection[:srcx, :srcy, :srcz],
+            zoomFactor,
+            order=0)
+
+        if zoomed.shape == (destx, desty, destz):
+            xferred[:destx, :desty, :destz] = zoomed
+
+        return xferred
+        
+
+    def __updateSelectionBlock(self, block, offset):
+        """Replaces the current selection at the specified ``offset`` with the
+        given ``block``.
+
+        The old values for the block are stored, and can be retrieved via the
+        :meth:`getLastChange` method.
+
+        :arg block:  A ``numpy.uint8`` array containing the new selection
+                     values.
+
+        :arg offset: Voxel coordinates specifying the location of ``block``.
+        """
+
+        block = np.array(block, dtype=np.uint8)
+
+        if block.size == 0:
+            return
+
+        if offset is None:
+            offset = (0, 0, 0)
+
+        xlo, ylo, zlo = offset
+
+        xhi = xlo + block.shape[0]
+        yhi = ylo + block.shape[1]
+        zhi = zlo + block.shape[2]
+
+        self.__lastChangeOffset   = offset
+        self.__lastChangeOldBlock = np.array(self.selection[xlo:xhi,
+                                                            ylo:yhi,
+                                                            zlo:zhi])
+        self.__lastChangeNewBlock = np.array(block)
+
+        log.debug('Updating selection ({}) block [{}:{}, {}:{}, {}:{}]'.format(
+            id(self), xlo, xhi, ylo, yhi, zlo, zhi))
+
+        self.selection[xlo:xhi, ylo:yhi, zlo:zhi] = block
+        self.notify('selection') 
+
+        
+    def __getSelectionBlock(self, size, offset):
+        """Extracts a block from the selection image starting from the
+        specified ``offset``, and of the specified ``size``.
+        """
+        
+        xlo, ylo, zlo = offset
+        xhi, yhi, zhi = size
+
+        xhi = xlo + size[0]
+        yhi = ylo + size[1]
+        zhi = zlo + size[2]
+
+        return np.array(self.selection[xlo:xhi, ylo:yhi, zlo:zhi])
+
+    
+    @classmethod
+    def generateBlock(cls, voxel, blockSize, shape, axes=(0, 1, 2)):
+        """Convenience method to generates a square/cube of ones, with the
+        specified voxel at its centre, to fit in an image of the given shape.
+
+        If the specified voxel would result in part of the block being located
+        outside of the image shape, the block is truncated to fit inside
+        the image bounds.
+
+        :arg voxel:     Coordinates of the voxel around which the block is to
+                        be centred.
+        
+        :arg blockSize: Desired width/height/depth
+        
+        :arg shape:     Shape of the image in which the block is to be located.
+        
+        :arg axes:      Axes along which the block is to be located.
+
+        :returns:       A tuple containing the block - a ``numpy.uint8`` array
+                        filled with ones, and an offset specifying the block
+                        location within an image of the specified ``shape``.
+        """
+
+        if blockSize == 1:
+            return np.array([True], dtype=np.uint8).reshape(1, 1, 1), voxel
+
+        blockLo = [v - int(np.floor((blockSize - 1) / 2.0)) for v in voxel]
+        blockHi = [v + int(np.ceil(( blockSize - 1) / 2.0)) for v in voxel]
+
+        for i in range(3):
+            if i not in axes:
+                blockLo[i] = voxel[i]
+                blockHi[i] = voxel[i] + 1
+            else:
+                blockLo[i] = max(blockLo[i],     0)
+                blockHi[i] = min(blockHi[i] + 1, shape[i])
+
+            if blockHi[i] <= blockLo[i]:
+                return np.ones((0, 0, 0), dtype=np.uint8), voxel
+
+        block = np.ones((blockHi[0] - blockLo[0],
+                         blockHi[1] - blockLo[1],
+                         blockHi[2] - blockLo[2]), dtype=np.uint8)
+
+        offset = blockLo
+
+        return block, offset