From c686395ebed336047eb008641a5964a6ecfdf05d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 2 Aug 2012 22:46:08 -0400 Subject: [PATCH] ImageView fix: display correct coordinates in ROI plot for scaled, single-frame images Minor documentation updates --- documentation/source/conf.py | 1 + functions.py | 55 +++++++++++++++---------- graphicsItems/ROI.py | 80 +++++++++++++++++++++++++----------- imageview/ImageView.py | 10 +++-- 4 files changed, 97 insertions(+), 49 deletions(-) diff --git a/documentation/source/conf.py b/documentation/source/conf.py index 8145ba4a..2fd718e4 100644 --- a/documentation/source/conf.py +++ b/documentation/source/conf.py @@ -18,6 +18,7 @@ import sys, os # documentation root, use os.path.abspath to make it absolute, like shown here. path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(path, '..', '..', '..')) +print sys.path # -- General configuration ----------------------------------------------------- diff --git a/functions.py b/functions.py index 4fd629c8..7b108dbd 100644 --- a/functions.py +++ b/functions.py @@ -358,28 +358,36 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0) -def affineSlice(data, shape, origin, vectors, axes, **kargs): +def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs): """ Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data. - The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. + The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates (see the scipy documentation for more information about this). - For a graphical interface to this function, see :func:`ROI.getArrayRegion` + For a graphical interface to this function, see :func:`ROI.getArrayRegion ` + ============== ==================================================================================================== Arguments: + *data* (ndarray) the original dataset + *shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape)) + *origin* the location in the original dataset that will become the origin of the sliced data. + *vectors* list of unit vectors which point in the direction of the slice axes. Each vector must have the same + length as *axes*. If the vectors are not unit length, the result will be scaled relative to the + original data. If the vectors are not orthogonal, the result will be sheared relative to the + original data. + *axes* The axes in the original dataset which correspond to the slice *vectors* + *order* The order of spline interpolation. Default is 1 (linear). See scipy.ndimage.map_coordinates + for more information. + *returnCoords* If True, return a tuple (result, coords) where coords is the array of coordinates used to select + values from the original dataset. + *All extra keyword arguments are passed to scipy.ndimage.map_coordinates.* + -------------------------------------------------------------------------------------------------------------------- + ============== ==================================================================================================== - | *data* (ndarray): the original dataset - | *shape*: the shape of the slice to take (Note the return value may have more dimensions than len(shape)) - | *origin*: the location in the original dataset that will become the origin in the sliced data. - | *vectors*: list of unit vectors which point in the direction of the slice axes + Note the following must be true: - * each vector must have the same length as *axes* - * If the vectors are not unit length, the result will be scaled. - * If the vectors are not orthogonal, the result will be sheared. - - *axes*: the axes in the original dataset which correspond to the slice *vectors* - - All extra keyword arguments are passed to scipy.ndimage.map_coordinates + | len(shape) == len(vectors) + | len(origin) == len(axes) == len(vectors[i]) Example: start with a 4D fMRI data set, take a diagonal-planar slice out of the last 3 axes @@ -392,10 +400,6 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) - Note the following must be true: - - | len(shape) == len(vectors) - | len(origin) == len(axes) == len(vectors[0]) """ # sanity check @@ -437,7 +441,7 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): for inds in np.ndindex(*extraShape): ind = (Ellipsis,) + inds #print data[ind].shape, x.shape, output[ind].shape, output.shape - output[ind] = scipy.ndimage.map_coordinates(data[ind], x, **kargs) + output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs) tr = list(range(output.ndim)) trb = [] @@ -448,9 +452,18 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): tr2 = tuple(trb+tr) ## Untranspose array before returning - return output.transpose(tr2) - + output = output.transpose(tr2) + if returnCoords: + return (output, x) + else: + return output +def transformToArray(tr): + """ + Given a QTransform, return a 3x3 numpy array. + """ + return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]]) + def solve3DTransform(points1, points2): """ Find a 3D transformation matrix that maps points1 onto points2 diff --git a/graphicsItems/ROI.py b/graphicsItems/ROI.py index 7606160a..68fb65ed 100644 --- a/graphicsItems/ROI.py +++ b/graphicsItems/ROI.py @@ -832,35 +832,34 @@ class ROI(GraphicsObject): else: return bounds, tr - - def getArrayRegion(self, data, img, axes=(0,1)): - """Use the position of this ROI relative to an imageItem to pull a slice from an array.""" + def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): + """Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array. + This method uses :func:`affineSlice ` to generate + the slice from *data* and uses :func:`getAffineSliceParams ` to determine the parameters to + pass to :func:`affineSlice `. - shape = self.state['size'] + If *returnMappedCoords* is True, then the method returns a tuple (result, coords) + such that coords is the set of coordinates used to interpolate values from the original + data, mapped into the parent coordinate system of the image. This is useful, when slicing + data from images that have been transformed, for determining the location of each value + in the sliced data. - origin = self.mapToItem(img, QtCore.QPointF(0, 0)) - - ## vx and vy point in the directions of the slice axes, but must be scaled properly - vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin - vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin - - lvx = np.sqrt(vx.x()**2 + vx.y()**2) - lvy = np.sqrt(vy.x()**2 + vy.y()**2) - pxLen = img.width() / float(data.shape[axes[0]]) - sx = pxLen / lvx - sy = pxLen / lvy - - vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy)) - shape = self.state['size'] - shape = [abs(shape[0]/sx), abs(shape[1]/sy)] - - origin = (origin.x(), origin.y()) - - #print "shape", shape, "vectors", vectors, "origin", origin - - return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, order=1) + All extra keyword arguments are passed to :func:`affineSlice `. + """ + shape, vectors, origin = self.getAffineSliceParams(data, img, axes) + if not returnMappedCoords: + return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + else: + kwds['returnCoords'] = True + result, coords = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + tr = fn.transformToArray(img.transform())[:,:2].reshape((3, 2) + (1,)*(coords.ndim-1)) + coords = coords[np.newaxis, ...] + mapped = (tr*coords).sum(axis=0) + return result, mapped + + ### transpose data so x and y are the first 2 axes #trAx = range(0, data.ndim) #trAx.remove(axes[0]) @@ -959,6 +958,37 @@ class ROI(GraphicsObject): ### Untranspose array before returning #return arr5.transpose(tr2) + def getAffineSliceParams(self, data, img, axes=(0.1)): + """ + Returns the parameters needed to use :func:`affineSlice ` to + extract a subset of *data* using this ROI and *img* to specify the subset. + + See :func:`getArrayRegion ` for more information. + """ + + shape = self.state['size'] + + origin = self.mapToItem(img, QtCore.QPointF(0, 0)) + + ## vx and vy point in the directions of the slice axes, but must be scaled properly + vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin + vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin + + lvx = np.sqrt(vx.x()**2 + vx.y()**2) + lvy = np.sqrt(vy.x()**2 + vy.y()**2) + pxLen = img.width() / float(data.shape[axes[0]]) + #img.width is number of pixels or width of item? + #need pxWidth and pxHeight instead of pxLen ? + sx = pxLen / lvx + sy = pxLen / lvy + + vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy)) + shape = self.state['size'] + shape = [abs(shape[0]/sx), abs(shape[1]/sy)] + + origin = (origin.x(), origin.y()) + return shape, vectors, origin + def getGlobalTransform(self, relativeTo=None): """Return global transformation (rotation angle+translation) required to move from relative state to current state. If relative state isn't specified, diff --git a/imageview/ImageView.py b/imageview/ImageView.py index 41a43639..ce0a420f 100644 --- a/imageview/ImageView.py +++ b/imageview/ImageView.py @@ -38,7 +38,7 @@ from pyqtgraph.SignalProxy import SignalProxy class PlotROI(ROI): def __init__(self, size): - ROI.__init__(self, pos=[0,0], size=size, scaleSnap=True, translateSnap=True) + ROI.__init__(self, pos=[0,0], size=size) #, scaleSnap=True, translateSnap=True) self.addScaleHandle([1, 1], [0, 0]) self.addRotateHandle([0, 0], [0.5, 0.5]) @@ -531,14 +531,18 @@ class ImageView(QtGui.QWidget): axes = (1, 2) else: return - data = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes) + data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) if data is not None: while data.ndim > 1: data = data.mean(axis=1) if image.ndim == 3: self.roiCurve.setData(y=data, x=self.tVals) else: - self.roiCurve.setData(y=data, x=list(range(len(data)))) + while coords.ndim > 2: + coords = coords[:,:,0] + coords = coords - coords[:,0,np.newaxis] + xvals = (coords**2).sum(axis=0) ** 0.5 + self.roiCurve.setData(y=data, x=xvals) #self.ui.roiPlot.replot()