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:
parent
27c90c5dd5
commit
c2f0bebe09
@ -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")
|
||||
|
70
functions.py
70
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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user