diff --git a/fsl/fsleyes/displaycontext/display.py b/fsl/fsleyes/displaycontext/display.py
index 43cdbe3009c3d8e0cc216cc4f2a98f7aa674eef4..4041d2473506ba1559532b4ef17464a423ff7e28 100644
--- a/fsl/fsleyes/displaycontext/display.py
+++ b/fsl/fsleyes/displaycontext/display.py
@@ -366,12 +366,14 @@ class DisplayOpts(actions.ActionProvider):
     bounds = props.Bounds(ndims=3)
     """Specifies a bounding box in the display coordinate system which is big
     enough to contain the overlay described by this ``DisplayOpts``
-    instance. The values in this ``bounds`` property must be updated by
-    ``DisplayOpts`` subclasses.
-
-    Whenever the spatial representation of this overlay changes, but the
-    bounds do not change, subclass implementations should force notification
-    on this property (via the :meth:`.HasProperties.notify` method).
+    instance.
+
+    The values in this ``bounds`` property must be updated by ``DisplayOpts``
+    subclasses whenever the spatial representation of their overlay changes.
+    Additionally, whenever the spatial representation changes, sub-classes
+    must call the :meth:`.DisplayContext.cacheStandardCoordinates` method,
+    with a 'standard' space version of the current
+    :attr:`.DisplayContext.location`.
     """
 
     
@@ -400,6 +402,10 @@ class DisplayOpts(actions.ActionProvider):
                           :class:`.ActionProvider` constructor.
         """
 
+        nounbind = kwargs.get('nounbind', [])
+        nounbind.append('bounds')
+        kwargs['nounbind'] = nounbind
+
         actions.ActionProvider.__init__(
             self,
             overlayList,
@@ -459,19 +465,28 @@ class DisplayOpts(actions.ActionProvider):
             return self.overlay
         return None
 
+
+    def displayToStandardCoordinates(self, coords):
+        """This method transforms the given display system coordinates into a
+        standard coordinate system which will remain constant for the given
+        overlay.
+
+        This method must be overridden by any sub-classes for which 
+        the display space representation may change - for example, the
+        :class:`.Image` overlays can be transformed into the display
+        coordinate system in different ways, as defined by the
+        :attr:`.ImageOpts.transform`  property.
+        """
+        return coords
+
     
-    def transformDisplayLocation(self, coords):
-        """This method may be called after the overlay :attr:`bounds` have
-        changed.
-
-        If the bounds were changed as a result of a change to the spatial
-        representation of the overlay (e.g. the :attr:`.ImageOpts.transform`
-        property for :class:`.Image` overlays), this method should return
-        a copy of the given coordinates which has been transformed into the
-        new space.
-
-        If the bounds were changed for some other reason, this method can
-        just return the ``coords`` unchanged.
+    def standardToDisplayCoordinates(self, coords):
+        """This method transforms the given coordinates, assumed to be in
+        the standard coordinate system of the overlay, into the display
+        coordinate system.
+
+        This method must be overridden by sub-classes for which their standard
+        coordinate system is not the same as the display coordinate system.
         """
         return coords
 
diff --git a/fsl/fsleyes/displaycontext/displaycontext.py b/fsl/fsleyes/displaycontext/displaycontext.py
index c547ded1fd749f7e952f8b83ae769623b38cecfc..62226cf229a45e1fae1391bb66adea95552863c8 100644
--- a/fsl/fsleyes/displaycontext/displaycontext.py
+++ b/fsl/fsleyes/displaycontext/displaycontext.py
@@ -12,6 +12,8 @@ general display settings for displaying the overlays in a
 import sys
 import logging
 
+import numpy as np
+
 import props
 
 import display             as fsldisplay
@@ -163,7 +165,7 @@ class DisplayContext(props.SyncableHasProperties):
         props.SyncableHasProperties.__init__(
             self,
             parent=parent,
-            nounbind=['overlayGroups', 'displaySpace'],
+            nounbind=['overlayGroups', 'displaySpace', 'bounds'],
             nobind=[  'syncOverlayDisplay'])
 
         self.__overlayList = overlayList
@@ -190,10 +192,30 @@ class DisplayContext(props.SyncableHasProperties):
         # reset the location.
         if parent is None: self.__prevOverlayListLen = 0
         else:              self.__prevOverlayListLen = len(overlayList)
-            
+
+        # This dict contains the Display objects for
+        # every overlay in the overlay list, as
+        # {Overlay : Display} mappings
+        self.__displays = {}
+
+        # We keep a cache of 'standard' coordinates,
+        # one for each overlay - see the cacheStandardCoordinates
+        # method.  We're storing this cache in a tricky
+        # way though - as an attribute of the location
+        # property. We do this so that the cache will be
+        # automatically synchronised between parent/child
+        # DC objects.
+        locPropVal = self.getPropVal('location')
+
+        # Only set the standardCoords cache if
+        # it has not already been set (if this
+        # is a child DC, the cache will have
+        # already been set on the parent)
+        try:             locPropVal.getAttribute('standardCoords')
+        except KeyError: locPropVal.setAttribute('standardCoords', {})
+
         # The overlayListChanged and displaySpaceChanged
         # methods do important things - check them out
-        self.__displays = {}
         self.__overlayListChanged()
         self.__displaySpaceChanged()
         
@@ -354,6 +376,39 @@ class DisplayContext(props.SyncableHasProperties):
         return [self.__overlayList[idx] for idx in self.overlayOrder]
 
 
+    def cacheStandardCoordinates(self, overlay, coords):
+        """Stores the given 'standard' coordinates for the given overlay.
+
+        This method must be called by :class:`.DisplayOpts` sub-classes
+        whenever the spatial representation of their overlay changes. This is
+        necessary in order for the ``DisplayContext`` to update the
+        :attr:`location` with respect to the currently selected overlay - if
+        the current overlay has shifted in the display coordinate system, we
+        want the :attr:`location` to shift with it.
+
+        :arg overlay: The overlay object (e.g. an :class:`.Image`).
+
+        :arg coords:  Coordinates in the standard coordinate system of the
+                      overlay.
+        """
+
+        if self.getParent() is not None and self.isSyncedToParent('location'):
+            return
+
+        locPropVal     = self.getPropVal('location')
+        standardCoords = dict(locPropVal.getAttribute('standardCoords'))
+        
+        standardCoords[overlay] = np.array(coords).tolist()
+        
+        locPropVal.setAttribute('standardCoords', standardCoords)
+
+        log.debug('Standard coordinates cached '
+                  'for overlay {}: {} <-> {}'.format(
+                      overlay.name,
+                      self.location.xyz,
+                      standardCoords))
+
+
     def __overlayListChanged(self, *a):
         """Called when the :attr:`.OverlayList.overlays` property
         changes.
@@ -514,12 +569,51 @@ class DisplayContext(props.SyncableHasProperties):
         in the :class:`.OverlayList`.
         """
 
+        # If this DC is synced to a parent, let the
+        # parent do the update - our location property
+        # will be automatically synced to it. If we
+        # don't do this check, we will clobber the
+        # parent's updated location (or vice versa)
+        if self.getParent() is not None and self.isSyncedToParent('location'):
+            return
+
+        selectedOverlay = self.getSelectedOverlay()
+
+        if selectedOverlay is None:
+            return
+
+        selectedOpts = self.getOpts(selectedOverlay)
+
+        # The transform of the currently selected
+        # overlay might change, so we want the
+        # location to be preserved with respect to it.
+        stdLoc = selectedOpts.displayToStandardCoordinates(self.location.xyz)
+
         for overlay in self.__overlayList:
             
             if not isinstance(overlay, fslimage.Image):
                 continue
 
+            opts = self.getOpts(overlay)
+
+            # Disable the bounds listener, so the
+            # __overlayBoundsChanged method does
+            # not get called when the image
+            # transform changes.
+            opts.disableListener('bounds', self.__name)
             self.__setTransform(overlay)
+            opts.enableListener( 'bounds', self.__name)
+
+        # Update the display world bounds,
+        # and then update the location
+        self.disableNotification('location')
+        self.__updateBounds()
+        self.enableNotification('location')
+
+        # making sure that the location is kept
+        # in the same place, relative to the
+        # currently selected overlay
+        self.location.xyz = selectedOpts.standardToDisplayCoordinates(stdLoc)
 
             
     def __syncOverlayOrder(self):
@@ -594,58 +688,35 @@ class DisplayContext(props.SyncableHasProperties):
         overlay.
         """
 
-        # This check is ugly, and is required due to
-        # an ugly circular relationship which exists
-        # between parent/child DCs and the *Opts/
-        # location properties:
-        # 
-        # 1. When a property of a child DC DisplayOpts
-        #    object changes (e.g. ImageOpts.transform)
-        #    this should always be due to user input),
-        #    that change is propagated to the parent DC
-        #    DisplayOpts object.
-        #
-        # 2. This results in the DC._displayOptsChanged
-        #    method (this method) being called on the
-        #    parent DC.
-        #
-        # 3. Said method correctly updates the DC.location
-        #    property, so that the world location of the
-        #    selected overlay is preserved.
-        #
-        # 4. This location update is propagated back to
-        #    the child DC.location property, which is
-        #    updated to have the new correct location
-        #    value.
-        #
-        # 5. Then, the child DC._displayOpteChanged method
-        #    is called, which goes and updates the child
-        #    DC.location property to contain a bogus
-        #    value.
-        #
-        # So this test is in place to prevent this horrible
-        # circular loop behaviour from occurring. If the
-        # location properties are synced, we assume that
-        # they don't need to be updated again, and escape
-        # from ths system.
+        # See the note at top of __displaySpaceChanged
+        # method regarding this test
         if self.getParent() is not None and self.isSyncedToParent('location'):
             return
 
         overlay = opts.display.getOverlay()
 
-        # Save a copy of the location before
-        # updating the bounds, as the change
-        # to the bounds may result in the
-        # location being modified
-        oldDispLoc = self.location.xyz
-
+        # If the bounds of an overlay have changed, the
+        # overlay might have been moved in the dispaly
+        # coordinate system. We want to keep the
+        # current location in the same position, relative
+        # to that overlay. So we get the cached standard
+        # coords (which should have been updated by the
+        # overlay's DisplayOpts instance - see the docs
+        # for the cacheStandardCoordinates), and use them
+        # below to restore the location
+        locPropVal = self.getPropVal('location')
+        stdLoc     = locPropVal.getAttribute('standardCoords')[overlay]
+        
         # Update the display context bounds
         # to take into account any changes 
-        # to individual overlay bounds
+        # to individual overlay bounds.
+        # Inhibit notification on the location
+        # property - it will be updated properly
+        # below
         self.disableNotification('location')
         self.__updateBounds()
         self.enableNotification('location')
- 
+
         # The main purpose of this method is to preserve
         # the current display location in terms of the
         # currently selected overlay, when the overlay
@@ -658,7 +729,7 @@ class DisplayContext(props.SyncableHasProperties):
         # Now we want to update the display location
         # so that it is preserved with respect to the
         # currently selected overlay.
-        newDispLoc = opts.transformDisplayLocation(oldDispLoc)
+        newDispLoc = opts.standardToDisplayCoordinates(stdLoc)
 
         # Ignore the new display location
         # if it is not in the display bounds
@@ -668,7 +739,7 @@ class DisplayContext(props.SyncableHasProperties):
                           overlay,
                           type(opts).__name__,
                           name,
-                          oldDispLoc,
+                          stdLoc,
                           newDispLoc))
 
             self.location.xyz = newDispLoc
diff --git a/fsl/fsleyes/displaycontext/group.py b/fsl/fsleyes/displaycontext/group.py
index 392592f0c5c300caff475636ffb9944060caf380..a40339b969b2880473296e59695c241823e89f06 100644
--- a/fsl/fsleyes/displaycontext/group.py
+++ b/fsl/fsleyes/displaycontext/group.py
@@ -63,8 +63,7 @@ class OverlayGroup(props.HasProperties):
     
     _groupBindings = td.TypeDict({
         'Display'        : ['enabled'],
-        'ImageOpts'      : ['volume',
-                            'transform'],
+        'ImageOpts'      : ['volume'],
         'VolumeOpts'     : ['interpolation'],
         'LabelOpts'      : ['outline',
                             'outlineWidth'],
diff --git a/fsl/fsleyes/displaycontext/volumeopts.py b/fsl/fsleyes/displaycontext/volumeopts.py
index 8dbad52349ab14933cad5d33e9ff75d76d48904a..76f29a39fe82c2e00fc87481a204d0d8110b25f1 100644
--- a/fsl/fsleyes/displaycontext/volumeopts.py
+++ b/fsl/fsleyes/displaycontext/volumeopts.py
@@ -146,8 +146,14 @@ class ImageOpts(fsldisplay.DisplayOpts):
 
         overlay = self.overlay
 
-        self.addListener('transform',   self.name, self.__transformChanged)
-        self.addListener('customXform', self.name, self.__customXformChanged)
+        self.addListener('transform',
+                         self.name,
+                         self.__transformChanged,
+                         immediate=True)
+        self.addListener('customXform',
+                         self.name,
+                         self.__customXformChanged,
+                         immediate=True)
 
         # The display<->* transformation matrices
         # are created in the _setupTransforms method
@@ -179,6 +185,17 @@ class ImageOpts(fsldisplay.DisplayOpts):
         :attr:`.DisplayOpts.bounds` property accordingly.
         """
 
+        oldValue = self.getLastValue('transform')
+
+        if oldValue is None:
+            oldValue = self.transform
+
+        self.displayCtx.cacheStandardCoordinates(
+            self.overlay,
+            self.transformCoords(self.displayCtx.location.xyz,
+                                 oldValue,
+                                 'world'))
+
         lo, hi = transform.axisBounds(
             self.overlay.shape[:3],
             self.getTransform('voxel', 'display'))
@@ -193,10 +210,23 @@ class ImageOpts(fsldisplay.DisplayOpts):
         :meth:`__transformChanged`).
         """
 
+        stdLoc = self.displayToStandardCoordinates(
+            self.displayCtx.location.xyz)
+        
         self.__setupTransforms()
-        self.__transformChanged()
+        if self.transform == 'custom':
+            self.__transformChanged()
+
+            # if transform == custom, the cached value
+            # calculated in __transformChanged will be
+            # wrong, so we have to overwrite it here.
+            # The stdLoc value calculated above is valid,
+            # because it was calculated before the
+            # transformation matrices were recalculated
+            # in __setupTransforms
+            self.displayCtx.cacheStandardCoordinates(self.overlay, stdLoc)
+ 
         
-                            
     def __setupTransforms(self):
         """Calculates transformation matrices between all of the possible
         spaces in which the overlay may be displayed.
@@ -347,8 +377,6 @@ class ImageOpts(fsldisplay.DisplayOpts):
            - ``world``:   The image world coordinate system
            - ``custom``:  The coordinate system defined by the custom
                           transformation matrix (see :attr:`customXform`)
-
-        See also the :meth:`transformToVoxels` method.
         """
 
         xform     = self.getTransform(       from_, to_)
@@ -360,36 +388,22 @@ class ImageOpts(fsldisplay.DisplayOpts):
         return coords + post
 
 
-    def transformDisplayLocation(self, oldLoc):
-        """Overrides :meth:`.DisplayOpts.transformDisplayLocation`.
-
-        If the :attr:`transform` property has changed, returns the given
-        location, assumed to be in the old display coordinate system,
-        transformed into the new display coordinate system.
-
-        .. note:: This method will probably break if the ``custom``
-                  transformation matrix changes between the time that
-                  the ``transform`` property changes, and the time that
-                  this method is called.
+    def displayToStandardCoordinates(self, coords):
+        """Overrides :meth:`.DisplayOpts.displayToStandardCoordinates`.
+        Transforms the given display system coordinates into the world
+        coordinates of the :class:`.Image` associated with this
+        ``ImageOpts`` instance.
         """
+        return self.transformCoords(coords, 'display', 'world')
 
-        lastVal = self.getLastValue('transform')
-
-        if lastVal is None:
-            lastVal = self.transform
-        
-        # Calculate the image world location using the
-        # old display<-> world transform, then transform
-        # it back to the new world->display transform. 
-        worldLoc = transform.transform(
-            [oldLoc],
-            self.getTransform(lastVal, 'world'))[0]
-        
-        newLoc  = transform.transform(
-            [worldLoc],
-            self.getTransform('world', 'display'))[0]
-        
-        return newLoc
+    
+    def standardToDisplayCoordinates(self, coords):
+        """Overrides :meth:`.DisplayOpts.standardToDisplayCoordinates`.
+        Transforms the given coordinates (assumed to be in the world
+        coordinate system of the ``Image`` associated with this ``ImageOpts``
+        instance) into the display coordinate system.
+        """ 
+        return self.transformCoords(coords, 'world', 'display')
 
 
 class VolumeOpts(ImageOpts):
@@ -592,10 +606,9 @@ class VolumeOpts(ImageOpts):
         # The parent.getChildren() method will
         # contain this VolumeOpts instance,
         # so the below loop toggles listeners
-        # for this instance, the parent instance,
-        # and all of the other children of the
-        # parent
-        peers  = [parent] + parent.getChildren()
+        # for this instance and all of the other
+        # children of the parent
+        peers  = parent.getChildren()
 
         for peer in peers: