diff --git a/Transform.py b/SRTTransform.py similarity index 93% rename from Transform.py rename to SRTTransform.py index cdc9ceae..d55d72cf 100644 --- a/Transform.py +++ b/SRTTransform.py @@ -4,7 +4,7 @@ from .Point import Point import numpy as np import pyqtgraph as pg -class Transform(QtGui.QTransform): +class SRTTransform(QtGui.QTransform): """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate This transform has no shear; angles are always preserved. """ @@ -16,7 +16,7 @@ class Transform(QtGui.QTransform): return elif isinstance(init, dict): self.restoreState(init) - elif isinstance(init, Transform): + elif isinstance(init, SRTTransform): self._state = { 'pos': Point(init._state['pos']), 'scale': Point(init._state['scale']), @@ -28,7 +28,7 @@ class Transform(QtGui.QTransform): elif isinstance(init, QtGui.QMatrix4x4): self.setFromMatrix4x4(init) else: - raise Exception("Cannot create Transform from input type: %s" % str(type(init))) + raise Exception("Cannot create SRTTransform from input type: %s" % str(type(init))) def getScale(self): @@ -73,9 +73,10 @@ class Transform(QtGui.QTransform): self.update() def setFromMatrix4x4(self, m): - m = pg.Transform3D(m) + m = pg.SRTTransform3D(m) angle, axis = m.getRotation() if angle != 0 and (axis[0] != 0 or axis[1] != 0 or axis[2] != 1): + print angle, axis raise Exception("Can only convert 4x4 matrix to 3x3 if rotation is around Z-axis.") self._state = { 'pos': Point(m.getTranslation()), @@ -128,10 +129,10 @@ class Transform(QtGui.QTransform): def __div__(self, t): """A / B == B^-1 * A""" dt = t.inverted()[0] * self - return Transform(dt) + return SRTTransform(dt) def __mul__(self, t): - return Transform(QtGui.QTransform.__mul__(self, t)) + return SRTTransform(QtGui.QTransform.__mul__(self, t)) def saveState(self): p = self._state['pos'] @@ -203,12 +204,12 @@ if __name__ == '__main__': s.addItem(l1) s.addItem(l2) - tr1 = Transform() - tr2 = Transform() + tr1 = SRTTransform() + tr2 = SRTTransform() tr3 = QtGui.QTransform() tr3.translate(20, 0) tr3.rotate(45) - print("QTransform -> Transform:", Transform(tr3)) + print("QTransform -> Transform:", SRTTransform(tr3)) print("tr1:", tr1) @@ -221,7 +222,7 @@ if __name__ == '__main__': print("tr2 * tr1 = ", tr2*tr1) - tr4 = Transform() + tr4 = SRTTransform() tr4.scale(-1, 1) tr4.rotate(30) print("tr1 * tr4 = ", tr1*tr4) diff --git a/SRTTransform3D.py b/SRTTransform3D.py new file mode 100644 index 00000000..8b093008 --- /dev/null +++ b/SRTTransform3D.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +from Qt import QtCore, QtGui +from Vector import Vector +from SRTTransform import SRTTransform +import pyqtgraph as pg +import numpy as np +import scipy.linalg + +class SRTTransform3D(QtGui.QMatrix4x4): + """4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate + This transform has no shear; angles are always preserved. + """ + def __init__(self, init=None): + QtGui.QMatrix4x4.__init__(self) + self.reset() + if init is None: + return + if init.__class__ is QtGui.QTransform: + init = SRTTransform(init) + + if isinstance(init, dict): + self.restoreState(init) + elif isinstance(init, SRTTransform3D): + self._state = { + 'pos': Vector(init._state['pos']), + 'scale': Vector(init._state['scale']), + 'angle': init._state['angle'], + 'axis': Vector(init._state['axis']), + } + self.update() + elif isinstance(init, SRTTransform): + self._state = { + 'pos': Vector(init._state['pos']), + 'scale': Vector(init._state['scale']), + 'angle': init._state['angle'], + 'axis': Vector(0, 0, 1), + } + self.update() + elif isinstance(init, QtGui.QMatrix4x4): + self.setFromMatrix(init) + else: + raise Exception("Cannot build SRTTransform3D from argument type:", type(init)) + + + def getScale(self): + return pg.Vector(self._state['scale']) + + def getRotation(self): + """Return (angle, axis) of rotation""" + return self._state['angle'], pg.Vector(self._state['axis']) + + def getTranslation(self): + return pg.Vector(self._state['pos']) + + def reset(self): + self._state = { + 'pos': Vector(0,0,0), + 'scale': Vector(1,1,1), + 'angle': 0.0, ## in degrees + 'axis': (0, 0, 1) + } + self.update() + + def translate(self, *args): + """Adjust the translation of this transform""" + t = Vector(*args) + self.setTranslate(self._state['pos']+t) + + def setTranslate(self, *args): + """Set the translation of this transform""" + self._state['pos'] = Vector(*args) + self.update() + + def scale(self, *args): + """adjust the scale of this transform""" + ## try to prevent accidentally setting 0 scale on z axis + if len(args) == 1 and hasattr(args[0], '__len__'): + args = args[0] + if len(args) == 2: + args = args + (1,) + + s = Vector(*args) + self.setScale(self._state['scale'] * s) + + def setScale(self, *args): + """Set the scale of this transform""" + if len(args) == 1 and hasattr(args[0], '__len__'): + args = args[0] + if len(args) == 2: + args = args + (1,) + self._state['scale'] = Vector(*args) + self.update() + + def rotate(self, angle, axis=(0,0,1)): + """Adjust the rotation of this transform""" + origAxis = self._state['axis'] + if axis[0] == origAxis[0] and axis[1] == origAxis[1] and axis[2] == origAxis[2]: + self.setRotate(self._state['angle'] + angle) + else: + m = QtGui.QMatrix4x4() + m.translate(*self._state['pos']) + m.rotate(self._state['angle'], *self._state['axis']) + m.rotate(angle, *axis) + m.scale(*self._state['scale']) + self.setFromMatrix(m) + + def setRotate(self, angle, axis=(0,0,1)): + """Set the transformation rotation to angle (in degrees)""" + + self._state['angle'] = angle + self._state['axis'] = Vector(axis) + self.update() + + def setFromMatrix(self, m): + """ + Set this transform mased on the elements of *m* + The input matrix must be affine AND have no shear, + otherwise the conversion will most likely fail. + """ + for i in range(4): + self.setRow(i, m.row(i)) + m = self.matrix().reshape(4,4) + ## translation is 4th column + self._state['pos'] = m[:3,3] + + ## scale is vector-length of first three columns + scale = (m[:3,:3]**2).sum(axis=0)**0.5 + ## see whether there is an inversion + z = np.cross(m[0, :3], m[1, :3]) + if np.dot(z, m[2, :3]) < 0: + scale[1] *= -1 ## doesn't really matter which axis we invert + self._state['scale'] = scale + + ## rotation axis is the eigenvector with eigenvalue=1 + r = m[:3, :3] / scale[:, np.newaxis] + try: + evals, evecs = scipy.linalg.eig(r) + except: + print "Rotation matrix:", r + print "Scale:", scale + print "Original matrix:", m + raise + eigIndex = np.argwhere(np.abs(evals-1) < 1e-7) + if len(eigIndex) < 1: + print "eigenvalues:", evals + print "eigenvectors:", evecs + print "index:", eigIndex, evals-1 + raise Exception("Could not determine rotation axis.") + axis = evecs[eigIndex[0,0]].real + axis /= ((axis**2).sum())**0.5 + self._state['axis'] = axis + + ## trace(r) == 2 cos(angle) + 1, so: + self._state['angle'] = np.arccos((r.trace()-1)*0.5) * 180 / np.pi + if self._state['angle'] == 0: + self._state['axis'] = (0,0,1) + + def as2D(self): + """Return a QTransform representing the x,y portion of this transform (if possible)""" + return pg.SRTTransform(self) + + #def __div__(self, t): + #"""A / B == B^-1 * A""" + #dt = t.inverted()[0] * self + #return SRTTransform(dt) + + #def __mul__(self, t): + #return SRTTransform(QtGui.QTransform.__mul__(self, t)) + + def saveState(self): + p = self._state['pos'] + s = self._state['scale'] + ax = self._state['axis'] + #if s[0] == 0: + #raise Exception('Invalid scale: %s' % str(s)) + return { + 'pos': (p[0], p[1], p[2]), + 'scale': (s[0], s[1], s[2]), + 'angle': self._state['angle'], + 'axis': (ax[0], ax[1], ax[2]) + } + + def restoreState(self, state): + self._state['pos'] = Vector(state.get('pos', (0.,0.,0.))) + scale = state.get('scale', (1.,1.,1.)) + scale = scale + (1.,) * (3-len(scale)) + self._state['scale'] = Vector(scale) + self._state['angle'] = state.get('angle', 0.) + self._state['axis'] = state.get('axis', (0, 0, 1)) + self.update() + + def update(self): + QtGui.QMatrix4x4.setToIdentity(self) + ## modifications to the transform are multiplied on the right, so we need to reverse order here. + QtGui.QMatrix4x4.translate(self, *self._state['pos']) + QtGui.QMatrix4x4.rotate(self, self._state['angle'], *self._state['axis']) + QtGui.QMatrix4x4.scale(self, *self._state['scale']) + + def __repr__(self): + return str(self.saveState()) + + def matrix(self, nd=3): + if nd == 3: + return np.array(self.copyDataTo()) + elif nd == 2: + m = np.array(self.copyDataTo()) + m[2] = m[3] + m[:,2] = n[:,3] + return m[:3,:3] + else: + raise Exception("Argument 'nd' must be 2 or 3") + +if __name__ == '__main__': + import widgets + import GraphicsView + from functions import * + app = QtGui.QApplication([]) + win = QtGui.QMainWindow() + win.show() + cw = GraphicsView.GraphicsView() + #cw.enableMouse() + win.setCentralWidget(cw) + s = QtGui.QGraphicsScene() + cw.setScene(s) + win.resize(600,600) + cw.enableMouse() + cw.setRange(QtCore.QRectF(-100., -100., 200., 200.)) + + class Item(QtGui.QGraphicsItem): + def __init__(self): + QtGui.QGraphicsItem.__init__(self) + self.b = QtGui.QGraphicsRectItem(20, 20, 20, 20, self) + self.b.setPen(QtGui.QPen(mkPen('y'))) + self.t1 = QtGui.QGraphicsTextItem(self) + self.t1.setHtml('R') + self.t1.translate(20, 20) + self.l1 = QtGui.QGraphicsLineItem(10, 0, -10, 0, self) + self.l2 = QtGui.QGraphicsLineItem(0, 10, 0, -10, self) + self.l1.setPen(QtGui.QPen(mkPen('y'))) + self.l2.setPen(QtGui.QPen(mkPen('y'))) + def boundingRect(self): + return QtCore.QRectF() + def paint(self, *args): + pass + + #s.addItem(b) + #s.addItem(t1) + item = Item() + s.addItem(item) + l1 = QtGui.QGraphicsLineItem(10, 0, -10, 0) + l2 = QtGui.QGraphicsLineItem(0, 10, 0, -10) + l1.setPen(QtGui.QPen(mkPen('r'))) + l2.setPen(QtGui.QPen(mkPen('r'))) + s.addItem(l1) + s.addItem(l2) + + tr1 = SRTTransform() + tr2 = SRTTransform() + tr3 = QtGui.QTransform() + tr3.translate(20, 0) + tr3.rotate(45) + print "QTransform -> Transform:", SRTTransform(tr3) + + print "tr1:", tr1 + + tr2.translate(20, 0) + tr2.rotate(45) + print "tr2:", tr2 + + dt = tr2/tr1 + print "tr2 / tr1 = ", dt + + print "tr2 * tr1 = ", tr2*tr1 + + tr4 = SRTTransform() + tr4.scale(-1, 1) + tr4.rotate(30) + print "tr1 * tr4 = ", tr1*tr4 + + w1 = widgets.TestROI((19,19), (22, 22), invertible=True) + #w2 = widgets.TestROI((0,0), (150, 150)) + w1.setZValue(10) + s.addItem(w1) + #s.addItem(w2) + w1Base = w1.getState() + #w2Base = w2.getState() + def update(): + tr1 = w1.getGlobalTransform(w1Base) + #tr2 = w2.getGlobalTransform(w2Base) + item.setTransform(tr1) + + #def update2(): + #tr1 = w1.getGlobalTransform(w1Base) + #tr2 = w2.getGlobalTransform(w2Base) + #t1.setTransform(tr1) + #w1.setState(w1Base) + #w1.applyGlobalTransform(tr2) + + w1.sigRegionChanged.connect(update) + #w2.sigRegionChanged.connect(update2) + + \ No newline at end of file diff --git a/__init__.py b/__init__.py index 3dd80d75..e968d355 100644 --- a/__init__.py +++ b/__init__.py @@ -19,12 +19,12 @@ if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] from . import python2_3 -## in general openGL is poorly supported in Qt. +## in general openGL is poorly supported with Qt+GraphicsView. ## we only enable it where the performance benefit is critical. ## Note this only applies to 2D graphics; 3D graphics always use OpenGL. if 'linux' in sys.platform: ## linux has numerous bugs in opengl implementation useOpenGL = False -elif 'darwin' in sys.platform: ## openGL greatly speeds up display on mac +elif 'darwin' in sys.platform: ## openGL can have a major impact on mac, but also has serious bugs useOpenGL = True else: useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff. @@ -111,8 +111,8 @@ from .imageview import * from .WidgetGroup import * from .Point import Point from .Vector import Vector -from .Transform import Transform -from .Transform3D import Transform3D +from .SRTTransform import SRTTransform +from .SRTTransform3D import SRTTransform3D from .functions import * from .graphicsWindows import * from .SignalProxy import * diff --git a/canvas/CanvasItem.py b/canvas/CanvasItem.py index 1c89e87a..6473896b 100644 --- a/canvas/CanvasItem.py +++ b/canvas/CanvasItem.py @@ -92,7 +92,7 @@ class CanvasItem(QtCore.QObject): if 'transform' in self.opts: self.baseTransform = self.opts['transform'] else: - self.baseTransform = pg.Transform() + self.baseTransform = pg.SRTTransform() if 'pos' in self.opts and self.opts['pos'] is not None: self.baseTransform.translate(self.opts['pos']) if 'angle' in self.opts and self.opts['angle'] is not None: @@ -120,8 +120,8 @@ class CanvasItem(QtCore.QObject): self.itemScale = QtGui.QGraphicsScale() self._graphicsItem.setTransformations([self.itemRotation, self.itemScale]) - self.tempTransform = pg.Transform() ## holds the additional transform that happens during a move - gets added to the userTransform when move is done. - self.userTransform = pg.Transform() ## stores the total transform of the object + self.tempTransform = pg.SRTTransform() ## holds the additional transform that happens during a move - gets added to the userTransform when move is done. + self.userTransform = pg.SRTTransform() ## stores the total transform of the object self.resetUserTransform() ## now happens inside resetUserTransform -> selectBoxToItem @@ -196,7 +196,7 @@ class CanvasItem(QtCore.QObject): #flip = self.transformGui.mirrorImageCheck.isChecked() #tr = self.userTransform.saveState() - inv = pg.Transform() + inv = pg.SRTTransform() inv.scale(-1, 1) self.userTransform = self.userTransform * inv self.updateTransform() @@ -226,7 +226,7 @@ class CanvasItem(QtCore.QObject): if not self.isMovable(): return self.rotate(180.) - # inv = pg.Transform() + # inv = pg.SRTTransform() # inv.scale(-1, -1) # self.userTransform = self.userTransform * inv #flip lr/ud # s=self.updateTransform() @@ -311,7 +311,7 @@ class CanvasItem(QtCore.QObject): def resetTemporaryTransform(self): - self.tempTransform = pg.Transform() ## don't use Transform.reset()--this transform might be used elsewhere. + self.tempTransform = pg.SRTTransform() ## don't use Transform.reset()--this transform might be used elsewhere. self.updateTransform() def transform(self): @@ -363,7 +363,7 @@ class CanvasItem(QtCore.QObject): try: #self.userTranslate = pg.Point(tr['trans']) #self.userRotate = tr['rot'] - self.userTransform = pg.Transform(tr) + self.userTransform = pg.SRTTransform(tr) self.updateTransform() self.selectBoxFromUser() ## move select box to match @@ -372,7 +372,7 @@ class CanvasItem(QtCore.QObject): except: #self.userTranslate = pg.Point([0,0]) #self.userRotate = 0 - self.userTransform = pg.Transform() + self.userTransform = pg.SRTTransform() debug.printExc("Failed to load transform:") #print "set transform", self, self.userTranslate diff --git a/flowchart/library/Data.py b/flowchart/library/Data.py index e24ba121..da3f6b0e 100644 --- a/flowchart/library/Data.py +++ b/flowchart/library/Data.py @@ -3,7 +3,7 @@ from ..Node import Node from pyqtgraph.Qt import QtGui, QtCore import numpy as np from .common import * -from pyqtgraph.Transform import Transform +from pyqtgraph.SRTTransform import SRTTransform from pyqtgraph.Point import Point from pyqtgraph.widgets.TreeWidget import TreeWidget from pyqtgraph.graphicsItems.LinearRegionItem import LinearRegionItem diff --git a/graphicsItems/ImageItem.py b/graphicsItems/ImageItem.py index a814c0ca..bc4c86ee 100644 --- a/graphicsItems/ImageItem.py +++ b/graphicsItems/ImageItem.py @@ -355,7 +355,6 @@ class ImageItem(GraphicsObject): self.drawAt(ev.pos(), ev) def raiseContextMenu(self, ev): - ## only raise menu if this terminal is removable menu = self.getMenu() if menu is None: return False @@ -443,4 +442,8 @@ class ImageItem(GraphicsObject): self.drawMask = mask def removeClicked(self): - self.sigRemoveRequested.emit(self) \ No newline at end of file + ## Send remove event only after we have exited the menu event handler + self.removeTimer = QtCore.QTimer() + self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self)) + self.removeTimer.start(0) + diff --git a/graphicsItems/ROI.py b/graphicsItems/ROI.py index 279054b3..5d099490 100644 --- a/graphicsItems/ROI.py +++ b/graphicsItems/ROI.py @@ -19,7 +19,7 @@ import numpy as np from numpy.linalg import norm import scipy.ndimage as ndimage from pyqtgraph.Point import * -from pyqtgraph.Transform import Transform +from pyqtgraph.SRTTransform import SRTTransform from math import cos, sin import pyqtgraph.functions as fn from .GraphicsObject import GraphicsObject @@ -45,8 +45,9 @@ class ROI(GraphicsObject): sigRegionChanged = QtCore.Signal(object) sigHoverEvent = QtCore.Signal(object) sigClicked = QtCore.Signal(object, object) + sigRemoveRequested = QtCore.Signal(object) - def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True): + def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True, removable=False): #QObjectWorkaround.__init__(self) GraphicsObject.__init__(self, parent) self.setAcceptedMouseButtons(QtCore.Qt.NoButton) @@ -55,6 +56,8 @@ class ROI(GraphicsObject): self.aspectLocked = False self.translatable = movable self.rotateAllowed = True + self.removable = removable + self.menu = None self.freeHandleMoved = False ## keep track of whether free handles have moved since last change signal was emitted. self.mouseHovering = False @@ -387,13 +390,19 @@ class ROI(GraphicsObject): if not ev.isExit(): if self.translatable and ev.acceptDrags(QtCore.Qt.LeftButton): hover=True + for btn in [QtCore.Qt.LeftButton, QtCore.Qt.RightButton, QtCore.Qt.MidButton]: if int(self.acceptedMouseButtons() & btn) > 0 and ev.acceptClicks(btn): hover=True - + if self.contextMenuEnabled(): + ev.acceptClicks(QtCore.Qt.RightButton) + if hover: self.setMouseHover(True) self.sigHoverEvent.emit(self) + ev.acceptClicks(QtCore.Qt.LeftButton) ## If the ROI is hilighted, we should accept all clicks to avoid confusion. + ev.acceptClicks(QtCore.Qt.RightButton) + ev.acceptClicks(QtCore.Qt.MidButton) else: self.setMouseHover(False) @@ -407,8 +416,36 @@ class ROI(GraphicsObject): else: self.currentPen = self.pen self.update() + + def contextMenuEnabled(self): + return self.removable + + def raiseContextMenu(self, ev): + if not self.contextMenuEnabled(): + return + menu = self.getMenu() + menu = self.scene().addParentContextMenus(self, menu, ev) + pos = ev.screenPos() + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + + def getMenu(self): + if self.menu is None: + self.menu = QtGui.QMenu() + self.menu.setTitle("ROI") + remAct = QtGui.QAction("Remove ROI", self.menu) + remAct.triggered.connect(self.removeClicked) + self.menu.addAction(remAct) + self.menu.remAct = remAct + return self.menu + + def removeClicked(self): + ## Send remove event only after we have exited the menu event handler + self.removeTimer = QtCore.QTimer() + self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self)) + self.removeTimer.start(0) - + + def mouseDragEvent(self, ev): if ev.isStart(): #p = ev.pos() @@ -442,6 +479,9 @@ class ROI(GraphicsObject): if ev.button() == QtCore.Qt.RightButton and self.isMoving: ev.accept() self.cancelMove() + if ev.button() == QtCore.Qt.RightButton and self.contextMenuEnabled(): + self.raiseContextMenu(ev) + ev.accept() elif int(ev.button() & self.acceptedMouseButtons()) > 0: ev.accept() self.sigClicked.emit(self, ev) @@ -933,8 +973,8 @@ class ROI(GraphicsObject): - t1 = Transform(relativeTo) - t2 = Transform(st) + t1 = SRTTransform(relativeTo) + t2 = SRTTransform(st) return t2/t1 @@ -962,7 +1002,7 @@ class ROI(GraphicsObject): st = self.getState() st['scale'] = st['size'] - st = Transform(st) + st = SRTTransform(st) st = (st * tr).saveState() st['size'] = st['scale'] self.setState(st)