From 2ca08c69ce119bac8b181be125a7dec6e85c0ff1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 24 Jul 2010 14:29:09 -0400 Subject: [PATCH] merged many changes in from acq4: - added vertical lines / regions for plots - added gradient editor widget - many bugfixes - cleaned up imageview a bit --- GradientWidget.py | 364 ++++++++ GradientWidgetTest.py | 26 + GraphicsView.py | 37 +- ImageView.py | 360 ++++++-- ImageViewTemplate.py | 355 ++++--- ImageViewTemplate.ui | 164 ++-- ObjectWorkaround.py | 22 + PlotItem.py | 179 +++- PlotWidget.py | 17 +- examples/test_ImageView.py | 29 +- examples/test_PlotWidget.py | 24 +- examples/test_ROItypes.py | 15 +- examples/test_viewBox.py | 27 +- graphicsItems.py | 813 ++++++++++++++--- graphicsWindows.py | 12 +- plotConfigTemplate.py | 11 +- plotConfigTemplate.ui | 14 +- widgets.py | 1726 ++++++++++++++++++----------------- 18 files changed, 2811 insertions(+), 1384 deletions(-) create mode 100644 GradientWidget.py create mode 100644 GradientWidgetTest.py create mode 100644 ObjectWorkaround.py diff --git a/GradientWidget.py b/GradientWidget.py new file mode 100644 index 00000000..5685edfb --- /dev/null +++ b/GradientWidget.py @@ -0,0 +1,364 @@ +# -*- coding: utf-8 -*- +from PyQt4 import QtGui, QtCore + + +class TickSlider(QtGui.QGraphicsView): + def __init__(self, parent=None, orientation='bottom', allowAdd=True, **kargs): + QtGui.QGraphicsView.__init__(self, parent) + #self.orientation = orientation + self.allowAdd = allowAdd + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor) + self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter) + self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) + self.length = 100 + self.tickSize = 15 + self.orientations = { + 'left': (270, 1, -1), + 'right': (270, 1, 1), + 'top': (0, 1, -1), + 'bottom': (0, 1, 1) + } + + self.scene = QtGui.QGraphicsScene() + self.setScene(self.scene) + + self.ticks = {} + self.maxDim = 20 + self.setOrientation(orientation) + self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain) + self.setBackgroundRole(QtGui.QPalette.NoRole) + self.setMouseTracking(True) + + def keyPressEvent(self, ev): + ev.ignore() + + def setMaxDim(self, mx=None): + if mx is None: + mx = self.maxDim + else: + self.maxDim = mx + + if self.orientation in ['bottom', 'top']: + self.setFixedHeight(mx) + self.setMaximumWidth(16777215) + else: + self.setFixedWidth(mx) + self.setMaximumHeight(16777215) + + def setOrientation(self, ort): + self.orientation = ort + self.resetTransform() + self.rotate(self.orientations[ort][0]) + self.scale(*self.orientations[ort][1:]) + self.setMaxDim() + + def addTick(self, x, color=None, movable=True): + if color is None: + color = QtGui.QColor(255,255,255) + tick = Tick(self, [x*self.length, 0], color, movable, self.tickSize) + self.ticks[tick] = x + self.scene.addItem(tick) + return tick + + def removeTick(self, tick): + del self.ticks[tick] + self.scene.removeItem(tick) + + def tickMoved(self, tick, pos): + #print "tick changed" + ## Correct position of tick if it has left bounds. + newX = min(max(0, pos.x()), self.length) + pos.setX(newX) + tick.setPos(pos) + self.ticks[tick] = float(newX) / self.length + + def tickClicked(self, tick, ev): + if ev.button() == QtCore.Qt.RightButton: + self.removeTick(tick) + + def widgetLength(self): + if self.orientation in ['bottom', 'top']: + return self.width() + else: + return self.height() + + def resizeEvent(self, ev): + wlen = max(40, self.widgetLength()) + self.setLength(wlen-self.tickSize) + bounds = self.scene.itemsBoundingRect() + bounds.setLeft(min(-self.tickSize*0.5, bounds.left())) + bounds.setRight(max(self.length + self.tickSize, bounds.right())) + #bounds.setTop(min(bounds.top(), self.tickSize)) + #bounds.setBottom(max(0, bounds.bottom())) + self.setSceneRect(bounds) + self.fitInView(bounds, QtCore.Qt.KeepAspectRatio) + + def setLength(self, newLen): + for t, x in self.ticks.items(): + t.setPos(x * newLen, t.pos().y()) + self.length = float(newLen) + + def mousePressEvent(self, ev): + QtGui.QGraphicsView.mousePressEvent(self, ev) + self.ignoreRelease = False + if len(self.items(ev.pos())) > 0: ## Let items handle their own clicks + self.ignoreRelease = True + + def mouseReleaseEvent(self, ev): + QtGui.QGraphicsView.mouseReleaseEvent(self, ev) + if self.ignoreRelease: + return + + pos = self.mapToScene(ev.pos()) + if pos.x() < 0 or pos.x() > self.length: + return + if pos.y() < 0 or pos.y() > self.tickSize: + return + + if ev.button() == QtCore.Qt.LeftButton and self.allowAdd: + pos.setX(min(max(pos.x(), 0), self.length)) + self.addTick(pos.x()/self.length) + elif ev.button() == QtCore.Qt.RightButton: + self.showMenu(ev) + + + def showMenu(self, ev): + pass + + def setTickColor(self, tick, color): + tick = self.getTick(tick) + tick.color = color + tick.setBrush(QtGui.QBrush(QtGui.QColor(tick.color))) + + def setTickValue(self, tick, val): + tick = self.getTick(tick) + val = min(max(0.0, val), 1.0) + x = val * self.length + pos = tick.pos() + pos.setX(x) + tick.setPos(pos) + self.ticks[tick] = val + + def tickValue(self, tick): + tick = self.getTick(tick) + return self.ticks[tick] + + def getTick(self, tick): + if type(tick) is int: + tick = self.listTicks()[tick][0] + return tick + + def mouseMoveEvent(self, ev): + QtGui.QGraphicsView.mouseMoveEvent(self, ev) + #print ev.pos(), ev.buttons() + + def listTicks(self): + ticks = self.ticks.items() + ticks.sort(lambda a,b: cmp(a[1], b[1])) + return ticks + + +class GradientWidget(TickSlider): + def __init__(self, *args, **kargs): + TickSlider.__init__(self, *args, **kargs) + self.rectSize = 15 + self.gradRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, -self.rectSize, 100, self.rectSize)) + self.colorMode = 'rgb' + + + #self.gradient = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(100,0)) + self.scene.addItem(self.gradRect) + self.addTick(0, QtGui.QColor(0,0,0), True) + self.addTick(1, QtGui.QColor(255,0,0), True) + + self.setMaxDim(self.rectSize + self.tickSize) + self.updateGradient() + + #self.btn = QtGui.QPushButton('RGB') + #self.btnProxy = self.scene.addWidget(self.btn) + #self.btnProxy.setFlag(self.btnProxy.ItemIgnoresTransformations) + #self.btnProxy.scale(0.7, 0.7) + #self.btnProxy.translate(-self.btnProxy.sceneBoundingRect().width()+self.tickSize/2., 0) + #if self.orientation == 'bottom': + #self.btnProxy.translate(0, -self.rectSize) + + def setColorMode(self, cm): + if cm not in ['rgb', 'hsv']: + raise Exception("Unknown color mode %s" % str(cm)) + self.colorMode = cm + self.updateGradient() + + def updateGradient(self): + self.gradient = self.getGradient() + self.gradRect.setBrush(QtGui.QBrush(self.gradient)) + self.emit(QtCore.SIGNAL('gradientChanged'), self) + + def setLength(self, newLen): + TickSlider.setLength(self, newLen) + self.gradRect.setRect(0, -self.rectSize, newLen, self.rectSize) + self.updateGradient() + + + + def tickClicked(self, tick, ev): + if ev.button() == QtCore.Qt.LeftButton: + if not tick.colorChangeAllowed: + return + color = QtGui.QColorDialog.getColor(tick.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) + if color.isValid(): + self.setTickColor(tick, color) + self.updateGradient() + elif ev.button() == QtCore.Qt.RightButton: + if not tick.removeAllowed: + return + if len(self.ticks) > 2: + self.removeTick(tick) + self.updateGradient() + + def tickMoved(self, tick, pos): + TickSlider.tickMoved(self, tick, pos) + self.updateGradient() + + + def getGradient(self): + g = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(self.length,0)) + if self.colorMode == 'rgb': + ticks = self.listTicks() + g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks]) + elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop + ticks = self.listTicks() + stops = [] + stops.append((ticks[0][1], ticks[0][0].color)) + for i in range(1,len(ticks)): + x1 = ticks[i-1][1] + x2 = ticks[i][1] + dx = (x2-x1) / 10. + for j in range(1,10): + x = x1 + dx*j + stops.append((x, self.getColor(x))) + stops.append((x2, self.getColor(x2))) + g.setStops(stops) + return g + + def getColor(self, x): + ticks = self.listTicks() + if x <= ticks[0][1]: + return QtGui.QColor(ticks[0][0].color) # always copy colors before handing them out + if x >= ticks[-1][1]: + return QtGui.QColor(ticks[-1][0].color) + + x2 = ticks[0][1] + for i in range(1,len(ticks)): + x1 = x2 + x2 = ticks[i][1] + if x1 <= x and x2 >= x: + break + + dx = (x2-x1) + if dx == 0: + f = 0. + else: + f = (x-x1) / dx + c1 = ticks[i-1][0].color + c2 = ticks[i][0].color + if self.colorMode == 'rgb': + r = c1.red() * (1.-f) + c2.red() * f + g = c1.green() * (1.-f) + c2.green() * f + b = c1.blue() * (1.-f) + c2.blue() * f + return QtGui.QColor(r, g, b) + elif self.colorMode == 'hsv': + h1,s1,v1,_ = c1.getHsv() + h2,s2,v2,_ = c2.getHsv() + h = h1 * (1.-f) + h2 * f + s = s1 * (1.-f) + s2 * f + v = v1 * (1.-f) + v2 * f + c = QtGui.QColor() + c.setHsv(h,s,v) + return c + + + + def mouseReleaseEvent(self, ev): + TickSlider.mouseReleaseEvent(self, ev) + self.updateGradient() + + def addTick(self, x, color=None, movable=True): + if color is None: + color = self.getColor(x) + t = TickSlider.addTick(self, x, color=color, movable=movable) + t.colorChangeAllowed = True + t.removeAllowed = True + return t + + +class GammaWidget(TickSlider): + pass + + +class Tick(QtGui.QGraphicsPolygonItem): + def __init__(self, view, pos, color, movable=True, scale=10): + #QObjectWorkaround.__init__(self) + self.movable = movable + self.view = view + self.scale = scale + self.color = color + #self.endTick = endTick + self.pg = QtGui.QPolygonF([QtCore.QPointF(0,0), QtCore.QPointF(-scale/3**0.5,scale), QtCore.QPointF(scale/3**0.5,scale)]) + QtGui.QGraphicsPolygonItem.__init__(self, self.pg) + self.setPos(pos[0], pos[1]) + self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemIsSelectable) + self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) + if self.movable: + self.setZValue(1) + else: + self.setZValue(0) + + #def x(self): + #return self.pos().x()/100. + + def mouseMoveEvent(self, ev): + #print self, "move", ev.scenePos() + if not self.movable: + return + if not ev.buttons() & QtCore.Qt.LeftButton: + return + + + newPos = ev.scenePos() + self.mouseOffset + newPos.setY(self.pos().y()) + #newPos.setX(min(max(newPos.x(), 0), 100)) + self.setPos(newPos) + self.view.tickMoved(self, newPos) + self.movedSincePress = True + #self.emit(QtCore.SIGNAL('tickChanged'), self) + ev.accept() + + def mousePressEvent(self, ev): + self.movedSincePress = False + if ev.button() == QtCore.Qt.LeftButton: + ev.accept() + self.mouseOffset = self.pos() - ev.scenePos() + self.pressPos = ev.scenePos() + elif ev.button() == QtCore.Qt.RightButton: + ev.accept() + #if self.endTick: + #return + #self.view.tickChanged(self, delete=True) + + def mouseReleaseEvent(self, ev): + #print self, "release", ev.scenePos() + if not self.movedSincePress: + 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) + #if color.isValid(): + #self.color = color + #self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) + ##self.emit(QtCore.SIGNAL('tickChanged'), self) + #self.view.tickChanged(self) + + + \ No newline at end of file diff --git a/GradientWidgetTest.py b/GradientWidgetTest.py new file mode 100644 index 00000000..95b9eeae --- /dev/null +++ b/GradientWidgetTest.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from GradientWidget import * +from PyQt4 import QtGui + +app = QtGui.QApplication([]) +w = QtGui.QMainWindow() +w.show() +w.resize(400,400) +cw = QtGui.QWidget() +w.setCentralWidget(cw) + +l = QtGui.QGridLayout() +l.setSpacing(0) +cw.setLayout(l) + +w1 = GradientWidget(orientation='top') +w2 = GradientWidget(orientation='right', allowAdd=False) +w2.setTickColor(1, QtGui.QColor(255,255,255)) +w3 = GradientWidget(orientation='bottom') +w4 = TickSlider(orientation='left') + +l.addWidget(w1, 0, 1) +l.addWidget(w2, 1, 2) +l.addWidget(w3, 2, 1) +l.addWidget(w4, 1, 0) diff --git a/GraphicsView.py b/GraphicsView.py index cb6c23fb..89467f13 100644 --- a/GraphicsView.py +++ b/GraphicsView.py @@ -10,11 +10,11 @@ from PyQt4 import QtCore, QtGui, QtOpenGL, QtSvg #import time from Point import * #from vector import * - +import sys class GraphicsView(QtGui.QGraphicsView): - def __init__(self, *args): + 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 that is automatically scaled to the full view geometry. @@ -26,8 +26,9 @@ 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().""" - QtGui.QGraphicsView.__init__(self, *args) - self.setViewport(QtOpenGL.QGLWidget()) + QtGui.QGraphicsView.__init__(self, parent) + self.useOpenGL(useOpenGL) + palette = QtGui.QPalette() brush = QtGui.QBrush(QtGui.QColor(0,0,0)) brush.setStyle(QtCore.Qt.SolidPattern) @@ -49,6 +50,7 @@ class GraphicsView(QtGui.QGraphicsView): #self.setResizeAnchor(QtGui.QGraphicsView.NoAnchor) self.setViewportUpdateMode(QtGui.QGraphicsView.SmartViewportUpdate) self.setSceneRect(QtCore.QRectF(-1e10, -1e10, 2e10, 2e10)) + #self.setSceneRect(1, 1, 0, 0) ## Set an empty (but non-zero) scene rect so that the view doesn't try to automatically update for us. #self.setInteractive(False) self.lockedViewports = [] self.lastMousePos = None @@ -68,6 +70,18 @@ class GraphicsView(QtGui.QGraphicsView): self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) self.clickAccepted = False + def useOpenGL(self, b=True): + if b: + v = QtOpenGL.QGLWidget() + else: + v = QtGui.QWidget() + + #v.setStyleSheet("background-color: #000000;") + self.setViewport(v) + + def keyPressEvent(self, ev): + ev.ignore() + def setCentralItem(self, item): if self.centralWidget is not None: self.scene().removeItem(self.centralWidget) @@ -77,6 +91,9 @@ class GraphicsView(QtGui.QGraphicsView): def addItem(self, *args): return self.scene().addItem(*args) + def removeItem(self, *args): + return self.scene().removeItem(*args) + def enableMouse(self, b=True): self.mouseEnabled = b self.autoPixelRange = (not b) @@ -128,6 +145,8 @@ class GraphicsView(QtGui.QGraphicsView): v.setXRange(self.range, padding=0) def visibleRange(self): + """Return the boundaries of the view in scene coordinates""" + ## easier to just return self.range ? r = QtCore.QRectF(self.rect()) return self.viewportTransform().inverted()[0].mapRect(r) @@ -347,6 +366,16 @@ class GraphicsView(QtGui.QGraphicsView): self.setRenderHints(rh) self.png.save(fileName) + def writePs(self, fileName=None): + if fileName is None: + fileName = str(QtGui.QFileDialog.getSaveFileName()) + printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution) + printer.setOutputFileName(fileName) + painter = QtGui.QPainter(printer) + self.render(painter) + painter.end() + + #def getFreehandLine(self): ## Wait for click diff --git a/ImageView.py b/ImageView.py index 21be5819..f7dadfe4 100644 --- a/ImageView.py +++ b/ImageView.py @@ -17,6 +17,11 @@ from ImageViewTemplate import * from graphicsItems import * from widgets import ROI from PyQt4 import QtCore, QtGui +import sys +from numpy import ndarray +import ptime + +from SignalProxy import proxyConnect class PlotROI(ROI): def __init__(self, size): @@ -35,12 +40,25 @@ class ImageView(QtGui.QWidget): self.ui = Ui_Form() self.ui.setupUi(self) self.scene = self.ui.graphicsView.sceneObj + + self.ignoreTimeLine = False + + if 'linux' in sys.platform.lower(): ## Stupid GL bug in linux. + self.ui.graphicsView.setViewport(QtGui.QWidget()) + self.ui.graphicsView.enableMouse(True) self.ui.graphicsView.autoPixelRange = False self.ui.graphicsView.setAspectLocked(True) self.ui.graphicsView.invertY() self.ui.graphicsView.enableMouse() + self. ticks = [t[0] for t in self.ui.gradientWidget.listTicks()] + self.ticks[0].colorChangeAllowed = False + self.ticks[1].colorChangeAllowed = False + self.ui.gradientWidget.allowAdd = False + self.ui.gradientWidget.setTickColor(self.ticks[1], QtGui.QColor(255,255,255)) + self.ui.gradientWidget.setOrientation('right') + self.imageItem = ImageItem() self.scene.addItem(self.imageItem) self.currentIndex = 0 @@ -51,26 +69,46 @@ class ImageView(QtGui.QWidget): self.roi.setZValue(20) self.scene.addItem(self.roi) self.roi.hide() - self.ui.roiPlot.hide() + self.normRoi = PlotROI(10) + self.normRoi.setPen(QtGui.QPen(QtGui.QColor(255,255,0))) + self.normRoi.setZValue(20) + self.scene.addItem(self.normRoi) + self.normRoi.hide() + #self.ui.roiPlot.hide() self.roiCurve = self.ui.roiPlot.plot() - self.roiTimeLine = InfiniteLine(self.ui.roiPlot, 0) - self.roiTimeLine.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0, 200))) - self.ui.roiPlot.addItem(self.roiTimeLine) + self.timeLine = InfiniteLine(self.ui.roiPlot, 0, movable=True) + self.timeLine.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0, 200))) + self.timeLine.setZValue(1) + self.ui.roiPlot.addItem(self.timeLine) + self.ui.splitter.setSizes([self.height()-35, 35]) + self.ui.roiPlot.showScale('left', False) - self.normLines = [] - for i in [0,1]: - l = InfiniteLine(self.ui.roiPlot, 0) - l.setPen(QtGui.QPen(QtGui.QColor(0, 100, 200, 200))) - self.ui.roiPlot.addItem(l) - self.normLines.append(l) - l.hide() + self.keysPressed = {} + self.playTimer = QtCore.QTimer() + self.playRate = 0 + self.lastPlayTime = 0 + + #self.normLines = [] + #for i in [0,1]: + #l = InfiniteLine(self.ui.roiPlot, 0) + #l.setPen(QtGui.QPen(QtGui.QColor(0, 100, 200, 200))) + #self.ui.roiPlot.addItem(l) + #self.normLines.append(l) + #l.hide() + self.normRgn = LinearRegionItem(self.ui.roiPlot, 'vertical') + self.normRgn.setZValue(0) + self.ui.roiPlot.addItem(self.normRgn) + self.normRgn.hide() - for fn in ['addItem']: + ## wrap functions from graphics view + for fn in ['addItem', 'removeItem']: setattr(self, fn, getattr(self.ui.graphicsView, fn)) - QtCore.QObject.connect(self.ui.timeSlider, QtCore.SIGNAL('valueChanged(int)'), self.timeChanged) - 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.timeSlider, QtCore.SIGNAL('valueChanged(int)'), self.timeChanged) + self.timeLine.connect(QtCore.SIGNAL('positionChanged'), 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(QtCore.SIGNAL('regionChanged'), self.roiChanged) QtCore.QObject.connect(self.ui.normBtn, QtCore.SIGNAL('toggled(bool)'), self.normToggled) @@ -80,21 +118,151 @@ class ImageView(QtGui.QWidget): 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.ui.normStartSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm) - QtCore.QObject.connect(self.ui.normStopSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm) + QtCore.QObject.connect(self.playTimer, QtCore.SIGNAL('timeout()'), 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(QtCore.SIGNAL('regionChangeFinished'), self.updateNorm) self.ui.roiPlot.registerPlot(self.name + '_ROI') + + self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown] + + #def __dtor__(self): + ##print "Called ImageView sip destructor" + #self.quit() + #QtGui.QWidget.__dtor__(self) + + def quit(self): + self.scene.clear() + del self.image + del self.imageDisp + + + def keyPressEvent(self, ev): + if ev.key() == QtCore.Qt.Key_Space: + if self.playRate == 0: + fps = (self.getProcessedImage().shape[0]-1) / (self.tVals[-1] - self.tVals[0]) + self.play(fps) + #print fps + else: + self.play(0) + ev.accept() + elif ev.key() == QtCore.Qt.Key_Home: + self.setCurrentIndex(0) + self.play(0) + ev.accept() + elif ev.key() == QtCore.Qt.Key_End: + self.setCurrentIndex(self.getProcessedImage().shape[0]-1) + self.play(0) + ev.accept() + elif ev.key() in self.noRepeatKeys: + ev.accept() + if ev.isAutoRepeat(): + return + self.keysPressed[ev.key()] = 1 + self.evalKeyState() + else: + QtGui.QWidget.keyPressEvent(self, ev) + + def keyReleaseEvent(self, ev): + if ev.key() in [QtCore.Qt.Key_Space, QtCore.Qt.Key_Home, QtCore.Qt.Key_End]: + ev.accept() + elif ev.key() in self.noRepeatKeys: + ev.accept() + if ev.isAutoRepeat(): + return + try: + del self.keysPressed[ev.key()] + except: + self.keysPressed = {} + self.evalKeyState() + else: + QtGui.QWidget.keyReleaseEvent(self, ev) + + + def evalKeyState(self): + if len(self.keysPressed) == 1: + key = self.keysPressed.keys()[0] + if key == QtCore.Qt.Key_Right: + self.play(20) + self.lastPlayTime = ptime.time() + 0.2 ## 2ms wait before start + self.jumpFrames(1) + elif key == QtCore.Qt.Key_Left: + self.play(-20) + self.lastPlayTime = ptime.time() + 0.2 + self.jumpFrames(-1) + elif key == QtCore.Qt.Key_Up: + self.play(-100) + elif key == QtCore.Qt.Key_Down: + self.play(100) + elif key == QtCore.Qt.Key_PageUp: + self.play(-1000) + elif key == QtCore.Qt.Key_PageDown: + self.play(1000) + else: + self.play(0) + + def play(self, rate): + #print "play:", rate + self.playRate = rate + if rate == 0: + self.playTimer.stop() + return + + self.lastPlayTime = ptime.time() + if not self.playTimer.isActive(): + self.playTimer.start(16) + + + def timeout(self): + now = ptime.time() + dt = now - self.lastPlayTime + if dt < 0: + return + n = int(self.playRate * dt) + #print n, dt + if n != 0: + #print n, dt, self.lastPlayTime + self.lastPlayTime += (float(n)/self.playRate) + if self.currentIndex+n > self.image.shape[0]: + self.play(0) + self.jumpFrames(n) + + def setCurrentIndex(self, ind): + self.currentIndex = clip(ind, 0, self.getProcessedImage().shape[0]-1) + self.updateImage() + self.ignoreTimeLine = True + self.timeLine.setValue(self.tVals[self.currentIndex]) + self.ignoreTimeLine = False + + def jumpFrames(self, n): + """If this is a video, move ahead n frames""" + if self.axes['t'] is not None: + self.setCurrentIndex(self.currentIndex + n) def updateNorm(self): - for l, sl in zip(self.normLines, [self.ui.normStartSlider, self.ui.normStopSlider]): - if self.ui.normTimeRangeCheck.isChecked(): - l.show() - else: - l.hide() + #for l, sl in zip(self.normLines, [self.ui.normStartSlider, self.ui.normStopSlider]): + #if self.ui.normTimeRangeCheck.isChecked(): + #l.show() + #else: + #l.hide() - i, t = self.timeIndex(sl) - l.setPos(t) + #i, t = self.timeIndex(sl) + #l.setPos(t) + if self.ui.normTimeRangeCheck.isChecked(): + #print "show!" + self.normRgn.show() + else: + self.normRgn.hide() + + if self.ui.normROICheck.isChecked(): + #print "show!" + self.normRoi.show() + else: + self.normRoi.hide() self.imageDisp = None self.updateImage() @@ -102,15 +270,25 @@ class ImageView(QtGui.QWidget): def normToggled(self, b): self.ui.normGroup.setVisible(b) + self.normRoi.setVisible(b and self.ui.normROICheck.isChecked()) + self.normRgn.setVisible(b and self.ui.normTimeRangeCheck.isChecked()) def roiClicked(self): if self.ui.roiBtn.isChecked(): self.roi.show() - self.ui.roiPlot.show() + #self.ui.roiPlot.show() + self.ui.roiPlot.setMouseEnabled(True, True) + self.ui.splitter.setSizes([self.height()*0.6, self.height()*0.4]) + self.roiCurve.show() self.roiChanged() + self.ui.roiPlot.showScale('left', True) else: self.roi.hide() - self.ui.roiPlot.hide() + self.ui.roiPlot.setMouseEnabled(False, False) + self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) + self.ui.splitter.setSizes([self.height()-35, 35]) + self.roiCurve.hide() + self.ui.roiPlot.showScale('left', False) def roiChanged(self): if self.image is None: @@ -130,25 +308,41 @@ class ImageView(QtGui.QWidget): self.roiCurve.setData(y=data, x=self.tVals) #self.ui.roiPlot.replot() - def setImage(self, img, autoRange=True, autoLevels=True, levels=None): + def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None): + """Set the image to be displayed in the widget. + Options are: + img: ndarray; the image to be displayed. + autoRange: bool; whether to scale/pan the view to fit the image. + autoLevels: bool; whether to update the white/black levels to fit the image. + levels: (min, max); the white and black level values to use. + 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, ndarray): + raise Exception("Image must be specified as ndarray.") self.image = img - if hasattr(img, 'xvals'): + + if xvals is not None: + self.tVals = xvals + elif hasattr(img, 'xvals'): self.tVals = img.xvals(0) else: self.tVals = arange(img.shape[0]) - self.ui.timeSlider.setValue(0) + #self.ui.timeSlider.setValue(0) #self.ui.normStartSlider.setValue(0) #self.ui.timeSlider.setMaximum(img.shape[0]-1) - - if img.ndim == 2: - self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None} - elif img.ndim == 3: - if img.shape[2] <= 3: - self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2} - else: - self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None} - elif img.ndim == 4: - self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} + + if axes is None: + if img.ndim == 2: + self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None} + elif img.ndim == 3: + if img.shape[2] <= 4: + self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2} + else: + self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None} + elif img.ndim == 4: + self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} self.imageDisp = None @@ -163,11 +357,36 @@ class ImageView(QtGui.QWidget): if self.ui.roiBtn.isChecked(): self.roiChanged() + + if self.axes['t'] is not None: + #self.ui.roiPlot.show() + self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) + #self.ui.roiPlot.setMouseEnabled(False, False) + if len(self.tVals) > 1: + start = self.tVals.min() + stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02 + elif len(self.tVals) == 1: + start = self.tVals[0] - 0.5 + stop = self.tVals[0] + 0.5 + else: + start = 0 + stop = 1 + for s in [self.timeLine, self.normRgn]: + s.setBounds([start, stop]) + #else: + #self.ui.roiPlot.hide() + + self.roiClicked() + + def autoLevels(self): image = self.getProcessedImage() - self.ui.whiteSlider.setValue(self.ui.whiteSlider.maximum()) - self.ui.blackSlider.setValue(0) + #self.ui.whiteSlider.setValue(self.ui.whiteSlider.maximum()) + #self.ui.blackSlider.setValue(0) + + self.ui.gradientWidget.setTickValue(self.ticks[0], 0.0) + self.ui.gradientWidget.setTickValue(self.ticks[1], 1.0) self.imageItem.setLevels(white=self.whiteLevel(), black=self.blackLevel()) def autoRange(self): @@ -189,7 +408,7 @@ class ImageView(QtGui.QWidget): return image div = self.ui.normDivideRadio.isChecked() - norm = image.copy() + norm = image.view(ndarray).copy() #if div: #norm = ones(image.shape) #else: @@ -198,8 +417,8 @@ class ImageView(QtGui.QWidget): norm = norm.astype(float32) if self.ui.normTimeRangeCheck.isChecked() and image.ndim == 3: - (sind, start) = self.timeIndex(self.ui.normStartSlider) - (eind, end) = self.timeIndex(self.ui.normStopSlider) + (sind, start) = self.timeIndex(self.normRgn.lines[0]) + (eind, end) = self.timeIndex(self.normRgn.lines[1]) #print start, end, sind, eind n = image[sind:eind+1].mean(axis=0) n.shape = (1,) + n.shape @@ -216,17 +435,27 @@ class ImageView(QtGui.QWidget): else: norm -= n + 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] + #print start, end, sind, eind + if div: + norm /= n + else: + norm -= n + return norm - - - def timeChanged(self): - (ind, time) = self.timeIndex(self.ui.timeSlider) + def timeLineChanged(self): + #(ind, time) = self.timeIndex(self.ui.timeSlider) + if self.ignoreTimeLine: + return + self.play(0) + (ind, time) = self.timeIndex(self.timeLine) if ind != self.currentIndex: self.currentIndex = ind self.updateImage() - self.roiTimeLine.setPos(time) - #self.ui.roiPlot.replot() + #self.timeLine.setPos(time) self.emit(QtCore.SIGNAL('timeChanged'), ind, time) def updateImage(self): @@ -237,29 +466,38 @@ class ImageView(QtGui.QWidget): image = self.getProcessedImage() #print "update:", image.ndim, image.max(), image.min(), self.blackLevel(), self.whiteLevel() if self.axes['t'] is None: - self.ui.timeSlider.hide() + #self.ui.timeSlider.hide() self.imageItem.updateImage(image, white=self.whiteLevel(), black=self.blackLevel()) + self.ui.roiPlot.hide() + self.ui.roiBtn.hide() else: - self.ui.timeSlider.show() + self.ui.roiBtn.show() + self.ui.roiPlot.show() + #self.ui.timeSlider.show() self.imageItem.updateImage(image[self.currentIndex], white=self.whiteLevel(), black=self.blackLevel()) + def timeIndex(self, slider): """Return the time and frame index indicated by a slider""" if self.image is None: return (0,0) - v = slider.value() - vmax = slider.maximum() - f = float(v) / vmax - t = 0.0 + #v = slider.value() + #vmax = slider.maximum() + #f = float(v) / vmax + + t = slider.value() + + #t = 0.0 #xv = self.image.xvals('Time') xv = self.tVals if xv is None: - ind = int(f * self.image.shape[0]) + ind = int(t) + #ind = int(f * self.image.shape[0]) else: if len(xv) < 2: return (0,0) totTime = xv[-1] + (xv[-1]-xv[-2]) - t = f * totTime + #t = f * totTime inds = argwhere(xv < t) if len(inds) < 1: return (0,t) @@ -268,8 +506,10 @@ class ImageView(QtGui.QWidget): return ind, t def whiteLevel(self): - return self.levelMin + (self.levelMax-self.levelMin) * self.ui.whiteSlider.value() / self.ui.whiteSlider.maximum() + return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[1]) + #return self.levelMin + (self.levelMax-self.levelMin) * self.ui.whiteSlider.value() / self.ui.whiteSlider.maximum() def blackLevel(self): - return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value() + return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[0]) + #return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value() \ No newline at end of file diff --git a/ImageViewTemplate.py b/ImageViewTemplate.py index 7521dd53..6e2cdc9f 100644 --- a/ImageViewTemplate.py +++ b/ImageViewTemplate.py @@ -1,188 +1,167 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'ImageViewTemplate.ui' -# -# Created: Mon Mar 29 22:40:48 2010 -# by: PyQt4 UI code generator 4.6 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(757, 495) - self.verticalLayout = QtGui.QVBoxLayout(Form) - self.verticalLayout.setSpacing(0) - self.verticalLayout.setMargin(0) - self.verticalLayout.setObjectName("verticalLayout") - self.splitter = QtGui.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Vertical) - self.splitter.setObjectName("splitter") - self.layoutWidget = QtGui.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout = QtGui.QGridLayout(self.layoutWidget) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.graphicsView = GraphicsView(self.layoutWidget) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.graphicsView.sizePolicy().hasHeightForWidth()) - self.graphicsView.setSizePolicy(sizePolicy) - self.graphicsView.setObjectName("graphicsView") - self.gridLayout.addWidget(self.graphicsView, 0, 0, 3, 1) - self.blackSlider = QtGui.QSlider(self.layoutWidget) - self.blackSlider.setMaximum(4096) - self.blackSlider.setOrientation(QtCore.Qt.Vertical) - self.blackSlider.setInvertedAppearance(False) - self.blackSlider.setInvertedControls(False) - self.blackSlider.setTickPosition(QtGui.QSlider.TicksBelow) - self.blackSlider.setTickInterval(410) - self.blackSlider.setObjectName("blackSlider") - self.gridLayout.addWidget(self.blackSlider, 0, 1, 1, 1) - self.whiteSlider = QtGui.QSlider(self.layoutWidget) - self.whiteSlider.setMaximum(4096) - self.whiteSlider.setProperty("value", 4096) - self.whiteSlider.setOrientation(QtCore.Qt.Vertical) - self.whiteSlider.setObjectName("whiteSlider") - self.gridLayout.addWidget(self.whiteSlider, 0, 2, 1, 2) - self.label = QtGui.QLabel(self.layoutWidget) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 1, 1, 1, 1) - self.label_2 = QtGui.QLabel(self.layoutWidget) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 1, 2, 1, 1) - self.roiBtn = QtGui.QPushButton(self.layoutWidget) - self.roiBtn.setMaximumSize(QtCore.QSize(40, 16777215)) - self.roiBtn.setCheckable(True) - self.roiBtn.setObjectName("roiBtn") - self.gridLayout.addWidget(self.roiBtn, 2, 1, 1, 3) - self.timeSlider = QtGui.QSlider(self.layoutWidget) - self.timeSlider.setMaximum(65535) - self.timeSlider.setOrientation(QtCore.Qt.Horizontal) - self.timeSlider.setObjectName("timeSlider") - self.gridLayout.addWidget(self.timeSlider, 4, 0, 1, 1) - self.normBtn = QtGui.QPushButton(self.layoutWidget) - self.normBtn.setMaximumSize(QtCore.QSize(50, 16777215)) - self.normBtn.setCheckable(True) - self.normBtn.setObjectName("normBtn") - self.gridLayout.addWidget(self.normBtn, 4, 1, 1, 2) - self.normGroup = QtGui.QGroupBox(self.layoutWidget) - self.normGroup.setObjectName("normGroup") - self.gridLayout_2 = QtGui.QGridLayout(self.normGroup) - self.gridLayout_2.setMargin(0) - self.gridLayout_2.setSpacing(0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.normSubtractRadio = QtGui.QRadioButton(self.normGroup) - self.normSubtractRadio.setObjectName("normSubtractRadio") - self.gridLayout_2.addWidget(self.normSubtractRadio, 0, 2, 1, 1) - self.normDivideRadio = QtGui.QRadioButton(self.normGroup) - self.normDivideRadio.setChecked(False) - self.normDivideRadio.setObjectName("normDivideRadio") - self.gridLayout_2.addWidget(self.normDivideRadio, 0, 1, 1, 1) - self.label_5 = QtGui.QLabel(self.normGroup) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.label_5.setFont(font) - self.label_5.setObjectName("label_5") - self.gridLayout_2.addWidget(self.label_5, 0, 0, 1, 1) - self.label_3 = QtGui.QLabel(self.normGroup) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.label_3.setFont(font) - self.label_3.setObjectName("label_3") - self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) - self.label_4 = QtGui.QLabel(self.normGroup) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.label_4.setFont(font) - self.label_4.setObjectName("label_4") - self.gridLayout_2.addWidget(self.label_4, 4, 0, 1, 1) - self.normROICheck = QtGui.QCheckBox(self.normGroup) - self.normROICheck.setObjectName("normROICheck") - self.gridLayout_2.addWidget(self.normROICheck, 1, 1, 1, 1) - self.normStartSlider = QtGui.QSlider(self.normGroup) - self.normStartSlider.setMaximum(65535) - self.normStartSlider.setOrientation(QtCore.Qt.Horizontal) - self.normStartSlider.setObjectName("normStartSlider") - self.gridLayout_2.addWidget(self.normStartSlider, 2, 0, 1, 6) - self.normStopSlider = QtGui.QSlider(self.normGroup) - self.normStopSlider.setMaximum(65535) - self.normStopSlider.setOrientation(QtCore.Qt.Horizontal) - self.normStopSlider.setObjectName("normStopSlider") - self.gridLayout_2.addWidget(self.normStopSlider, 3, 0, 1, 6) - self.normXBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) - self.normXBlurSpin.setObjectName("normXBlurSpin") - self.gridLayout_2.addWidget(self.normXBlurSpin, 4, 2, 1, 1) - self.label_8 = QtGui.QLabel(self.normGroup) - self.label_8.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_8.setObjectName("label_8") - self.gridLayout_2.addWidget(self.label_8, 4, 1, 1, 1) - self.label_9 = QtGui.QLabel(self.normGroup) - self.label_9.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_9.setObjectName("label_9") - self.gridLayout_2.addWidget(self.label_9, 4, 3, 1, 1) - self.normYBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) - self.normYBlurSpin.setObjectName("normYBlurSpin") - self.gridLayout_2.addWidget(self.normYBlurSpin, 4, 4, 1, 1) - self.label_10 = QtGui.QLabel(self.normGroup) - self.label_10.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_10.setObjectName("label_10") - self.gridLayout_2.addWidget(self.label_10, 4, 5, 1, 1) - self.normStopLabel = QtGui.QLabel(self.normGroup) - self.normStopLabel.setObjectName("normStopLabel") - self.gridLayout_2.addWidget(self.normStopLabel, 3, 6, 1, 1) - self.normStartLabel = QtGui.QLabel(self.normGroup) - self.normStartLabel.setObjectName("normStartLabel") - self.gridLayout_2.addWidget(self.normStartLabel, 2, 6, 1, 1) - self.normOffRadio = QtGui.QRadioButton(self.normGroup) - self.normOffRadio.setChecked(True) - self.normOffRadio.setObjectName("normOffRadio") - self.gridLayout_2.addWidget(self.normOffRadio, 0, 3, 1, 1) - self.normTimeRangeCheck = QtGui.QCheckBox(self.normGroup) - self.normTimeRangeCheck.setObjectName("normTimeRangeCheck") - self.gridLayout_2.addWidget(self.normTimeRangeCheck, 1, 3, 1, 1) - self.normFrameCheck = QtGui.QCheckBox(self.normGroup) - self.normFrameCheck.setObjectName("normFrameCheck") - self.gridLayout_2.addWidget(self.normFrameCheck, 1, 2, 1, 1) - self.normTBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) - self.normTBlurSpin.setObjectName("normTBlurSpin") - self.gridLayout_2.addWidget(self.normTBlurSpin, 4, 6, 1, 1) - self.gridLayout.addWidget(self.normGroup, 5, 0, 1, 4) - self.roiPlot = PlotWidget(self.splitter) - self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) - self.roiPlot.setObjectName("roiPlot") - self.verticalLayout.addWidget(self.splitter) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("Form", "B", None, QtGui.QApplication.UnicodeUTF8)) - self.label_2.setText(QtGui.QApplication.translate("Form", "W", None, QtGui.QApplication.UnicodeUTF8)) - self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) - self.normBtn.setText(QtGui.QApplication.translate("Form", "Norm", None, QtGui.QApplication.UnicodeUTF8)) - self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8)) - self.normSubtractRadio.setText(QtGui.QApplication.translate("Form", "Subtract", None, QtGui.QApplication.UnicodeUTF8)) - self.normDivideRadio.setText(QtGui.QApplication.translate("Form", "Divide", None, QtGui.QApplication.UnicodeUTF8)) - self.label_5.setText(QtGui.QApplication.translate("Form", "Operation:", None, QtGui.QApplication.UnicodeUTF8)) - self.label_3.setText(QtGui.QApplication.translate("Form", "Mean:", None, QtGui.QApplication.UnicodeUTF8)) - self.label_4.setText(QtGui.QApplication.translate("Form", "Blur:", None, QtGui.QApplication.UnicodeUTF8)) - self.normROICheck.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) - self.label_8.setText(QtGui.QApplication.translate("Form", "X", None, QtGui.QApplication.UnicodeUTF8)) - self.label_9.setText(QtGui.QApplication.translate("Form", "Y", None, QtGui.QApplication.UnicodeUTF8)) - self.label_10.setText(QtGui.QApplication.translate("Form", "T", None, QtGui.QApplication.UnicodeUTF8)) - self.normStopLabel.setText(QtGui.QApplication.translate("Form", "Stop", None, QtGui.QApplication.UnicodeUTF8)) - self.normStartLabel.setText(QtGui.QApplication.translate("Form", "Start", None, QtGui.QApplication.UnicodeUTF8)) - self.normOffRadio.setText(QtGui.QApplication.translate("Form", "Off", None, QtGui.QApplication.UnicodeUTF8)) - self.normTimeRangeCheck.setText(QtGui.QApplication.translate("Form", "Time range", None, QtGui.QApplication.UnicodeUTF8)) - self.normFrameCheck.setText(QtGui.QApplication.translate("Form", "Frame", None, QtGui.QApplication.UnicodeUTF8)) - -from GraphicsView import GraphicsView -from PlotWidget import PlotWidget +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ImageViewTemplate.ui' +# +# Created: Sat Jul 17 13:05:44 2010 +# by: PyQt4 UI code generator 4.5.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(726, 588) + self.verticalLayout = QtGui.QVBoxLayout(Form) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setMargin(0) + self.verticalLayout.setObjectName("verticalLayout") + self.splitter = QtGui.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtGui.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout = QtGui.QGridLayout(self.layoutWidget) + self.gridLayout.setMargin(0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.graphicsView = GraphicsView(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(10) + sizePolicy.setVerticalStretch(10) + sizePolicy.setHeightForWidth(self.graphicsView.sizePolicy().hasHeightForWidth()) + self.graphicsView.setSizePolicy(sizePolicy) + self.graphicsView.setObjectName("graphicsView") + self.gridLayout.addWidget(self.graphicsView, 1, 0, 3, 1) + self.roiBtn = QtGui.QPushButton(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.roiBtn.sizePolicy().hasHeightForWidth()) + self.roiBtn.setSizePolicy(sizePolicy) + self.roiBtn.setMaximumSize(QtCore.QSize(30, 16777215)) + self.roiBtn.setCheckable(True) + self.roiBtn.setObjectName("roiBtn") + self.gridLayout.addWidget(self.roiBtn, 3, 3, 1, 1) + self.gradientWidget = GradientWidget(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(100) + sizePolicy.setHeightForWidth(self.gradientWidget.sizePolicy().hasHeightForWidth()) + self.gradientWidget.setSizePolicy(sizePolicy) + self.gradientWidget.setObjectName("gradientWidget") + self.gridLayout.addWidget(self.gradientWidget, 1, 3, 1, 1) + self.normBtn = QtGui.QPushButton(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth()) + self.normBtn.setSizePolicy(sizePolicy) + self.normBtn.setMaximumSize(QtCore.QSize(30, 16777215)) + self.normBtn.setCheckable(True) + self.normBtn.setObjectName("normBtn") + self.gridLayout.addWidget(self.normBtn, 2, 3, 1, 1) + self.normGroup = QtGui.QGroupBox(self.layoutWidget) + self.normGroup.setObjectName("normGroup") + self.gridLayout_2 = QtGui.QGridLayout(self.normGroup) + self.gridLayout_2.setMargin(0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.normSubtractRadio = QtGui.QRadioButton(self.normGroup) + self.normSubtractRadio.setObjectName("normSubtractRadio") + self.gridLayout_2.addWidget(self.normSubtractRadio, 0, 2, 1, 1) + self.normDivideRadio = QtGui.QRadioButton(self.normGroup) + self.normDivideRadio.setChecked(False) + self.normDivideRadio.setObjectName("normDivideRadio") + self.gridLayout_2.addWidget(self.normDivideRadio, 0, 1, 1, 1) + self.label_5 = QtGui.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_5.setFont(font) + self.label_5.setObjectName("label_5") + self.gridLayout_2.addWidget(self.label_5, 0, 0, 1, 1) + self.label_3 = QtGui.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_3.setFont(font) + self.label_3.setObjectName("label_3") + self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) + self.label_4 = QtGui.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_4.setFont(font) + self.label_4.setObjectName("label_4") + self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) + self.normROICheck = QtGui.QCheckBox(self.normGroup) + self.normROICheck.setObjectName("normROICheck") + self.gridLayout_2.addWidget(self.normROICheck, 1, 1, 1, 1) + self.normXBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) + self.normXBlurSpin.setObjectName("normXBlurSpin") + self.gridLayout_2.addWidget(self.normXBlurSpin, 2, 2, 1, 1) + self.label_8 = QtGui.QLabel(self.normGroup) + self.label_8.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_8.setObjectName("label_8") + self.gridLayout_2.addWidget(self.label_8, 2, 1, 1, 1) + self.label_9 = QtGui.QLabel(self.normGroup) + self.label_9.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_9.setObjectName("label_9") + self.gridLayout_2.addWidget(self.label_9, 2, 3, 1, 1) + self.normYBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) + self.normYBlurSpin.setObjectName("normYBlurSpin") + self.gridLayout_2.addWidget(self.normYBlurSpin, 2, 4, 1, 1) + self.label_10 = QtGui.QLabel(self.normGroup) + self.label_10.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_10.setObjectName("label_10") + self.gridLayout_2.addWidget(self.label_10, 2, 5, 1, 1) + self.normOffRadio = QtGui.QRadioButton(self.normGroup) + self.normOffRadio.setChecked(True) + self.normOffRadio.setObjectName("normOffRadio") + self.gridLayout_2.addWidget(self.normOffRadio, 0, 3, 1, 1) + self.normTimeRangeCheck = QtGui.QCheckBox(self.normGroup) + self.normTimeRangeCheck.setObjectName("normTimeRangeCheck") + self.gridLayout_2.addWidget(self.normTimeRangeCheck, 1, 3, 1, 1) + self.normFrameCheck = QtGui.QCheckBox(self.normGroup) + self.normFrameCheck.setObjectName("normFrameCheck") + self.gridLayout_2.addWidget(self.normFrameCheck, 1, 2, 1, 1) + self.normTBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) + self.normTBlurSpin.setObjectName("normTBlurSpin") + self.gridLayout_2.addWidget(self.normTBlurSpin, 2, 6, 1, 1) + self.gridLayout.addWidget(self.normGroup, 0, 0, 1, 4) + self.roiPlot = PlotWidget(self.splitter) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.roiPlot.sizePolicy().hasHeightForWidth()) + self.roiPlot.setSizePolicy(sizePolicy) + self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) + self.roiPlot.setObjectName("roiPlot") + self.verticalLayout.addWidget(self.splitter) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.roiBtn.setText(QtGui.QApplication.translate("Form", "R", None, QtGui.QApplication.UnicodeUTF8)) + self.normBtn.setText(QtGui.QApplication.translate("Form", "N", None, QtGui.QApplication.UnicodeUTF8)) + self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8)) + self.normSubtractRadio.setText(QtGui.QApplication.translate("Form", "Subtract", None, QtGui.QApplication.UnicodeUTF8)) + self.normDivideRadio.setText(QtGui.QApplication.translate("Form", "Divide", None, QtGui.QApplication.UnicodeUTF8)) + self.label_5.setText(QtGui.QApplication.translate("Form", "Operation:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_3.setText(QtGui.QApplication.translate("Form", "Mean:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_4.setText(QtGui.QApplication.translate("Form", "Blur:", None, QtGui.QApplication.UnicodeUTF8)) + self.normROICheck.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) + self.label_8.setText(QtGui.QApplication.translate("Form", "X", None, QtGui.QApplication.UnicodeUTF8)) + self.label_9.setText(QtGui.QApplication.translate("Form", "Y", None, QtGui.QApplication.UnicodeUTF8)) + self.label_10.setText(QtGui.QApplication.translate("Form", "T", None, QtGui.QApplication.UnicodeUTF8)) + self.normOffRadio.setText(QtGui.QApplication.translate("Form", "Off", None, QtGui.QApplication.UnicodeUTF8)) + self.normTimeRangeCheck.setText(QtGui.QApplication.translate("Form", "Time range", None, QtGui.QApplication.UnicodeUTF8)) + self.normFrameCheck.setText(QtGui.QApplication.translate("Form", "Frame", None, QtGui.QApplication.UnicodeUTF8)) + +from GraphicsView import GraphicsView +from pyqtgraph.GradientWidget import GradientWidget +from PlotWidget import PlotWidget diff --git a/ImageViewTemplate.ui b/ImageViewTemplate.ui index eb2c3f03..98d58a1f 100644 --- a/ImageViewTemplate.ui +++ b/ImageViewTemplate.ui @@ -6,8 +6,8 @@ 0 0 - 757 - 495 + 726 + 588 @@ -27,113 +27,77 @@ + + 0 + 0 - + - 0 - 0 + 10 + 10 - normGroup - normGroup - - - - 4096 - - - Qt::Vertical - - - false - - - false - - - QSlider::TicksBelow - - - 410 - - - - - - - 4096 - - - 4096 - - - Qt::Vertical - - - - - - - B - - - - - - - W - - - - + + + + 0 + 1 + + - 40 + 30 16777215 - ROI + R true - - - - 65535 - - - Qt::Horizontal + + + + + 0 + 100 + - + + + + 0 + 1 + + - 50 + 30 16777215 - Norm + N true - + Normalization @@ -188,7 +152,7 @@ - + @@ -208,30 +172,10 @@ - - - - 65535 - - - Qt::Horizontal - - - - - - - 65535 - - - Qt::Horizontal - - - - + - + X @@ -241,7 +185,7 @@ - + Y @@ -251,10 +195,10 @@ - + - + T @@ -264,20 +208,6 @@ - - - - Stop - - - - - - - Start - - - @@ -302,7 +232,7 @@ - + @@ -311,6 +241,12 @@ + + + 0 + 0 + + 0 @@ -323,6 +259,12 @@ + + GradientWidget + QWidget +
pyqtgraph.GradientWidget
+ 1 +
GraphicsView QWidget diff --git a/ObjectWorkaround.py b/ObjectWorkaround.py new file mode 100644 index 00000000..67982dd4 --- /dev/null +++ b/ObjectWorkaround.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from PyQt4 import QtGui, QtCore + +class QObjectWorkaround: + def __init__(self): + self._qObj_ = QtCore.QObject() + 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) + +class QGraphicsObject(QtGui.QGraphicsItem, QObjectWorkaround): + def __init__(self, *args): + QtGui.QGraphicsItem.__init__(self, *args) + QObjectWorkaround.__init__(self) diff --git a/PlotItem.py b/PlotItem.py index ce6d70cf..72ab48c5 100644 --- a/PlotItem.py +++ b/PlotItem.py @@ -20,6 +20,8 @@ This class is very heavily featured: from graphicsItems import * from plotConfigTemplate import * from PyQt4 import QtGui, QtCore, QtSvg +#from ObjectWorkaround import * +#tryWorkaround(QtCore, QtGui) import weakref try: @@ -42,7 +44,7 @@ class PlotItem(QtGui.QGraphicsWidget): lastFileDir = None managers = {} - def __init__(self, parent=None): + def __init__(self, parent=None, name=None): QtGui.QGraphicsWidget.__init__(self, parent) ## Set up control buttons @@ -56,6 +58,7 @@ class PlotItem(QtGui.QGraphicsWidget): for b in [self.ctrlBtn, self.autoBtn]: proxy = QtGui.QGraphicsProxyWidget(self) proxy.setWidget(b) + proxy.setAcceptHoverEvents(False) b.setStyleSheet("background-color: #000000; color: #888; font-size: 6pt") QtCore.QObject.connect(self.ctrlBtn, QtCore.SIGNAL('clicked()'), self.ctrlBtnClicked) QtCore.QObject.connect(self.autoBtn, QtCore.SIGNAL('clicked()'), self.enableAutoScale) @@ -124,7 +127,7 @@ class PlotItem(QtGui.QGraphicsWidget): ## Wrap a few methods from viewBox - for m in ['setXRange', 'setYRange', 'setRange', 'autoRange', 'viewRect']: + for m in ['setXRange', 'setYRange', 'setRange', 'autoRange', 'viewRect', 'setMouseEnabled']: setattr(self, m, getattr(self.vb, m)) self.items = [] @@ -178,9 +181,13 @@ class PlotItem(QtGui.QGraphicsWidget): 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.gridGroup, QtCore.SIGNAL('toggled(bool)'), self.updateGrid) + QtCore.QObject.connect(c.gridAlphaSlider, QtCore.SIGNAL('valueChanged(int)'), 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.gridGroup, QtCore.SIGNAL('toggled(bool)'), self.updateGrid) #QtCore.QObject.connect(c.gridAlphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateGrid) @@ -215,7 +222,11 @@ class PlotItem(QtGui.QGraphicsWidget): self.showScale('top', False) self.showScale('left', True) self.showScale('bottom', True) - + + if name is not None: + self.registerPlot(name) + + def __del__(self): if self.manager is not None: self.manager.removeWidget(self.name) @@ -250,6 +261,13 @@ class PlotItem(QtGui.QGraphicsWidget): refs= gc.get_referrers(self) print " error during update. Referrers are:", refs raise + + def updateGrid(self, *args): + g = self.ctrl.gridGroup.isChecked() + if g: + g = self.ctrl.gridAlphaSlider.value() + for k in self.scales: + self.scales[k]['item'].setGrid(g) def viewGeometry(self): """return the screen geometry of the viewbox""" @@ -261,6 +279,8 @@ class PlotItem(QtGui.QGraphicsWidget): return wr + + def viewChanged(self, *args): self.emit(QtCore.SIGNAL('viewChanged'), *args) @@ -273,23 +293,31 @@ class PlotItem(QtGui.QGraphicsWidget): def yLinkComboChanged(self): self.setYLink(str(self.ctrl.yLinkCombo.currentText())) - def setXLink(self, plotName=None): - if self.manager is None: - return - if self.xLinkPlot is not None: - self.manager.unlinkX(self, self.xLinkPlot) - plot = self.manager.getWidget(plotName) + def setXLink(self, plot=None): + """Link this plot's X axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" + if isinstance(plot, basestring): + if self.manager is None: + return + if self.xLinkPlot is not None: + self.manager.unlinkX(self, self.xLinkPlot) + plot = self.manager.getWidget(plot) + if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): + plot = plot.getPlotItem() self.xLinkPlot = plot if plot is not None: self.setManualXScale() self.manager.linkX(self, plot) - def setYLink(self, plotName=None): - if self.manager is None: - return - if self.yLinkPlot is not None: - self.manager.unlinkY(self, self.yLinkPlot) - plot = self.manager.getWidget(plotName) + def setYLink(self, plot=None): + """Link this plot's Y axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" + if isinstance(plot, basestring): + if self.manager is None: + return + if self.yLinkPlot is not None: + self.manager.unlinkY(self, self.yLinkPlot) + plot = self.manager.getWidget(plot) + if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): + plot = plot.getPlotItem() self.yLinkPlot = plot if plot is not None: self.setManualYScale() @@ -339,6 +367,8 @@ class PlotItem(QtGui.QGraphicsWidget): self.recomputeAverages() def recomputeAverages(self): + if not self.ctrl.averageGroup.isChecked(): + return for k in self.avgCurves: self.removeItem(self.avgCurves[k][1]) #Qwt.QwtPlotCurve.detach(self.avgCurves[k][1]) @@ -406,6 +436,8 @@ class PlotItem(QtGui.QGraphicsWidget): self.vb.setMouseEnabled(*state) def xRangeChanged(self, _, range): + if any(isnan(range)) or any(isinf(range)): + raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) self.ctrl.xMinText.setText('%0.5g' % range[0]) self.ctrl.xMaxText.setText('%0.5g' % range[1]) @@ -423,6 +455,8 @@ class PlotItem(QtGui.QGraphicsWidget): self.emit(QtCore.SIGNAL('xRangeChanged'), self, range) def yRangeChanged(self, _, range): + if any(isnan(range)) or any(isinf(range)): + raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) self.ctrl.yMinText.setText('%0.5g' % range[0]) self.ctrl.yMaxText.setText('%0.5g' % range[1]) @@ -511,18 +545,25 @@ class PlotItem(QtGui.QGraphicsWidget): if not item in self.items: return self.items.remove(item) - self.vb.removeItem(item) + if item.scene() is not None: + self.vb.removeItem(item) if item in self.curves: self.curves.remove(item) self.updateDecimation() self.updateParamList() - QtCore.QObject.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) + item.connect(QtCore.SIGNAL('plotChanged'), self.plotChanged) def clear(self): for i in self.items[:]: self.removeItem(i) self.avgCurves = {} + + def clearPlots(self): + for i in self.curves[:]: + self.removeItem(i) + self.avgCurves = {} + def plot(self, data=None, x=None, clear=False, params=None, pen=None): if clear: self.clear() @@ -549,6 +590,8 @@ class PlotItem(QtGui.QGraphicsWidget): return curve def addCurve(self, c, params=None): + if params is None: + params = {} c.setMeta(params) self.curves.append(c) #Qwt.QwtPlotCurve.attach(c, self) @@ -569,7 +612,7 @@ class PlotItem(QtGui.QGraphicsWidget): if self.ctrl.averageGroup.isChecked(): self.addAvgCurve(c) - QtCore.QObject.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) + c.connect(QtCore.SIGNAL('plotChanged'), self.plotChanged) self.plotChanged() def plotChanged(self, curve=None): @@ -587,7 +630,7 @@ class PlotItem(QtGui.QGraphicsWidget): mn = cmn if mx is None or cmx > mx: mx = cmx - if mn is None or mx is None: + if mn is None or mx is None or any(isnan([mn, mx])) or any(isinf([mn, mx])): continue if mn == mx: mn -= 1 @@ -632,24 +675,58 @@ class PlotItem(QtGui.QGraphicsWidget): if fileName is None: fileName = QtGui.QFileDialog.getSaveFileName() fileName = str(fileName) - + PlotItem.lastFileDir = os.path.dirname(fileName) self.svg = QtSvg.QSvgGenerator() self.svg.setFileName(fileName) res = 120. - self.svg.setResolution(res) - self.svg.setSize(QtCore.QSize(self.size().width(), self.size().height())) - painter = QtGui.QPainter(self.svg) - #self.scene().render(painter, QtCore.QRectF(), self.mapRectToScene(self.boundingRect())) - items = self.scene().items() - self.scene().views()[0].drawItems(painter, len(items), items) + #bounds = self.mapRectToScene(self.boundingRect()) + view = self.scene().views()[0] + bounds = view.viewport().rect() + bounds = QtCore.QRectF(0, 0, bounds.width(), bounds.height()) + self.svg.setResolution(res) + #self.svg.setSize(QtCore.QSize(self.size().width(), self.size().height())) + self.svg.setViewBox(bounds) + + self.svg.setSize(QtCore.QSize(bounds.width(), bounds.height())) + + painter = QtGui.QPainter(self.svg) + #self.scene().render(painter, QtCore.QRectF(), view.mapToScene(bounds).boundingRect()) + + #items = self.scene().items() + #self.scene().views()[0].drawItems(painter, len(items), items) + + #print view, type(view) + view.render(painter, bounds) + + painter.end() + + ## 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'( 0. (got %s)" % str(frac)) else: - bins = 1000 - h = histogram(d, bins) - s = len(d) * (1.0-frac) - mnTot = mxTot = 0 - mnInd = mxInd = 0 - for i in range(bins): - mnTot += h[0][i] - if mnTot > s: - mnInd = i - break - for i in range(bins): - mxTot += h[0][-i-1] - if mxTot > s: - mxInd = -i-1 - break - #print mnInd, mxInd, h[1][mnInd], h[1][mxInd] - return(h[1][mnInd], h[1][mxInd]) + return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + #bins = 1000 + #h = histogram(d, bins) + #s = len(d) * (1.0-frac) + #mnTot = mxTot = 0 + #mnInd = mxInd = 0 + #for i in range(bins): + #mnTot += h[0][i] + #if mnTot > s: + #mnInd = i + #break + #for i in range(bins): + #mxTot += h[0][-i-1] + #if mxTot > s: + #mxInd = -i-1 + #break + ##print mnInd, mxInd, h[1][mnInd], h[1][mxInd] + #return(h[1][mnInd], h[1][mxInd]) @@ -446,10 +576,18 @@ class PlotCurveItem(QtGui.QGraphicsWidget): if x is None or y is None or len(x) == 0 or len(y) == 0: return QtCore.QRectF() - xmin = x.min() - xmax = x.max() - ymin = y.min() - ymax = y.max() + + if self.shadow is not None: + lineWidth = (max(self.pen.width(), self.shadow.width()) + 1) + else: + lineWidth = (self.pen.width()+1) + + + pixels = self.pixelVectors() + xmin = x.min() - pixels[0].x() * lineWidth + xmax = x.max() + pixels[0].x() * lineWidth + ymin = y.min() - abs(pixels[1].y()) * lineWidth + ymax = y.max() + abs(pixels[1].y()) * lineWidth return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) def paint(self, p, opt, widget): @@ -486,6 +624,10 @@ class PlotCurveItem(QtGui.QGraphicsWidget): p.setPen(cp) p.drawPath(path) + #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) + #p.drawRect(self.boundingRect()) + + def free(self): self.xData = None ## raw values self.yData = None @@ -521,38 +663,39 @@ class ROIPlotItem(PlotCurveItem): -class UIGraphicsItem(QtGui.QGraphicsItem): +class UIGraphicsItem(GraphicsObject): """Base class for graphics items with boundaries relative to a GraphicsView widget""" def __init__(self, view, bounds=None): - QtGui.QGraphicsItem.__init__(self) - self._view = view + GraphicsObject.__init__(self) + self._view = weakref.ref(view) if bounds is None: self._bounds = QtCore.QRectF(0, 0, 1, 1) else: self._bounds = bounds - self._viewRect = self._view.rect() + self._viewRect = self._view().rect() self._viewTransform = self.viewTransform() self.setNewBounds() + QtCore.QObject.connect(view, QtCore.SIGNAL('viewChanged'), self.viewChangedEvent) def viewRect(self): """Return the viewport widget rect""" - return self._view.rect() + return self._view().rect() def viewTransform(self): """Returns a matrix that maps viewport coordinates onto scene coordinates""" - if self._view is None: + if self._view() is None: return QtGui.QTransform() else: - return self._view.viewportTransform() + return self._view().viewportTransform() def boundingRect(self): - if self._view is None: + if self._view() is None: self.bounds = self._bounds else: - vr = self._view.rect() + vr = self._view().rect() tr = self.viewTransform() if vr != self._viewRect or tr != self._viewTransform: - self.viewChangedEvent(vr, self._viewRect) + #self.viewChangedEvent(vr, self._viewRect) self._viewRect = vr self._viewTransform = tr self.setNewBounds() @@ -569,9 +712,10 @@ class UIGraphicsItem(QtGui.QGraphicsItem): self.bounds = self.viewTransform().inverted()[0].mapRect(bounds) self.prepareGeometryChange() - def viewChangedEvent(self, newRect, oldRect): + def viewChangedEvent(self): """Called when the view widget is resized""" - pass + self.boundingRect() + self.update() def unitRect(self): return self.viewTransform().inverted()[0].mapRect(QtCore.QRectF(0, 0, 1, 1)) @@ -664,7 +808,8 @@ class LabelItem(QtGui.QGraphicsWidget): class ScaleItem(QtGui.QGraphicsWidget): def __init__(self, orientation, pen=None, linkView=None, parent=None): """GraphicsItem showing a single plot axis with ticks, values, and label. - Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items.""" + Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items. + Ticks can be extended to make a grid.""" QtGui.QGraphicsWidget.__init__(self, parent) self.label = QtGui.QGraphicsTextItem(self) self.orientation = orientation @@ -706,8 +851,16 @@ class ScaleItem(QtGui.QGraphicsWidget): self.linkToView(linkView) self.showLabel(False) + + self.grid = False + def setGrid(self, grid): + """Set the alpha value for the grid, or False to disable.""" + self.grid = grid + self.update() + + def resizeEvent(self, ev=None): #s = self.size() @@ -818,6 +971,8 @@ class ScaleItem(QtGui.QGraphicsWidget): self.update() def setRange(self, mn, mx): + if mn in [nan, inf, -inf] or mx in [nan, inf, -inf]: + raise Exception("Not setting range to [%s, %s]" % (str(mn), str(mx))) self.range = [mn, mx] if self.autoScale: self.setScale() @@ -838,37 +993,51 @@ class ScaleItem(QtGui.QGraphicsWidget): self.setRange(*newRange) def boundingRect(self): - return self.mapRectFromParent(self.geometry()) + if 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())) def paint(self, p, opt, widget): p.setPen(self.pen) - bounds = self.boundingRect() - #p.setPen(QtGui.QPen(QtGui.QColor(255, 0, 0))) - #p.drawRect(bounds) + + #bounds = self.boundingRect() + bounds = self.mapRectFromParent(self.geometry()) + + if self.linkedView is None or self.grid is False: + tbounds = bounds + else: + tbounds = self.mapRectFromScene(self.linkedView.mapRectToScene(self.linkedView.boundingRect())) if self.orientation == 'left': p.drawLine(bounds.topRight(), bounds.bottomRight()) - tickStart = bounds.right() + tickStart = tbounds.right() + tickStop = bounds.right() tickDir = -1 axis = 0 elif self.orientation == 'right': p.drawLine(bounds.topLeft(), bounds.bottomLeft()) - tickStart = bounds.left() + tickStart = tbounds.left() + tickStop = bounds.left() tickDir = 1 axis = 0 elif self.orientation == 'top': p.drawLine(bounds.bottomLeft(), bounds.bottomRight()) - tickStart = bounds.bottom() + tickStart = tbounds.bottom() + tickStop = bounds.bottom() tickDir = -1 axis = 1 elif self.orientation == 'bottom': p.drawLine(bounds.topLeft(), bounds.topRight()) - tickStart = bounds.top() + tickStart = tbounds.top() + tickStop = bounds.top() tickDir = 1 axis = 1 ## Determine optimal tick spacing - intervals = [1., 2., 5., 10., 20., 50.] + #intervals = [1., 2., 5., 10., 20., 50.] + #intervals = [1., 2.5, 5., 10., 25., 50.] + intervals = [1., 2., 10., 20., 100.] dif = abs(self.range[1] - self.range[0]) if dif == 0.0: return @@ -879,6 +1048,7 @@ class ScaleItem(QtGui.QGraphicsWidget): if dif / (pw*intervals[i]) < 10: break + textLevel = i1 ## draw text at this scale level #print "range: %s dif: %f power: %f interval: %f spacing: %f" % (str(self.range), dif, pw, intervals[i1], sp) @@ -892,6 +1062,8 @@ class ScaleItem(QtGui.QGraphicsWidget): ## draw ticks and text for i in [i1, i1+1, i1+2]: ## draw three different intervals + if i > len(intervals): + continue ## spacing for this interval sp = pw*intervals[i] @@ -925,7 +1097,7 @@ class ScaleItem(QtGui.QGraphicsWidget): p1 = [0, 0] p2 = [0, 0] p1[axis] = tickStart - p2[axis] = tickStart + h*tickDir + p2[axis] = tickStop + h*tickDir p1[1-axis] = p2[1-axis] = x if p1[1-axis] > [bounds.width(), bounds.height()][1-axis]: @@ -934,7 +1106,7 @@ class ScaleItem(QtGui.QGraphicsWidget): continue p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100, a))) p.drawLine(Point(p1), Point(p2)) - if i == i1+1: + if i == textLevel: if abs(v) < .001 or abs(v) >= 10000: vstr = "%g" % (v * self.scale) else: @@ -945,16 +1117,16 @@ class ScaleItem(QtGui.QGraphicsWidget): self.textHeight = height if self.orientation == 'left': textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStart-100, x-(height/2), 100-self.tickLength, height) + rect = QtCore.QRectF(tickStop-100, x-(height/2), 100-self.tickLength, height) elif self.orientation == 'right': textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStart+self.tickLength, x-(height/2), 100-self.tickLength, height) + rect = QtCore.QRectF(tickStop+self.tickLength, x-(height/2), 100-self.tickLength, height) elif self.orientation == 'top': textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom - rect = QtCore.QRectF(x-100, tickStart-self.tickLength-height, 200, height) + rect = QtCore.QRectF(x-100, tickStop-self.tickLength-height, 200, height) elif self.orientation == 'bottom': textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop - rect = QtCore.QRectF(x-100, tickStart+self.tickLength, 200, height) + rect = QtCore.QRectF(x-100, tickStop+self.tickLength, 200, height) p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100))) p.drawText(rect, textFlags, vstr) @@ -1006,24 +1178,20 @@ class ScaleItem(QtGui.QGraphicsWidget): -#class ViewBox(QtGui.QGraphicsItem, QObjectWorkaround): class ViewBox(QtGui.QGraphicsWidget): """Box that allows internal scaling/panning of children by mouse drag. Not compatible with GraphicsView having the same functionality.""" def __init__(self, parent=None): - #QObjectWorkaround.__init__(self) QtGui.QGraphicsWidget.__init__(self, parent) #self.gView = view #self.showGrid = showGrid self.range = [[0,1], [0,1]] ## child coord. range visible [[xmin, xmax], [ymin, ymax]] self.aspectLocked = False - QtGui.QGraphicsItem.__init__(self, parent) self.setFlag(QtGui.QGraphicsItem.ItemClipsChildrenToShape) #self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape) - #self.childScale = [1.0, 1.0] - #self.childTranslate = [0.0, 0.0] - self.childGroup = QtGui.QGraphicsItemGroup(self) + #self.childGroup = QtGui.QGraphicsItemGroup(self) + self.childGroup = ItemGroup(self) self.currentScale = Point(1, 1) self.yInverted = False @@ -1179,6 +1347,7 @@ class ViewBox(QtGui.QGraphicsWidget): tr = dif*mask self.translateBy(tr, viewCoords=True) self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) + ev.accept() elif ev.buttons() & QtCore.Qt.RightButton: dif = ev.screenPos() - ev.lastScreenPos() dif = array([dif.x(), dif.y()]) @@ -1188,11 +1357,14 @@ class ViewBox(QtGui.QGraphicsWidget): center = Point(self.childGroup.transform().inverted()[0].map(ev.buttonDownPos(QtCore.Qt.RightButton))) self.scaleBy(s, center) self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) + ev.accept() + else: + ev.ignore() def mousePressEvent(self, ev): self.mousePos = array([ev.pos().x(), ev.pos().y()]) self.pressPos = self.mousePos.copy() - #Qwt.QwtPlot.mousePressEvent(self, ev) + ev.accept() def mouseReleaseEvent(self, ev): pos = array([ev.pos().x(), ev.pos().y()]) @@ -1200,7 +1372,7 @@ class ViewBox(QtGui.QGraphicsWidget): #if ev.button() == QtCore.Qt.RightButton: #self.ctrlMenu.popup(self.mapToGlobal(ev.pos())) self.mousePos = pos - #Qwt.QwtPlot.mouseReleaseEvent(self, ev) + ev.accept() def setRange(self, ax, min, max, padding=0.02, update=True): if ax == 0: @@ -1210,8 +1382,16 @@ class ViewBox(QtGui.QGraphicsWidget): def setYRange(self, min, max, update=True, padding=0.02): #print "setYRange:", min, max - if min == max: - raise Exception("Tried to set range with 0 width.") + if min == max: ## If we requested no range, try to preserve previous scale. Otherwise just pick an arbitrary scale. + dy = self.range[1][1] - self.range[1][0] + if dy == 0: + dy = 1 + min -= dy*0.5 + max += dy*0.5 + #raise Exception("Tried to set range with 0 width.") + if any(isnan([min, max])) or any(isinf([min, max])): + raise Exception("Not setting range [%s, %s]" % (str(min), str(max))) + padding = (max-min) * padding min -= padding max += padding @@ -1228,8 +1408,15 @@ class ViewBox(QtGui.QGraphicsWidget): def setXRange(self, min, max, update=True, padding=0.02): #print "setXRange:", min, max if min == max: - print "Warning: Tried to set range with 0 width." + dx = self.range[0][1] - self.range[0][0] + if dx == 0: + dx = 1 + min -= dx*0.5 + max += dx*0.5 + #print "Warning: Tried to set range with 0 width." #raise Exception("Tried to set range with 0 width.") + if any(isnan([min, max])) or any(isinf([min, max])): + raise Exception("Not setting range [%s, %s]" % (str(min), str(max))) padding = (max-min) * padding min -= padding max += padding @@ -1262,21 +1449,52 @@ class ViewBox(QtGui.QGraphicsWidget): p.drawRect(bounds) -class InfiniteLine(QtGui.QGraphicsItem): - def __init__(self, view, pos, angle=90, pen=None): - QtGui.QGraphicsItem.__init__(self) - self.view = view +class InfiniteLine(GraphicsObject): + def __init__(self, view, pos=0, angle=90, pen=None, movable=False, bounds=None): + GraphicsObject.__init__(self) + self.bounds = QtCore.QRectF() ## graphicsitem boundary + + if bounds is None: ## allowed value boundaries for orthogonal lines + self.maxRange = [None, None] + else: + self.maxRange = bounds + self.movable = movable + self.view = weakref.ref(view) self.p = [0, 0] self.setAngle(angle) self.setPos(pos) + + if movable: + self.setAcceptHoverEvents(True) + + if pen is None: pen = QtGui.QPen(QtGui.QColor(200, 200, 100)) self.setPen(pen) - QtCore.QObject.connect(self.view, QtCore.SIGNAL('viewChanged'), self.updateLine) + self.currentPen = self.pen + #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) + + def setBounds(self, bounds): + self.maxRange = bounds + self.setValue(self.value()) + + def hoverEnterEvent(self, ev): + self.currentPen = QtGui.QPen(QtGui.QColor(255, 0,0)) + self.update() + ev.ignore() + + def hoverLeaveEvent(self, ev): + self.currentPen = self.pen + self.update() + ev.ignore() def setPen(self, pen): self.pen = pen + self.currentPen = self.pen def setAngle(self, angle): self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 @@ -1284,43 +1502,111 @@ class InfiniteLine(QtGui.QGraphicsItem): def setPos(self, pos): if type(pos) in [list, tuple]: - self.p = pos + newPos = pos elif isinstance(pos, QtCore.QPointF): - self.p = [pos.x(), pos.y()] + newPos = [pos.x(), pos.y()] else: if self.angle == 90: - self.p = [pos, 0] + newPos = [pos, 0] elif self.angle == 0: - self.p = [0, pos] + newPos = [0, pos] else: raise Exception("Must specify 2D coordinate for non-orthogonal lines.") - self.updateLine() + + ## check bounds (only works for orthogonal lines) + if self.angle == 90: + if self.maxRange[0] is not None: + newPos[0] = max(newPos[0], self.maxRange[0]) + if self.maxRange[1] is not None: + newPos[0] = min(newPos[0], self.maxRange[1]) + elif self.angle == 0: + if self.maxRange[0] is not None: + newPos[1] = max(newPos[1], self.maxRange[0]) + if self.maxRange[1] is not None: + newPos[1] = min(newPos[1], self.maxRange[1]) + + + if self.p != newPos: + self.p = newPos + self.updateLine() + self.emit(QtCore.SIGNAL('positionChanged'), self) + + def getXPos(self): + return self.p[0] + + def getYPos(self): + return self.p[1] + + def getPos(self): + return self.p + + def value(self): + if self.angle%180 == 0: + return self.getYPos() + elif self.angle%180 == 90: + return self.getXPos() + else: + return self.getPos() + + def setValue(self, v): + self.setPos(v) + + ## broken in 4.7 + #def itemChange(self, change, val): + #if change in [self.ItemScenePositionHasChanged, self.ItemSceneHasChanged]: + #self.updateLine() + #print "update", change + #print self.getBoundingParents() + #else: + #print "ignore", change + #return GraphicsObject.itemChange(self, change, val) def updateLine(self): - vr = self.view.viewRect() + + #unit = QtCore.QRect(0, 0, 10, 10) + #if self.scene() is not None: + #gv = self.scene().views()[0] + #unit = gv.mapToScene(unit).boundingRect() + ##print unit + #unit = self.mapRectFromScene(unit) + ##print unit + + vr = self.view().viewRect() + #vr = self.viewBounds() + if vr is None: + return + #print 'before', self.bounds if self.angle > 45: m = tan((90-self.angle) * pi / 180.) - y1 = vr.bottom() - y2 = vr.top() + y2 = vr.bottom() + y1 = vr.top() x1 = self.p[0] + (y1 - self.p[1]) * m x2 = self.p[0] + (y2 - self.p[1]) * m else: m = tan(self.angle * pi / 180.) x1 = vr.left() x2 = vr.right() - y1 = self.p[1] + (x1 - self.p[0]) * m - y2 = self.p[1] + (x2 - self.p[0]) * m + y2 = self.p[1] + (x1 - self.p[0]) * m + y1 = self.p[1] + (x2 - self.p[0]) * m #print vr, x1, y1, x2, y2 self.prepareGeometryChange() self.line = (QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2)) self.bounds = QtCore.QRectF(self.line[0], self.line[1]) ## Stupid bug causes lines to disappear: - if self.bounds.width() == 0: - self.bounds.setWidth(1e-9) - if self.bounds.height() == 0: - self.bounds.setHeight(1e-9) + if self.angle % 180 == 90: + px = self.pixelWidth() + #self.bounds.setWidth(1e-9) + self.bounds.setX(x1 + px*-5) + self.bounds.setWidth(px*10) + if self.angle % 180 == 0: + px = self.pixelHeight() + #self.bounds.setHeight(1e-9) + self.bounds.setY(y1 + px*-5) + self.bounds.setHeight(px*10) + #QtGui.QGraphicsLineItem.setLine(self, x1, y1, x2, y2) + #self.update() def boundingRect(self): #self.updateLine() @@ -1329,10 +1615,123 @@ class InfiniteLine(QtGui.QGraphicsItem): return self.bounds def paint(self, p, *args): - p.setPen(self.pen) - #print "paint", self.line - p.drawLine(self.line[0], self.line[1]) + w,h = self.pixelWidth()*5, self.pixelHeight()*5*1.1547 + #self.updateLine() + l = self.line + p.setPen(self.currentPen) + #print "paint", self.line + p.drawLine(l[0], l[1]) + + p.setBrush(QtGui.QBrush(self.currentPen.color())) + p.drawConvexPolygon(QtGui.QPolygonF([ + l[0] + QtCore.QPointF(-w, 0), + l[0] + QtCore.QPointF(0, h), + l[0] + QtCore.QPointF(w, 0), + ])) + + #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) + #p.drawRect(self.boundingRect()) + + def mousePressEvent(self, ev): + if self.movable and ev.button() == QtCore.Qt.LeftButton: + ev.accept() + self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) + else: + ev.ignore() + + def mouseMoveEvent(self, ev): + self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) + self.emit(QtCore.SIGNAL('dragged'), self) + + + +class LinearRegionItem(GraphicsObject): + """Used for marking a horizontal or vertical region in plots.""" + def __init__(self, view, orientation="horizontal", vals=[0,1], brush=None, movable=True, bounds=None): + GraphicsObject.__init__(self) + self.orientation = orientation + if hasattr(self, "ItemHasNoContents"): + self.setFlag(self.ItemHasNoContents) + self.rect = QtGui.QGraphicsRectItem(self) + self.rect.setParentItem(self) + self.bounds = QtCore.QRectF() + self.view = weakref.ref(view) + + self.setBrush = self.rect.setBrush + self.brush = self.rect.brush + + if orientation[0] == 'h': + self.lines = [ + InfiniteLine(view, QtCore.QPointF(0, vals[0]), 0, movable=movable, bounds=bounds), + InfiniteLine(view, QtCore.QPointF(0, vals[1]), 0, movable=movable, bounds=bounds)] + else: + 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) + + for l in self.lines: + l.setParentItem(self) + l.connect(QtCore.SIGNAL('positionChanged'), self.lineMoved) + + if brush is None: + brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) + self.setBrush(brush) + + def setBounds(self, bounds): + for l in self.lines: + l.setBounds(bounds) + + + def boundingRect(self): + return self.rect.boundingRect() + + def lineMoved(self): + self.updateBounds() + self.emit(QtCore.SIGNAL('regionChanged'), self) + + def updateBounds(self): + vb = self.view().viewRect() + vals = [self.lines[0].value(), self.lines[1].value()] + if self.orientation[0] == 'h': + vb.setTop(max(vals)) + vb.setBottom(min(vals)) + else: + vb.setLeft(min(vals)) + vb.setRight(max(vals)) + if vb != self.bounds: + self.bounds = vb + self.rect.setRect(vb) + + def mousePressEvent(self, ev): + for l in self.lines: + l.mousePressEvent(ev) + #if self.movable and ev.button() == QtCore.Qt.LeftButton: + #ev.accept() + #self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) + #else: + #ev.ignore() + + def mouseMoveEvent(self, ev): + self.lines[0].blockSignals(True) # only want to update once + for l in self.lines: + l.mouseMoveEvent(ev) + self.lines[0].blockSignals(False) + #self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) + #self.emit(QtCore.SIGNAL('dragged'), self) + + def getRegion(self): + if self.orientation[0] == 'h': + r = (self.bounds.top(), self.bounds.bottom()) + else: + r = (self.bounds.left(), self.bounds.right()) + return (min(r), max(r)) + + def setRegion(self, rgn): + self.lines[0].setValue(rgn[0]) + self.lines[1].setValue(rgn[1]) + class VTickGroup(QtGui.QGraphicsPathItem): def __init__(self, xvals=None, yrange=None, pen=None, relative=False, view=None): @@ -1345,7 +1744,10 @@ class VTickGroup(QtGui.QGraphicsPathItem): pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) self.ticks = [] self.xvals = [] - self.view = view + if view is None: + self.view = None + else: + self.view = weakref.ref(view) self.yrange = [0,1] self.setPen(pen) self.setYRange(yrange, relative) @@ -1363,7 +1765,7 @@ class VTickGroup(QtGui.QGraphicsPathItem): def setXVals(self, vals): self.xvals = vals - #self.rebuildTicks() + self.rebuildTicks() self.valid = False def setYRange(self, vals, relative=False): @@ -1371,37 +1773,71 @@ class VTickGroup(QtGui.QGraphicsPathItem): self.relative = relative 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.rebuildTicks) + QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.rescale) else: try: - QtCore.QObject.disconnect(self.view, QtCore.SIGNAL('viewChanged'), self.rebuildTicks) + #QtCore.QObject.disconnect(self.view, QtCore.SIGNAL('viewChanged'), self.rebuildTicks) + QtCore.QObject.disconnect(self.view(), QtCore.SIGNAL('viewChanged'), self.rescale) except: pass - #self.rebuildTicks() + self.rebuildTicks() self.valid = False + def rescale(self): + #print "RESCALE:" + self.resetTransform() + #height = self.view.size().height() + #p1 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[0])))) + #p2 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[1])))) + #yr = [p1.y(), p2.y()] + vb = self.view().viewRect() + p1 = vb.bottom() - vb.height() * self.yrange[0] + p2 = vb.bottom() - vb.height() * self.yrange[1] + yr = [p1, p2] + + #print " ", vb, yr + self.translate(0.0, yr[0]) + self.scale(1.0, (yr[1]-yr[0])) + #print " ", self.mapRectToScene(self.boundingRect()) + self.boundingRect() + self.update() + + def boundingRect(self): + #print "--request bounds:" + b = QtGui.QGraphicsPathItem.boundingRect(self) + #print " ", self.mapRectToScene(b) + return b + def yRange(self): - if self.relative: - height = self.view.size().height() - p1 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[0])))) - p2 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[1])))) - return [p1.y(), p2.y()] - else: - return self.yrange + #if self.relative: + #height = self.view.size().height() + #p1 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[0])))) + #p2 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[1])))) + #return [p1.y(), p2.y()] + #else: + #return self.yrange + + return self.yrange def rebuildTicks(self): - path = QtGui.QPainterPath() + self.path = QtGui.QPainterPath() yrange = self.yRange() #print "rebuild ticks:", yrange for x in self.xvals: - path.moveTo(x, yrange[0]) - path.lineTo(x, yrange[1]) - self.setPath(path) + #path.moveTo(x, yrange[0]) + #path.lineTo(x, yrange[1]) + self.path.moveTo(x, 0.) + self.path.lineTo(x, 1.) + self.setPath(self.path) self.valid = True + self.rescale() + #print " done..", self.boundingRect() def paint(self, *args): if not self.valid: self.rebuildTicks() + #print "Paint", self.boundingRect() QtGui.QGraphicsPathItem.paint(self, *args) @@ -1491,3 +1927,136 @@ class GridItem(UIGraphicsItem): x = tr.map(t[0]) p.drawText(x, t[1]) p.end() + +class ScaleBar(UIGraphicsItem): + def __init__(self, view, size, width=5, color=(100, 100, 255)): + self.size = size + UIGraphicsItem.__init__(self, view) + self.setAcceptedMouseButtons(QtCore.Qt.NoButton) + #self.pen = QtGui.QPen(QtGui.QColor(*color)) + #self.pen.setWidth(width) + #self.pen.setCosmetic(True) + #self.pen2 = QtGui.QPen(QtGui.QColor(0,0,0)) + #self.pen2.setWidth(width+2) + #self.pen2.setCosmetic(True) + self.brush = QtGui.QBrush(QtGui.QColor(*color)) + self.pen = QtGui.QPen(QtGui.QColor(0,0,0)) + self.width = width + + def paint(self, p, opt, widget): + rect = self.boundingRect() + unit = self.unitRect() + y = rect.bottom() + (rect.top()-rect.bottom()) * 0.02 + y1 = y + unit.height()*self.width + x = rect.right() + (rect.left()-rect.right()) * 0.02 + x1 = x - self.size + + + p.setPen(self.pen) + p.setBrush(self.brush) + rect = QtCore.QRectF( + QtCore.QPointF(x1, y1), + QtCore.QPointF(x, y) + ) + p.translate(x1, y1) + p.scale(rect.width(), rect.height()) + p.drawRect(0, 0, 1, 1) + + alpha = clip(((self.size/unit.width()) - 40.) * 255. / 80., 0, 255) + p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha))) + for i in range(1, 10): + #x2 = x + (x1-x) * 0.1 * i + x2 = 0.1 * i + p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1)) + + + def setSize(self, s): + self.size = s + +class ColorScaleBar(UIGraphicsItem): + def __init__(self, view, size, offset): + self.size = size + self.offset = offset + UIGraphicsItem.__init__(self, view) + self.setAcceptedMouseButtons(QtCore.Qt.NoButton) + self.brush = QtGui.QBrush(QtGui.QColor(200,0,0)) + self.pen = QtGui.QPen(QtGui.QColor(0,0,0)) + self.labels = {'max': 1, 'min': 0} + self.gradient = QtGui.QLinearGradient() + self.gradient.setColorAt(0, QtGui.QColor(0,0,0)) + self.gradient.setColorAt(1, QtGui.QColor(255,0,0)) + + def setGradient(self, g): + self.gradient = g + self.update() + + def setLabels(self, l): + self.labels = l + self.update() + + def paint(self, p, opt, widget): + rect = self.boundingRect() ## Boundaries of visible area in scene coords. + unit = self.unitRect() ## Size of one view pixel in scene coords. + + ## determine max width of all labels + 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) + labelWidth = max(labelWidth, b.width()) + labelHeight = max(labelHeight, b.height()) + + labelWidth *= unit.width() + labelHeight *= unit.height() + + textPadding = 2 # in px + + if self.offset[0] < 0: + x3 = rect.right() + unit.width() * self.offset[0] + x2 = x3 - labelWidth - unit.width()*textPadding*2 + x1 = x2 - unit.width() * self.size[0] + else: + x1 = rect.left() + unit.width() * self.offset[0] + x2 = x1 + unit.width() * self.size[0] + x3 = x2 + labelWidth + unit.width()*textPadding*2 + if self.offset[1] < 0: + y2 = rect.top() - unit.height() * self.offset[1] + y1 = y2 + unit.height() * self.size[1] + else: + y1 = rect.bottom() - unit.height() * self.offset[1] + y2 = y1 - unit.height() * self.size[1] + self.b = [x1,x2,x3,y1,y2,labelWidth] + + ## Draw background + p.setPen(self.pen) + p.setBrush(QtGui.QBrush(QtGui.QColor(255,255,255,100))) + rect = QtCore.QRectF( + QtCore.QPointF(x1 - unit.width()*textPadding, y1 + labelHeight/2 + unit.height()*textPadding), + QtCore.QPointF(x3, y2 - labelHeight/2 - unit.height()*textPadding) + ) + p.drawRect(rect) + + + ## Have to scale painter so that text and gradients are correct size. Bleh. + p.scale(unit.width(), unit.height()) + + ## Draw color bar + self.gradient.setStart(0, y1/unit.height()) + self.gradient.setFinalStop(0, y2/unit.height()) + p.setBrush(self.gradient) + rect = QtCore.QRectF( + QtCore.QPointF(x1/unit.width(), y1/unit.height()), + QtCore.QPointF(x2/unit.width(), y2/unit.height()) + ) + p.drawRect(rect) + + + ## draw labels + p.setPen(QtGui.QPen(QtGui.QColor(0,0,0))) + tx = x2 + unit.width()*textPadding + 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) + + diff --git a/graphicsWindows.py b/graphicsWindows.py index 79faf1cc..55eba8e8 100644 --- a/graphicsWindows.py +++ b/graphicsWindows.py @@ -8,13 +8,16 @@ Distributed under MIT/X11 license. See license.txt for more infomation. from PyQt4 import QtCore, QtGui from PlotWidget import * from ImageView import * - +QAPP = None class PlotWindow(QtGui.QMainWindow): def __init__(self, title=None): + if QtGui.QApplication.instance() is None: + global QAPP + QAPP = QtGui.QApplication([]) QtGui.QMainWindow.__init__(self) self.cw = PlotWidget() self.setCentralWidget(self.cw) - for m in ['plot', 'autoRange', 'addItem', 'setLabel', 'clear']: + for m in ['plot', 'autoRange', 'addItem', 'removeItem', 'setLabel', 'clear']: setattr(self, m, getattr(self.cw, m)) if title is not None: self.setWindowTitle(title) @@ -22,10 +25,13 @@ class PlotWindow(QtGui.QMainWindow): class ImageWindow(QtGui.QMainWindow): def __init__(self, title=None): + if QtGui.QApplication.instance() is None: + global QAPP + QAPP = QtGui.QApplication([]) QtGui.QMainWindow.__init__(self) self.cw = ImageView() self.setCentralWidget(self.cw) - for m in ['setImage', 'autoRange', 'addItem']: + for m in ['setImage', 'autoRange', 'addItem', 'removeItem', 'blackLevel', 'whiteLevel', 'imageItem']: setattr(self, m, getattr(self.cw, m)) if title is not None: self.setWindowTitle(title) diff --git a/plotConfigTemplate.py b/plotConfigTemplate.py index 355d9347..5d30654a 100644 --- a/plotConfigTemplate.py +++ b/plotConfigTemplate.py @@ -2,8 +2,8 @@ # Form implementation generated from reading ui file 'plotConfigTemplate.ui' # -# Created: Mon Mar 29 22:40:47 2010 -# by: PyQt4 UI code generator 4.6 +# Created: Sat Jul 17 00:28:43 2010 +# by: PyQt4 UI code generator 4.7.2 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,7 @@ from PyQt4 import QtCore, QtGui class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(210, 340) + Form.resize(250, 340) Form.setMaximumSize(QtCore.QSize(250, 350)) self.gridLayout_3 = QtGui.QGridLayout(Form) self.gridLayout_3.setMargin(0) @@ -187,6 +187,7 @@ class Ui_Form(object): self.verticalLayout_3.addWidget(self.alphaGroup) self.gridGroup = QtGui.QGroupBox(self.tab_3) self.gridGroup.setCheckable(True) + self.gridGroup.setChecked(False) self.gridGroup.setObjectName("gridGroup") self.verticalLayout_4 = QtGui.QVBoxLayout(self.gridGroup) self.verticalLayout_4.setObjectName("verticalLayout_4") @@ -227,6 +228,9 @@ class Ui_Form(object): self.saveMaBtn = QtGui.QPushButton(self.tab_4) self.saveMaBtn.setObjectName("saveMaBtn") self.gridLayout_6.addWidget(self.saveMaBtn, 2, 0, 1, 1) + self.saveCsvBtn = QtGui.QPushButton(self.tab_4) + self.saveCsvBtn.setObjectName("saveCsvBtn") + self.gridLayout_6.addWidget(self.saveCsvBtn, 3, 0, 1, 1) self.gridLayout_7.addLayout(self.gridLayout_6, 0, 1, 1, 1) spacerItem2 = QtGui.QSpacerItem(59, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.gridLayout_7.addItem(spacerItem2, 0, 2, 1, 1) @@ -281,5 +285,6 @@ class Ui_Form(object): self.saveSvgBtn.setText(QtGui.QApplication.translate("Form", "SVG", None, QtGui.QApplication.UnicodeUTF8)) self.saveImgBtn.setText(QtGui.QApplication.translate("Form", "Image", None, QtGui.QApplication.UnicodeUTF8)) self.saveMaBtn.setText(QtGui.QApplication.translate("Form", "MetaArray", None, QtGui.QApplication.UnicodeUTF8)) + self.saveCsvBtn.setText(QtGui.QApplication.translate("Form", "CSV", None, QtGui.QApplication.UnicodeUTF8)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), QtGui.QApplication.translate("Form", "Save", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/plotConfigTemplate.ui b/plotConfigTemplate.ui index 59cfd119..7baeb337 100644 --- a/plotConfigTemplate.ui +++ b/plotConfigTemplate.ui @@ -6,7 +6,7 @@ 0 0 - 210 + 250 340 @@ -35,7 +35,7 @@ - 1 + 0 @@ -419,6 +419,9 @@ true + + false + @@ -514,6 +517,13 @@ + + + + CSV + + + diff --git a/widgets.py b/widgets.py index 7ee8b2c2..c306740e 100644 --- a/widgets.py +++ b/widgets.py @@ -1,837 +1,889 @@ -# -*- coding: utf-8 -*- -""" -widgets.py - Interactive graphics items for GraphicsView (ROI widgets) -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. - -Implements a series of graphics items which display movable/scalable/rotatable shapes -for use as region-of-interest markers. ROI class automatically handles extraction -of array data from ImageItems. -""" - -from PyQt4 import QtCore, QtGui, QtOpenGL, QtSvg -from numpy import array, arccos, dot, pi, zeros, vstack, ubyte, fromfunction, ceil, floor -from numpy.linalg import norm -import scipy.ndimage as ndimage -from Point import * -from math import cos, sin - -def rectStr(r): - return "[%f, %f] + [%f, %f]" % (r.x(), r.y(), r.width(), r.height()) - -## Multiple inheritance not allowed in PyQt. Retarded workaround: -class QObjectWorkaround: - def __init__(self): - self._qObj_ = QtCore.QObject() - def __getattr__(self, attr): - if attr == '_qObj_': - raise Exception("QObjectWorkaround not initialized!") - return getattr(self._qObj_, attr) - def connect(self, *args): - return QtCore.QObject.connect(self._qObj_, *args) - - -class ROI(QtGui.QGraphicsItem, QObjectWorkaround): - 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): - QObjectWorkaround.__init__(self) - QtGui.QGraphicsItem.__init__(self, parent) - pos = Point(pos) - size = Point(size) - self.aspectLocked = False - self.translatable = True - - self.pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) - self.handlePen = QtGui.QPen(QtGui.QColor(150, 255, 255)) - self.handles = [] - self.state = {'pos': pos, 'size': size, 'angle': angle} - self.lastState = None - self.setPos(pos) - self.rotate(-angle) - self.setZValue(10) - - self.handleSize = 4 - self.invertible = invertible - self.maxBounds = maxBounds - - self.snapSize = snapSize - self.translateSnap = translateSnap - self.rotateSnap = rotateSnap - self.scaleSnap = scaleSnap - self.setFlag(self.ItemIsSelectable, True) - - def setZValue(self, z): - QtGui.QGraphicsItem.setZValue(self, z) - for h in self.handles: - h['item'].setZValue(z+1) - - def sceneBounds(self): - return self.sceneTransform().mapRect(self.boundingRect()) - - def parentBounds(self): - return self.mapToParent(self.boundingRect()).boundingRect() - - def setPen(self, pen): - self.pen = pen - self.update() - - def setPos(self, pos, update=True): - pos = Point(pos) - self.state['pos'] = pos - QtGui.QGraphicsItem.setPos(self, pos) - if update: - self.handleChange() - - def setSize(self, size, update=True): - size = Point(size) - self.prepareGeometryChange() - self.state['size'] = size - if update: - self.updateHandles() - self.handleChange() - - def addTranslateHandle(self, pos, axes=None, item=None): - pos = Point(pos) - return self.addHandle({'type': 't', 'pos': pos, 'item': item}) - - def addScaleHandle(self, pos, center, axes=None, item=None): - pos = Point(pos) - center = Point(center) - info = {'type': 's', 'center': center, 'pos': pos, 'item': item} - if pos.x() == center.x(): - info['xoff'] = True - if pos.y() == center.y(): - info['yoff'] = True - return self.addHandle(info) - - def addRotateHandle(self, pos, center, item=None): - pos = Point(pos) - center = Point(center) - return self.addHandle({'type': 'r', 'center': center, 'pos': pos, 'item': item}) - - def addScaleRotateHandle(self, pos, center, item=None): - pos = Point(pos) - center = Point(center) - if pos[0] != center[0] and pos[1] != center[1]: - raise Exception("Scale/rotate handles must have either the same x or y coordinate as their center point.") - return self.addHandle({'type': 'sr', 'center': center, 'pos': pos, 'item': item}) - - def addHandle(self, info): - if not info.has_key('item') or info['item'] is None: - #print "BEFORE ADD CHILD:", self.childItems() - h = Handle(self.handleSize, typ=info['type'], pen=self.handlePen, parent=self) - #print "AFTER ADD CHILD:", self.childItems() - h.setPos(info['pos'] * self.state['size']) - info['item'] = h - else: - h = info['item'] - iid = len(self.handles) - h.connectROI(self, iid) - #h.mouseMoveEvent = lambda ev: self.pointMoveEvent(iid, ev) - #h.mousePressEvent = lambda ev: self.pointPressEvent(iid, ev) - #h.mouseReleaseEvent = lambda ev: self.pointReleaseEvent(iid, ev) - self.handles.append(info) - h.setZValue(self.zValue()+1) - #if self.isSelected(): - #h.show() - #else: - #h.hide() - return h - - def mapSceneToParent(self, pt): - return self.mapToParent(self.mapFromScene(pt)) - - def setSelected(self, s): - QtGui.QGraphicsItem.setSelected(self, s) - #print "select", self, s - if s: - for h in self.handles: - h['item'].show() - else: - for h in self.handles: - h['item'].hide() - - def mousePressEvent(self, ev): - if ev.button() == QtCore.Qt.LeftButton: - self.setSelected(True) - if self.translatable: - self.cursorOffset = self.scenePos() - ev.scenePos() - self.emit(QtCore.SIGNAL('regionChangeStarted'), self) - ev.accept() - else: - ev.ignore() - - def mouseMoveEvent(self, ev): - #print "mouse move", ev.pos() - if self.translatable: - snap = None - if self.translateSnap or (ev.modifiers() & QtCore.Qt.ControlModifier): - snap = Point(self.snapSize, self.snapSize) - newPos = ev.scenePos() + self.cursorOffset - newPos = self.mapSceneToParent(newPos) - self.translate(newPos - self.pos(), snap=snap) - - def mouseReleaseEvent(self, ev): - if self.translatable: - self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - - - - def pointPressEvent(self, pt, ev): - #print "press" - self.emit(QtCore.SIGNAL('regionChangeStarted'), self) - #self.pressPos = self.mapFromScene(ev.scenePos()) - #self.pressHandlePos = self.handles[pt]['item'].pos() - - def pointReleaseEvent(self, pt, ev): - #print "release" - self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - - def stateCopy(self): - sc = {} - sc['pos'] = Point(self.state['pos']) - sc['size'] = Point(self.state['size']) - sc['angle'] = self.state['angle'] - return sc - - def updateHandles(self): - #print "update", self.handles - for h in self.handles: - #print " try", h - if h['item'] in self.childItems(): - p = h['pos'] - h['item'].setPos(h['pos'] * self.state['size']) - #else: - #print " Not child!", self.childItems() - - - def checkPointMove(self, pt, pos, modifiers): - return True - - def pointMoveEvent(self, pt, ev): - self.movePoint(pt, ev.scenePos(), ev.modifiers()) - - - def movePoint(self, pt, pos, modifiers=QtCore.Qt.KeyboardModifier()): - #print "movePoint", pos - newState = self.stateCopy() - h = self.handles[pt] - #p0 = self.mapToScene(h['item'].pos()) - p0 = self.mapToScene(h['pos'] * self.state['size']) - p1 = Point(pos) - p0 = self.mapSceneToParent(p0) - p1 = self.mapSceneToParent(p1) - - if h.has_key('center'): - c = h['center'] - cs = c * self.state['size'] - #lpOrig = h['pos'] - - #lp0 = self.mapFromScene(p0) - cs - #lp1 = self.mapFromScene(p1) - cs - lp0 = self.mapFromParent(p0) - cs - lp1 = self.mapFromParent(p1) - cs - - if h['type'] == 't': - #p0 = Point(self.mapToScene(h['item'].pos())) - #p1 = Point(pos + self.mapToScene(self.pressHandlePos) - self.mapToScene(self.pressPos)) - snap = None - if self.translateSnap or (modifiers & QtCore.Qt.ControlModifier): - snap = Point(self.snapSize, self.snapSize) - self.translate(p1-p0, snap=snap, update=False) - - elif h['type'] == 's': - #c = h['center'] - #cs = c * self.state['size'] - #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs - - if h['center'][0] == h['pos'][0]: - lp1[0] = 0 - if h['center'][1] == h['pos'][1]: - lp1[1] = 0 - - if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): - lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize - lp1[1] = round(lp1[1] / self.snapSize) * self.snapSize - - hs = h['pos'] - c - if hs[0] == 0: - hs[0] = 1 - if hs[1] == 0: - hs[1] = 1 - newSize = lp1 / hs - - if newSize[0] == 0: - newSize[0] = newState['size'][0] - if newSize[1] == 0: - newSize[1] = newState['size'][1] - if not self.invertible: - if newSize[0] < 0: - newSize[0] = newState['size'][0] - if newSize[1] < 0: - newSize[1] = newState['size'][1] - if self.aspectLocked: - newSize[0] = newSize[1] - - s0 = c * self.state['size'] - s1 = c * newSize - cc = self.mapToParent(s0 - s1) - self.mapToParent(Point(0, 0)) - - newState['size'] = newSize - newState['pos'] = newState['pos'] + cc - if self.maxBounds is not None: - r = self.stateRect(newState) - if not self.maxBounds.contains(r): - return - - self.setPos(newState['pos'], update=False) - self.prepareGeometryChange() - self.state = newState - - self.updateHandles() - - elif h['type'] == 'r': - #newState = self.stateCopy() - #c = h['center'] - #cs = c * self.state['size'] - #p0 = Point(h['item'].pos()) - cs - #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs - if lp1.length() == 0 or lp0.length() == 0: - return - - ang = newState['angle'] + lp0.angle(lp1) - if ang is None: - return - if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - ang = round(ang / (pi/12.)) * (pi/12.) - - - tr = QtGui.QTransform() - tr.rotate(-ang * 180. / pi) - - cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos']) - newState['angle'] = ang - newState['pos'] = newState['pos'] + cc - if self.maxBounds is not None: - r = self.stateRect(newState) - if not self.maxBounds.contains(r): - return - self.setTransform(tr) - self.setPos(newState['pos'], update=False) - self.state = newState - - elif h['type'] == 'sr': - #newState = self.stateCopy() - if h['center'][0] == h['pos'][0]: - scaleAxis = 1 - else: - scaleAxis = 0 - - #c = h['center'] - #cs = c * self.state['size'] - #p0 = Point(h['item'].pos()) - cs - #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs - if lp1.length() == 0 or lp0.length() == 0: - return - - ang = newState['angle'] + lp0.angle(lp1) - if ang is None: - return - if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - ang = round(ang / (pi/12.)) * (pi/12.) - - hs = abs(h['pos'][scaleAxis] - c[scaleAxis]) - newState['size'][scaleAxis] = lp1.length() / hs - if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): - newState['size'][scaleAxis] = round(newState['size'][scaleAxis] / self.snapSize) * self.snapSize - if newState['size'][scaleAxis] == 0: - newState['size'][scaleAxis] = 1 - - c1 = c * newState['size'] - tr = QtGui.QTransform() - tr.rotate(-ang * 180. / pi) - - cc = self.mapToParent(cs) - (tr.map(c1) + self.state['pos']) - newState['angle'] = ang - newState['pos'] = newState['pos'] + cc - if self.maxBounds is not None: - r = self.stateRect(newState) - if not self.maxBounds.contains(r): - return - self.setTransform(tr) - self.setPos(newState['pos'], update=False) - self.prepareGeometryChange() - self.state = newState - - self.updateHandles() - - self.handleChange() - - def handleChange(self): - changed = False - if self.lastState is None: - changed = True - else: - for k in self.state.keys(): - if self.state[k] != self.lastState[k]: - #print "state %s has changed; emit signal" % k - changed = True - self.lastState = self.stateCopy() - if changed: - self.update() - self.emit(QtCore.SIGNAL('regionChanged'), self) - - - def scale(self, s, center=[0,0]): - c = self.mapToScene(Point(center) * self.state['size']) - self.prepareGeometryChange() - self.state['size'] = self.state['size'] * s - c1 = self.mapToScene(Point(center) * self.state['size']) - self.state['pos'] = self.state['pos'] + c - c1 - self.setPos(self.state['pos']) - self.updateHandles() - - def translate(self, *args, **kargs): - """accepts either (x, y, snap) or ([x,y], snap) as arguments""" - if 'snap' not in kargs: - snap = None - else: - snap = kargs['snap'] - - if len(args) == 1: - pt = args[0] - else: - pt = args - - newState = self.stateCopy() - newState['pos'] = newState['pos'] + pt - if snap != None: - newState['pos'][0] = round(newState['pos'][0] / snap[0]) * snap[0] - newState['pos'][1] = round(newState['pos'][1] / snap[1]) * snap[1] - - - #d = ev.scenePos() - self.mapToScene(self.pressPos) - if self.maxBounds is not None: - r = self.stateRect(newState) - #r0 = self.sceneTransform().mapRect(self.boundingRect()) - d = Point(0,0) - if self.maxBounds.left() > r.left(): - d[0] = self.maxBounds.left() - r.left() - elif self.maxBounds.right() < r.right(): - d[0] = self.maxBounds.right() - r.right() - if self.maxBounds.top() > r.top(): - d[1] = self.maxBounds.top() - r.top() - elif self.maxBounds.bottom() < r.bottom(): - d[1] = self.maxBounds.bottom() - r.bottom() - newState['pos'] += d - - self.state['pos'] = newState['pos'] - self.setPos(self.state['pos']) - #if 'update' not in kargs or kargs['update'] is True: - self.handleChange() - - def stateRect(self, state): - r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1]) - tr = QtGui.QTransform() - tr.rotate(-state['angle'] * 180 / pi) - r = tr.mapRect(r) - return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1]) - - def boundingRect(self): - return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]) - - def paint(self, p, opt, widget): - r = self.boundingRect() - p.setRenderHint(QtGui.QPainter.Antialiasing) - p.setPen(self.pen) - p.drawRect(r) - - - def getArraySlice(self, data, img, axes=(0,1), returnSlice=True): - """Return a tuple of slice objects that can be used to slice the region from data covered by this ROI. - Also returns the transform which maps the ROI into data coordinates. - - If returnSlice is set to False, the function returns a pair of tuples with the values that would have - been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))""" - #print "getArraySlice" - - ## Determine shape of array along ROI axes - dShape = (data.shape[axes[0]], data.shape[axes[1]]) - #print " dshape", dShape - - ## Determine transform that maps ROI bounding box to image coordinates - tr = self.sceneTransform() * img.sceneTransform().inverted()[0] - - ## Modify transform to scale from image coords to data coords - #m = QtGui.QTransform() - tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) - #tr = tr * m - - ## Transform ROI bounds into data bounds - dataBounds = tr.mapRect(self.boundingRect()) - #print " boundingRect:", self.boundingRect() - #print " dataBounds:", dataBounds - - ## Intersect transformed ROI bounds with data bounds - intBounds = dataBounds.intersect(QtCore.QRectF(0, 0, dShape[0], dShape[1])) - #print " intBounds:", intBounds - - ## Determine index values to use when referencing the array. - bounds = ( - (int(min(intBounds.left(), intBounds.right())), int(1+max(intBounds.left(), intBounds.right()))), - (int(min(intBounds.bottom(), intBounds.top())), int(1+max(intBounds.bottom(), intBounds.top()))) - ) - #print " bounds:", bounds - - if returnSlice: - ## Create slice objects - sl = [slice(None)] * data.ndim - sl[axes[0]] = slice(*bounds[0]) - sl[axes[1]] = slice(*bounds[1]) - return tuple(sl), tr - else: - return bounds, tr - - - def getArrayRegion(self, data, img, axes=(0,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])) - ) - - ## 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) - - - ## Rotate array - if abs(self.state['angle']) > 1e-5: - arr2 = ndimage.rotate(arr1, self.state['angle'] * 180 / pi, order=1) - - ## update data transforms to reflect this rotation - rot = QtGui.QTransform().rotate(self.state['angle'] * 180 / 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()) - - else: - arr2 = arr1 - - - - ### 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 = 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 - - - ### 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())) - - #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]] - - ### Create zero array in size of ROI - arr5 = 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]] - - - ## figure out the reverse transpose order - tr2 = array(tr1) - for i in range(0, len(tr2)): - tr2[tr1[i]] = i - tr2 = tuple(tr2) - - ## Untranspose array before returning - return arr5.transpose(tr2) - - - - - - - -class Handle(QtGui.QGraphicsItem): - def __init__(self, radius, typ=None, pen=QtGui.QPen(QtGui.QColor(200, 200, 220)), parent=None): - #print " create item with parent", parent - self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10) - QtGui.QGraphicsItem.__init__(self, parent) - self.setZValue(11) - self.roi = [] - self.radius = radius - self.typ = typ - self.prepareGeometryChange() - self.pen = pen - if typ == 't': - self.sides = 4 - self.startAng = pi/4 - elif typ == 's': - self.sides = 4 - self.startAng = 0 - elif typ == 'r': - self.sides = 12 - self.startAng = 0 - elif typ == 'sr': - self.sides = 12 - self.startAng = 0 - else: - self.sides = 4 - self.startAng = pi/4 - - def connectROI(self, roi, i): - self.roi.append((roi, i)) - - def boundingRect(self): - return self.bounds - - def mousePressEvent(self, ev): - #print "handle press" - if ev.button() != QtCore.Qt.LeftButton: - ev.ignore() - return - self.cursorOffset = self.scenePos() - ev.scenePos() - for r in self.roi: - r[0].pointPressEvent(r[1], ev) - #print " accepted." - ev.accept() - - def mouseReleaseEvent(self, ev): - #print "release" - for r in self.roi: - r[0].pointReleaseEvent(r[1], ev) - - def mouseMoveEvent(self, ev): - #print "handle mouseMove", ev.pos() - pos = ev.scenePos() + self.cursorOffset - self.movePoint(pos, ev.modifiers()) - - def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier()): - for r in self.roi: - if not r[0].checkPointMove(r[1], pos, modifiers): - return - #print "point moved; inform %d ROIs" % len(self.roi) - for r in self.roi: - r[0].movePoint(r[1], pos, modifiers) - - def paint(self, p, opt, widget): - m = p.transform() - mi = m.inverted()[0] - - ## Determine length of unit vector in painter's coords - size = mi.map(Point(self.radius, self.radius)) - mi.map(Point(0, 0)) - size = (size.x()*size.x() + size.y() * size.y()) ** 0.5 - - bounds = QtCore.QRectF(-size, -size, size*2, size*2) - if bounds != self.bounds: - self.bounds = bounds - self.prepareGeometryChange() - p.setRenderHint(QtGui.QPainter.Antialiasing) - p.setPen(self.pen) - ang = self.startAng - dt = 2*pi / self.sides - for i in range(0, self.sides): - x1 = size * cos(ang) - y1 = size * sin(ang) - x2 = size * cos(ang+dt) - y2 = size * sin(ang+dt) - ang += dt - p.drawLine(Point(x1, y1), Point(x2, y2)) - - - - - -class TestROI(ROI): - def __init__(self, pos, size, **args): - #QtGui.QGraphicsRectItem.__init__(self, pos[0], pos[1], size[0], size[1]) - ROI.__init__(self, pos, size, **args) - #self.addTranslateHandle([0, 0]) - self.addTranslateHandle([0.5, 0.5]) - self.addScaleHandle([1, 1], [0, 0]) - self.addScaleHandle([0, 0], [1, 1]) - self.addScaleRotateHandle([1, 0.5], [0.5, 0.5]) - self.addScaleHandle([0.5, 1], [0.5, 0.5]) - self.addRotateHandle([1, 0], [0, 0]) - self.addRotateHandle([0, 1], [1, 1]) - - - -class RectROI(ROI): - def __init__(self, pos, size, centered=False, sideScalers=False, **args): - #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) - ROI.__init__(self, pos, size, **args) - if centered: - center = [0.5, 0.5] - else: - center = [0, 0] - - #self.addTranslateHandle(center) - self.addScaleHandle([1, 1], center) - if sideScalers: - self.addScaleHandle([1, 0.5], [center[0], 0.5]) - self.addScaleHandle([0.5, 1], [0.5, center[1]]) - -class LineROI(ROI): - def __init__(self, pos1, pos2, width, **args): - pos1 = Point(pos1) - pos2 = Point(pos2) - d = pos2-pos1 - l = d.length() - ang = Point(1, 0).angle(d) - c = Point(-width/2. * sin(ang), -width/2. * cos(ang)) - pos1 = pos1 + c - - ROI.__init__(self, pos1, size=Point(l, width), angle=ang*180/pi, **args) - self.addScaleRotateHandle([0, 0.5], [1, 0.5]) - self.addScaleRotateHandle([1, 0.5], [0, 0.5]) - self.addScaleHandle([0.5, 1], [0.5, 0.5]) - - -class MultiLineROI(QtGui.QGraphicsItem, QObjectWorkaround): - def __init__(self, points, width, **args): - QObjectWorkaround.__init__(self) - QtGui.QGraphicsItem.__init__(self) - self.roiArgs = args - if len(points) < 2: - raise Exception("Must start with at least 2 points") - self.lines = [] - self.lines.append(ROI([0, 0], [1, 5], parent=self)) - self.lines[-1].addScaleHandle([0.5, 1], [0.5, 0.5]) - h = self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5]) - h.movePoint(points[0]) - h.movePoint(points[0]) - for i in range(1, len(points)): - h = self.lines[-1].addScaleRotateHandle([1, 0.5], [0, 0.5]) - if i < len(points)-1: - self.lines.append(ROI([0, 0], [1, 5], parent=self)) - self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5], item=h) - h.movePoint(points[i]) - h.movePoint(points[i]) - - for l in self.lines: - l.translatable = False - #self.addToGroup(l) - l.connect(QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) - l.connect(QtCore.SIGNAL('regionChangeStarted'), self.roiChangeStartedEvent) - l.connect(QtCore.SIGNAL('regionChangeFinished'), self.roiChangeFinishedEvent) - - def paint(self, *args): - pass - - def boundingRect(self): - return QtCore.QRectF() - - def roiChangedEvent(self): - w = self.lines[0].state['size'][1] - 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) - - def roiChangeStartedEvent(self): - self.emit(QtCore.SIGNAL('regionChangeStarted'), self) - - def roiChangeFinishedEvent(self): - self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - - - def getArrayRegion(self, arr, img=None): - rgns = [] - for l in self.lines: - rgn = l.getArrayRegion(arr, img) - if rgn is None: - continue - #return None - rgns.append(rgn) - #print l.state['size'] - #print [(r.shape) for r in rgns] - return vstack(rgns) - - -class EllipseROI(ROI): - def __init__(self, pos, size, **args): - #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) - ROI.__init__(self, pos, size, **args) - self.addRotateHandle([1.0, 0.5], [0.5, 0.5]) - self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) - - def paint(self, p, opt, widget): - r = self.boundingRect() - p.setRenderHint(QtGui.QPainter.Antialiasing) - p.setPen(self.pen) - - p.scale(r.width(), r.height())## workaround for GL bug - r = QtCore.QRectF(r.x()/r.width(), r.y()/r.height(), 1,1) - - p.drawEllipse(r) - - def getArrayRegion(self, arr, img=None): - arr = ROI.getArrayRegion(self, arr, img) - if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0: - return None - w = arr.shape[0] - h = arr.shape[1] - ## generate an ellipsoidal mask - mask = fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h)) - - return arr * mask - - def shape(self): - self.path = QtGui.QPainterPath() - self.path.addEllipse(self.boundingRect()) - return self.path - - -class CircleROI(EllipseROI): - def __init__(self, pos, size, **args): - ROI.__init__(self, pos, size, **args) - self.aspectLocked = True - #self.addTranslateHandle([0.5, 0.5]) - self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) - +# -*- coding: utf-8 -*- +""" +widgets.py - Interactive graphics items for GraphicsView (ROI widgets) +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +Implements a series of graphics items which display movable/scalable/rotatable shapes +for use as region-of-interest markers. ROI class automatically handles extraction +of array data from ImageItems. +""" + +from PyQt4 import QtCore, QtGui, QtOpenGL, QtSvg +from numpy import array, arccos, dot, pi, zeros, vstack, ubyte, fromfunction, ceil, floor, arctan2 +from numpy.linalg import norm +import scipy.ndimage as ndimage +from Point import * +from math import cos, sin +from ObjectWorkaround import * + +def rectStr(r): + return "[%f, %f] + [%f, %f]" % (r.x(), r.y(), r.width(), r.height()) + +# Multiple inheritance not allowed in PyQt. Retarded workaround: +#class QObjectWorkaround: + #def __init__(self): + #self._qObj_ = QtCore.QObject() + #def __getattr__(self, attr): + #if attr == '_qObj_': + #raise Exception("QObjectWorkaround not initialized!") + #return getattr(self._qObj_, attr) + #def connect(self, *args): + #return QtCore.QObject.connect(self._qObj_, *args) + + +class ROI(QtGui.QGraphicsItem, QObjectWorkaround): + 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): + QObjectWorkaround.__init__(self) + QtGui.QGraphicsItem.__init__(self, parent) + pos = Point(pos) + size = Point(size) + self.aspectLocked = False + self.translatable = True + + self.pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) + self.handlePen = QtGui.QPen(QtGui.QColor(150, 255, 255)) + self.handles = [] + self.state = {'pos': pos, 'size': size, 'angle': angle} + self.lastState = None + self.setPos(pos) + self.rotate(-angle * 180. / pi) + self.setZValue(10) + + self.handleSize = 5 + self.invertible = invertible + self.maxBounds = maxBounds + + self.snapSize = snapSize + self.translateSnap = translateSnap + self.rotateSnap = rotateSnap + self.scaleSnap = scaleSnap + self.setFlag(self.ItemIsSelectable, True) + + def setZValue(self, z): + QtGui.QGraphicsItem.setZValue(self, z) + for h in self.handles: + h['item'].setZValue(z+1) + + def sceneBounds(self): + return self.sceneTransform().mapRect(self.boundingRect()) + + def parentBounds(self): + return self.mapToParent(self.boundingRect()).boundingRect() + + def setPen(self, pen): + self.pen = pen + self.update() + + def setPos(self, pos, update=True): + pos = Point(pos) + self.state['pos'] = pos + QtGui.QGraphicsItem.setPos(self, pos) + if update: + self.handleChange() + + def setSize(self, size, update=True): + size = Point(size) + self.prepareGeometryChange() + self.state['size'] = size + if update: + self.updateHandles() + self.handleChange() + + def addTranslateHandle(self, pos, axes=None, item=None): + pos = Point(pos) + return self.addHandle({'type': 't', 'pos': pos, 'item': item}) + + def addFreeHandle(self, pos, axes=None, item=None): + pos = Point(pos) + return self.addHandle({'type': 'f', 'pos': pos, 'item': item}) + + def addScaleHandle(self, pos, center, axes=None, item=None): + pos = Point(pos) + center = Point(center) + info = {'type': 's', 'center': center, 'pos': pos, 'item': item} + if pos.x() == center.x(): + info['xoff'] = True + if pos.y() == center.y(): + info['yoff'] = True + return self.addHandle(info) + + def addRotateHandle(self, pos, center, item=None): + pos = Point(pos) + center = Point(center) + return self.addHandle({'type': 'r', 'center': center, 'pos': pos, 'item': item}) + + def addScaleRotateHandle(self, pos, center, item=None): + pos = Point(pos) + center = Point(center) + if pos[0] != center[0] and pos[1] != center[1]: + raise Exception("Scale/rotate handles must have either the same x or y coordinate as their center point.") + return self.addHandle({'type': 'sr', 'center': center, 'pos': pos, 'item': item}) + + def addHandle(self, info): + if not info.has_key('item') or info['item'] is None: + #print "BEFORE ADD CHILD:", self.childItems() + h = Handle(self.handleSize, typ=info['type'], pen=self.handlePen, parent=self) + #print "AFTER ADD CHILD:", self.childItems() + h.setPos(info['pos'] * self.state['size']) + info['item'] = h + else: + h = info['item'] + iid = len(self.handles) + h.connectROI(self, iid) + #h.mouseMoveEvent = lambda ev: self.pointMoveEvent(iid, ev) + #h.mousePressEvent = lambda ev: self.pointPressEvent(iid, ev) + #h.mouseReleaseEvent = lambda ev: self.pointReleaseEvent(iid, ev) + self.handles.append(info) + h.setZValue(self.zValue()+1) + #if self.isSelected(): + #h.show() + #else: + #h.hide() + return h + + def mapSceneToParent(self, pt): + return self.mapToParent(self.mapFromScene(pt)) + + def setSelected(self, s): + QtGui.QGraphicsItem.setSelected(self, s) + #print "select", self, s + if s: + for h in self.handles: + h['item'].show() + else: + for h in self.handles: + h['item'].hide() + + def mousePressEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton: + self.setSelected(True) + if self.translatable: + self.cursorOffset = self.scenePos() - ev.scenePos() + self.emit(QtCore.SIGNAL('regionChangeStarted'), self) + ev.accept() + else: + ev.ignore() + + def mouseMoveEvent(self, ev): + #print "mouse move", ev.pos() + if self.translatable: + snap = None + if self.translateSnap or (ev.modifiers() & QtCore.Qt.ControlModifier): + snap = Point(self.snapSize, self.snapSize) + newPos = ev.scenePos() + self.cursorOffset + newPos = self.mapSceneToParent(newPos) + self.translate(newPos - self.pos(), snap=snap) + + def mouseReleaseEvent(self, ev): + if self.translatable: + self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + + + + def pointPressEvent(self, pt, ev): + #print "press" + self.emit(QtCore.SIGNAL('regionChangeStarted'), self) + #self.pressPos = self.mapFromScene(ev.scenePos()) + #self.pressHandlePos = self.handles[pt]['item'].pos() + + def pointReleaseEvent(self, pt, ev): + #print "release" + self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + + def stateCopy(self): + sc = {} + sc['pos'] = Point(self.state['pos']) + sc['size'] = Point(self.state['size']) + sc['angle'] = self.state['angle'] + return sc + + def updateHandles(self): + #print "update", self.handles + for h in self.handles: + #print " try", h + if h['item'] in self.childItems(): + p = h['pos'] + h['item'].setPos(h['pos'] * self.state['size']) + #else: + #print " Not child!", self.childItems() + + + def checkPointMove(self, pt, pos, modifiers): + return True + + def pointMoveEvent(self, pt, ev): + self.movePoint(pt, ev.scenePos(), ev.modifiers()) + + + def movePoint(self, pt, pos, modifiers=QtCore.Qt.KeyboardModifier()): + #print "movePoint", pos + newState = self.stateCopy() + h = self.handles[pt] + #p0 = self.mapToScene(h['item'].pos()) + p0 = self.mapToScene(h['pos'] * self.state['size']) + p1 = Point(pos) + p0 = self.mapSceneToParent(p0) + p1 = self.mapSceneToParent(p1) + + if h.has_key('center'): + c = h['center'] + cs = c * self.state['size'] + #lpOrig = h['pos'] - + #lp0 = self.mapFromScene(p0) - cs + #lp1 = self.mapFromScene(p1) - cs + lp0 = self.mapFromParent(p0) - cs + lp1 = self.mapFromParent(p1) - cs + + if h['type'] == 't': + #p0 = Point(self.mapToScene(h['item'].pos())) + #p1 = Point(pos + self.mapToScene(self.pressHandlePos) - self.mapToScene(self.pressPos)) + snap = None + if self.translateSnap or (modifiers & QtCore.Qt.ControlModifier): + snap = Point(self.snapSize, self.snapSize) + self.translate(p1-p0, snap=snap, update=False) + + elif h['type'] == 'f': + h['item'].setPos(self.mapFromScene(pos)) + + elif h['type'] == 's': + #c = h['center'] + #cs = c * self.state['size'] + #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs + + if h['center'][0] == h['pos'][0]: + lp1[0] = 0 + if h['center'][1] == h['pos'][1]: + lp1[1] = 0 + + if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): + lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize + lp1[1] = round(lp1[1] / self.snapSize) * self.snapSize + + hs = h['pos'] - c + if hs[0] == 0: + hs[0] = 1 + if hs[1] == 0: + hs[1] = 1 + newSize = lp1 / hs + + if newSize[0] == 0: + newSize[0] = newState['size'][0] + if newSize[1] == 0: + newSize[1] = newState['size'][1] + if not self.invertible: + if newSize[0] < 0: + newSize[0] = newState['size'][0] + if newSize[1] < 0: + newSize[1] = newState['size'][1] + if self.aspectLocked: + newSize[0] = newSize[1] + + s0 = c * self.state['size'] + s1 = c * newSize + cc = self.mapToParent(s0 - s1) - self.mapToParent(Point(0, 0)) + + newState['size'] = newSize + newState['pos'] = newState['pos'] + cc + if self.maxBounds is not None: + r = self.stateRect(newState) + if not self.maxBounds.contains(r): + return + + self.setPos(newState['pos'], update=False) + self.prepareGeometryChange() + self.state = newState + + self.updateHandles() + + elif h['type'] == 'r': + #newState = self.stateCopy() + #c = h['center'] + #cs = c * self.state['size'] + #p0 = Point(h['item'].pos()) - cs + #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs + if lp1.length() == 0 or lp0.length() == 0: + return + + ang = newState['angle'] + lp0.angle(lp1) + if ang is None: + return + if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): + ang = round(ang / (pi/12.)) * (pi/12.) + + + tr = QtGui.QTransform() + tr.rotate(-ang * 180. / pi) + + cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos']) + newState['angle'] = ang + newState['pos'] = newState['pos'] + cc + if self.maxBounds is not None: + r = self.stateRect(newState) + if not self.maxBounds.contains(r): + return + self.setTransform(tr) + self.setPos(newState['pos'], update=False) + self.state = newState + + elif h['type'] == 'sr': + #newState = self.stateCopy() + if h['center'][0] == h['pos'][0]: + scaleAxis = 1 + else: + scaleAxis = 0 + + #c = h['center'] + #cs = c * self.state['size'] + #p0 = Point(h['item'].pos()) - cs + #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs + if lp1.length() == 0 or lp0.length() == 0: + return + + ang = newState['angle'] + lp0.angle(lp1) + if ang is None: + return + if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): + ang = round(ang / (pi/12.)) * (pi/12.) + + hs = abs(h['pos'][scaleAxis] - c[scaleAxis]) + newState['size'][scaleAxis] = lp1.length() / hs + if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): + newState['size'][scaleAxis] = round(newState['size'][scaleAxis] / self.snapSize) * self.snapSize + if newState['size'][scaleAxis] == 0: + newState['size'][scaleAxis] = 1 + + c1 = c * newState['size'] + tr = QtGui.QTransform() + tr.rotate(-ang * 180. / pi) + + cc = self.mapToParent(cs) - (tr.map(c1) + self.state['pos']) + newState['angle'] = ang + newState['pos'] = newState['pos'] + cc + if self.maxBounds is not None: + r = self.stateRect(newState) + if not self.maxBounds.contains(r): + return + self.setTransform(tr) + self.setPos(newState['pos'], update=False) + self.prepareGeometryChange() + self.state = newState + + self.updateHandles() + + self.handleChange() + + def handleChange(self): + changed = False + if self.lastState is None: + changed = True + else: + for k in self.state.keys(): + if self.state[k] != self.lastState[k]: + #print "state %s has changed; emit signal" % k + changed = True + self.lastState = self.stateCopy() + if changed: + self.update() + self.emit(QtCore.SIGNAL('regionChanged'), self) + + + def scale(self, s, center=[0,0]): + c = self.mapToScene(Point(center) * self.state['size']) + self.prepareGeometryChange() + self.state['size'] = self.state['size'] * s + c1 = self.mapToScene(Point(center) * self.state['size']) + self.state['pos'] = self.state['pos'] + c - c1 + self.setPos(self.state['pos']) + self.updateHandles() + + def translate(self, *args, **kargs): + """accepts either (x, y, snap) or ([x,y], snap) as arguments""" + if 'snap' not in kargs: + snap = None + else: + snap = kargs['snap'] + + if len(args) == 1: + pt = args[0] + else: + pt = args + + newState = self.stateCopy() + newState['pos'] = newState['pos'] + pt + if snap != None: + newState['pos'][0] = round(newState['pos'][0] / snap[0]) * snap[0] + newState['pos'][1] = round(newState['pos'][1] / snap[1]) * snap[1] + + + #d = ev.scenePos() - self.mapToScene(self.pressPos) + if self.maxBounds is not None: + r = self.stateRect(newState) + #r0 = self.sceneTransform().mapRect(self.boundingRect()) + d = Point(0,0) + if self.maxBounds.left() > r.left(): + d[0] = self.maxBounds.left() - r.left() + elif self.maxBounds.right() < r.right(): + d[0] = self.maxBounds.right() - r.right() + if self.maxBounds.top() > r.top(): + d[1] = self.maxBounds.top() - r.top() + elif self.maxBounds.bottom() < r.bottom(): + d[1] = self.maxBounds.bottom() - r.bottom() + newState['pos'] += d + + self.state['pos'] = newState['pos'] + self.setPos(self.state['pos']) + #if 'update' not in kargs or kargs['update'] is True: + self.handleChange() + + def stateRect(self, state): + r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1]) + tr = QtGui.QTransform() + tr.rotate(-state['angle'] * 180 / pi) + r = tr.mapRect(r) + return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1]) + + def boundingRect(self): + return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]) + + def paint(self, p, opt, widget): + p.save() + r = self.boundingRect() + p.setRenderHint(QtGui.QPainter.Antialiasing) + p.setPen(self.pen) + p.translate(r.left(), r.top()) + p.scale(r.width(), r.height()) + p.drawRect(0, 0, 1, 1) + p.restore() + + def getArraySlice(self, data, img, axes=(0,1), returnSlice=True): + """Return a tuple of slice objects that can be used to slice the region from data covered by this ROI. + Also returns the transform which maps the ROI into data coordinates. + + If returnSlice is set to False, the function returns a pair of tuples with the values that would have + been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))""" + #print "getArraySlice" + + ## Determine shape of array along ROI axes + dShape = (data.shape[axes[0]], data.shape[axes[1]]) + #print " dshape", dShape + + ## Determine transform that maps ROI bounding box to image coordinates + tr = self.sceneTransform() * img.sceneTransform().inverted()[0] + + ## Modify transform to scale from image coords to data coords + #m = QtGui.QTransform() + tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) + #tr = tr * m + + ## Transform ROI bounds into data bounds + dataBounds = tr.mapRect(self.boundingRect()) + #print " boundingRect:", self.boundingRect() + #print " dataBounds:", dataBounds + + ## Intersect transformed ROI bounds with data bounds + intBounds = dataBounds.intersect(QtCore.QRectF(0, 0, dShape[0], dShape[1])) + #print " intBounds:", intBounds + + ## Determine index values to use when referencing the array. + bounds = ( + (int(min(intBounds.left(), intBounds.right())), int(1+max(intBounds.left(), intBounds.right()))), + (int(min(intBounds.bottom(), intBounds.top())), int(1+max(intBounds.bottom(), intBounds.top()))) + ) + #print " bounds:", bounds + + if returnSlice: + ## Create slice objects + sl = [slice(None)] * data.ndim + sl[axes[0]] = slice(*bounds[0]) + sl[axes[1]] = slice(*bounds[1]) + return tuple(sl), tr + else: + return bounds, tr + + + def getArrayRegion(self, data, img, axes=(0,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])) + ) + + ## 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) + + + ## Rotate array + if abs(self.state['angle']) > 1e-5: + arr2 = ndimage.rotate(arr1, self.state['angle'] * 180 / pi, order=1) + + ## update data transforms to reflect this rotation + rot = QtGui.QTransform().rotate(self.state['angle'] * 180 / 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()) + + else: + arr2 = arr1 + + + + ### 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 = 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 + + + ### 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())) + + #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]] + + ### Create zero array in size of ROI + arr5 = 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]] + + + ## figure out the reverse transpose order + tr2 = array(tr1) + for i in range(0, len(tr2)): + tr2[tr1[i]] = i + tr2 = tuple(tr2) + + ## Untranspose array before returning + return arr5.transpose(tr2) + + + + + + + +class Handle(QtGui.QGraphicsItem): + def __init__(self, radius, typ=None, pen=QtGui.QPen(QtGui.QColor(200, 200, 220)), parent=None): + #print " create item with parent", parent + self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10) + QtGui.QGraphicsItem.__init__(self, parent) + self.setFlag(self.ItemIgnoresTransformations) + self.setZValue(11) + self.roi = [] + self.radius = radius + self.typ = typ + self.prepareGeometryChange() + self.pen = pen + self.pen.setWidth(0) + self.pen.setCosmetic(True) + if typ == 't': + self.sides = 4 + self.startAng = pi/4 + elif typ == 'f': + self.sides = 4 + self.startAng = pi/4 + elif typ == 's': + self.sides = 4 + self.startAng = 0 + elif typ == 'r': + self.sides = 12 + self.startAng = 0 + elif typ == 'sr': + self.sides = 12 + self.startAng = 0 + else: + self.sides = 4 + self.startAng = pi/4 + + def connectROI(self, roi, i): + self.roi.append((roi, i)) + + def boundingRect(self): + return self.bounds + + def mousePressEvent(self, ev): + #print "handle press" + if ev.button() != QtCore.Qt.LeftButton: + ev.ignore() + return + self.cursorOffset = self.scenePos() - ev.scenePos() + for r in self.roi: + r[0].pointPressEvent(r[1], ev) + #print " accepted." + ev.accept() + + def mouseReleaseEvent(self, ev): + #print "release" + for r in self.roi: + r[0].pointReleaseEvent(r[1], ev) + + def mouseMoveEvent(self, ev): + #print "handle mouseMove", ev.pos() + pos = ev.scenePos() + self.cursorOffset + self.movePoint(pos, ev.modifiers()) + + def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier()): + for r in self.roi: + if not r[0].checkPointMove(r[1], pos, modifiers): + return + #print "point moved; inform %d ROIs" % len(self.roi) + for r in self.roi: + r[0].movePoint(r[1], pos, modifiers) + + def paint(self, p, opt, widget): + ## determine rotation of transform + m = self.sceneTransform() + #mi = m.inverted()[0] + v = m.map(QtCore.QPointF(1, 0)) - m.map(QtCore.QPointF(0, 0)) + va = arctan2(v.y(), v.x()) + + ## Determine length of unit vector in painter's coords + #size = mi.map(Point(self.radius, self.radius)) - mi.map(Point(0, 0)) + #size = (size.x()*size.x() + size.y() * size.y()) ** 0.5 + size = self.radius + + bounds = QtCore.QRectF(-size, -size, size*2, size*2) + if bounds != self.bounds: + self.bounds = bounds + self.prepareGeometryChange() + p.setRenderHints(p.Antialiasing, True) + p.setPen(self.pen) + ang = self.startAng + va + dt = 2*pi / self.sides + for i in range(0, self.sides): + x1 = size * cos(ang) + y1 = size * sin(ang) + x2 = size * cos(ang+dt) + y2 = size * sin(ang+dt) + ang += dt + p.drawLine(Point(x1, y1), Point(x2, y2)) + + + + + +class TestROI(ROI): + def __init__(self, pos, size, **args): + #QtGui.QGraphicsRectItem.__init__(self, pos[0], pos[1], size[0], size[1]) + ROI.__init__(self, pos, size, **args) + #self.addTranslateHandle([0, 0]) + self.addTranslateHandle([0.5, 0.5]) + self.addScaleHandle([1, 1], [0, 0]) + self.addScaleHandle([0, 0], [1, 1]) + self.addScaleRotateHandle([1, 0.5], [0.5, 0.5]) + self.addScaleHandle([0.5, 1], [0.5, 0.5]) + self.addRotateHandle([1, 0], [0, 0]) + self.addRotateHandle([0, 1], [1, 1]) + + + +class RectROI(ROI): + def __init__(self, pos, size, centered=False, sideScalers=False, **args): + #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) + ROI.__init__(self, pos, size, **args) + if centered: + center = [0.5, 0.5] + else: + center = [0, 0] + + #self.addTranslateHandle(center) + self.addScaleHandle([1, 1], center) + if sideScalers: + self.addScaleHandle([1, 0.5], [center[0], 0.5]) + self.addScaleHandle([0.5, 1], [0.5, center[1]]) + +class LineROI(ROI): + def __init__(self, pos1, pos2, width, **args): + pos1 = Point(pos1) + pos2 = Point(pos2) + d = pos2-pos1 + l = d.length() + ang = Point(1, 0).angle(d) + c = Point(-width/2. * sin(ang), -width/2. * cos(ang)) + pos1 = pos1 + c + + ROI.__init__(self, pos1, size=Point(l, width), angle=ang*180/pi, **args) + self.addScaleRotateHandle([0, 0.5], [1, 0.5]) + self.addScaleRotateHandle([1, 0.5], [0, 0.5]) + self.addScaleHandle([0.5, 1], [0.5, 0.5]) + + +class MultiLineROI(QtGui.QGraphicsItem, QObjectWorkaround): + def __init__(self, points, width, **args): + QObjectWorkaround.__init__(self) + QtGui.QGraphicsItem.__init__(self) + self.roiArgs = args + if len(points) < 2: + raise Exception("Must start with at least 2 points") + self.lines = [] + self.lines.append(ROI([0, 0], [1, 5], parent=self)) + self.lines[-1].addScaleHandle([0.5, 1], [0.5, 0.5]) + h = self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5]) + h.movePoint(points[0]) + h.movePoint(points[0]) + for i in range(1, len(points)): + h = self.lines[-1].addScaleRotateHandle([1, 0.5], [0, 0.5]) + if i < len(points)-1: + self.lines.append(ROI([0, 0], [1, 5], parent=self)) + self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5], item=h) + h.movePoint(points[i]) + h.movePoint(points[i]) + + 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) + + def paint(self, *args): + pass + + def boundingRect(self): + return QtCore.QRectF() + + def roiChangedEvent(self): + w = self.lines[0].state['size'][1] + 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) + + def roiChangeStartedEvent(self): + self.emit(QtCore.SIGNAL('regionChangeStarted'), self) + + def roiChangeFinishedEvent(self): + self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + + + def getArrayRegion(self, arr, img=None): + rgns = [] + for l in self.lines: + rgn = l.getArrayRegion(arr, img) + if rgn is None: + continue + #return None + rgns.append(rgn) + #print l.state['size'] + #print [(r.shape) for r in rgns] + return vstack(rgns) + + +class EllipseROI(ROI): + def __init__(self, pos, size, **args): + #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) + ROI.__init__(self, pos, size, **args) + self.addRotateHandle([1.0, 0.5], [0.5, 0.5]) + self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) + + def paint(self, p, opt, widget): + r = self.boundingRect() + p.setRenderHint(QtGui.QPainter.Antialiasing) + p.setPen(self.pen) + + p.scale(r.width(), r.height())## workaround for GL bug + r = QtCore.QRectF(r.x()/r.width(), r.y()/r.height(), 1,1) + + p.drawEllipse(r) + + def getArrayRegion(self, arr, img=None): + arr = ROI.getArrayRegion(self, arr, img) + if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0: + return None + w = arr.shape[0] + h = arr.shape[1] + ## generate an ellipsoidal mask + mask = fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h)) + + return arr * mask + + def shape(self): + self.path = QtGui.QPainterPath() + self.path.addEllipse(self.boundingRect()) + return self.path + + +class CircleROI(EllipseROI): + def __init__(self, pos, size, **args): + ROI.__init__(self, pos, size, **args) + self.aspectLocked = True + #self.addTranslateHandle([0.5, 0.5]) + self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) + +class PolygonROI(ROI): + def __init__(self, positions): + ROI.__init__(self, [0,0], [100,100]) + for p in positions: + self.addFreeHandle(p) + + def movePoint(self, *args, **kargs): + ROI.movePoint(self, *args, **kargs) + self.prepareGeometryChange() + + def paint(self, p, *args): + p.setRenderHint(QtGui.QPainter.Antialiasing) + p.setPen(self.pen) + for i in range(len(self.handles)): + 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() + 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 + \ No newline at end of file