From 5a9a1eee8f1d47d52caca47dbe8d47aa8c334bbc Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Fri, 14 Aug 2015 12:01:21 +0100 Subject: [PATCH] Moved all that offset stuff into the VolumeOpts class, as it will be useful to more than just the location panel. --- fsl/fslview/controls/locationpanel.py | 171 ++++------------------- fsl/fslview/displaycontext/volumeopts.py | 101 +++++++++++++ 2 files changed, 125 insertions(+), 147 deletions(-) diff --git a/fsl/fslview/controls/locationpanel.py b/fsl/fslview/controls/locationpanel.py index 7a2c6860b..784eab565 100644 --- a/fsl/fslview/controls/locationpanel.py +++ b/fsl/fslview/controls/locationpanel.py @@ -64,17 +64,10 @@ class LocationPanel(fslpanel.FSLViewPanel): # of the coordinate system may be obtained. # # When the current overlay is either an Image instance, or has - # an associated reference image, these attributes are used to - # store references to the image, and to the matrices that allow - # transformations between the different coordinate systems. - self._refImage = None - self._voxToDisplayMat = None - self._displayToVoxMat = None - self._worldToDisplayMat = None - self._displayToWorldMat = None - self._voxToWorldMat = None - self._worldToVoxMat = None - + # an associated reference image, this attributes is used to + # store a reference to the image. + self._refImage = None + # When the currently selected overlay is 4D, # this attribute will refer to the # corresponding DisplayOpts instance, which @@ -297,23 +290,7 @@ class LocationPanel(fslpanel.FSLViewPanel): overlay, refImage)) self._refImage = refImage - - if refImage is not None: - opts = self._displayCtx.getOpts(refImage) - self._voxToDisplayMat = opts.getTransform('voxel', 'display') - self._displayToVoxMat = opts.getTransform('display', 'voxel') - self._worldToDisplayMat = opts.getTransform('world', 'display') - self._displayToWorldMat = opts.getTransform('display', 'world') - self._voxToWorldMat = opts.getTransform('voxel', 'world') - self._worldToVoxMat = opts.getTransform('world', 'voxel') - else: - self._voxToDisplayMat = None - self._displayToVoxMat = None - self._worldToDisplayMat = None - self._displayToWorldMat = None - self._voxToWorldMat = None - self._worldToVoxMat = None - + def _updateWidgets(self): @@ -348,10 +325,12 @@ class LocationPanel(fslpanel.FSLViewPanel): # Figure out the limits for the # voxel/world location widgets if self._refImage is not None: + opts = self._displayCtx.getOpts(self._refImage) + v2w = opts.getTransform('voxel', 'world') shape = self._refImage.shape[:3] vlo = [0, 0, 0] vhi = np.array(shape) - 1 - wlo, whi = transform.axisBounds(shape, self._voxToWorldMat) + wlo, whi = transform.axisBounds(shape, v2w) else: vlo = [0, 0, 0] vhi = [0, 0, 0] @@ -428,111 +407,18 @@ class LocationPanel(fslpanel.FSLViewPanel): self.Freeze() - - def _getOffsets(self, overlay, source, target, displaySpace): - """When an image is displayed in id/pixdim space, voxel coordinates - map to the voxel corner; i.e. a voxel at ``(0, 1, 2)`` occupies the - space ``(0 - 1, 1 - 2, 2 - 3)``. - - In contrast, when an image is displayed in affine space, voxel - coordinates map to the voxel centre, so our voxel from above will - occupy the space ``(-0.5 - 0.5, 0.5 - 1.5, 1.5 - 2.5)``. This is - dictated by the NIFTI specification. - - This function returns some offsets to ensure that the coordinate - transformation from the source space to the target space is valid, - given the above requirements. - - A tuple containing two sets of offsets (each of which is a tuple of - three values). The first set is to be applied to the source coordinates - before transformation, and the second set to the target coordinates - after the transformation. - """ - - pixdim = np.array(overlay.pixdim[:3]) - offsets = { - - # world to voxel transformation - # (regardless of the display space): - # - # add 0.5 to the resulting voxel - # coords, so the _propagate method - # can just floor them to get the - # integer voxel coordinates - ('world', 'voxel', displaySpace) : ((0, 0, 0), (0.5, 0.5, 0.5)), - - # World to display transformation: - # - # if displaying in id/pixdim space, - # we add half a voxel so that the - # resulting coords are centered - # within a voxel, instead of being - # in the voxel corner - ('world', 'display', 'id') : ((0, 0, 0), (0.5, 0.5, 0.5)), - ('world', 'display', 'pixdim') : ((0, 0, 0), pixdim / 2.0), - - # Display to voxel space: - - # If we're displaying in affine space, - # we have the same situation as the - # world -> voxel transform above - ('display', 'voxel', 'affine') : ((0, 0, 0), (0.5, 0.5, 0.5)), - - # Display to world space: - # - # If we're displaying in id/pixdim - # space, voxel coordinates map to - # the voxel corner, so we need to - # subtract half the voxel width to - # the coordinates before transforming - # to world space. - ('display', 'world', 'id') : ((-0.5, -0.5, -0.5), (0, 0, 0)), - ('display', 'world', 'pixdim') : (-pixdim / 2.0, (0, 0, 0)), - - # Voxel to display space: - # - # If the voxel location was changed, - # we want the display to be moved to - # the centre of the voxel If displaying - # in affine space, voxel coordinates - # map to the voxel centre, so we don't - # need to offset. But if in id/pixdim, - # we need to add 0.5 to the voxel coords, - # as otherwise the transformation will - # put us in the voxel corner. - ('voxel', 'display', 'id') : ((0.5, 0.5, 0.5), (0, 0, 0)), - ('voxel', 'display', 'pixdim') : ((0.5, 0.5, 0.5), (0, 0, 0)), - } - - return offsets.get((source, target, displaySpace), - ((0, 0, 0), (0, 0, 0))) - - def _propagate(self, source, target, xform): + def _propagate(self, source, target): - displaySpace = None - if self._refImage is not None: - opts = self._displayCtx.getOpts(self._refImage) - displaySpace = opts.transform - - pre, post = self._getOffsets(self._refImage, - source, - target, - displaySpace) - else: - pre = [0, 0, 0] - post = [0, 0, 0] - if source == 'display': coords = self._displayCtx.location.xyz elif source == 'voxel': coords = self.voxelLocation.xyz elif source == 'world': coords = self.worldLocation.xyz - c = [coords[0] + pre[0], coords[1] + pre[1], coords[2] + pre[2]] - - if xform is not None: xformed = transform.transform([c], xform)[0] - else: xformed = np.array(c) - - xformed += post + if self._refImage is not None: + opts = self._displayCtx.getOpts(self._refImage) + xformed = opts.transformCoords([coords], source, target)[0] + else: + xformed = coords log.debug('Updating location ({} {} -> {} {})'.format( source, coords, target, xformed)) @@ -561,8 +447,8 @@ class LocationPanel(fslpanel.FSLViewPanel): if len(self._overlayList) == 0: return self._prePropagate() - self._propagate('display', 'voxel', self._displayToVoxMat) - self._propagate('display', 'world', self._displayToWorldMat) + self._propagate('display', 'voxel') + self._propagate('display', 'world') self._postPropagate() self._updateLocationInfo() @@ -572,8 +458,8 @@ class LocationPanel(fslpanel.FSLViewPanel): if len(self._overlayList) == 0: return self._prePropagate() - self._propagate('world', 'voxel', self._worldToVoxMat) - self._propagate('world', 'display', self._worldToDisplayMat) + self._propagate('world', 'voxel') + self._propagate('world', 'display') self._postPropagate() self._updateLocationInfo() @@ -583,8 +469,8 @@ class LocationPanel(fslpanel.FSLViewPanel): if len(self._overlayList) == 0: return self._prePropagate() - self._propagate('voxel', 'world', self._voxToWorldMat) - self._propagate('voxel', 'display', self._voxToDisplayMat) + self._propagate('voxel', 'world') + self._propagate('voxel', 'display') self._postPropagate() self._updateLocationInfo() @@ -616,19 +502,10 @@ class LocationPanel(fslpanel.FSLViewPanel): info = '{}'.format(strings.labels[self, 'noData']) else: opts = self._displayCtx.getOpts(overlay) - vloc = transform.transform( - [self._displayCtx.location.xyz], - opts.getTransform('display', 'voxel'))[0] - - # When displaying in world/affine space, - # the above transformation gives us - # values between [x - 0.5, x + 0.5] for - # voxel x, so we need to floor(x + 0.5) - # to get the actual voxel coordinates - if opts.transform == 'affine': vloc = np.floor(vloc + 0.5) - else: vloc = np.floor(vloc) - - vloc = tuple(map(int, vloc)) + vloc = opts.transformCoords( + [self._displayCtx.location.xyz], 'display', 'voxel')[0] + + vloc = tuple(map(int, np.floor(vloc))) if overlay.is4DImage(): vloc = vloc + (opts.volume,) diff --git a/fsl/fslview/displaycontext/volumeopts.py b/fsl/fslview/displaycontext/volumeopts.py index 9495186b0..dc6c970da 100644 --- a/fsl/fslview/displaycontext/volumeopts.py +++ b/fsl/fslview/displaycontext/volumeopts.py @@ -191,6 +191,107 @@ class ImageOpts(fsldisplay.DisplayOpts): return self.__xforms[from_, to] + def getTransformOffsets(self, from_, to_): + """When an image is displayed in id/pixdim space, voxel coordinates + map to the voxel corner; i.e. a voxel at ``(0, 1, 2)`` occupies the + space ``(0 - 1, 1 - 2, 2 - 3)``. + + In contrast, when an image is displayed in affine space, voxel + coordinates map to the voxel centre, so our voxel from above will + occupy the space ``(-0.5 - 0.5, 0.5 - 1.5, 1.5 - 2.5)``. This is + dictated by the NIFTI specification. + + This function returns some offsets to ensure that the coordinate + transformation from the source space to the target space is valid, + given the above requirements. + + A tuple containing two sets of offsets (each of which is a tuple of + three values). The first set is to be applied to the source coordinates + before transformation, and the second set to the target coordinates + after the transformation. + + See also the :meth:`transformCoords` method, which will perform the + transformation correctly for you, without you having to worry about + these offsets. + """ + displaySpace = self.transform + pixdim = np.array(self.overlay.pixdim[:3]) + offsets = { + + # world to voxel transformation + # (regardless of the display space): + # + # add 0.5 to the resulting voxel + # coords, so the _propagate method + # can just floor them to get the + # integer voxel coordinates + ('world', 'voxel', displaySpace) : ((0, 0, 0), (0.5, 0.5, 0.5)), + + # World to display transformation: + # + # if displaying in id/pixdim space, + # we add half a voxel so that the + # resulting coords are centered + # within a voxel, instead of being + # in the voxel corner + ('world', 'display', 'id') : ((0, 0, 0), (0.5, 0.5, 0.5)), + ('world', 'display', 'pixdim') : ((0, 0, 0), pixdim / 2.0), + + # Display to voxel space: + + # If we're displaying in affine space, + # we have the same situation as the + # world -> voxel transform above + ('display', 'voxel', 'affine') : ((0, 0, 0), (0.5, 0.5, 0.5)), + + # Display to world space: + # + # If we're displaying in id/pixdim + # space, voxel coordinates map to + # the voxel corner, so we need to + # subtract half the voxel width to + # the coordinates before transforming + # to world space. + ('display', 'world', 'id') : ((-0.5, -0.5, -0.5), (0, 0, 0)), + ('display', 'world', 'pixdim') : (-pixdim / 2.0, (0, 0, 0)), + + # Voxel to display space: + # + # If the voxel location was changed, + # we want the display to be moved to + # the centre of the voxel If displaying + # in affine space, voxel coordinates + # map to the voxel centre, so we don't + # need to offset. But if in id/pixdim, + # we need to add 0.5 to the voxel coords, + # as otherwise the transformation will + # put us in the voxel corner. + ('voxel', 'display', 'id') : ((0.5, 0.5, 0.5), (0, 0, 0)), + ('voxel', 'display', 'pixdim') : ((0.5, 0.5, 0.5), (0, 0, 0)), + } + + return offsets.get((from_, to_, displaySpace), ((0, 0, 0), (0, 0, 0))) + + + def transformCoords(self, coords, from_, to_): + """Transforms the given coordinates from ``from_`` to ``to_``, including + correcting for display space offsets (see :meth:`getTransformOffsets`). + + The ``from_`` and ``to_`` parameters must both be one of: + - ``display`` + - ``voxel`` + - ``world`` + """ + + xform = self.getTransform( from_, to_) + pre, post = self.getTransformOffsets(from_, to_) + + coords = np.array(coords) + pre + coords = transform.transform(coords, xform) + + return coords + post + + def transformDisplayLocation(self, oldLoc): lastVal = self.getLastValue('transform') -- GitLab