From 7629bca34da105b59be85f5d55731964fe6a07a9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 5 Apr 2011 10:35:50 -0400 Subject: [PATCH] Updates merged in from ACQ4: - converted most old-style signals into new-style for PySide compatibility (beware: API changes) - removed ObjectWorkaround, now just using QGraphicsWidget - performance enhancements, particularly in ROI.getArrayRegion - numerous bugfixes --- GradientWidget.py | 20 +- GraphicsView.py | 46 ++++- ImageView.py | 63 ++++-- MultiPlotItem.py | 3 + MultiPlotWidget.py | 4 + ObjectWorkaround.py | 42 ---- PlotItem.py | 337 ++++++++++++++++++++++--------- PlotWidget.py | 24 ++- Point.py | 3 + SignalProxy.py | 89 ++++---- examples/test_ImageItem.py | 30 +-- examples/test_ImageView.py | 35 ++-- examples/test_MultiPlotWidget.py | 11 +- examples/test_PlotWidget.py | 76 +++---- examples/test_ROItypes.py | 69 +++---- examples/test_draw.py | 14 +- examples/test_scatterPlot.py | 22 +- examples/test_viewBox.py | 30 +-- functions.py | 94 ++++++++- graphicsItems.py | 265 +++++++++++++++++------- widgets.py | 271 ++++++++++++++++--------- 21 files changed, 1006 insertions(+), 542 deletions(-) delete mode 100644 ObjectWorkaround.py diff --git a/GradientWidget.py b/GradientWidget.py index b4e957ce..033c62db 100644 --- a/GradientWidget.py +++ b/GradientWidget.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from PyQt4 import QtGui, QtCore - +import weakref class TickSlider(QtGui.QGraphicsView): def __init__(self, parent=None, orientation='bottom', allowAdd=True, **kargs): @@ -161,6 +161,9 @@ class TickSlider(QtGui.QGraphicsView): class GradientWidget(TickSlider): + + sigGradientChanged = QtCore.Signal(object) + def __init__(self, *args, **kargs): TickSlider.__init__(self, *args, **kargs) self.currentTick = None @@ -171,8 +174,10 @@ class GradientWidget(TickSlider): self.colorDialog = QtGui.QColorDialog() self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) self.colorDialog.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) - QtCore.QObject.connect(self.colorDialog, QtCore.SIGNAL('currentColorChanged(const QColor&)'), self.currentColorChanged) - QtCore.QObject.connect(self.colorDialog, QtCore.SIGNAL('rejected()'), self.currentColorRejected) + #QtCore.QObject.connect(self.colorDialog, QtCore.SIGNAL('currentColorChanged(const QColor&)'), self.currentColorChanged) + self.colorDialog.currentColorChanged.connect(self.currentColorChanged) + #QtCore.QObject.connect(self.colorDialog, QtCore.SIGNAL('rejected()'), self.currentColorRejected) + self.colorDialog.rejected.connect(self.currentColorRejected) #self.gradient = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(100,0)) self.scene.addItem(self.gradRect) @@ -199,7 +204,8 @@ class GradientWidget(TickSlider): def updateGradient(self): self.gradient = self.getGradient() self.gradRect.setBrush(QtGui.QBrush(self.gradient)) - self.emit(QtCore.SIGNAL('gradientChanged'), self) + #self.emit(QtCore.SIGNAL('gradientChanged'), self) + self.sigGradientChanged.emit(self) def setLength(self, newLen): TickSlider.setLength(self, newLen) @@ -356,7 +362,7 @@ class Tick(QtGui.QGraphicsPolygonItem): def __init__(self, view, pos, color, movable=True, scale=10): #QObjectWorkaround.__init__(self) self.movable = movable - self.view = view + self.view = weakref.ref(view) self.scale = scale self.color = color #self.endTick = endTick @@ -385,7 +391,7 @@ class Tick(QtGui.QGraphicsPolygonItem): newPos.setY(self.pos().y()) #newPos.setX(min(max(newPos.x(), 0), 100)) self.setPos(newPos) - self.view.tickMoved(self, newPos) + self.view().tickMoved(self, newPos) self.movedSincePress = True #self.emit(QtCore.SIGNAL('tickChanged'), self) ev.accept() @@ -405,7 +411,7 @@ class Tick(QtGui.QGraphicsPolygonItem): def mouseReleaseEvent(self, ev): #print self, "release", ev.scenePos() if not self.movedSincePress: - self.view.tickClicked(self, ev) + self.view().tickClicked(self, ev) #if ev.button() == QtCore.Qt.LeftButton and ev.scenePos() == self.pressPos: #color = QtGui.QColorDialog.getColor(self.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) diff --git a/GraphicsView.py b/GraphicsView.py index ac582b65..6ab2aa42 100644 --- a/GraphicsView.py +++ b/GraphicsView.py @@ -11,9 +11,15 @@ from PyQt4 import QtCore, QtGui, QtOpenGL, QtSvg from Point import * #from vector import * import sys - +#import debug class GraphicsView(QtGui.QGraphicsView): + + sigRangeChanged = QtCore.Signal(object, object) + sigMouseReleased = QtCore.Signal(object) + sigSceneMouseMoved = QtCore.Signal(object) + #sigRegionChanged = QtCore.Signal(object) + def __init__(self, parent=None, useOpenGL=True): """Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the viewed coordinate range. Also automatically creates a QGraphicsScene and a central QGraphicsWidget @@ -25,6 +31,7 @@ class GraphicsView(QtGui.QGraphicsView): The view can be panned using the middle mouse button and scaled using the right mouse button if enabled via enableMouse().""" + self.closed = False QtGui.QGraphicsView.__init__(self, parent) if 'linux' in sys.platform: ## linux has bugs in opengl implementation @@ -42,7 +49,7 @@ class GraphicsView(QtGui.QGraphicsView): brush.setStyle(QtCore.Qt.SolidPattern) palette.setBrush(QtGui.QPalette.Disabled,QtGui.QPalette.Base,brush) self.setPalette(palette) - self.setProperty("cursor",QtCore.QVariant(QtCore.Qt.ArrowCursor)) + #self.setProperty("cursor",QtCore.QVariant(QtCore.Qt.ArrowCursor)) self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setFrameShape(QtGui.QFrame.NoFrame) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) @@ -78,6 +85,14 @@ class GraphicsView(QtGui.QGraphicsView): self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) self.clickAccepted = False + #def paintEvent(self, *args): + #prof = debug.Profiler('GraphicsView.paintEvent '+str(id(self)), disabled=True) + #QtGui.QGraphicsView.paintEvent(self, *args) + #prof.finish() + + def close(self): + self.closed = True + def useOpenGL(self, b=True): if b: v = QtOpenGL.QGLWidget() @@ -112,6 +127,8 @@ class GraphicsView(QtGui.QGraphicsView): self.lastButtonReleased = None def resizeEvent(self, ev): + if self.closed: + return if self.autoPixelRange: self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height()) self.setRange(self.range, padding=0, disableAutoPixel=False) @@ -148,7 +165,8 @@ class GraphicsView(QtGui.QGraphicsView): #print " translate:", st self.setMatrix(m) self.currentScale = scale - self.emit(QtCore.SIGNAL('viewChanged'), self.range) + #self.emit(QtCore.SIGNAL('viewChanged'), self.range) + self.sigRangeChanged.emit(self, self.range) if propagate: for v in self.lockedViewports: @@ -200,6 +218,16 @@ class GraphicsView(QtGui.QGraphicsView): #print "New Range:", self.range self.centralWidget.setGeometry(self.range) self.updateMatrix(propagate) + + def scaleToImage(self, image): + """Scales such that pixels in image are the same size as screen pixels. This may result in a significant performance increase.""" + pxSize = image.pixelSize() + tl = image.sceneBoundingRect().topLeft() + w = self.size().width() * pxSize[0] + h = self.size().height() * pxSize[1] + range = QtCore.QRectF(tl.x(), tl.y(), w, h) + self.setRange(range, padding=0) + def lockXRange(self, v1): @@ -299,7 +327,8 @@ class GraphicsView(QtGui.QGraphicsView): if not self.mouseEnabled: return #self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) - self.emit(QtCore.SIGNAL("mouseReleased"), ev) + #self.emit(QtCore.SIGNAL("mouseReleased"), ev) + self.sigMouseReleased.emit(ev) self.lastButtonReleased = ev.button() return ## Everything below disabled for now.. @@ -320,7 +349,8 @@ class GraphicsView(QtGui.QGraphicsView): QtGui.QGraphicsView.mouseMoveEvent(self, ev) if not self.mouseEnabled: return - self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos())) + #self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos())) + self.sigSceneMouseMoved.emit(self.mapToScene(ev.pos())) #print "moved. Grabber:", self.scene().mouseGrabberItem() @@ -333,13 +363,15 @@ class GraphicsView(QtGui.QGraphicsView): #if self.yInverted: #scale[0] = 1. / scale[0] self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos)) - self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) + #self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) + self.sigRangeChanged.emit(self, self.range) elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button. tr = -delta / self.currentScale self.translate(tr[0], tr[1]) - self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) + #self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) + self.sigRangeChanged.emit(self, self.range) #return ## Everything below disabled for now.. diff --git a/ImageView.py b/ImageView.py index c93731a4..e1d3ad66 100644 --- a/ImageView.py +++ b/ImageView.py @@ -31,6 +31,9 @@ class PlotROI(ROI): class ImageView(QtGui.QWidget): + + sigTimeChanged = QtCore.Signal(object, object) + def __init__(self, parent=None, name="ImageView", *args): QtGui.QWidget.__init__(self, parent, *args) self.levelMax = 4096 @@ -106,25 +109,38 @@ class ImageView(QtGui.QWidget): setattr(self, fn, getattr(self.ui.graphicsView, fn)) #QtCore.QObject.connect(self.ui.timeSlider, QtCore.SIGNAL('valueChanged(int)'), self.timeChanged) - self.timeLine.connect(self.timeLine, QtCore.SIGNAL('positionChanged'), self.timeLineChanged) + #self.timeLine.connect(self.timeLine, QtCore.SIGNAL('positionChanged'), self.timeLineChanged) + self.timeLine.sigPositionChanged.connect(self.timeLineChanged) #QtCore.QObject.connect(self.ui.whiteSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateImage) #QtCore.QObject.connect(self.ui.blackSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateImage) - QtCore.QObject.connect(self.ui.gradientWidget, QtCore.SIGNAL('gradientChanged'), self.updateImage) - QtCore.QObject.connect(self.ui.roiBtn, QtCore.SIGNAL('clicked()'), self.roiClicked) - self.roi.connect(self.roi, QtCore.SIGNAL('regionChanged'), self.roiChanged) - QtCore.QObject.connect(self.ui.normBtn, QtCore.SIGNAL('toggled(bool)'), self.normToggled) - QtCore.QObject.connect(self.ui.normDivideRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) - QtCore.QObject.connect(self.ui.normSubtractRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) - QtCore.QObject.connect(self.ui.normOffRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) - QtCore.QObject.connect(self.ui.normROICheck, QtCore.SIGNAL('clicked()'), self.updateNorm) - QtCore.QObject.connect(self.ui.normFrameCheck, QtCore.SIGNAL('clicked()'), self.updateNorm) - QtCore.QObject.connect(self.ui.normTimeRangeCheck, QtCore.SIGNAL('clicked()'), self.updateNorm) - QtCore.QObject.connect(self.playTimer, QtCore.SIGNAL('timeout()'), self.timeout) + #QtCore.QObject.connect(self.ui.gradientWidget, QtCore.SIGNAL('gradientChanged'), self.updateImage) + self.ui.gradientWidget.sigGradientChanged.connect(self.updateImage) + #QtCore.QObject.connect(self.ui.roiBtn, QtCore.SIGNAL('clicked()'), self.roiClicked) + self.ui.roiBtn.clicked.connect(self.roiClicked) + #self.roi.connect(self.roi, QtCore.SIGNAL('regionChanged'), self.roiChanged) + self.roi.sigRegionChanged.connect(self.roiChanged) + #QtCore.QObject.connect(self.ui.normBtn, QtCore.SIGNAL('toggled(bool)'), self.normToggled) + self.ui.normBtn.toggled.connect(self.normToggled) + #QtCore.QObject.connect(self.ui.normDivideRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) + self.ui.normDivideRadio.clicked.connect(self.updateNorm) + #QtCore.QObject.connect(self.ui.normSubtractRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) + self.ui.normSubtractRadio.clicked.connect(self.updateNorm) + #QtCore.QObject.connect(self.ui.normOffRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) + self.ui.normOffRadio.clicked.connect(self.updateNorm) + #QtCore.QObject.connect(self.ui.normROICheck, QtCore.SIGNAL('clicked()'), self.updateNorm) + self.ui.normROICheck.clicked.connect(self.updateNorm) + #QtCore.QObject.connect(self.ui.normFrameCheck, QtCore.SIGNAL('clicked()'), self.updateNorm) + self.ui.normFrameCheck.clicked.connect(self.updateNorm) + #QtCore.QObject.connect(self.ui.normTimeRangeCheck, QtCore.SIGNAL('clicked()'), self.updateNorm) + self.ui.normTimeRangeCheck.clicked.connect(self.updateNorm) + #QtCore.QObject.connect(self.playTimer, QtCore.SIGNAL('timeout()'), self.timeout) + self.playTimer.timeout.connect(self.timeout) ##QtCore.QObject.connect(self.ui.normStartSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm) #QtCore.QObject.connect(self.ui.normStopSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm) - self.normProxy = proxyConnect(self.normRgn, QtCore.SIGNAL('regionChanged'), self.updateNorm) - self.normRoi.connect(self.normRoi, QtCore.SIGNAL('regionChangeFinished'), self.updateNorm) + self.normProxy = proxyConnect(None, self.normRgn.sigRegionChanged, self.updateNorm) + #self.normRoi.connect(self.normRoi, QtCore.SIGNAL('regionChangeFinished'), self.updateNorm) + self.normRoi.sigRegionChangeFinished.connect(self.updateNorm) self.ui.roiPlot.registerPlot(self.name + '_ROI') @@ -135,11 +151,16 @@ class ImageView(QtGui.QWidget): #self.quit() #QtGui.QWidget.__dtor__(self) - def quit(self): + def close(self): + self.ui.graphicsView.close() + self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage) self.scene.clear() del self.image del self.imageDisp - + #self.image = None + #self.imageDisp = None + self.ui.roiPlot.close() + self.setParent(None) def keyPressEvent(self, ev): if ev.key() == QtCore.Qt.Key_Space: @@ -319,7 +340,6 @@ class ImageView(QtGui.QWidget): axes: {'t':0, 'x':1, 'y':2, 'c':3}; Dictionary indicating the interpretation for each axis. This is only needed to override the default guess. """ - if not isinstance(img, np.ndarray): raise Exception("Image must be specified as ndarray.") self.image = img @@ -348,7 +368,7 @@ class ImageView(QtGui.QWidget): elif img.ndim == 4: self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} else: - raise Exception("Can not interpret image with dimensions %s" % (str(img))) + raise Exception("Can not interpret image with dimensions %s" % (str(img.shape))) elif isinstance(axes, dict): self.axes = axes.copy() elif isinstance(axes, list) or isinstance(axes, tuple): @@ -441,7 +461,7 @@ class ImageView(QtGui.QWidget): #else: #norm = zeros(image.shape) if div: - norm = norm.astype(float32) + norm = norm.astype(np.float32) if self.ui.normTimeRangeCheck.isChecked() and image.ndim == 3: (sind, start) = self.timeIndex(self.normRgn.lines[0]) @@ -464,7 +484,7 @@ class ImageView(QtGui.QWidget): if self.ui.normROICheck.isChecked() and image.ndim == 3: n = self.normRoi.getArrayRegion(norm, self.imageItem, (1, 2)).mean(axis=1).mean(axis=1) - n = n[:,newaxis,newaxis] + n = n[:,np.newaxis,np.newaxis] #print start, end, sind, eind if div: norm /= n @@ -483,7 +503,8 @@ class ImageView(QtGui.QWidget): self.currentIndex = ind self.updateImage() #self.timeLine.setPos(time) - self.emit(QtCore.SIGNAL('timeChanged'), ind, time) + #self.emit(QtCore.SIGNAL('timeChanged'), ind, time) + self.sigTimeChanged.emit(ind, time) def updateImage(self): ## Redraw image on screen diff --git a/MultiPlotItem.py b/MultiPlotItem.py index 83f08505..81fae566 100644 --- a/MultiPlotItem.py +++ b/MultiPlotItem.py @@ -63,4 +63,7 @@ class MultiPlotItem(QtGui.QGraphicsWidget): else: raise Exception("Data type %s not (yet?) supported for MultiPlot." % type(data)) + def close(self): + for p in self.plots: + p[0].close() \ No newline at end of file diff --git a/MultiPlotWidget.py b/MultiPlotWidget.py index 2791de67..0bf576f9 100644 --- a/MultiPlotWidget.py +++ b/MultiPlotWidget.py @@ -37,3 +37,7 @@ class MultiPlotWidget(GraphicsView): def restoreState(self, state): pass #return self.plotItem.restoreState(state) + + def close(self): + self.mPlotItem.close() + self.setParent(None) \ No newline at end of file diff --git a/ObjectWorkaround.py b/ObjectWorkaround.py deleted file mode 100644 index 7573b64f..00000000 --- a/ObjectWorkaround.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -from PyQt4 import QtGui, QtCore -"""For circumventing PyQt's lack of multiple inheritance (just until PySide becomes stable)""" - - -class Obj(QtCore.QObject): - def event(self, ev): - self.emit(QtCore.SIGNAL('event'), ev) - return QtCore.QObject.event(self, ev) - -class QObjectWorkaround: - def __init__(self): - self._qObj_ = Obj() - self.connect(QtCore.SIGNAL('event'), self.event) - def connect(self, *args): - if args[0] is self: - return QtCore.QObject.connect(self._qObj_, *args[1:]) - else: - return QtCore.QObject.connect(self._qObj_, *args) - def disconnect(self, *args): - return QtCore.QObject.disconnect(self._qObj_, *args) - def emit(self, *args): - return QtCore.QObject.emit(self._qObj_, *args) - def blockSignals(self, b): - return self._qObj_.blockSignals(b) - def setProperty(self, prop, val): - return self._qObj_.setProperty(prop, val) - def property(self, prop): - return self._qObj_.property(prop) - def event(self, ev): - pass - -#class QGraphicsObject(QtGui.QGraphicsItem, QObjectWorkaround): - #def __init__(self, *args): - #QtGui.QGraphicsItem.__init__(self, *args) - #QObjectWorkaround.__init__(self) - -class QGraphicsObject(QtGui.QGraphicsWidget): - def shape(self): - return QtGui.QGraphicsItem.shape(self) - -#QGraphicsObject = QtGui.QGraphicsObject \ No newline at end of file diff --git a/PlotItem.py b/PlotItem.py index 286e621e..49835bd2 100644 --- a/PlotItem.py +++ b/PlotItem.py @@ -25,6 +25,7 @@ from functions import * #tryWorkaround(QtCore, QtGui) import weakref import numpy as np +#import debug try: from WidgetGroup import * @@ -40,6 +41,11 @@ except: class PlotItem(QtGui.QGraphicsWidget): + + sigYRangeChanged = QtCore.Signal(object, object) + sigXRangeChanged = QtCore.Signal(object, object) + sigRangeChanged = QtCore.Signal(object, object) + """Plot graphics item that can be added to any graphics scene. Implements axis titles, scales, interactive viewbox.""" lastFileDir = None managers = {} @@ -60,8 +66,10 @@ class PlotItem(QtGui.QGraphicsWidget): proxy.setWidget(b) proxy.setAcceptHoverEvents(False) b.setStyleSheet("background-color: #000000; color: #888; font-size: 6pt") - QtCore.QObject.connect(self.ctrlBtn, QtCore.SIGNAL('clicked()'), self.ctrlBtnClicked) - QtCore.QObject.connect(self.autoBtn, QtCore.SIGNAL('clicked()'), self.enableAutoScale) + #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) + self.autoBtn.clicked.connect(self.enableAutoScale) self.layout = QtGui.QGraphicsGridLayout() @@ -71,11 +79,15 @@ class PlotItem(QtGui.QGraphicsWidget): self.layout.setVerticalSpacing(0) self.vb = ViewBox() - QtCore.QObject.connect(self.vb, QtCore.SIGNAL('xRangeChanged'), self.xRangeChanged) - QtCore.QObject.connect(self.vb, QtCore.SIGNAL('yRangeChanged'), self.yRangeChanged) - QtCore.QObject.connect(self.vb, QtCore.SIGNAL('rangeChangedManually'), self.enableManualScale) + #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('xRangeChanged'), self.xRangeChanged) + self.vb.sigXRangeChanged.connect(self.xRangeChanged) + #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('yRangeChanged'), self.yRangeChanged) + self.vb.sigYRangeChanged.connect(self.yRangeChanged) + #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('rangeChangedManually'), self.enableManualScale) + self.vb.sigRangeChangedManually.connect(self.enableManualScale) - QtCore.QObject.connect(self.vb, QtCore.SIGNAL('viewChanged'), self.viewChanged) + #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('viewChanged'), self.viewChanged) + self.vb.sigRangeChanged.connect(self.viewRangeChanged) self.layout.addItem(self.vb, 2, 1) self.alpha = 1.0 @@ -161,53 +173,81 @@ class PlotItem(QtGui.QGraphicsWidget): self.setAcceptHoverEvents(True) ## Connect control widgets - QtCore.QObject.connect(c.xMinText, QtCore.SIGNAL('editingFinished()'), self.setManualXScale) - QtCore.QObject.connect(c.xMaxText, QtCore.SIGNAL('editingFinished()'), self.setManualXScale) - QtCore.QObject.connect(c.yMinText, QtCore.SIGNAL('editingFinished()'), self.setManualYScale) - QtCore.QObject.connect(c.yMaxText, QtCore.SIGNAL('editingFinished()'), self.setManualYScale) + #QtCore.QObject.connect(c.xMinText, QtCore.SIGNAL('editingFinished()'), self.setManualXScale) + c.xMinText.editingFinished.connect(self.setManualXScale) + #QtCore.QObject.connect(c.xMaxText, QtCore.SIGNAL('editingFinished()'), self.setManualXScale) + c.xMaxText.editingFinished.connect(self.setManualXScale) + #QtCore.QObject.connect(c.yMinText, QtCore.SIGNAL('editingFinished()'), self.setManualYScale) + c.yMinText.editingFinished.connect(self.setManualYScale) + #QtCore.QObject.connect(c.yMaxText, QtCore.SIGNAL('editingFinished()'), self.setManualYScale) + c.yMaxText.editingFinished.connect(self.setManualYScale) - QtCore.QObject.connect(c.xManualRadio, QtCore.SIGNAL('clicked()'), self.updateXScale) - QtCore.QObject.connect(c.yManualRadio, QtCore.SIGNAL('clicked()'), self.updateYScale) + #QtCore.QObject.connect(c.xManualRadio, QtCore.SIGNAL('clicked()'), self.updateXScale) + c.xManualRadio.clicked.connect(lambda: self.updateXScale()) + #QtCore.QObject.connect(c.yManualRadio, QtCore.SIGNAL('clicked()'), self.updateYScale) + c.yManualRadio.clicked.connect(lambda: self.updateYScale()) - QtCore.QObject.connect(c.xAutoRadio, QtCore.SIGNAL('clicked()'), self.updateXScale) - QtCore.QObject.connect(c.yAutoRadio, QtCore.SIGNAL('clicked()'), self.updateYScale) + #QtCore.QObject.connect(c.xAutoRadio, QtCore.SIGNAL('clicked()'), self.updateXScale) + c.xAutoRadio.clicked.connect(self.updateXScale) + #QtCore.QObject.connect(c.yAutoRadio, QtCore.SIGNAL('clicked()'), self.updateYScale) + c.yAutoRadio.clicked.connect(self.updateYScale) - QtCore.QObject.connect(c.xAutoPercentSpin, QtCore.SIGNAL('valueChanged(int)'), self.replot) - QtCore.QObject.connect(c.yAutoPercentSpin, QtCore.SIGNAL('valueChanged(int)'), self.replot) + #QtCore.QObject.connect(c.xAutoPercentSpin, QtCore.SIGNAL('valueChanged(int)'), self.replot) + c.xAutoPercentSpin.valueChanged.connect(self.replot) + #QtCore.QObject.connect(c.yAutoPercentSpin, QtCore.SIGNAL('valueChanged(int)'), self.replot) + c.yAutoPercentSpin.valueChanged.connect(self.replot) #QtCore.QObject.connect(c.xLogCheck, QtCore.SIGNAL('toggled(bool)'), self.setXLog) #QtCore.QObject.connect(c.yLogCheck, QtCore.SIGNAL('toggled(bool)'), self.setYLog) - QtCore.QObject.connect(c.alphaGroup, QtCore.SIGNAL('toggled(bool)'), self.updateAlpha) - QtCore.QObject.connect(c.alphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateAlpha) - QtCore.QObject.connect(c.autoAlphaCheck, QtCore.SIGNAL('toggled(bool)'), self.updateAlpha) + #QtCore.QObject.connect(c.alphaGroup, QtCore.SIGNAL('toggled(bool)'), self.updateAlpha) + c.alphaGroup.toggled.connect(self.updateAlpha) + #QtCore.QObject.connect(c.alphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateAlpha) + c.alphaSlider.valueChanged.connect(self.updateAlpha) + #QtCore.QObject.connect(c.autoAlphaCheck, QtCore.SIGNAL('toggled(bool)'), self.updateAlpha) + c.autoAlphaCheck.toggled.connect(self.updateAlpha) - QtCore.QObject.connect(c.gridGroup, QtCore.SIGNAL('toggled(bool)'), self.updateGrid) - QtCore.QObject.connect(c.gridAlphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateGrid) + #QtCore.QObject.connect(c.gridGroup, QtCore.SIGNAL('toggled(bool)'), self.updateGrid) + c.gridGroup.toggled.connect(self.updateGrid) + #QtCore.QObject.connect(c.gridAlphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateGrid) + c.gridAlphaSlider.valueChanged.connect(self.updateGrid) - QtCore.QObject.connect(c.powerSpectrumGroup, QtCore.SIGNAL('toggled(bool)'), self.updateSpectrumMode) - QtCore.QObject.connect(c.saveSvgBtn, QtCore.SIGNAL('clicked()'), self.saveSvgClicked) - QtCore.QObject.connect(c.saveImgBtn, QtCore.SIGNAL('clicked()'), self.saveImgClicked) - QtCore.QObject.connect(c.saveCsvBtn, QtCore.SIGNAL('clicked()'), self.saveCsvClicked) + #QtCore.QObject.connect(c.powerSpectrumGroup, QtCore.SIGNAL('toggled(bool)'), self.updateSpectrumMode) + c.powerSpectrumGroup.toggled.connect(self.updateSpectrumMode) + #QtCore.QObject.connect(c.saveSvgBtn, QtCore.SIGNAL('clicked()'), self.saveSvgClicked) + c.saveSvgBtn.clicked.connect(self.saveSvgClicked) + #QtCore.QObject.connect(c.saveImgBtn, QtCore.SIGNAL('clicked()'), self.saveImgClicked) + c.saveImgBtn.clicked.connect(self.saveImgClicked) + #QtCore.QObject.connect(c.saveCsvBtn, QtCore.SIGNAL('clicked()'), self.saveCsvClicked) + c.saveCsvBtn.clicked.connect(self.saveCsvClicked) #QtCore.QObject.connect(c.gridGroup, QtCore.SIGNAL('toggled(bool)'), self.updateGrid) #QtCore.QObject.connect(c.gridAlphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateGrid) - QtCore.QObject.connect(self.ctrl.xLinkCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.xLinkComboChanged) - QtCore.QObject.connect(self.ctrl.yLinkCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.yLinkComboChanged) + #QtCore.QObject.connect(self.ctrl.xLinkCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.xLinkComboChanged) + self.ctrl.xLinkCombo.currentIndexChanged.connect(self.xLinkComboChanged) + #QtCore.QObject.connect(self.ctrl.yLinkCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.yLinkComboChanged) + self.ctrl.yLinkCombo.currentIndexChanged.connect(self.yLinkComboChanged) - QtCore.QObject.connect(c.downsampleSpin, QtCore.SIGNAL('valueChanged(int)'), self.updateDownsampling) + #QtCore.QObject.connect(c.downsampleSpin, QtCore.SIGNAL('valueChanged(int)'), self.updateDownsampling) + c.downsampleSpin.valueChanged.connect(self.updateDownsampling) - QtCore.QObject.connect(self.ctrl.avgParamList, QtCore.SIGNAL('itemClicked(QListWidgetItem*)'), self.avgParamListClicked) - QtCore.QObject.connect(self.ctrl.averageGroup, QtCore.SIGNAL('toggled(bool)'), self.avgToggled) + #QtCore.QObject.connect(self.ctrl.avgParamList, QtCore.SIGNAL('itemClicked(QListWidgetItem*)'), self.avgParamListClicked) + self.ctrl.avgParamList.itemClicked.connect(self.avgParamListClicked) + #QtCore.QObject.connect(self.ctrl.averageGroup, QtCore.SIGNAL('toggled(bool)'), self.avgToggled) + self.ctrl.averageGroup.toggled.connect(self.avgToggled) #QtCore.QObject.connect(self.ctrl.pointsGroup, QtCore.SIGNAL('toggled(bool)'), self.updatePointMode) #QtCore.QObject.connect(self.ctrl.autoPointsCheck, QtCore.SIGNAL('toggled(bool)'), self.updatePointMode) - QtCore.QObject.connect(self.ctrl.maxTracesCheck, QtCore.SIGNAL('toggled(bool)'), self.updateDecimation) - QtCore.QObject.connect(self.ctrl.maxTracesSpin, QtCore.SIGNAL('valueChanged(int)'), self.updateDecimation) - QtCore.QObject.connect(c.xMouseCheck, QtCore.SIGNAL('toggled(bool)'), self.mouseCheckChanged) - QtCore.QObject.connect(c.yMouseCheck, QtCore.SIGNAL('toggled(bool)'), self.mouseCheckChanged) + #QtCore.QObject.connect(self.ctrl.maxTracesCheck, QtCore.SIGNAL('toggled(bool)'), self.updateDecimation) + self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) + #QtCore.QObject.connect(self.ctrl.maxTracesSpin, QtCore.SIGNAL('valueChanged(int)'), self.updateDecimation) + self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) + #QtCore.QObject.connect(c.xMouseCheck, QtCore.SIGNAL('toggled(bool)'), self.mouseCheckChanged) + c.xMouseCheck.toggled.connect(self.mouseCheckChanged) + #QtCore.QObject.connect(c.yMouseCheck, QtCore.SIGNAL('toggled(bool)'), self.mouseCheckChanged) + c.yMouseCheck.toggled.connect(self.mouseCheckChanged) self.xLinkPlot = None self.yLinkPlot = None @@ -236,9 +276,16 @@ class PlotItem(QtGui.QGraphicsWidget): if len(kargs) > 0: self.plot(**kargs) - - def __del__(self): + #def paint(self, *args): + #prof = debug.Profiler('PlotItem.paint', disabled=True) + #QtGui.QGraphicsWidget.paint(self, *args) + #prof.finish() + + + def close(self): + #print "delete", self if self.manager is not None: + self.manager.sigWidgetListChanged.disconnect(self.updatePlotList) self.manager.removeWidget(self.name) def registerPlot(self, name): @@ -249,7 +296,8 @@ class PlotItem(QtGui.QGraphicsWidget): PlotItem.managers[win] = PlotWidgetManager() self.manager = PlotItem.managers[win] self.manager.addWidget(self, name) - QtCore.QObject.connect(self.manager, QtCore.SIGNAL('widgetListChanged'), self.updatePlotList) + #QtCore.QObject.connect(self.manager, QtCore.SIGNAL('widgetListChanged'), self.updatePlotList) + self.manager.sigWidgetListChanged.connect(self.updatePlotList) self.updatePlotList() def updatePlotList(self): @@ -269,7 +317,8 @@ class PlotItem(QtGui.QGraphicsWidget): except: import gc refs= gc.get_referrers(self) - print " error during update. Referrers are:", refs + print " error during update of", self + print " Referrers are:", refs raise def updateGrid(self, *args): @@ -291,8 +340,9 @@ class PlotItem(QtGui.QGraphicsWidget): - def viewChanged(self, *args): - self.emit(QtCore.SIGNAL('viewChanged'), *args) + def viewRangeChanged(self, vb, range): + #self.emit(QtCore.SIGNAL('viewChanged'), *args) + self.sigRangeChanged.emit(self, range) def blockLink(self, b): self.linksBlocked = b @@ -466,7 +516,8 @@ class PlotItem(QtGui.QGraphicsWidget): #self.setLabel(l, unitPrefix='') #self.getScale(l).setScale(1.0) - self.emit(QtCore.SIGNAL('xRangeChanged'), self, range) + #self.emit(QtCore.SIGNAL('xRangeChanged'), self, range) + self.sigXRangeChanged.emit(self, range) def yRangeChanged(self, _, range): if any(np.isnan(range)) or any(np.isinf(range)): @@ -484,7 +535,8 @@ class PlotItem(QtGui.QGraphicsWidget): #else: #self.setLabel(l, unitPrefix='') #self.getScale(l).setScale(1.0) - self.emit(QtCore.SIGNAL('yRangeChanged'), self, range) + #self.emit(QtCore.SIGNAL('yRangeChanged'), self, range) + self.sigYRangeChanged.emit(self, range) def enableAutoScale(self): @@ -568,7 +620,8 @@ class PlotItem(QtGui.QGraphicsWidget): self.curves.remove(item) self.updateDecimation() self.updateParamList() - item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) + #item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) + item.sigPlotChanged.connect(self.plotChanged) def clear(self): for i in self.items[:]: @@ -644,7 +697,8 @@ class PlotItem(QtGui.QGraphicsWidget): if self.ctrl.averageGroup.isChecked(): self.addAvgCurve(c) - c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) + #c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) + c.sigPlotChanged.connect(self.plotChanged) self.plotChanged() def plotChanged(self, curve=None): @@ -704,60 +758,141 @@ class PlotItem(QtGui.QGraphicsWidget): self.paramList[p] = (i.checkState() == QtCore.Qt.Checked) #print "paramList:", self.paramList + + ## This is bullshit. def writeSvg(self, fileName=None): if fileName is None: fileName = QtGui.QFileDialog.getSaveFileName() + if isinstance(fileName, tuple): + raise Exception("Not implemented yet..") fileName = str(fileName) PlotItem.lastFileDir = os.path.dirname(fileName) - self.svg = QtSvg.QSvgGenerator() - self.svg.setFileName(fileName) - res = 120. - #bounds = self.mapRectToScene(self.boundingRect()) - view = self.scene().views()[0] - bounds = view.viewport().rect() - bounds = QtCore.QRectF(0, 0, bounds.width(), bounds.height()) + rect = self.vb.viewRect() + xRange = rect.left(), rect.right() - self.svg.setResolution(res) - #self.svg.setSize(QtCore.QSize(self.size().width(), self.size().height())) - self.svg.setViewBox(bounds) + svg = "" + fh = open(fileName, 'w') + + dx = max(rect.right(),0) - min(rect.left(),0) + ymn = min(rect.top(), rect.bottom()) + ymx = max(rect.top(), rect.bottom()) + dy = max(ymx,0) - min(ymn,0) + sx = 1. + sy = 1. + while dx*sx < 10: + sx *= 1000 + while dy*sy < 10: + sy *= 1000 + sy *= -1 + + #fh.write('\n' % (rect.left()*sx, rect.top()*sx, rect.width()*sy, rect.height()*sy)) + fh.write('\n') + fh.write('\n' % (rect.left()*sx, rect.right()*sx)) + fh.write('\n' % (rect.top()*sy, rect.bottom()*sy)) + + + for item in self.curves: + if isinstance(item, PlotCurveItem): + color = colorStr(item.pen.color()) + opacity = item.pen.color().alpha() / 255. + color = color[:6] + x, y = item.getData() + mask = (x > xRange[0]) * (x < xRange[1]) + mask[:-1] += mask[1:] + m2 = mask.copy() + mask[1:] += m2[:-1] + x = x[mask] + y = y[mask] + + x *= sx + y *= sy + + #fh.write('\n' % color) + fh.write('') + #fh.write("") + for item in self.dataItems: + if isinstance(item, ScatterPlotItem): + + pRect = item.boundingRect() + vRect = pRect.intersected(rect) + + for point in item.points(): + pos = point.pos() + if not rect.contains(pos): + continue + color = colorStr(point.brush.color()) + opacity = point.brush.color().alpha() / 255. + color = color[:6] + x = pos.x() * sx + y = pos.y() * sy + + fh.write('\n' % (x, y, color, opacity)) + #fh.write('') + + ## get list of curves, scatter plots - self.svg.setSize(QtCore.QSize(bounds.width(), bounds.height())) - painter = QtGui.QPainter(self.svg) - #self.scene().render(painter, QtCore.QRectF(), view.mapToScene(bounds).boundingRect()) + fh.write("\n") - #items = self.scene().items() - #self.scene().views()[0].drawItems(painter, len(items), items) - #print view, type(view) - view.render(painter, bounds) + + #def writeSvg(self, fileName=None): + #if fileName is None: + #fileName = QtGui.QFileDialog.getSaveFileName() + #fileName = str(fileName) + #PlotItem.lastFileDir = os.path.dirname(fileName) - painter.end() + #self.svg = QtSvg.QSvgGenerator() + #self.svg.setFileName(fileName) + #res = 120. + #view = self.scene().views()[0] + #bounds = view.viewport().rect() + #bounds = QtCore.QRectF(0, 0, bounds.width(), bounds.height()) - ## Workaround to set pen widths correctly - import re - data = open(fileName).readlines() - for i in range(len(data)): - line = data[i] - m = re.match(r'(= self.waitUntil: - return self.flush() - else: - if self.timers == 0: - self.timers += 1 - QtCore.QTimer.singleShot((self.waitUntil - t) * 1000, self.tryEmit) - return True - - def disconnect(self): self.block = True - \ No newline at end of file + + +def proxyConnect(source, signal, slot, delay=0.3): + """Connect a signal to a slot with delay. Returns the SignalProxy + object that was created. Be sure to store this object so it is not + garbage-collected immediately.""" + sp = SignalProxy(source, signal, delay) + if source is None: + sp.connect(sp, QtCore.SIGNAL('signal'), slot) + else: + sp.connect(sp, signal, slot) + return sp + + +if __name__ == '__main__': + from PyQt4 import QtGui + app = QtGui.QApplication([]) + win = QtGui.QMainWindow() + spin = QtGui.QSpinBox() + win.setCentralWidget(spin) + win.show() + + def fn(*args): + print "Got signal:", args + + proxy = proxyConnect(spin, QtCore.SIGNAL('valueChanged(int)'), fn) + + \ No newline at end of file diff --git a/examples/test_ImageItem.py b/examples/test_ImageItem.py index 323edb9d..71466658 100644 --- a/examples/test_ImageItem.py +++ b/examples/test_ImageItem.py @@ -3,17 +3,16 @@ import sys, os sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) -from pyqtgraph.GraphicsView import * -from pyqtgraph.graphicsItems import * -from numpy import random + from PyQt4 import QtCore, QtGui -from scipy.ndimage import * +import numpy as np +import pyqtgraph as pg app = QtGui.QApplication([]) ## Create window with GraphicsView widget win = QtGui.QMainWindow() -view = GraphicsView() +view = pg.GraphicsView() #view.useOpenGL(True) win.setCentralWidget(view) win.show() @@ -25,26 +24,29 @@ view.enableMouse() view.setAspectLocked(True) ## Create image item -img = ImageItem() +img = pg.ImageItem() view.scene().addItem(img) ## Set initial view bounds view.setRange(QtCore.QRectF(0, 0, 200, 200)) +## Create random image +data = np.random.normal(size=(50, 200, 200)) +i = 0 + def updateData(): - global img - ## Create random image - data = random.random((200, 200)) + global img, data, i ## Display the data - img.updateImage(data) + img.updateImage(data[i]) + i = (i+1) % data.shape[0] # update image data every 20ms (or so) t = QtCore.QTimer() -QtCore.QObject.connect(t, QtCore.SIGNAL('timeout()'), updateData) +t.timeout.connect(updateData) t.start(20) - - -app.exec_() +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/test_ImageView.py b/examples/test_ImageView.py index 25b9cd64..226dc81f 100644 --- a/examples/test_ImageView.py +++ b/examples/test_ImageView.py @@ -3,24 +3,25 @@ import sys, os sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) -from pyqtgraph.ImageView import * -from numpy import random, linspace + +import numpy as np +import scipy from PyQt4 import QtCore, QtGui -from scipy.ndimage import * +import pyqtgraph as pg app = QtGui.QApplication([]) ## Create window with ImageView widget win = QtGui.QMainWindow() -imv = ImageView() +imv = pg.ImageView() win.setCentralWidget(imv) win.show() -## Create random 3D data set -img = gaussian_filter(random.normal(size=(200, 200)), (5, 5)) * 20 + 100 -img = img[newaxis,:,:] -decay = exp(-linspace(0,0.3,100))[:,newaxis,newaxis] -data = random.normal(size=(100, 200, 200)) +## Create random 3D data set with noisy signals +img = scipy.ndimage.gaussian_filter(np.random.normal(size=(200, 200)), (5, 5)) * 20 + 100 +img = img[np.newaxis,:,:] +decay = np.exp(-np.linspace(0,0.3,100))[:,np.newaxis,np.newaxis] +data = np.random.normal(size=(100, 200, 200)) data += img * decay #for i in range(data.shape[0]): @@ -28,16 +29,18 @@ data += img * decay data += 2 ## Add time-varying signal -sig = zeros(data.shape[0]) -sig[30:] += exp(-linspace(1,10, 70)) -sig[40:] += exp(-linspace(1,10, 60)) -sig[70:] += exp(-linspace(1,10, 30)) +sig = np.zeros(data.shape[0]) +sig[30:] += np.exp(-np.linspace(1,10, 70)) +sig[40:] += np.exp(-np.linspace(1,10, 60)) +sig[70:] += np.exp(-np.linspace(1,10, 30)) -sig = sig[:,newaxis,newaxis] * 3 +sig = sig[:,np.newaxis,np.newaxis] * 3 data[:,50:60,50:60] += sig ## Display the data -imv.setImage(data, xvals=linspace(1., 3., data.shape[0])) +imv.setImage(data, xvals=np.linspace(1., 3., data.shape[0])) -app.exec_() +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/test_MultiPlotWidget.py b/examples/test_MultiPlotWidget.py index c24053ea..690e367b 100755 --- a/examples/test_MultiPlotWidget.py +++ b/examples/test_MultiPlotWidget.py @@ -4,14 +4,16 @@ import sys, os sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) + from scipy import random from numpy import linspace from PyQt4 import QtGui, QtCore -from pyqtgraph.MultiPlotWidget import * +import pyqtgraph as pg +from pyqtgraph.MultiPlotWidget import MultiPlotWidget try: from metaarray import * except: - print "MultiPlot is only used with MetaArray for now (and you do not have the metaarray module)" + print "MultiPlot is only used with MetaArray for now (and you do not have the metaarray package)" exit() app = QtGui.QApplication([]) @@ -23,4 +25,7 @@ mw.show() ma = MetaArray(random.random((3, 1000)), info=[{'name': 'Signal', 'cols': [{'name': 'Col1'}, {'name': 'Col2'}, {'name': 'Col3'}]}, {'name': 'Time', 'vals': linspace(0., 1., 1000)}]) pw.plot(ma) -app.exec_() \ No newline at end of file +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() + diff --git a/examples/test_PlotWidget.py b/examples/test_PlotWidget.py index 8c98ca22..cfd314ff 100755 --- a/examples/test_PlotWidget.py +++ b/examples/test_PlotWidget.py @@ -4,11 +4,10 @@ import sys, os sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) -from scipy import random -from PyQt4 import QtGui, QtCore -from pyqtgraph.PlotWidget import * -from pyqtgraph.graphicsItems import * +from PyQt4 import QtGui, QtCore +import numpy as np +import pyqtgraph as pg app = QtGui.QApplication([]) mw = QtGui.QMainWindow() @@ -17,77 +16,56 @@ mw.setCentralWidget(cw) l = QtGui.QVBoxLayout() cw.setLayout(l) -pw = PlotWidget() +pw = pg.PlotWidget(name='Plot1') ## giving the plots names allows us to link their axes together l.addWidget(pw) -pw2 = PlotWidget() +pw2 = pg.PlotWidget(name='Plot2') l.addWidget(pw2) -pw3 = PlotWidget() +pw3 = pg.PlotWidget() l.addWidget(pw3) -pw.registerPlot('Plot1') -pw2.registerPlot('Plot2') +mw.show() -#p1 = PlotCurveItem() -#pw.addItem(p1) +## Create an empty plot curve to be filled later, set its pen p1 = pw.plot() +p1.setPen((200,200,100)) + +## Add in some extra graphics rect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, 0, 1, 1)) rect.setPen(QtGui.QPen(QtGui.QColor(100, 200, 100))) pw.addItem(rect) -#pen = QtGui.QPen(QtGui.QBrush(QtGui.QColor(255, 255, 255, 50)), 5) -#pen.setCosmetic(True) -#pen.setJoinStyle(QtCore.Qt.MiterJoin) -#p1.setShadowPen(pen) -p1.setPen(QtGui.QPen(QtGui.QColor(255, 255, 255))) - -#l1 = QtGui.QGraphicsLineItem(0, 2, 2, 3) -#l1.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) - -#l2 = InfiniteLine(pw2, 1.5, 90, movable=True) -# -#lr1 = LinearRegionItem(pw2, 'vertical', [1.1, 1.3]) -#pw2.addItem(lr1) -#lr2 = LinearRegionItem(pw2, 'horizontal', [50, 100]) -#pw2.addItem(lr2) - - -#l3 = InfiniteLine(pw, [1.5, 1.5], 45) -#pw.addItem(l1) -#pw2.addItem(l2) -#pw.addItem(l3) - -pw3.plot(array([100000]*100)) - - -mw.show() - def rand(n): - data = random.random(n) + data = np.random.random(n) data[int(n*0.1):int(n*0.13)] += .5 data[int(n*0.18)] += 2 data[int(n*0.1):int(n*0.13)] *= 5 data[int(n*0.18)] *= 20 - return data, arange(n, n+len(data)) / float(n) + data *= 1e-12 + return data, np.arange(n, n+len(data)) / float(n) def updateData(): yd, xd = rand(10000) p1.updateData(yd, x=xd) -yd, xd = rand(10000) -updateData() -pw.autoRange() - +## Start a timer to rapidly update the plot in pw t = QtCore.QTimer() - -QtCore.QObject.connect(t, QtCore.SIGNAL('timeout()'), updateData) +t.timeout.connect(updateData) t.start(50) - +## Multiple parameterized plots--we can autogenerate averages for these. for i in range(0, 5): for j in range(0, 3): yd, xd = rand(10000) pw2.plot(y=yd*(j+1), x=xd, params={'iter': i, 'val': j}) - -#app.exec_() + +## Test large numbers +curve = pw3.plot(np.random.normal(size=100)*1e6) +curve.setPen('w') ## white pen +curve.setShadowPen(pg.mkPen((70,70,30), width=6, cosmetic=True)) + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/test_ROItypes.py b/examples/test_ROItypes.py index eb9fe7a8..1354f4ff 100755 --- a/examples/test_ROItypes.py +++ b/examples/test_ROItypes.py @@ -4,32 +4,26 @@ import sys, os sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) -from numpy import random -from scipy import zeros, ones -from pyqtgraph.graphicsWindows import * -from pyqtgraph.graphicsItems import * -from pyqtgraph.widgets import * -from pyqtgraph.PlotWidget import * -from pyqtgraph.functions import mkPen + from PyQt4 import QtCore, QtGui +import numpy as np +import pyqtgraph as pg +## create GUI app = QtGui.QApplication([]) - -class Win(QtGui.QMainWindow): - pass - -w = Win() -v = GraphicsView(useOpenGL=False) -v.invertY(True) +w = QtGui.QMainWindow() +v = pg.GraphicsView() +v.invertY(True) ## Images usually have their Y-axis pointing downward v.setAspectLocked(True) v.enableMouse(True) v.autoPixelScale = False - w.setCentralWidget(v) s = v.scene() v.setRange(QtCore.QRect(-2, -2, 220, 220)) +w.show() -arr = ones((100, 100), dtype=float) +## Create image to display +arr = np.ones((100, 100), dtype=float) arr[45:55, 45:55] = 0 arr[25, :] = 5 arr[:, 25] = 5 @@ -38,21 +32,23 @@ arr[:, 75] = 5 arr[50, :] = 10 arr[:, 50] = 10 -im1 = ImageItem(arr) -im2 = ImageItem(arr) +## Create image items, add to scene and set position +im1 = pg.ImageItem(arr) +im2 = pg.ImageItem(arr) s.addItem(im1) s.addItem(im2) im2.moveBy(110, 20) -im3 = ImageItem() +im3 = pg.ImageItem() s.addItem(im3) im3.moveBy(0, 130) im3.setZValue(10) -im4 = ImageItem() +im4 = pg.ImageItem() s.addItem(im4) im4.moveBy(110, 130) im4.setZValue(10) -pi1 = PlotItem() +## create the plot +pi1 = pg.PlotItem() s.addItem(pi1) pi1.scale(0.5, 0.5) pi1.setGeometry(0, 170, 300, 100) @@ -76,34 +72,39 @@ def updateRoiPlot(roi, data=None): if data is not None: roi.curve.updateData(data.mean(axis=1)) -#def updatePlot(roi) +## Create a variety of different ROI types rois = [] -rois.append(TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=mkPen(0))) -rois.append(LineROI([0, 0], [20, 20], width=5, pen=mkPen(1))) -rois.append(MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=mkPen(2))) -rois.append(EllipseROI([110, 10], [30, 20], pen=mkPen(3))) -rois.append(CircleROI([110, 50], [20, 20], pen=mkPen(4))) -rois.append(PolygonROI([[2,0], [2.1,0], [2,.1]], pen=mkPen(5))) +rois.append(pg.widgets.TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=(0,9))) +rois.append(pg.widgets.LineROI([0, 0], [20, 20], width=5, pen=(1,9))) +rois.append(pg.widgets.MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9))) +rois.append(pg.widgets.EllipseROI([110, 10], [30, 20], pen=(3,9))) +rois.append(pg.widgets.CircleROI([110, 50], [20, 20], pen=(4,9))) +rois.append(pg.widgets.PolygonROI([[2,0], [2.1,0], [2,.1]], pen=(5,9))) #rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0))) + +## Add each ROI to the scene and link its data to a plot curve with the same color for r in rois: s.addItem(r) c = pi1.plot(pen=r.pen) r.curve = c - r.connect(r, QtCore.SIGNAL('regionChanged'), updateRoi) + r.sigRegionChanged.connect(updateRoi) def updateImage(): global im1, arr, lastRoi - r = abs(random.normal(loc=0, scale=(arr.max()-arr.min())*0.1, size=arr.shape)) + r = abs(np.random.normal(loc=0, scale=(arr.max()-arr.min())*0.1, size=arr.shape)) im1.updateImage(arr + r) updateRoi(lastRoi) for r in rois: updateRoiPlot(r) - +## Rapidly update one of the images with random noise t = QtCore.QTimer() -t.connect(t, QtCore.SIGNAL('timeout()'), updateImage) +t.timeout.connect(updateImage) t.start(50) -w.show() -app.exec_() + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/test_draw.py b/examples/test_draw.py index 70da1493..a1dc7454 100644 --- a/examples/test_draw.py +++ b/examples/test_draw.py @@ -3,18 +3,16 @@ import sys, os sys.path = [os.path.join(os.path.dirname(__file__), '..', '..')] + sys.path -from pyqtgraph.GraphicsView import * -from pyqtgraph.graphicsItems import * -#from numpy import random + from PyQt4 import QtCore, QtGui -from scipy.ndimage import * import numpy as np +import pyqtgraph as pg app = QtGui.QApplication([]) ## Create window with GraphicsView widget win = QtGui.QMainWindow() -view = GraphicsView() +view = pg.GraphicsView() #view.useOpenGL(True) win.setCentralWidget(view) win.show() @@ -26,7 +24,7 @@ view.enableMouse() view.setAspectLocked(True) ## Create image item -img = ImageItem(np.zeros((200,200))) +img = pg.ImageItem(np.zeros((200,200))) view.scene().addItem(img) ## Set initial view bounds @@ -35,4 +33,6 @@ view.setRange(QtCore.QRectF(0, 0, 200, 200)) img.setDrawKernel(1) img.setLevels(10,0) -#app.exec_() +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/test_scatterPlot.py b/examples/test_scatterPlot.py index f3bb2006..329e07f1 100644 --- a/examples/test_scatterPlot.py +++ b/examples/test_scatterPlot.py @@ -1,32 +1,26 @@ # -*- coding: utf-8 -*- import sys, os +## Add path to library (just for examples; you do not need this) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) from PyQt4 import QtGui, QtCore -from pyqtgraph.PlotWidget import * -from pyqtgraph.graphicsItems import * - +import pyqtgraph as pg +import numpy as np app = QtGui.QApplication([]) mw = QtGui.QMainWindow() -cw = PlotWidget() +cw = pg.PlotWidget() mw.setCentralWidget(cw) mw.show() - -#s1 = SpotItem(5, pxMode=True, brush=QtGui.QBrush(QtGui.QColor(0, 0, 200)), pen=QtGui.QPen(QtGui.QColor(100,100,100))) -#s1.setPos(1, 0) -#s2 = SpotItem(.1, pxMode=False, brush=QtGui.QBrush(QtGui.QColor(0, 200, 0)), pen=QtGui.QPen(QtGui.QColor(100,100,100))) -#s2.setPos(0, 1) -#cw.addItem(s1) -#cw.addItem(s2) - -import numpy as np -s1 = ScatterPlotItem(size=10, pen=QtGui.QPen(QtCore.Qt.NoPen), brush=QtGui.QBrush(QtGui.QColor(255, 255, 255, 20))) +s1 = pg.ScatterPlotItem(size=10, pen=QtGui.QPen(QtCore.Qt.NoPen), brush=QtGui.QBrush(QtGui.QColor(255, 255, 255, 20))) pos = np.random.normal(size=(2,3000)) spots = [{'pos': pos[:,i]} for i in range(3000)] s1.addPoints(spots) cw.addDataItem(s1) +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/test_viewBox.py b/examples/test_viewBox.py index bbb77942..b9eb2bc0 100755 --- a/examples/test_viewBox.py +++ b/examples/test_viewBox.py @@ -4,10 +4,12 @@ import sys, os sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) -from scipy import random +## This example uses a ViewBox to create a PlotWidget-like interface + +#from scipy import random +import numpy as np from PyQt4 import QtGui, QtCore -from pyqtgraph.GraphicsView import * -from pyqtgraph.graphicsItems import * +import pyqtgraph as pg app = QtGui.QApplication([]) mw = QtGui.QMainWindow() @@ -19,15 +21,15 @@ mw.show() mw.resize(800, 600) -gv = GraphicsView(cw) -gv.enableMouse(False) +gv = pg.GraphicsView(cw) +gv.enableMouse(False) ## Mouse interaction will be handled by the ViewBox l = QtGui.QGraphicsGridLayout() l.setHorizontalSpacing(0) l.setVerticalSpacing(0) -vb = ViewBox() -p1 = PlotCurveItem() +vb = pg.ViewBox() +p1 = pg.PlotCurveItem() vb.addItem(p1) vl.addWidget(gv) @@ -61,21 +63,21 @@ l.addItem(vb, 0, 1) gv.centralWidget.setLayout(l) -xScale = ScaleItem(orientation='bottom', linkView=vb) +xScale = pg.ScaleItem(orientation='bottom', linkView=vb) l.addItem(xScale, 1, 1) -yScale = ScaleItem(orientation='left', linkView=vb) +yScale = pg.ScaleItem(orientation='left', linkView=vb) l.addItem(yScale, 0, 0) xScale.setLabel(text=u"X Axis", units="s") yScale.setLabel('Y Axis', units='V') def rand(n): - data = random.random(n) + data = np.random.random(n) data[int(n*0.1):int(n*0.13)] += .5 data[int(n*0.18)] += 2 data[int(n*0.1):int(n*0.13)] *= 5 data[int(n*0.18)] *= 20 - return data, arange(n, n+len(data)) / float(n) + return data, np.arange(n, n+len(data)) / float(n) def updateData(): @@ -87,7 +89,9 @@ updateData() vb.autoRange() t = QtCore.QTimer() -QtCore.QObject.connect(t, QtCore.SIGNAL('timeout()'), updateData) +t.timeout.connect(updateData) t.start(50) -#app.exec_() +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/functions.py b/functions.py index f5227146..cdb2c9fc 100644 --- a/functions.py +++ b/functions.py @@ -18,7 +18,8 @@ colorAbbrev = { from PyQt4 import QtGui -from numpy import clip, floor, log +import numpy as np +import scipy.ndimage ## Copied from acq4/lib/util/functions SI_PREFIXES = u'yzafpnµm kMGTPEZY' @@ -28,7 +29,7 @@ def siScale(x, minVal=1e-25): m = 0 x = 0 else: - m = int(clip(floor(log(abs(x))/log(1000)), -9.0, 9.0)) + m = int(np.clip(np.floor(np.log(abs(x))/np.log(1000)), -9.0, 9.0)) if m == 0: pref = '' elif m < -8 or m > 8: @@ -145,13 +146,13 @@ def mkColor(*args): return QtGui.QColor(r, g, b, a) def colorTuple(c): - return (c.red(), c.blue(), c.green(), c.alpha()) + return (c.red(), c.green(), c.blue(), c.alpha()) def colorStr(c): """Generate a hex string code from a QColor""" return ('%02x'*4) % colorTuple(c) -def intColor(index, hues=9, values=3, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255): +def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs): """Creates a QColor from a single index. Useful for stepping through a predefined list of colors. - The argument "index" determines which color from the set will be returned - All other arguments determine what the set of predefined colors will be @@ -163,9 +164,90 @@ def intColor(index, hues=9, values=3, maxValue=255, minValue=150, maxHue=360, mi ind = int(index) % (hues * values) indh = ind % hues indv = ind / hues - v = minValue + indv * ((maxValue-minValue) / (values-1)) + if values > 1: + v = minValue + indv * ((maxValue-minValue) / (values-1)) + else: + v = maxValue h = minHue + (indh * (maxHue-minHue)) / hues c = QtGui.QColor() c.setHsv(h, sat, v) - return c \ No newline at end of file + c.setAlpha(alpha) + return c + + +def affineSlice(data, shape, origin, vectors, axes, **kargs): + """Take an arbitrary slice through an array. + Parameters: + data: the original dataset + shape: the shape of the slice to take (Note the return value may have more dimensions than len(shape)) + origin: the location in the original dataset that will become the origin in the sliced data. + vectors: list of unit vectors which point in the direction of the slice axes + each vector must be the same length as axes + If the vectors are not unit length, the result will be scaled. + If the vectors are not orthogonal, the result will be sheared. + axes: the axes in the original dataset which correspond to the slice vectors + + Example: start with a 4D data set, take a diagonal-planar slice out of the last 3 axes + - data = array with dims (time, x, y, z) = (100, 40, 40, 40) + - The plane to pull out is perpendicular to the vector (x,y,z) = (1,1,1) + - The origin of the slice will be at (x,y,z) = (40, 0, 0) + - The we will slice a 20x20 plane from each timepoint, giving a final shape (100, 20, 20) + affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) + + Note the following: + len(shape) == len(vectors) + len(origin) == len(axes) == len(vectors[0]) + """ + + # sanity check + if len(shape) != len(vectors): + raise Exception("shape and vectors must have same length.") + if len(origin) != len(axes): + raise Exception("origin and axes must have same length.") + for v in vectors: + if len(v) != len(axes): + raise Exception("each vector must be same length as axes.") + shape = (np.ceil(shape[0]), np.ceil(shape[1])) + + ## transpose data so slice axes come first + trAx = range(data.ndim) + for x in axes: + trAx.remove(x) + tr1 = tuple(axes) + tuple(trAx) + data = data.transpose(tr1) + #print "tr1:", tr1 + ## dims are now [(slice axes), (other axes)] + + + ## make sure vectors are arrays + vectors = np.array(vectors) + origin = np.array(origin) + origin.shape = (len(axes),) + (1,)*len(shape) + + ## Build array of sample locations. + grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes + #print shape, grid.shape + x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic + x += origin + #print "X values:" + #print x + ## iterate manually over unused axes since map_coordinates won't do it for us + extraShape = data.shape[len(axes):] + output = np.empty(tuple(shape) + extraShape, dtype=data.dtype) + for inds in np.ndindex(*extraShape): + ind = (Ellipsis,) + inds + #print data[ind].shape, x.shape, output[ind].shape, output.shape + output[ind] = scipy.ndimage.map_coordinates(data[ind], x, **kargs) + + tr = range(output.ndim) + trb = [] + for i in range(min(axes)): + ind = tr1.index(i) + (len(shape)-len(axes)) + tr.remove(ind) + trb.append(ind) + tr2 = tuple(trb+tr) + + ## Untranspose array before returning + return output.transpose(tr2) + diff --git a/graphicsItems.py b/graphicsItems.py index a5ac37f6..23d7ce5e 100644 --- a/graphicsItems.py +++ b/graphicsItems.py @@ -11,7 +11,7 @@ Provides ImageItem, PlotCurveItem, and ViewBox, amongst others. from PyQt4 import QtGui, QtCore if not hasattr(QtCore, 'Signal'): QtCore.Signal = QtCore.pyqtSignal -from ObjectWorkaround import * +#from ObjectWorkaround import * #tryWorkaround(QtCore, QtGui) #from numpy import * import numpy as np @@ -28,8 +28,16 @@ from Point import * from functions import * import types, sys, struct import weakref +#import debug #from debug import * +## QGraphicsObject didn't appear until 4.6; this is for compatibility with 4.5 +if not hasattr(QtGui, 'QGraphicsObject'): + class QGraphicsObject(QtGui.QGraphicsWidget): + def shape(self): + return QtGui.QGraphicsItem.shape(self) + QtGui.QGraphicsObject = QGraphicsObject + ## Should probably just use QGraphicsGroupItem and instruct it to pass events on to children.. class ItemGroup(QtGui.QGraphicsItem): @@ -68,12 +76,12 @@ class ItemGroup(QtGui.QGraphicsItem): -class GraphicsObject(QGraphicsObject): +class GraphicsObject(QtGui.QGraphicsObject): """Extends QGraphicsObject with a few important functions. (Most of these assume that the object is in a scene with a single view)""" def __init__(self, *args): - QGraphicsObject.__init__(self, *args) + QtGui.QGraphicsObject.__init__(self, *args) self._view = None def getViewWidget(self): @@ -174,14 +182,19 @@ class GraphicsObject(QGraphicsObject): -class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround): +class ImageItem(QtGui.QGraphicsObject): + + sigImageChanged = QtCore.Signal() + if 'linux' not in sys.platform: ## disable weave optimization on linux--broken there. useWeave = True else: useWeave = False def __init__(self, image=None, copy=True, parent=None, border=None, *args): - QObjectWorkaround.__init__(self) + #QObjectWorkaround.__init__(self) + QtGui.QGraphicsObject.__init__(self) + #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) self.qimage = QtGui.QImage() self.pixmap = None #self.useWeave = True @@ -195,7 +208,7 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround): border = mkPen(border) self.border = border - QtGui.QGraphicsPixmapItem.__init__(self, parent, *args) + #QtGui.QGraphicsPixmapItem.__init__(self, parent, *args) #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) if image is not None: self.updateImage(image, copy, autoRange=True) @@ -218,7 +231,12 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround): if self.pixmap is None: return None return self.pixmap.height() - + + def boundingRect(self): + if self.pixmap is None: + return QtCore.QRectF(0., 0., 0., 0.) + return QtCore.QRectF(0., 0., float(self.width()), float(self.height())) + def setClipLevel(self, level=None): self.clipLevel = level @@ -252,6 +270,8 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround): return else: gotNewData = True + if self.image is None or image.shape != self.image.shape: + self.prepareGeometryChange() if copy: self.image = image.view(np.ndarray).copy() else: @@ -323,7 +343,7 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround): im2 = im.transpose(axh['y'], axh['x'], axh['c']) for i in range(0, im.shape[axh['c']]): - im1[..., i] = im2[..., i] + im1[..., 2-i] = im2[..., i] ## for some reason, the colors line up as BGR in the final image. for i in range(im.shape[axh['c']], 3): im1[..., i] = 0 @@ -345,14 +365,16 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround): im1[..., 2][mask] = 255 #print "Final image:", im1.dtype, im1.min(), im1.max(), im1.shape self.ims = im1.tostring() ## Must be held in memory here because qImage won't do it for us :( - qimage = QtGui.QImage(self.ims, im1.shape[1], im1.shape[0], QtGui.QImage.Format_ARGB32) + qimage = QtGui.QImage(buffer(self.ims), im1.shape[1], im1.shape[0], QtGui.QImage.Format_ARGB32) self.pixmap = QtGui.QPixmap.fromImage(qimage) ##del self.ims - self.setPixmap(self.pixmap) + #self.pixmapItem.setPixmap(self.pixmap) + self.update() if gotNewData: - self.emit(QtCore.SIGNAL('imageChanged')) + #self.emit(QtCore.SIGNAL('imageChanged')) + self.sigImageChanged.emit() def getPixmap(self): return self.pixmap.copy() @@ -397,8 +419,15 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround): p.setPen(self.border) p.drawRect(self.boundingRect()) + def pixelSize(self): + """return size of a single pixel in the image""" + br = self.sceneBoundingRect() + return br.width()/self.pixmap.width(), br.height()/self.pixmap.height() class PlotCurveItem(GraphicsObject): + + sigPlotChanged = QtCore.Signal(object) + """Class representing a single plot curve.""" sigClicked = QtCore.Signal(object) @@ -565,6 +594,7 @@ class PlotCurveItem(GraphicsObject): self.updateData(y, x, copy) def updateData(self, data, x=None, copy=False): + #prof = debug.Profiler('PlotCurveItem.updateData', disabled=True) if isinstance(data, list): data = np.array(data) if isinstance(x, list): @@ -591,7 +621,7 @@ class PlotCurveItem(GraphicsObject): x = data[tuple(ind)] elif data.ndim == 1: y = data - + #prof.mark("data checks") self.prepareGeometryChange() if copy: self.yData = y.copy() @@ -602,6 +632,7 @@ class PlotCurveItem(GraphicsObject): self.xData = x.copy() else: self.xData = x + #prof.mark('copy') if x is None: self.xData = np.arange(0, self.yData.shape[0]) @@ -610,10 +641,15 @@ class PlotCurveItem(GraphicsObject): raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) self.path = None - #self.specPath = None self.xDisp = self.yDisp = None + + #prof.mark('set') self.update() - self.emit(QtCore.SIGNAL('plotChanged'), self) + #prof.mark('update') + #self.emit(QtCore.SIGNAL('plotChanged'), self) + self.sigPlotChanged.emit(self) + #prof.mark('emit') + #prof.finish() def generatePath(self, x, y): path = QtGui.QPainterPath() @@ -634,25 +670,32 @@ class PlotCurveItem(GraphicsObject): ## 0(i4) ## ## All values are big endian--pack using struct.pack('>d') or struct.pack('>i') - # + + #prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True) + n = x.shape[0] # create empty array, pad with extra space on either end arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')]) + #prof.mark('create empty') # write first two integers arr.data[12:20] = struct.pack('>ii', n, 0) # Fill array with vertex values arr[1:-1]['x'] = x arr[1:-1]['y'] = y arr[1:-1]['c'] = 1 + #prof.mark('fill array') # write last 0 lastInd = 20*(n+1) arr.data[lastInd:lastInd+4] = struct.pack('>i', 0) # create datastream object and stream into path buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here + #prof.mark('create buffer') ds = QtCore.QDataStream(buf) + #prof.mark('create dataStream') ds >> path - + #prof.mark('load path') + #prof.finish() return path def boundingRect(self): @@ -677,6 +720,7 @@ class PlotCurveItem(GraphicsObject): return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) def paint(self, p, opt, widget): + #prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) if self.xData is None: return #if self.opts['spectrumMode']: @@ -688,6 +732,7 @@ class PlotCurveItem(GraphicsObject): if self.path is None: self.path = self.generatePath(*self.getData()) path = self.path + #prof.mark('generate path') if self.shadow is not None: sp = QtGui.QPen(self.shadow) @@ -709,7 +754,9 @@ class PlotCurveItem(GraphicsObject): p.drawPath(path) p.setPen(cp) p.drawPath(path) + #prof.mark('drawPath') + #prof.finish() #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) #p.drawRect(self.boundingRect()) @@ -742,7 +789,7 @@ class PlotCurveItem(GraphicsObject): self.sigClicked.emit(self) -class CurvePoint(QtGui.QGraphicsItem, QObjectWorkaround): +class CurvePoint(QtGui.QGraphicsObject): """A GraphicsItem that sets its location to a point on a PlotCurveItem. The position along the curve is a property, and thus can be easily animated.""" @@ -750,17 +797,16 @@ class CurvePoint(QtGui.QGraphicsItem, QObjectWorkaround): """Position can be set either as an index referring to the sample number or the position 0.0 - 1.0""" - QtGui.QGraphicsItem.__init__(self) - QObjectWorkaround.__init__(self) - self.curve = None + QtGui.QGraphicsObject.__init__(self) + #QObjectWorkaround.__init__(self) + self.curve = weakref.ref(curve) + self.setParentItem(curve) self.setProperty('position', 0.0) self.setProperty('index', 0) if hasattr(self, 'ItemHasNoContents'): self.setFlags(self.flags() | self.ItemHasNoContents) - self.curve = curve - self.setParentItem(curve) if pos is not None: self.setPos(pos) else: @@ -773,22 +819,22 @@ class CurvePoint(QtGui.QGraphicsItem, QObjectWorkaround): self.setProperty('index', index) def event(self, ev): - if not isinstance(ev, QtCore.QDynamicPropertyChangeEvent) or self.curve is None: - return + if not isinstance(ev, QtCore.QDynamicPropertyChangeEvent) or self.curve() is None: + return False if ev.propertyName() == 'index': index = self.property('index').toInt()[0] elif ev.propertyName() == 'position': index = None else: - return + return False - (x, y) = self.curve.getData() + (x, y) = self.curve().getData() if index is None: #print self.property('position').toDouble()[0], self.property('position').typeName() index = (len(x)-1) * clip(self.property('position').toDouble()[0], 0.0, 1.0) - if index != int(index): + if index != int(index): ## interpolate floating-point values i1 = int(index) i2 = clip(i1+1, 0, len(x)-1) s2 = index-i1 @@ -806,6 +852,7 @@ class CurvePoint(QtGui.QGraphicsItem, QObjectWorkaround): self.resetTransform() self.rotate(180+ ang * 180 / np.pi) QtGui.QGraphicsItem.setPos(self, *newPos) + return True def boundingRect(self): return QtCore.QRectF() @@ -814,7 +861,7 @@ class CurvePoint(QtGui.QGraphicsItem, QObjectWorkaround): pass def makeAnimation(self, prop='position', start=0.0, end=1.0, duration=10000, loop=1): - anim = QtCore.QPropertyAnimation(self._qObj_, prop) + anim = QtCore.QPropertyAnimation(self, prop) anim.setDuration(duration) anim.setStartValue(start) anim.setEndValue(end) @@ -939,22 +986,27 @@ class ScatterPlotItem(QtGui.QGraphicsWidget): for s in spots: pos = Point(s['pos']) size = s.get('size', self.size) + if self.pxMode: + psize = 0 + else: + psize = size + if xmn is None: + xmn = pos[0]-psize + xmx = pos[0]+psize + ymn = pos[1]-psize + ymx = pos[1]+psize + else: + xmn = min(xmn, pos[0]-psize) + xmx = max(xmx, pos[0]+psize) + ymn = min(ymn, pos[1]-psize) + ymx = max(ymx, pos[1]+psize) + #print pos, xmn, xmx, ymn, ymx brush = s.get('brush', self.brush) pen = s.get('pen', self.pen) pen.setCosmetic(True) data = s.get('data', None) item = self.mkSpot(pos, size, self.pxMode, brush, pen, data) self.spots.append(item) - if xmn is None: - xmn = pos[0]-size - xmx = pos[0]+size - ymn = pos[1]-size - ymx = pos[1]+size - else: - xmn = min(xmn, pos[0]-size) - xmx = max(xmx, pos[0]+size) - ymn = min(ymn, pos[1]-size) - ymx = max(ymx, pos[1]+size) self.range = [[xmn, xmx], [ymn, ymx]] @@ -967,7 +1019,8 @@ class ScatterPlotItem(QtGui.QGraphicsWidget): def boundingRect(self): ((xmn, xmx), (ymn, ymx)) = self.range - + if xmn is None or xmx is None or ymn is None or ymx is None: + return QtCore.QRectF() return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) def paint(self, p, *args): @@ -985,13 +1038,15 @@ class SpotItem(QtGui.QGraphicsWidget): def __init__(self, size, pxMode, brush, pen, data): QtGui.QGraphicsWidget.__init__(self) if pxMode: + self.setCacheMode(self.DeviceCoordinateCache) self.setFlags(self.flags() | self.ItemIgnoresTransformations) #self.setCacheMode(self.DeviceCoordinateCache) ## causes crash on linux self.pen = pen self.brush = brush self.path = QtGui.QPainterPath() - s2 = size/2. - self.path.addEllipse(QtCore.QRectF(-s2, -s2, size, size)) + #s2 = size/2. + self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) + self.scale(size, size) self.data = data def setBrush(self, brush): @@ -1036,6 +1091,7 @@ class SpotItem(QtGui.QGraphicsWidget): class ROIPlotItem(PlotCurveItem): + """Plot curve that monitors an ROI and image for changes to automatically replot.""" def __init__(self, roi, data, img, axes=(0,1), xVals=None, color=None): self.roi = roi self.roiData = data @@ -1043,7 +1099,8 @@ class ROIPlotItem(PlotCurveItem): self.axes = axes self.xVals = xVals PlotCurveItem.__init__(self, self.getRoiData(), x=self.xVals, color=color) - roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) + #roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) + roi.sigRegionChanged.connect(self.roiChangedEvent) #self.roiChangedEvent() def getRoiData(self): @@ -1073,7 +1130,8 @@ class UIGraphicsItem(GraphicsObject): self._viewRect = self._view().rect() self._viewTransform = self.viewTransform() self.setNewBounds() - QtCore.QObject.connect(view, QtCore.SIGNAL('viewChanged'), self.viewChangedEvent) + #QtCore.QObject.connect(view, QtCore.SIGNAL('viewChanged'), self.viewChangedEvent) + view.sigRangeChanged.connect(self.viewRangeChanged) def viewRect(self): """Return the viewport widget rect""" @@ -1110,7 +1168,7 @@ class UIGraphicsItem(GraphicsObject): self.bounds = self.viewTransform().inverted()[0].mapRect(bounds) self.prepareGeometryChange() - def viewChangedEvent(self): + def viewRangeChanged(self): """Called when the view widget is resized""" self.boundingRect() self.update() @@ -1378,23 +1436,31 @@ class ScaleItem(QtGui.QGraphicsWidget): def linkToView(self, view): if self.orientation in ['right', 'left']: - signal = QtCore.SIGNAL('yRangeChanged') + if self.linkedView is not None and self.linkedView() is not None: + #view.sigYRangeChanged.disconnect(self.linkedViewChanged) + ## should be this instead? + self.linkedView().sigYRangeChanged.disconnect(self.linkedViewChanged) + self.linkedView = weakref.ref(view) + view.sigYRangeChanged.connect(self.linkedViewChanged) + #signal = QtCore.SIGNAL('yRangeChanged') else: - signal = QtCore.SIGNAL('xRangeChanged') + if self.linkedView is not None and self.linkedView() is not None: + #view.sigYRangeChanged.disconnect(self.linkedViewChanged) + ## should be this instead? + self.linkedView().sigXRangeChanged.disconnect(self.linkedViewChanged) + self.linkedView = weakref.ref(view) + view.sigXRangeChanged.connect(self.linkedViewChanged) + #signal = QtCore.SIGNAL('xRangeChanged') - if self.linkedView is not None: - QtCore.QObject.disconnect(view, signal, self.linkedViewChanged) - self.linkedView = view - QtCore.QObject.connect(view, signal, self.linkedViewChanged) - def linkedViewChanged(self, _, newRange): + def linkedViewChanged(self, view, newRange): self.setRange(*newRange) def boundingRect(self): - if self.linkedView is None or self.grid is False: + if self.linkedView is None or self.linkedView() is None or self.grid is False: return self.mapRectFromParent(self.geometry()) else: - return self.mapRectFromParent(self.geometry()) | self.mapRectFromScene(self.linkedView.mapRectToScene(self.linkedView.boundingRect())) + return self.mapRectFromParent(self.geometry()) | self.mapRectFromScene(self.linkedView().mapRectToScene(self.linkedView().boundingRect())) def paint(self, p, opt, widget): p.setPen(self.pen) @@ -1402,10 +1468,10 @@ class ScaleItem(QtGui.QGraphicsWidget): #bounds = self.boundingRect() bounds = self.mapRectFromParent(self.geometry()) - if self.linkedView is None or self.grid is False: + if self.linkedView is None or self.linkedView() is None or self.grid is False: tbounds = bounds else: - tbounds = self.mapRectFromScene(self.linkedView.mapRectToScene(self.linkedView.boundingRect())) + tbounds = self.mapRectFromScene(self.linkedView().mapRectToScene(self.linkedView().boundingRect())) if self.orientation == 'left': p.drawLine(bounds.topRight(), bounds.bottomRight()) @@ -1577,6 +1643,12 @@ class ScaleItem(QtGui.QGraphicsWidget): class ViewBox(QtGui.QGraphicsWidget): + + sigYRangeChanged = QtCore.Signal(object, object) + sigXRangeChanged = QtCore.Signal(object, object) + sigRangeChangedManually = QtCore.Signal(object) + sigRangeChanged = QtCore.Signal(object, object) + """Box that allows internal scaling/panning of children by mouse drag. Not compatible with GraphicsView having the same functionality.""" def __init__(self, parent=None): QtGui.QGraphicsWidget.__init__(self, parent) @@ -1745,7 +1817,8 @@ class ViewBox(QtGui.QGraphicsWidget): mask *= np.array([1, -1]) tr = dif*mask self.translateBy(tr, viewCoords=True) - self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) + #self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) + self.sigRangeChangedManually.emit(self.mouseEnabled) ev.accept() elif ev.buttons() & QtCore.Qt.RightButton: dif = ev.screenPos() - ev.lastScreenPos() @@ -1755,7 +1828,8 @@ class ViewBox(QtGui.QGraphicsWidget): #print mask, dif, s center = Point(self.childGroup.transform().inverted()[0].map(ev.buttonDownPos(QtCore.Qt.RightButton))) self.scaleBy(s, center) - self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) + #self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) + self.sigRangeChangedManually.emit(self.mouseEnabled) ev.accept() else: ev.ignore() @@ -1802,8 +1876,10 @@ class ViewBox(QtGui.QGraphicsWidget): self.range[1] = [min, max] #self.ctrl.yMinText.setText('%g' % min) #self.ctrl.yMaxText.setText('%g' % max) - self.emit(QtCore.SIGNAL('yRangeChanged'), self, (min, max)) - self.emit(QtCore.SIGNAL('viewChanged'), self) + #self.emit(QtCore.SIGNAL('yRangeChanged'), self, (min, max)) + self.sigYRangeChanged.emit(self, (min, max)) + #self.emit(QtCore.SIGNAL('viewChanged'), self) + self.sigRangeChanged.emit(self, self.range) if update: self.updateMatrix() @@ -1827,8 +1903,10 @@ class ViewBox(QtGui.QGraphicsWidget): self.range[0] = [min, max] #self.ctrl.xMinText.setText('%g' % min) #self.ctrl.xMaxText.setText('%g' % max) - self.emit(QtCore.SIGNAL('xRangeChanged'), self, (min, max)) - self.emit(QtCore.SIGNAL('viewChanged'), self) + #self.emit(QtCore.SIGNAL('xRangeChanged'), self, (min, max)) + self.sigXRangeChanged.emit(self, (min, max)) + #self.emit(QtCore.SIGNAL('viewChanged'), self) + self.sigRangeChanged.emit(self, self.range) if update: self.updateMatrix() @@ -1852,6 +1930,11 @@ class ViewBox(QtGui.QGraphicsWidget): class InfiniteLine(GraphicsObject): + + sigDragged = QtCore.Signal(object) + sigPositionChangeFinished = QtCore.Signal(object) + sigPositionChanged = QtCore.Signal(object) + def __init__(self, view, pos=0, angle=90, pen=None, movable=False, bounds=None): GraphicsObject.__init__(self) self.bounds = QtCore.QRectF() ## graphicsitem boundary @@ -1877,7 +1960,8 @@ class InfiniteLine(GraphicsObject): #self.setFlag(self.ItemSendsScenePositionChanges) #for p in self.getBoundingParents(): #QtCore.QObject.connect(p, QtCore.SIGNAL('viewChanged'), self.updateLine) - QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.updateLine) + #QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.updateLine) + self.view().sigRangeChanged.connect(self.updateLine) def setMovable(self, m): self.movable = m @@ -1935,7 +2019,8 @@ class InfiniteLine(GraphicsObject): if self.p != newPos: self.p = newPos self.updateLine() - self.emit(QtCore.SIGNAL('positionChanged'), self) + #self.emit(QtCore.SIGNAL('positionChanged'), self) + self.sigPositionChanged.emit(self) def getXPos(self): return self.p[0] @@ -2048,17 +2133,23 @@ class InfiniteLine(GraphicsObject): def mouseMoveEvent(self, ev): self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) - self.emit(QtCore.SIGNAL('dragged'), self) + #self.emit(QtCore.SIGNAL('dragged'), self) + self.sigDragged.emit(self) self.hasMoved = True def mouseReleaseEvent(self, ev): if self.hasMoved and ev.button() == QtCore.Qt.LeftButton: self.hasMoved = False - self.emit(QtCore.SIGNAL('positionChangeFinished'), self) + #self.emit(QtCore.SIGNAL('positionChangeFinished'), self) + self.sigPositionChangeFinished.emit(self) class LinearRegionItem(GraphicsObject): + + sigRegionChangeFinished = QtCore.Signal(object) + sigRegionChanged = QtCore.Signal(object) + """Used for marking a horizontal or vertical region in plots.""" def __init__(self, view, orientation="vertical", vals=[0,1], brush=None, movable=True, bounds=None): GraphicsObject.__init__(self) @@ -2080,12 +2171,15 @@ class LinearRegionItem(GraphicsObject): self.lines = [ InfiniteLine(view, QtCore.QPointF(vals[0], 0), 90, movable=movable, bounds=bounds), InfiniteLine(view, QtCore.QPointF(vals[1], 0), 90, movable=movable, bounds=bounds)] - QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.updateBounds) + #QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.updateBounds) + self.view().sigRangeChanged.connect(self.updateBounds) for l in self.lines: l.setParentItem(self) - l.connect(l, QtCore.SIGNAL('positionChangeFinished'), self.lineMoveFinished) - l.connect(l, QtCore.SIGNAL('positionChanged'), self.lineMoved) + #l.connect(l, QtCore.SIGNAL('positionChangeFinished'), self.lineMoveFinished) + l.sigPositionChangeFinished.connect(self.lineMoveFinished) + #l.connect(l, QtCore.SIGNAL('positionChanged'), self.lineMoved) + l.sigPositionChanged.connect(self.lineMoved) if brush is None: brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) @@ -2106,10 +2200,12 @@ class LinearRegionItem(GraphicsObject): def lineMoved(self): self.updateBounds() - self.emit(QtCore.SIGNAL('regionChanged'), self) + #self.emit(QtCore.SIGNAL('regionChanged'), self) + self.sigRegionChanged.emit(self) def lineMoveFinished(self): - self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + self.sigRegionChangeFinished.emit(self) def updateBounds(self): @@ -2200,11 +2296,13 @@ class VTickGroup(QtGui.QGraphicsPathItem): if self.view is not None: if relative: #QtCore.QObject.connect(self.view, QtCore.SIGNAL('viewChanged'), self.rebuildTicks) - QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.rescale) + #QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.rescale) + self.view().sigRangeChanged.connect(self.rescale) else: try: #QtCore.QObject.disconnect(self.view, QtCore.SIGNAL('viewChanged'), self.rebuildTicks) - QtCore.QObject.disconnect(self.view(), QtCore.SIGNAL('viewChanged'), self.rescale) + #QtCore.QObject.disconnect(self.view(), QtCore.SIGNAL('viewChanged'), self.rescale) + self.view().sigRangeChanged.disconnect(self.rescale) except: pass self.rebuildTicks() @@ -2268,6 +2366,8 @@ class VTickGroup(QtGui.QGraphicsPathItem): class GridItem(UIGraphicsItem): + """Class used to make square grids in plots. NOT the grid used for running scanner sequences.""" + def __init__(self, view, bounds=None, *args): UIGraphicsItem.__init__(self, view, bounds) #QtGui.QGraphicsItem.__init__(self, *args) @@ -2277,9 +2377,9 @@ class GridItem(UIGraphicsItem): self.picture = None - def viewChangedEvent(self): + def viewRangeChanged(self): self.picture = None - UIGraphicsItem.viewChangedEvent(self) + UIGraphicsItem.viewRangeChanged(self) #self.update() def paint(self, p, opt, widget): @@ -2417,7 +2517,20 @@ class ColorScaleBar(UIGraphicsItem): self.gradient = g self.update() + def setIntColorScale(self, minVal, maxVal, *args, **kargs): + colors = [intColor(i, maxVal-minVal, *args, **kargs) for i in range(minVal, maxVal)] + g = QtGui.QLinearGradient() + for i in range(len(colors)): + x = float(i)/len(colors) + g.setColorAt(x, colors[i]) + self.setGradient(g) + if 'labels' not in kargs: + self.setLabels({str(minVal/10.): 0, str(maxVal): 1}) + else: + self.setLabels({kargs['labels'][0]:0, kargs['labels'][1]:1}) + def setLabels(self, l): + """Defines labels to appear next to the color scale""" self.labels = l self.update() @@ -2429,7 +2542,7 @@ class ColorScaleBar(UIGraphicsItem): labelWidth = 0 labelHeight = 0 for k in self.labels: - b = p.boundingRect(QtCore.QRectF(0, 0, 0, 0), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, k) + b = p.boundingRect(QtCore.QRectF(0, 0, 0, 0), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k)) labelWidth = max(labelWidth, b.width()) labelHeight = max(labelHeight, b.height()) @@ -2484,6 +2597,6 @@ class ColorScaleBar(UIGraphicsItem): lh = labelHeight/unit.height() for k in self.labels: y = y1 + self.labels[k] * (y2-y1) - p.drawText(QtCore.QRectF(tx/unit.width(), y/unit.height() - lh/2.0, 1000, lh), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, k) + p.drawText(QtCore.QRectF(tx/unit.width(), y/unit.height() - lh/2.0, 1000, lh), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k)) diff --git a/widgets.py b/widgets.py index 14e04ae5..5b275bc2 100644 --- a/widgets.py +++ b/widgets.py @@ -16,7 +16,8 @@ from numpy.linalg import norm import scipy.ndimage as ndimage from Point import * from math import cos, sin -from ObjectWorkaround import * +import functions as fn +#from ObjectWorkaround import * def rectStr(r): return "[%f, %f] + [%f, %f]" % (r.x(), r.y(), r.width(), r.height()) @@ -33,10 +34,15 @@ def rectStr(r): #return QtCore.QObject.connect(self._qObj_, *args) -class ROI(QtGui.QGraphicsItem, QObjectWorkaround): +class ROI(QtGui.QGraphicsObject): + + sigRegionChangeFinished = QtCore.Signal(object) + sigRegionChangeStarted = QtCore.Signal(object) + sigRegionChanged = 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): - QObjectWorkaround.__init__(self) - QtGui.QGraphicsItem.__init__(self, parent) + #QObjectWorkaround.__init__(self) + QtGui.QGraphicsObject.__init__(self, parent) pos = Point(pos) size = Point(size) self.aspectLocked = False @@ -45,7 +51,7 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): if pen is None: self.pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) else: - self.pen = pen + self.pen = fn.mkPen(pen) self.handlePen = QtGui.QPen(QtGui.QColor(150, 255, 255)) self.handles = [] self.state = {'pos': pos, 'size': size, 'angle': angle} @@ -212,7 +218,8 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): self.isMoving = True self.preMoveState = self.getState() self.cursorOffset = self.scenePos() - ev.scenePos() - self.emit(QtCore.SIGNAL('regionChangeStarted'), self) + #self.emit(QtCore.SIGNAL('regionChangeStarted'), self) + self.sigRegionChangeStarted.emit(self) ev.accept() elif ev.button() == QtCore.Qt.RightButton: if self.isMoving: @@ -236,7 +243,8 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): def mouseReleaseEvent(self, ev): if self.translatable: self.isMoving = False - self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + self.sigRegionChangeFinished.emit(self) def cancelMove(self): self.isMoving = False @@ -247,14 +255,16 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): self.isMoving = True self.preMoveState = self.getState() - self.emit(QtCore.SIGNAL('regionChangeStarted'), self) + #self.emit(QtCore.SIGNAL('regionChangeStarted'), self) + self.sigRegionChangeStarted.emit(self) #self.pressPos = self.mapFromScene(ev.scenePos()) #self.pressHandlePos = self.handles[pt]['item'].pos() def pointReleaseEvent(self, pt, ev): #print "release" self.isMoving = False - self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + self.sigRegionChangeFinished.emit(self) def stateCopy(self): sc = {} @@ -316,7 +326,8 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): elif h['type'] == 'f': h['item'].setPos(self.mapFromScene(pos)) - self.emit(QtCore.SIGNAL('regionChanged'), self) + #self.emit(QtCore.SIGNAL('regionChanged'), self) + self.sigRegionChanged.emit(self) elif h['type'] == 's': #c = h['center'] @@ -509,7 +520,8 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): if changed: #print "handle changed." self.update() - self.emit(QtCore.SIGNAL('regionChanged'), self) + #self.emit(QtCore.SIGNAL('regionChanged'), self) + self.sigRegionChanged.emit(self) def scale(self, s, center=[0,0]): @@ -628,104 +640,116 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround): def getArrayRegion(self, data, img, axes=(0,1)): + shape = self.state['size'] - ## transpose data so x and y are the first 2 axes - trAx = range(0, data.ndim) - trAx.remove(axes[0]) - trAx.remove(axes[1]) - tr1 = tuple(axes) + tuple(trAx) - arr = data.transpose(tr1) + origin = self.mapToItem(img, QtCore.QPointF(0, 0)) - ## Determine the minimal area of the data we will need - (dataBounds, roiDataTransform) = self.getArraySlice(data, img, returnSlice=False, axes=axes) + vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin + vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin + vectors = ((vx.x(), vx.y()), (vy.x(), vy.y())) + origin = (origin.x(), origin.y()) + + #print "shape", shape, "vectors", vectors, "origin", origin + + return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, order=1) + + ### transpose data so x and y are the first 2 axes + #trAx = range(0, data.ndim) + #trAx.remove(axes[0]) + #trAx.remove(axes[1]) + #tr1 = tuple(axes) + tuple(trAx) + #arr = data.transpose(tr1) + + ### Determine the minimal area of the data we will need + #(dataBounds, roiDataTransform) = self.getArraySlice(data, img, returnSlice=False, axes=axes) - ## Pad data boundaries by 1px if possible - dataBounds = ( - (max(dataBounds[0][0]-1, 0), min(dataBounds[0][1]+1, arr.shape[0])), - (max(dataBounds[1][0]-1, 0), min(dataBounds[1][1]+1, arr.shape[1])) - ) + ### Pad data boundaries by 1px if possible + #dataBounds = ( + #(max(dataBounds[0][0]-1, 0), min(dataBounds[0][1]+1, arr.shape[0])), + #(max(dataBounds[1][0]-1, 0), min(dataBounds[1][1]+1, arr.shape[1])) + #) - ## Extract minimal data from array - arr1 = arr[dataBounds[0][0]:dataBounds[0][1], dataBounds[1][0]:dataBounds[1][1]] + ### Extract minimal data from array + #arr1 = arr[dataBounds[0][0]:dataBounds[0][1], dataBounds[1][0]:dataBounds[1][1]] - ## Update roiDataTransform to reflect this extraction - roiDataTransform *= QtGui.QTransform().translate(-dataBounds[0][0], -dataBounds[1][0]) - ### (roiDataTransform now maps from ROI coords to extracted data coords) + ### Update roiDataTransform to reflect this extraction + #roiDataTransform *= QtGui.QTransform().translate(-dataBounds[0][0], -dataBounds[1][0]) + #### (roiDataTransform now maps from ROI coords to extracted data coords) - ## Rotate array - if abs(self.state['angle']) > 1e-5: - arr2 = ndimage.rotate(arr1, self.state['angle'] * 180 / np.pi, order=1) + ### Rotate array + #if abs(self.state['angle']) > 1e-5: + #arr2 = ndimage.rotate(arr1, self.state['angle'] * 180 / np.pi, order=1) - ## update data transforms to reflect this rotation - rot = QtGui.QTransform().rotate(self.state['angle'] * 180 / np.pi) - roiDataTransform *= rot + ### update data transforms to reflect this rotation + #rot = QtGui.QTransform().rotate(self.state['angle'] * 180 / np.pi) + #roiDataTransform *= rot - ## The rotation also causes a shift which must be accounted for: - dataBound = QtCore.QRectF(0, 0, arr1.shape[0], arr1.shape[1]) - rotBound = rot.mapRect(dataBound) - roiDataTransform *= QtGui.QTransform().translate(-rotBound.left(), -rotBound.top()) + ### The rotation also causes a shift which must be accounted for: + #dataBound = QtCore.QRectF(0, 0, arr1.shape[0], arr1.shape[1]) + #rotBound = rot.mapRect(dataBound) + #roiDataTransform *= QtGui.QTransform().translate(-rotBound.left(), -rotBound.top()) - else: - arr2 = arr1 + #else: + #arr2 = arr1 - ### Shift off partial pixels - # 1. map ROI into current data space - roiBounds = roiDataTransform.mapRect(self.boundingRect()) + #### Shift off partial pixels + ## 1. map ROI into current data space + #roiBounds = roiDataTransform.mapRect(self.boundingRect()) - # 2. Determine amount to shift data - shift = (int(roiBounds.left()) - roiBounds.left(), int(roiBounds.bottom()) - roiBounds.bottom()) - if abs(shift[0]) > 1e-6 or abs(shift[1]) > 1e-6: - # 3. pad array with 0s before shifting - arr2a = np.zeros((arr2.shape[0]+2, arr2.shape[1]+2) + arr2.shape[2:], dtype=arr2.dtype) - arr2a[1:-1, 1:-1] = arr2 + ## 2. Determine amount to shift data + #shift = (int(roiBounds.left()) - roiBounds.left(), int(roiBounds.bottom()) - roiBounds.bottom()) + #if abs(shift[0]) > 1e-6 or abs(shift[1]) > 1e-6: + ## 3. pad array with 0s before shifting + #arr2a = np.zeros((arr2.shape[0]+2, arr2.shape[1]+2) + arr2.shape[2:], dtype=arr2.dtype) + #arr2a[1:-1, 1:-1] = arr2 - # 4. shift array and udpate transforms - arr3 = ndimage.shift(arr2a, shift + (0,)*(arr2.ndim-2), order=1) - roiDataTransform *= QtGui.QTransform().translate(1+shift[0], 1+shift[1]) - else: - arr3 = arr2 + ## 4. shift array and udpate transforms + #arr3 = ndimage.shift(arr2a, shift + (0,)*(arr2.ndim-2), order=1) + #roiDataTransform *= QtGui.QTransform().translate(1+shift[0], 1+shift[1]) + #else: + #arr3 = arr2 - ### Extract needed region from rotated/shifted array - # 1. map ROI into current data space (round these values off--they should be exact integer values at this point) - roiBounds = roiDataTransform.mapRect(self.boundingRect()) - #print self, roiBounds.height() - #import traceback - #traceback.print_stack() + #### Extract needed region from rotated/shifted array + ## 1. map ROI into current data space (round these values off--they should be exact integer values at this point) + #roiBounds = roiDataTransform.mapRect(self.boundingRect()) + ##print self, roiBounds.height() + ##import traceback + ##traceback.print_stack() - roiBounds = QtCore.QRect(round(roiBounds.left()), round(roiBounds.top()), round(roiBounds.width()), round(roiBounds.height())) + #roiBounds = QtCore.QRect(round(roiBounds.left()), round(roiBounds.top()), round(roiBounds.width()), round(roiBounds.height())) - #2. intersect ROI with data bounds - dataBounds = roiBounds.intersect(QtCore.QRect(0, 0, arr3.shape[0], arr3.shape[1])) + ##2. intersect ROI with data bounds + #dataBounds = roiBounds.intersect(QtCore.QRect(0, 0, arr3.shape[0], arr3.shape[1])) - #3. Extract data from array - db = dataBounds - bounds = ( - (db.left(), db.right()+1), - (db.top(), db.bottom()+1) - ) - arr4 = arr3[bounds[0][0]:bounds[0][1], bounds[1][0]:bounds[1][1]] + ##3. Extract data from array + #db = dataBounds + #bounds = ( + #(db.left(), db.right()+1), + #(db.top(), db.bottom()+1) + #) + #arr4 = arr3[bounds[0][0]:bounds[0][1], bounds[1][0]:bounds[1][1]] - ### Create zero array in size of ROI - arr5 = np.zeros((roiBounds.width(), roiBounds.height()) + arr4.shape[2:], dtype=arr4.dtype) + #### Create zero array in size of ROI + #arr5 = np.zeros((roiBounds.width(), roiBounds.height()) + arr4.shape[2:], dtype=arr4.dtype) - ## Fill array with ROI data - orig = Point(dataBounds.topLeft() - roiBounds.topLeft()) - subArr = arr5[orig[0]:orig[0]+arr4.shape[0], orig[1]:orig[1]+arr4.shape[1]] - subArr[:] = arr4[:subArr.shape[0], :subArr.shape[1]] + ### Fill array with ROI data + #orig = Point(dataBounds.topLeft() - roiBounds.topLeft()) + #subArr = arr5[orig[0]:orig[0]+arr4.shape[0], orig[1]:orig[1]+arr4.shape[1]] + #subArr[:] = arr4[:subArr.shape[0], :subArr.shape[1]] - ## figure out the reverse transpose order - tr2 = np.array(tr1) - for i in range(0, len(tr2)): - tr2[tr1[i]] = i - tr2 = tuple(tr2) + ### figure out the reverse transpose order + #tr2 = np.array(tr1) + #for i in range(0, len(tr2)): + #tr2[tr1[i]] = i + #tr2 = tuple(tr2) - ## Untranspose array before returning - return arr5.transpose(tr2) + ### Untranspose array before returning + #return arr5.transpose(tr2) @@ -887,10 +911,14 @@ class LineROI(ROI): self.addScaleHandle([0.5, 1], [0.5, 0.5]) -class MultiLineROI(QtGui.QGraphicsItem, QObjectWorkaround): +class MultiLineROI(QtGui.QGraphicsObject): + + sigRegionChangeFinished = QtCore.Signal(object) + sigRegionChangeStarted = QtCore.Signal(object) + sigRegionChanged = QtCore.Signal(object) + def __init__(self, points, width, pen=None, **args): - QObjectWorkaround.__init__(self) - QtGui.QGraphicsItem.__init__(self) + QtGui.QGraphicsObject.__init__(self) self.pen = pen self.roiArgs = args if len(points) < 2: @@ -912,9 +940,12 @@ class MultiLineROI(QtGui.QGraphicsItem, QObjectWorkaround): for l in self.lines: l.translatable = False #self.addToGroup(l) - l.connect(l, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) - l.connect(l, QtCore.SIGNAL('regionChangeStarted'), self.roiChangeStartedEvent) - l.connect(l, QtCore.SIGNAL('regionChangeFinished'), self.roiChangeFinishedEvent) + #l.connect(l, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) + l.sigRegionChanged.connect(self.roiChangedEvent) + #l.connect(l, QtCore.SIGNAL('regionChangeStarted'), self.roiChangeStartedEvent) + l.sigRegionChangeStarted.connect(self.roiChangeStartedEvent) + #l.connect(l, QtCore.SIGNAL('regionChangeFinished'), self.roiChangeFinishedEvent) + l.sigRegionChangeFinished.connect(self.roiChangeFinishedEvent) def paint(self, *args): pass @@ -927,13 +958,16 @@ class MultiLineROI(QtGui.QGraphicsItem, QObjectWorkaround): for l in self.lines[1:]: w0 = l.state['size'][1] l.scale([1.0, w/w0], center=[0.5,0.5]) - self.emit(QtCore.SIGNAL('regionChanged'), self) + #self.emit(QtCore.SIGNAL('regionChanged'), self) + self.sigRegionChanged.emit(self) def roiChangeStartedEvent(self): - self.emit(QtCore.SIGNAL('regionChangeStarted'), self) + #self.emit(QtCore.SIGNAL('regionChangeStarted'), self) + self.sigRegionChangeStarted.emit(self) def roiChangeFinishedEvent(self): - self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + self.sigRegionChangeFinished.emit(self) def getArrayRegion(self, arr, img=None): @@ -1037,7 +1071,58 @@ class PolygonROI(ROI): sc['angle'] = self.state['angle'] #sc['handles'] = self.handles return sc + + +class LineSegmentROI(ROI): + def __init__(self, positions, pos=None, **args): + if pos is None: + pos = [0,0] + ROI.__init__(self, pos, [1,1], **args) + #ROI.__init__(self, positions[0]) + for p in positions: + self.addFreeHandle(p) + self.setZValue(1000) + + def listPoints(self): + return [p['item'].pos() for p in self.handles] + + def movePoint(self, *args, **kargs): + ROI.movePoint(self, *args, **kargs) + self.prepareGeometryChange() + for h in self.handles: + h['pos'] = h['item'].pos() + + def paint(self, p, *args): + p.setRenderHint(QtGui.QPainter.Antialiasing) + p.setPen(self.pen) + for i in range(len(self.handles)-1): + h1 = self.handles[i]['item'].pos() + h2 = self.handles[i-1]['item'].pos() + p.drawLine(h1, h2) + + def boundingRect(self): + r = QtCore.QRectF() + for h in self.handles: + r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs + return r + def shape(self): + p = QtGui.QPainterPath() + p.moveTo(self.handles[0]['item'].pos()) + for i in range(len(self.handles)): + p.lineTo(self.handles[i]['item'].pos()) + return p + + def stateCopy(self): + sc = {} + sc['pos'] = Point(self.state['pos']) + sc['size'] = Point(self.state['size']) + sc['angle'] = self.state['angle'] + #sc['handles'] = self.handles + return sc + + + class SpiralROI(ROI): def __init__(self, pos=None, size=None, **args): if size == None: