From c2f0bebe09c7e6e291e106f4c20dc52ccf97527c Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 6 Oct 2012 16:56:53 -0400 Subject: [PATCH] Added functions.transformCoordinates() for mapping numpy arrays of coordinates from QTransform and QMatrix4x4 Minor updates: - fixed SRTTransform3D.matrix() - ViewBox fix: updateAutoRange leaves unused axes completely unchanged - documentation updates --- SRTTransform3D.py | 6 +-- functions.py | 70 +++++++++++++++++++++++++++++++- graphicsItems/ROI.py | 15 +++++-- graphicsItems/ViewBox/ViewBox.py | 12 ++++-- parametertree/Parameter.py | 16 +++++--- 5 files changed, 102 insertions(+), 17 deletions(-) diff --git a/SRTTransform3D.py b/SRTTransform3D.py index 22ec7a3c..94c3df77 100644 --- a/SRTTransform3D.py +++ b/SRTTransform3D.py @@ -201,11 +201,11 @@ class SRTTransform3D(QtGui.QMatrix4x4): def matrix(self, nd=3): if nd == 3: - return np.array(self.copyDataTo()) + return np.array(self.copyDataTo()).reshape(4,4) elif nd == 2: - m = np.array(self.copyDataTo()) + m = np.array(self.copyDataTo()).reshape(4,4) m[2] = m[3] - m[:,2] = n[:,3] + m[:,2] = m[:,3] return m[:3,:3] else: raise Exception("Argument 'nd' must be 2 or 3") diff --git a/functions.py b/functions.py index 85564e9f..17cb64f3 100644 --- a/functions.py +++ b/functions.py @@ -461,9 +461,75 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, 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()]]) + Given a QMatrix4x4, return a 4x4 numpy array. + + Example: map an array of x,y coordinates through a transform:: + + ## coordinates to map are (1,5), (2,6), (3,7), and (4,8) + coords = np.array([[1,2,3,4], [5,6,7,8], [1,1,1,1]]) # the extra '1' coordinate is needed for translation to work + ## Make an example transform + tr = QtGui.QTransform() + tr.translate(3,4) + tr.scale(2, 0.1) + + ## convert to array + m = pg.transformToArray()[:2] # ignore the perspective portion of the transformation + + ## map coordinates through transform + mapped = np.dot(m, coords) + """ + #return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]]) + ## The order of elements given by the method names m11..m33 is misleading-- + ## It is most common for x,y translation to occupy the positions 1,3 and 2,3 in + ## a transformation matrix. However, with QTransform these values appear at m31 and m32. + ## So the correct interpretation is transposed: + if isinstance(tr, QtGui.QTransform): + return np.array([[tr.m11(), tr.m21(), tr.m31()], [tr.m12(), tr.m22(), tr.m32()], [tr.m13(), tr.m23(), tr.m33()]]) + elif isinstance(tr, QtGui.QMatrix4x4): + return np.array(tr.copyDataTo()).reshape(4,4) + else: + raise Exception("Transform argument must be either QTransform or QMatrix4x4.") + +def transformCoordinates(tr, coords): + """ + Map a set of 2D or 3D coordinates through a QTransform or QMatrix4x4. + The shape of coords must be (2,...) or (3,...) + The mapping will _ignore_ any perspective transformations. + """ + nd = coords.shape[0] + m = transformToArray(tr) + m = m[:m.shape[0]-1] # remove perspective + + ## If coords are 3D and tr is 2D, assume no change for Z axis + if m.shape == (2,3) and nd == 3: + m2 = np.zeros((3,4)) + m2[:2, :2] = m[:2,:2] + m2[:2, 3] = m[:2,2] + m2[2,2] = 1 + m = m2 + + ## if coords are 2D and tr is 3D, ignore Z axis + if m.shape == (3,4) and nd == 2: + m2 = np.empty((2,3)) + m2[:,:2] = m[:2,:2] + m2[:,2] = m[:2,3] + m = m2 + + ## reshape tr and coords to prepare for multiplication + m = m.reshape(m.shape + (1,)*(coords.ndim-1)) + coords = coords[np.newaxis, ...] + + # separate scale/rotate and translation + translate = m[:,-1] + m = m[:, :-1] + + ## map coordinates and return + mapped = (m*coords).sum(axis=0) ## apply scale/rotate + mapped += translate + return mapped + + 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 10765b76..78ac1ad1 100644 --- a/graphicsItems/ROI.py +++ b/graphicsItems/ROI.py @@ -854,9 +854,18 @@ class ROI(GraphicsObject): 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) + #tr = fn.transformToArray(img.transform())[:2] ## remove perspective transform values + + ### separate translation from scale/rotate + #translate = tr[:,2] + #tr = tr[:,:2] + #tr = tr.reshape((2,2) + (1,)*(coords.ndim-1)) + #coords = coords[np.newaxis, ...] + + ### map coordinates and return + #mapped = (tr*coords).sum(axis=0) ## apply scale/rotate + #mapped += translate.reshape((2,1,1)) + mapped = fn.transformCoordinates(img.transform(), coords) return result, mapped diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 6947a71c..2ec14a2c 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -538,6 +538,7 @@ class ViewBox(GraphicsWidget): if self.state['autoVisibleOnly'][0] is True: order = [1,0] + args = {} for ax in order: if self.state['autoRange'][ax] is False: continue @@ -563,6 +564,7 @@ class ViewBox(GraphicsWidget): targetRect[0][0] = childRect.left() targetRect[0][1] = childRect.right() + args['xRange'] = targetRect[0] else: ## Make corrections to Y range if self.state['autoPan'][1]: @@ -576,8 +578,11 @@ class ViewBox(GraphicsWidget): targetRect[1][0] = childRect.top() targetRect[1][1] = childRect.bottom() - - self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False) + args['yRange'] = targetRect[1] + args['padding'] = 0 + args['disableAutoRange'] = False + #self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False) + self.setRange(**args) def setXLink(self, view): """Link this view's X axis to another view. (see LinkView)""" @@ -1213,7 +1218,8 @@ class ViewBox(GraphicsWidget): @staticmethod def forgetView(vid, name): - + if ViewBox is None: ## can happen as python is shutting down + return ## Called with ID and name of view (the view itself is no longer available) for v in ViewBox.AllViews.iterkeys(): if id(v) == vid: diff --git a/parametertree/Parameter.py b/parametertree/Parameter.py index 2437c2d4..e32f0b4a 100644 --- a/parametertree/Parameter.py +++ b/parametertree/Parameter.py @@ -614,7 +614,8 @@ class Parameter(QtCore.QObject): about to be made to the tree and only one change signal should be emitted at the end. - Example: + Example:: + with param.treeChangeBlocker(): param.addChild(...) param.removeChild(...) @@ -638,11 +639,14 @@ class Parameter(QtCore.QObject): def treeStateChanged(self, param, changes): """ Called when the state of any sub-parameter has changed. + + ========== ================================================================ Arguments: - param: the immediate child whose tree state has changed. - note that the change may have originated from a grandchild. - changes: list of tuples describing all changes that have been made - in this event: (param, changeDescr, data) + param The immediate child whose tree state has changed. + note that the change may have originated from a grandchild. + changes List of tuples describing all changes that have been made + in this event: (param, changeDescr, data) + ========== ================================================================ This function can be extended to react to tree state changes. """ @@ -668,4 +672,4 @@ class SignalBlocker: self.exitFn() - \ No newline at end of file +