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):
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")

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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()