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
This commit is contained in:
Luke Campagnola 2012-10-06 16:56:53 -04:00
parent 27c90c5dd5
commit c2f0bebe09
5 changed files with 102 additions and 17 deletions

View File

@ -201,11 +201,11 @@ class SRTTransform3D(QtGui.QMatrix4x4):
def matrix(self, nd=3): def matrix(self, nd=3):
if nd == 3: if nd == 3:
return np.array(self.copyDataTo()) return np.array(self.copyDataTo()).reshape(4,4)
elif nd == 2: elif nd == 2:
m = np.array(self.copyDataTo()) m = np.array(self.copyDataTo()).reshape(4,4)
m[2] = m[3] m[2] = m[3]
m[:,2] = n[:,3] m[:,2] = m[:,3]
return m[:3,:3] return m[:3,:3]
else: else:
raise Exception("Argument 'nd' must be 2 or 3") raise Exception("Argument 'nd' must be 2 or 3")

View File

@ -461,9 +461,75 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
def transformToArray(tr): def transformToArray(tr):
""" """
Given a QTransform, return a 3x3 numpy array. Given a QTransform, return a 3x3 numpy array.
""" Given a QMatrix4x4, return a 4x4 numpy array.
return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]])
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): def solve3DTransform(points1, points2):
""" """
Find a 3D transformation matrix that maps points1 onto points2 Find a 3D transformation matrix that maps points1 onto points2

View File

@ -854,9 +854,18 @@ class ROI(GraphicsObject):
else: else:
kwds['returnCoords'] = True kwds['returnCoords'] = True
result, coords = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) 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)) #tr = fn.transformToArray(img.transform())[:2] ## remove perspective transform values
coords = coords[np.newaxis, ...]
mapped = (tr*coords).sum(axis=0) ### 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 return result, mapped

View File

@ -538,6 +538,7 @@ class ViewBox(GraphicsWidget):
if self.state['autoVisibleOnly'][0] is True: if self.state['autoVisibleOnly'][0] is True:
order = [1,0] order = [1,0]
args = {}
for ax in order: for ax in order:
if self.state['autoRange'][ax] is False: if self.state['autoRange'][ax] is False:
continue continue
@ -563,6 +564,7 @@ class ViewBox(GraphicsWidget):
targetRect[0][0] = childRect.left() targetRect[0][0] = childRect.left()
targetRect[0][1] = childRect.right() targetRect[0][1] = childRect.right()
args['xRange'] = targetRect[0]
else: else:
## Make corrections to Y range ## Make corrections to Y range
if self.state['autoPan'][1]: if self.state['autoPan'][1]:
@ -576,8 +578,11 @@ class ViewBox(GraphicsWidget):
targetRect[1][0] = childRect.top() targetRect[1][0] = childRect.top()
targetRect[1][1] = childRect.bottom() targetRect[1][1] = childRect.bottom()
args['yRange'] = targetRect[1]
self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False) 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): def setXLink(self, view):
"""Link this view's X axis to another view. (see LinkView)""" """Link this view's X axis to another view. (see LinkView)"""
@ -1213,7 +1218,8 @@ class ViewBox(GraphicsWidget):
@staticmethod @staticmethod
def forgetView(vid, name): 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) ## Called with ID and name of view (the view itself is no longer available)
for v in ViewBox.AllViews.iterkeys(): for v in ViewBox.AllViews.iterkeys():
if id(v) == vid: if id(v) == vid:

View File

@ -614,7 +614,8 @@ class Parameter(QtCore.QObject):
about to be made to the tree and only one change signal should be about to be made to the tree and only one change signal should be
emitted at the end. emitted at the end.
Example: Example::
with param.treeChangeBlocker(): with param.treeChangeBlocker():
param.addChild(...) param.addChild(...)
param.removeChild(...) param.removeChild(...)
@ -638,11 +639,14 @@ class Parameter(QtCore.QObject):
def treeStateChanged(self, param, changes): def treeStateChanged(self, param, changes):
""" """
Called when the state of any sub-parameter has changed. Called when the state of any sub-parameter has changed.
========== ================================================================
Arguments: Arguments:
param: the immediate child whose tree state has changed. param The immediate child whose tree state has changed.
note that the change may have originated from a grandchild. note that the change may have originated from a grandchild.
changes: list of tuples describing all changes that have been made changes List of tuples describing all changes that have been made
in this event: (param, changeDescr, data) in this event: (param, changeDescr, data)
========== ================================================================
This function can be extended to react to tree state changes. This function can be extended to react to tree state changes.
""" """
@ -668,4 +672,4 @@ class SignalBlocker:
self.exitFn() self.exitFn()