diff --git a/ColorButton.py b/ColorButton.py index 302c7204..cc967c3b 100644 --- a/ColorButton.py +++ b/ColorButton.py @@ -9,8 +9,8 @@ class ColorButton(QtGui.QPushButton): sigColorChanging = QtCore.Signal(object) ## emitted whenever a new color is picked in the color dialog sigColorChanged = QtCore.Signal(object) ## emitted when the selected color is accepted (user clicks OK) - def __init__(self, color=(128,128,128)): - QtGui.QPushButton.__init__(self) + def __init__(self, parent=None, color=(128,128,128)): + QtGui.QPushButton.__init__(self, parent) self.setColor(color) self.colorDialog = QtGui.QColorDialog() self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) diff --git a/GraphicsView.py b/GraphicsView.py index 246111b4..2895d09b 100644 --- a/GraphicsView.py +++ b/GraphicsView.py @@ -91,6 +91,11 @@ class GraphicsView(QtGui.QGraphicsView): #prof.finish() def close(self): + self.centralWidget = None + self.scene().clear() + #print " ", self.scene().itemCount() + self.currentItem = None + self.sceneObj = None self.closed = True def useOpenGL(self, b=True): diff --git a/ImageView.py b/ImageView.py index e1d3ad66..ce7a80be 100644 --- a/ImageView.py +++ b/ImageView.py @@ -152,6 +152,7 @@ class ImageView(QtGui.QWidget): #QtGui.QWidget.__dtor__(self) def close(self): + self.ui.roiPlot.close() self.ui.graphicsView.close() self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage) self.scene.clear() @@ -159,7 +160,6 @@ class ImageView(QtGui.QWidget): del self.imageDisp #self.image = None #self.imageDisp = None - self.ui.roiPlot.close() self.setParent(None) def keyPressEvent(self, ev): diff --git a/MultiPlotItem.py b/MultiPlotItem.py index 81fae566..2f73c9e5 100644 --- a/MultiPlotItem.py +++ b/MultiPlotItem.py @@ -66,4 +66,6 @@ class MultiPlotItem(QtGui.QGraphicsWidget): def close(self): for p in self.plots: p[0].close() - \ No newline at end of file + self.plots = None + for i in range(self.layout.count()): + self.layout.removeAt(i) \ No newline at end of file diff --git a/MultiPlotWidget.py b/MultiPlotWidget.py index 0bf576f9..8071127a 100644 --- a/MultiPlotWidget.py +++ b/MultiPlotWidget.py @@ -40,4 +40,6 @@ class MultiPlotWidget(GraphicsView): def close(self): self.mPlotItem.close() - self.setParent(None) \ No newline at end of file + self.mPlotItem = None + self.setParent(None) + GraphicsView.close(self) \ No newline at end of file diff --git a/PIL_Fix/Image.py-1.6 b/PIL_Fix/Image.py-1.6 old mode 100755 new mode 100644 diff --git a/PlotItem.py b/PlotItem.py index cb1973a6..347e0c12 100644 --- a/PlotItem.py +++ b/PlotItem.py @@ -60,12 +60,13 @@ class PlotItem(QtGui.QGraphicsWidget): self.autoBtn = QtGui.QToolButton() self.autoBtn.setText('A') self.autoBtn.hide() - + self.proxies = [] for b in [self.ctrlBtn, self.autoBtn]: proxy = QtGui.QGraphicsProxyWidget(self) proxy.setWidget(b) proxy.setAcceptHoverEvents(False) b.setStyleSheet("background-color: #000000; color: #888; font-size: 6pt") + self.proxies.append(proxy) #QtCore.QObject.connect(self.ctrlBtn, QtCore.SIGNAL('clicked()'), self.ctrlBtnClicked) self.ctrlBtn.clicked.connect(self.ctrlBtnClicked) #QtCore.QObject.connect(self.autoBtn, QtCore.SIGNAL('clicked()'), self.enableAutoScale) @@ -155,9 +156,9 @@ class PlotItem(QtGui.QGraphicsWidget): c.setupUi(w) dv = QtGui.QDoubleValidator(self) self.ctrlMenu = QtGui.QMenu() - ac = QtGui.QWidgetAction(self) - ac.setDefaultWidget(w) - self.ctrlMenu.addAction(ac) + self.menuAction = QtGui.QWidgetAction(self) + self.menuAction.setDefaultWidget(w) + self.ctrlMenu.addAction(self.menuAction) if HAVE_WIDGETGROUP: self.stateGroup = WidgetGroup(self.ctrlMenu) @@ -284,9 +285,46 @@ class PlotItem(QtGui.QGraphicsWidget): def close(self): #print "delete", self + ## All this crap is needed to avoid PySide trouble. + ## The problem seems to be whenever scene.clear() leads to deletion of widgets (either through proxies or qgraphicswidgets) + ## the solution is to manually remove all widgets before scene.clear() is called + if self.ctrlMenu is None: ## already shut down + return + self.ctrlMenu.setParent(None) + self.ctrlMenu = None + + self.ctrlBtn.setParent(None) + self.ctrlBtn = None + self.autoBtn.setParent(None) + self.autoBtn = None + + for k in self.scales: + i = self.scales[k]['item'] + i.close() + + self.scales = None + self.scene().removeItem(self.vb) + self.vb = None + for i in range(self.layout.count()): + self.layout.removeAt(i) + + for p in self.proxies: + try: + p.setWidget(None) + except RuntimeError: + break + self.scene().removeItem(p) + self.proxies = [] + + self.menuAction.releaseWidget(self.menuAction.defaultWidget()) + self.menuAction.setParent(None) + self.menuAction = None + if self.manager is not None: self.manager.sigWidgetListChanged.disconnect(self.updatePlotList) self.manager.removeWidget(self.name) + #else: + #print "no manager" def registerPlot(self, name): self.name = name @@ -1042,6 +1080,8 @@ class PlotItem(QtGui.QGraphicsWidget): #ev.accept() def resizeEvent(self, ev): + if self.ctrlBtn is None: ## already closed down + return self.ctrlBtn.move(0, self.size().height() - self.ctrlBtn.size().height()) self.autoBtn.move(self.ctrlBtn.width(), self.size().height() - self.autoBtn.size().height()) @@ -1190,6 +1230,8 @@ class PlotWidgetManager(QtCore.QObject): del self.widgets[name] #self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) self.sigWidgetListChanged.emit(self.widgets.keys()) + else: + print "plot %s not managed" % name def listWidgets(self): diff --git a/PlotWidget.py b/PlotWidget.py index 244dad2f..c95547e7 100644 --- a/PlotWidget.py +++ b/PlotWidget.py @@ -39,6 +39,7 @@ class PlotWidget(GraphicsView): #self.scene().clear() #self.mPlotItem.close() self.setParent(None) + GraphicsView.close(self) def __getattr__(self, attr): ## implicitly wrap methods from plotItem if hasattr(self.plotItem, attr): diff --git a/Point.py b/Point.py index a215755e..48f122da 100644 --- a/Point.py +++ b/Point.py @@ -6,7 +6,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation. """ from PyQt4 import QtCore -from math import acos +import numpy as np def clip(x, mn, mx): if x > mx: @@ -99,17 +99,17 @@ class Point(QtCore.QPointF): return (self[0]**2 + self[1]**2) ** 0.5 def angle(self, a): - """Returns the angle between this vector and the vector a.""" + """Returns the angle in degrees between this vector and the vector a.""" n1 = self.length() n2 = a.length() if n1 == 0. or n2 == 0.: return None ## Probably this should be done with arctan2 instead.. - ang = acos(clip(self.dot(a) / (n1 * n2), -1.0, 1.0)) ### in radians + ang = np.arccos(clip(self.dot(a) / (n1 * n2), -1.0, 1.0)) ### in radians c = self.cross(a) if c > 0: ang *= -1. - return ang + return ang * 180. / np.pi def dot(self, a): """Returns the dot product of a and this Point.""" @@ -119,6 +119,11 @@ class Point(QtCore.QPointF): def cross(self, a): a = Point(a) return self[0]*a[1] - self[1]*a[0] + + def proj(self, b): + """Return the projection of this vector onto the vector b""" + b1 = b / b.length() + return self.dot(b1) * b1 def __repr__(self): return "Point(%f, %f)" % (self[0], self[1]) @@ -128,4 +133,7 @@ class Point(QtCore.QPointF): return min(self[0], self[1]) def max(self): - return max(self[0], self[1]) \ No newline at end of file + return max(self[0], self[1]) + + def copy(self): + return Point(self) \ No newline at end of file diff --git a/Transform.py b/Transform.py new file mode 100644 index 00000000..0529b89c --- /dev/null +++ b/Transform.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +from PyQt4 import QtCore, QtGui +from Point import Point +import numpy as np + +class Transform(QtGui.QTransform): + """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate + + This transform always has 0 shear.""" + def __init__(self, init=None): + QtGui.QTransform.__init__(self) + self.reset() + + if isinstance(init, dict): + self.restoreState(init) + elif isinstance(init, Transform): + self._state = { + 'pos': Point(init._state['pos']), + 'scale': Point(init._state['scale']), + 'angle': init._state['angle'] + } + self.update() + elif isinstance(init, QtGui.QTransform): + self.setFromQTransform(init) + + def reset(self): + self._state = { + 'pos': Point(0,0), + 'scale': Point(1,1), + 'angle': 0.0 ## in degrees + } + self.update() + + def setFromQTransform(self, tr): + p1 = Point(tr.map(0., 0.)) + p2 = Point(tr.map(1., 0.)) + p3 = Point(tr.map(0., 1.)) + + dp2 = Point(p2-p1) + dp3 = Point(p3-p1) + + ## detect flipped axes + if dp2.angle(dp3) > 0: + da = 180 + sy = -1.0 + else: + da = 0 + sy = 1.0 + + self._state = { + 'pos': Point(p1), + 'scale': Point(dp2.length(), dp3.length() * sy), + 'angle': (np.arctan2(dp2[1], dp2[0]) * 180. / np.pi) + da + } + self.update() + + def translate(self, *args): + """Acceptable arguments are: + x, y + [x, y] + Point(x,y)""" + t = Point(*args) + self.setTranslate(self._state['pos']+t) + + def setTranslate(self, *args): + """Acceptable arguments are: + x, y + [x, y] + Point(x,y)""" + self._state['pos'] = Point(*args) + self.update() + + def scale(self, *args): + """Acceptable arguments are: + x, y + [x, y] + Point(x,y)""" + s = Point(*args) + self.setScale(self._state['scale'] * s) + + def setScale(self, *args): + """Acceptable arguments are: + x, y + [x, y] + Point(x,y)""" + self._state['scale'] = Point(*args) + self.update() + + def rotate(self, angle): + """Rotate the transformation by angle (in degrees)""" + self.setRotate(self._state['angle'] + angle) + + def setRotate(self, angle): + """Set the transformation rotation to angle (in degrees)""" + self._state['angle'] = angle + self.update() + + def __div__(self, t): + """A / B == B^-1 * A""" + dt = t.inverted()[0] * self + return Transform(dt) + + def __mul__(self, t): + return Transform(QtGui.QTransform.__mul__(self, t)) + + def saveState(self): + p = self._state['pos'] + s = self._state['scale'] + if s[0] == 0: + raise Exception('Invalid scale') + return {'pos': (p[0], p[1]), 'scale': (s[0], s[1]), 'angle': self._state['angle']} + + def restoreState(self, state): + self._state['pos'] = Point(state.get('pos', (0,0))) + self._state['scale'] = Point(state.get('scale', (1.,1.))) + self._state['angle'] = state.get('angle', 0) + self.update() + + def update(self): + QtGui.QTransform.reset(self) + ## modifications to the transform are multiplied on the right, so we need to reverse order here. + QtGui.QTransform.translate(self, *self._state['pos']) + QtGui.QTransform.rotate(self, self._state['angle']) + QtGui.QTransform.scale(self, *self._state['scale']) + + def __repr__(self): + return str(self.saveState()) + + def matrix(self): + return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]]) + +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) + + b = QtGui.QGraphicsRectItem(-5, -5, 10, 10) + b.setPen(QtGui.QPen(mkPen('y'))) + t1 = QtGui.QGraphicsTextItem() + t1.setHtml('R') + s.addItem(b) + s.addItem(t1) + + tr1 = Transform() + tr2 = Transform() + tr3 = QtGui.QTransform() + tr3.translate(20, 0) + tr3.rotate(45) + print "QTransform -> Transform:", Transform(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 = Transform() + tr4.scale(-1, 1) + tr4.rotate(30) + print "tr1 * tr4 = ", tr1*tr4 + + w1 = widgets.TestROI((0,0), (50, 50)) + w2 = widgets.TestROI((0,0), (150, 150)) + s.addItem(w1) + s.addItem(w2) + w1Base = w1.getState() + w2Base = w2.getState() + def update(): + tr1 = w1.getGlobalTransform(w1Base) + tr2 = w2.getGlobalTransform(w2Base) + t1.setTransform(tr1 * tr2) + w1.setState(w1Base) + w1.applyGlobalTransform(tr2) + w1.sigRegionChanged.connect(update) + w2.sigRegionChanged.connect(update) + + \ No newline at end of file diff --git a/__init__.py b/__init__.py index f15464ec..618436ac 100644 --- a/__init__.py +++ b/__init__.py @@ -7,6 +7,8 @@ from graphicsWindows import * #import PlotWidget #import ImageView from PyQt4 import QtGui +from Point import Point +from Transform import Transform plots = [] images = [] diff --git a/examples/test_Arrow.py b/examples/test_Arrow.py old mode 100644 new mode 100755 diff --git a/examples/test_ImageItem.py b/examples/test_ImageItem.py old mode 100644 new mode 100755 diff --git a/examples/test_ImageView.py b/examples/test_ImageView.py old mode 100644 new mode 100755 diff --git a/examples/test_PlotWidget.py b/examples/test_PlotWidget.py old mode 100644 new mode 100755 diff --git a/examples/test_draw.py b/examples/test_draw.py old mode 100644 new mode 100755 diff --git a/examples/test_scatterPlot.py b/examples/test_scatterPlot.py old mode 100644 new mode 100755 diff --git a/graphicsItems.py b/graphicsItems.py index 7d28784d..be3dc028 100644 --- a/graphicsItems.py +++ b/graphicsItems.py @@ -256,8 +256,11 @@ class ImageItem(QtGui.QGraphicsObject): def getLevels(self): return self.whiteLevel, self.blackLevel - def updateImage(self, image=None, copy=True, autoRange=False, clipMask=None, white=None, black=None): - axh = {'x': 0, 'y': 1, 'c': 2} + def updateImage(self, image=None, copy=True, autoRange=False, clipMask=None, white=None, black=None, axes=None): + if axes is None: + axh = {'x': 0, 'y': 1, 'c': 2} + else: + axh = axes #print "Update image", black, white if white is not None: self.whiteLevel = white @@ -280,8 +283,12 @@ class ImageItem(QtGui.QGraphicsObject): # Determine scale factors if autoRange or self.blackLevel is None: - self.blackLevel = self.image.min() - self.whiteLevel = self.image.max() + if self.image.dtype is np.ubyte: + self.blackLevel = 0 + self.whiteLevel = 255 + else: + self.blackLevel = self.image.min() + self.whiteLevel = self.image.max() #print "Image item using", self.blackLevel, self.whiteLevel if self.blackLevel != self.whiteLevel: @@ -324,7 +331,6 @@ class ImageItem(QtGui.QGraphicsObject): print "Weave compile failed, falling back to slower version." self.image.shape = shape im = ((self.image - black) * scale).clip(0.,255.).astype(np.ubyte) - try: im1 = np.empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=np.ubyte) @@ -341,10 +347,13 @@ class ImageItem(QtGui.QGraphicsObject): im1[..., 3] = alpha elif im.ndim == 3: #color image im2 = im.transpose(axh['y'], axh['x'], axh['c']) + ## [B G R A] Reorder colors + order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. for i in range(0, im.shape[axh['c']]): - im1[..., 2-i] = im2[..., i] ## for some reason, the colors line up as BGR in the final image. + im1[..., order[i]] = im2[..., i] + ## fill in unused channels with 0 or alpha for i in range(im.shape[axh['c']], 3): im1[..., i] = 0 if im.shape[axh['c']] < 4: @@ -781,7 +790,7 @@ class PlotCurveItem(GraphicsObject): def mouseMoveEvent(self, ev): #GraphicsObject.mouseMoveEvent(self, ev) self.mouseMoved = True - print "move" + #print "move" def mouseReleaseEvent(self, ev): #GraphicsObject.mouseReleaseEvent(self, ev) @@ -848,9 +857,9 @@ class CurvePoint(QtGui.QGraphicsObject): p1 = self.parentItem().mapToScene(QtCore.QPointF(x[i1], y[i1])) p2 = self.parentItem().mapToScene(QtCore.QPointF(x[i2], y[i2])) - ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x()) + ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x()) ## returns radians self.resetTransform() - self.rotate(180+ ang * 180 / np.pi) + self.rotate(180+ ang * 180 / np.pi) ## takes degrees QtGui.QGraphicsItem.setPos(self, *newPos) return True @@ -940,7 +949,7 @@ class CurveArrow(CurvePoint): class ScatterPlotItem(QtGui.QGraphicsWidget): - sigPointClicked = QtCore.Signal(object) + sigPointClicked = QtCore.Signal(object, object) def __init__(self, spots=None, pxMode=True, pen=None, brush=None, size=5): QtGui.QGraphicsWidget.__init__(self) @@ -1027,7 +1036,7 @@ class ScatterPlotItem(QtGui.QGraphicsWidget): pass def pointClicked(self, point): - self.sigPointClicked.emit(point) + self.sigPointClicked.emit(self, point) def points(self): return self.spots[:] @@ -1044,6 +1053,7 @@ class SpotItem(QtGui.QGraphicsWidget): self.pen = pen self.brush = brush self.path = QtGui.QPainterPath() + self.size = size #s2 = size/2. self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) self.scale(size, size) @@ -1236,7 +1246,7 @@ class LabelItem(QtGui.QGraphicsWidget): def setAngle(self, angle): self.angle = angle - self.item.resetMatrix() + self.item.resetTransform() self.item.rotate(angle) self.updateMin() @@ -1310,6 +1320,11 @@ class ScaleItem(QtGui.QGraphicsWidget): self.grid = False + + def close(self): + self.scene().removeItem(self.label) + self.label = None + self.scene().removeItem(self) def setGrid(self, grid): """Set the alpha value for the grid, or False to disable.""" @@ -1722,7 +1737,7 @@ class ViewBox(QtGui.QGraphicsWidget): m = QtGui.QTransform() ## First center the viewport at 0 - self.childGroup.resetMatrix() + self.childGroup.resetTransform() center = self.transform().inverted()[0].map(bounds.center()) #print " transform to center:", center if self.yInverted: @@ -2009,6 +2024,7 @@ class InfiniteLine(GraphicsObject): self.currentPen = self.pen def setAngle(self, angle): + """Takes angle argument in degrees.""" self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 self.updateLine() diff --git a/widgets.py b/widgets.py index 5b275bc2..6f36fe22 100644 --- a/widgets.py +++ b/widgets.py @@ -9,12 +9,15 @@ for use as region-of-interest markers. ROI class automatically handles extractio of array data from ImageItems. """ -from PyQt4 import QtCore, QtGui, QtOpenGL, QtSvg +from PyQt4 import QtCore, QtGui +if not hasattr(QtCore, 'Signal'): + QtCore.Signal = QtCore.pyqtSignal #from numpy import array, arccos, dot, pi, zeros, vstack, ubyte, fromfunction, ceil, floor, arctan2 import numpy as np from numpy.linalg import norm import scipy.ndimage as ndimage from Point import * +from Transform import Transform from math import cos, sin import functions as fn #from ObjectWorkaround import * @@ -35,6 +38,8 @@ def rectStr(r): class ROI(QtGui.QGraphicsObject): + """Generic region-of-interest widget. + Can be used for implementing many types of selection box with rotate/translate/scale handles.""" sigRegionChangeFinished = QtCore.Signal(object) sigRegionChangeStarted = QtCore.Signal(object) @@ -54,10 +59,11 @@ class ROI(QtGui.QGraphicsObject): self.pen = fn.mkPen(pen) self.handlePen = QtGui.QPen(QtGui.QColor(150, 255, 255)) self.handles = [] - self.state = {'pos': pos, 'size': size, 'angle': angle} + self.state = {'pos': pos, 'size': size, 'angle': angle} ## angle is in degrees for ease of Qt integration self.lastState = None self.setPos(pos) - self.rotate(-angle * 180. / np.pi) + #self.rotate(-angle * 180. / np.pi) + self.rotate(-angle) self.setZValue(10) self.isMoving = False @@ -114,7 +120,8 @@ class ROI(QtGui.QGraphicsObject): def setAngle(self, angle, update=True): self.state['angle'] = angle tr = QtGui.QTransform() - tr.rotate(-angle * 180 / np.pi) + #tr.rotate(-angle * 180 / np.pi) + tr.rotate(angle) self.setTransform(tr) if update: self.updateHandles() @@ -129,10 +136,10 @@ class ROI(QtGui.QGraphicsObject): pos = Point(pos) return self.addHandle({'name': name, 'type': 'f', 'pos': pos, 'item': item}) - def addScaleHandle(self, pos, center, axes=None, item=None, name=None): + def addScaleHandle(self, pos, center, axes=None, item=None, name=None, lockAspect=False): pos = Point(pos) center = Point(center) - info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item} + info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item, 'lockAspect': lockAspect} if pos.x() == center.x(): info['xoff'] = True if pos.y() == center.y(): @@ -212,6 +219,11 @@ class ROI(QtGui.QGraphicsObject): h['item'].hide() def mousePressEvent(self, ev): + ## Bug: sometimes we get events we shouldn't. + p = ev.pos() + if not self.isMoving and not self.shape().contains(p): + ev.ignore() + return if ev.button() == QtCore.Qt.LeftButton: self.setSelected(True) if self.translatable: @@ -344,6 +356,11 @@ class ROI(QtGui.QGraphicsObject): if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize lp1[1] = round(lp1[1] / self.snapSize) * self.snapSize + + ## preserve aspect ratio (this can override snapping) + if h['lockAspect'] or (modifiers & QtCore.Qt.AltModifier): + #arv = Point(self.preMoveState['size']) - + lp1 = lp1.proj(lp0) ## determine scale factors and new size of ROI hs = h['pos'] - c @@ -392,15 +409,16 @@ class ROI(QtGui.QGraphicsObject): return ## determine new rotation angle, constrained if necessary - ang = newState['angle'] + lp0.angle(lp1) + ang = newState['angle'] - lp0.angle(lp1) if ang is None: ## this should never happen.. return if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - ang = round(ang / (np.pi/12.)) * (np.pi/12.) + ang = round(ang / 15.) * 15. ## 180/12 = 15 ## create rotation transform tr = QtGui.QTransform() - tr.rotate(-ang * 180. / np.pi) + #tr.rotate(-ang * 180. / np.pi) + tr.rotate(ang) ## mvoe ROI so that center point remains stationary after rotate cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos']) @@ -473,7 +491,8 @@ class ROI(QtGui.QGraphicsObject): if ang is None: return if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - ang = round(ang / (np.pi/12.)) * (np.pi/12.) + #ang = round(ang / (np.pi/12.)) * (np.pi/12.) + ang = round(ang / 15.) * 15. hs = abs(h['pos'][scaleAxis] - c[scaleAxis]) newState['size'][scaleAxis] = lp1.length() / hs @@ -484,7 +503,8 @@ class ROI(QtGui.QGraphicsObject): c1 = c * newState['size'] tr = QtGui.QTransform() - tr.rotate(-ang * 180. / np.pi) + #tr.rotate(-ang * 180. / np.pi) + tr.rotate(-ang) cc = self.mapToParent(cs) - (tr.map(c1) + self.state['pos']) newState['angle'] = ang @@ -576,7 +596,8 @@ class ROI(QtGui.QGraphicsObject): def stateRect(self, state): r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1]) tr = QtGui.QTransform() - tr.rotate(-state['angle'] * 180 / np.pi) + #tr.rotate(-state['angle'] * 180 / np.pi) + tr.rotate(-state['angle']) r = tr.mapRect(r) return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1]) @@ -751,11 +772,60 @@ class ROI(QtGui.QGraphicsObject): ### Untranspose array before returning #return arr5.transpose(tr2) - - - - + def getGlobalTransform(self, relativeTo=None): + """Return global transformation (rotation angle+translation) required to move from relative state to current state. If relative state isn't specified, + then we use the state of the ROI when mouse is pressed.""" + if relativeTo == None: + relativeTo = self.preMoveState + st = self.getState() + ## this is only allowed because we will be comparing the two + relativeTo['scale'] = relativeTo['size'] + st['scale'] = st['size'] + + + + t1 = Transform(relativeTo) + t2 = Transform(st) + return t2/t1 + + + #st = self.getState() + + ### rotation + #ang = (st['angle']-relativeTo['angle']) * 180. / 3.14159265358 + #rot = QtGui.QTransform() + #rot.rotate(-ang) + + ### We need to come up with a universal transformation--one that can be applied to other objects + ### such that all maintain alignment. + ### More specifically, we need to turn the ROI's position and angle into + ### a rotation _around the origin_ and a translation. + + #p0 = Point(relativeTo['pos']) + + ### base position, rotated + #p1 = rot.map(p0) + + #trans = Point(st['pos']) - p1 + #return trans, ang + + def applyGlobalTransform(self, tr): + st = self.getState() + st['scale'] = st['size'] + st = Transform(st) + #trans = QtGui.QTransform() + #trans.translate(*translate) + #trans.rotate(-rotate) + + #x2, y2 = trans.map(*st['pos']) + + #self.setAngle(st['angle']+rotate*np.pi/180.) + #self.setPos([x2, y2]) + st = (st * tr).saveState() + st['size'] = st['scale'] + self.setState(st) + class Handle(QtGui.QGraphicsItem): @@ -783,6 +853,7 @@ class Handle(QtGui.QGraphicsItem): self.pen.setCosmetic(True) self.isMoving = False self.sides, self.startAng = self.types[typ] + self.buildPath() def connectROI(self, roi, i): self.roi.append((roi, i)) @@ -791,6 +862,12 @@ class Handle(QtGui.QGraphicsItem): return self.bounds def mousePressEvent(self, ev): + # Bug: sometimes we get events not meant for us! + p = ev.pos() + if not self.isMoving and not self.path.contains(p): + ev.ignore() + return + #print "handle press" if ev.button() == QtCore.Qt.LeftButton: self.isMoving = True @@ -833,6 +910,20 @@ class Handle(QtGui.QGraphicsItem): for r in self.roi: r[0].movePoint(r[1], pos, modifiers) + def buildPath(self): + size = self.radius + self.path = QtGui.QPainterPath() + ang = self.startAng + dt = 2*np.pi / self.sides + for i in range(0, self.sides+1): + x = size * cos(ang) + y = size * sin(ang) + ang += dt + if i == 0: + self.path.moveTo(x, y) + else: + self.path.lineTo(x, y) + def paint(self, p, opt, widget): ## determine rotation of transform m = self.sceneTransform() @@ -851,15 +942,19 @@ class Handle(QtGui.QGraphicsItem): self.prepareGeometryChange() p.setRenderHints(p.Antialiasing, True) p.setPen(self.pen) - ang = self.startAng + va - dt = 2*np.pi / self.sides - for i in range(0, self.sides): - x1 = size * cos(ang) - y1 = size * sin(ang) - x2 = size * cos(ang+dt) - y2 = size * sin(ang+dt) - ang += dt - p.drawLine(Point(x1, y1), Point(x2, y2)) + + p.rotate(va * 180. / 3.1415926) + p.drawPath(self.path) + + #ang = self.startAng + va + #dt = 2*np.pi / self.sides + #for i in range(0, self.sides): + #x1 = size * cos(ang) + #y1 = size * sin(ang) + #x2 = size * cos(ang+dt) + #y2 = size * sin(ang+dt) + #ang += dt + #p.drawLine(Point(x1, y1), Point(x2, y2)) @@ -902,10 +997,11 @@ class LineROI(ROI): d = pos2-pos1 l = d.length() ang = Point(1, 0).angle(d) - c = Point(-width/2. * sin(ang), -width/2. * cos(ang)) + ra = ang * np.pi / 180. + c = Point(-width/2. * sin(ra), -width/2. * cos(ra)) pos1 = pos1 + c - ROI.__init__(self, pos1, size=Point(l, width), angle=ang*180/np.pi, **args) + ROI.__init__(self, pos1, size=Point(l, width), angle=ang, **args) self.addScaleRotateHandle([0, 0.5], [1, 0.5]) self.addScaleRotateHandle([1, 0.5], [0, 0.5]) self.addScaleHandle([0.5, 1], [0.5, 0.5])