From 3d84aefbc45c941ae73694c529e4b021a229edcf Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 22 Mar 2010 01:48:52 -0400 Subject: [PATCH] Initial commit (again) --- GraphicsView.py | 413 ++++++++ ImageView.py | 275 +++++ ImageViewTemplate.py | 188 ++++ ImageViewTemplate.ui | 341 +++++++ MultiPlotItem.py | 66 ++ MultiPlotWidget.py | 39 + PIL_Fix/Image.py | 2099 +++++++++++++++++++++++++++++++++++++++ PIL_Fix/README | 11 + PlotItem.py | 957 ++++++++++++++++++ PlotWidget.py | 43 + Point.py | 118 +++ __init__.py | 0 functions.py | 91 ++ graphicsItems.py | 1481 +++++++++++++++++++++++++++ graphicsWindows.py | 32 + license.txt | 7 + plotConfigTemplate.py | 277 ++++++ plotConfigTemplate.ui | 527 ++++++++++ test_ImageView.py | 21 + test_MultiPlotWidget.py | 17 + test_PlotWidget.py | 80 ++ test_ROItypes.py | 88 ++ test_viewBox.py | 100 ++ widgets.py | 837 ++++++++++++++++ 24 files changed, 8108 insertions(+) create mode 100644 GraphicsView.py create mode 100644 ImageView.py create mode 100644 ImageViewTemplate.py create mode 100644 ImageViewTemplate.ui create mode 100644 MultiPlotItem.py create mode 100644 MultiPlotWidget.py create mode 100755 PIL_Fix/Image.py create mode 100644 PIL_Fix/README create mode 100644 PlotItem.py create mode 100644 PlotWidget.py create mode 100644 Point.py create mode 100644 __init__.py create mode 100644 functions.py create mode 100644 graphicsItems.py create mode 100644 graphicsWindows.py create mode 100644 license.txt create mode 100644 plotConfigTemplate.py create mode 100644 plotConfigTemplate.ui create mode 100644 test_ImageView.py create mode 100755 test_MultiPlotWidget.py create mode 100755 test_PlotWidget.py create mode 100755 test_ROItypes.py create mode 100755 test_viewBox.py create mode 100644 widgets.py diff --git a/GraphicsView.py b/GraphicsView.py new file mode 100644 index 00000000..46514ee1 --- /dev/null +++ b/GraphicsView.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- +""" +GraphicsView.py - Extension of QGraphicsView +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from PyQt4 import QtCore, QtGui, QtOpenGL, QtSvg +#from numpy import vstack +#import time +from Point import * +#from vector import * + + + +class GraphicsView(QtGui.QGraphicsView): + def __init__(self, *args): + """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. + + By default, the view coordinate system matches the widget's pixel coordinates and + automatically updates when the view is resized. This can be overridden by setting + autoPixelRange=False. The exact visible range can be set with setRange(). + + 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()) + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(0,0,0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active,QtGui.QPalette.Base,brush) + brush = QtGui.QBrush(QtGui.QColor(0,0,0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive,QtGui.QPalette.Base,brush) + brush = QtGui.QBrush(QtGui.QColor(244,244,244)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled,QtGui.QPalette.Base,brush) + self.setPalette(palette) + self.setProperty("cursor",QtCore.QVariant(QtCore.Qt.ArrowCursor)) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setFrameShape(QtGui.QFrame.NoFrame) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor) + self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter) + #self.setResizeAnchor(QtGui.QGraphicsView.NoAnchor) + self.setViewportUpdateMode(QtGui.QGraphicsView.SmartViewportUpdate) + self.setSceneRect(QtCore.QRectF(-1e10, -1e10, 2e10, 2e10)) + #self.setInteractive(False) + self.lockedViewports = [] + self.lastMousePos = None + #self.setMouseTracking(False) + self.aspectLocked = False + self.yInverted = True + self.range = QtCore.QRectF(0, 0, 1, 1) + self.autoPixelRange = True + self.currentItem = None + self.clearMouse() + self.updateMatrix() + self.sceneObj = QtGui.QGraphicsScene() + self.setScene(self.sceneObj) + self.centralWidget = None + self.setCentralItem(QtGui.QGraphicsWidget()) + self.mouseEnabled = False + self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) + self.clickAccepted = False + + def setCentralItem(self, item): + if self.centralWidget is not None: + self.scene().removeItem(self.centralWidget) + self.centralWidget = item + self.sceneObj.addItem(item) + + def addItem(self, *args): + return self.scene().addItem(*args) + + def enableMouse(self, b=True): + self.mouseEnabled = b + self.autoPixelRange = (not b) + + def clearMouse(self): + self.mouseTrail = [] + self.lastButtonReleased = None + + def resizeEvent(self, ev): + if self.autoPixelRange: + self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height()) + self.setRange(self.range, padding=0, disableAutoPixel=False) + self.updateMatrix() + + def updateMatrix(self, propagate=True): + #print "udpateMatrix:" + translate = Point(self.range.center()) + if self.range.width() == 0 or self.range.height() == 0: + return + scale = Point(self.size().width()/self.range.width(), self.size().height()/self.range.height()) + + m = QtGui.QMatrix() + + ## First center the viewport at 0 + self.resetMatrix() + center = self.viewportTransform().inverted()[0].map(Point(self.width()/2., self.height()/2.)) + if self.yInverted: + m.translate(center.x(), center.y()) + #print " inverted; translate", center.x(), center.y() + else: + m.translate(center.x(), -center.y()) + #print " not inverted; translate", center.x(), -center.y() + + ## Now scale and translate properly + if self.aspectLocked: + scale = Point(scale.min()) + if not self.yInverted: + scale = scale * Point(1, -1) + m.scale(scale[0], scale[1]) + #print " scale:", scale + st = translate + m.translate(-st[0], -st[1]) + #print " translate:", st + self.setMatrix(m) + self.currentScale = scale + + if propagate: + for v in self.lockedViewports: + v.setXRange(self.range, padding=0) + + def visibleRange(self): + r = QtCore.QRectF(self.rect()) + return self.viewportTransform().inverted()[0].mapRect(r) + + def translate(self, dx, dy): + self.range.adjust(dx, dy, dx, dy) + self.updateMatrix() + + def scale(self, sx, sy, center=None): + scale = [sx, sy] + if self.aspectLocked: + scale[0] = scale[1] + #adj = (self.range.width()*0.5*(1.0-(1.0/scale[0])), self.range.height()*0.5*(1.0-(1.0/scale[1]))) + #print "======\n", scale, adj + #print self.range + #self.range.adjust(adj[0], adj[1], -adj[0], -adj[1]) + #print self.range + + if self.scaleCenter: + center = None + if center is None: + center = self.range.center() + + w = self.range.width() / scale[0] + h = self.range.height() / scale[1] + self.range = QtCore.QRectF(center.x() - (center.x()-self.range.left()) / scale[0], center.y() - (center.y()-self.range.top()) /scale[1], w, h) + + + self.updateMatrix() + + def setRange(self, newRect=None, padding=0.05, lockAspect=None, propagate=True, disableAutoPixel=True): + if disableAutoPixel: + self.autoPixelRange=False + if newRect is None: + newRect = self.visibleRange() + padding = 0 + padding = Point(padding) + newRect = QtCore.QRectF(newRect) + pw = newRect.width() * padding[0] + ph = newRect.height() * padding[1] + self.range = newRect.adjusted(-pw, -ph, pw, ph) + #print "New Range:", self.range + self.centralWidget.setGeometry(self.range) + self.updateMatrix(propagate) + self.emit(QtCore.SIGNAL('viewChanged'), self.range) + + + def lockXRange(self, v1): + if not v1 in self.lockedViewports: + self.lockedViewports.append(v1) + + def setXRange(self, r, padding=0.05): + r1 = QtCore.QRectF(self.range) + r1.setLeft(r.left()) + r1.setRight(r.right()) + self.setRange(r1, padding=[padding, 0], propagate=False) + + def setYRange(self, r, padding=0.05): + r1 = QtCore.QRectF(self.range) + r1.setTop(r.top()) + r1.setBottom(r.bottom()) + self.setRange(r1, padding=[0, padding], propagate=False) + + def invertY(self, invert=True): + #if self.yInverted != invert: + #self.scale[1] *= -1. + self.yInverted = invert + self.updateMatrix() + + + def wheelEvent(self, ev): + if not self.mouseEnabled: + return + QtGui.QGraphicsView.wheelEvent(self, ev) + sc = 1.001 ** ev.delta() + #self.scale *= sc + #self.updateMatrix() + self.scale(sc, sc) + + + def setAspectLocked(self, s): + self.aspectLocked = s + + #def mouseDoubleClickEvent(self, ev): + #QtGui.QGraphicsView.mouseDoubleClickEvent(self, ev) + #pass + + ### This function is here because interactive mode is disabled due to bugs. + #def graphicsSceneEvent(self, ev, pev=None, fev=None): + #ev1 = GraphicsSceneMouseEvent() + #ev1.setPos(QtCore.QPointF(ev.pos().x(), ev.pos().y())) + #ev1.setButtons(ev.buttons()) + #ev1.setButton(ev.button()) + #ev1.setModifiers(ev.modifiers()) + #ev1.setScenePos(self.mapToScene(QtCore.QPoint(ev.pos()))) + #if pev is not None: + #ev1.setLastPos(pev.pos()) + #ev1.setLastScenePos(pev.scenePos()) + #ev1.setLastScreenPos(pev.screenPos()) + #if fev is not None: + #ev1.setButtonDownPos(fev.pos()) + #ev1.setButtonDownScenePos(fev.scenePos()) + #ev1.setButtonDownScreenPos(fev.screenPos()) + #return ev1 + + def mousePressEvent(self, ev): + QtGui.QGraphicsView.mousePressEvent(self, ev) + print "Press over:" + for i in self.items(ev.pos()): + print i.zValue(), int(i.acceptedMouseButtons()), i, i.scenePos() + print "Event accepted:", ev.isAccepted() + print "Grabber:", self.scene().mouseGrabberItem() + if not self.mouseEnabled: + return + self.lastMousePos = Point(ev.pos()) + self.mousePressPos = ev.pos() + self.clickAccepted = ev.isAccepted() + if not self.clickAccepted: + self.scene().clearSelection() + return ## Everything below disabled for now.. + + #self.currentItem = None + #maxZ = None + #for i in self.items(ev.pos()): + #if maxZ is None or maxZ < i.zValue(): + #self.currentItem = i + #maxZ = i.zValue() + #print "make event" + #self.pev = self.graphicsSceneEvent(ev) + #self.fev = self.pev + #if self.currentItem is not None: + #self.currentItem.mousePressEvent(self.pev) + ##self.clearMouse() + ##self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) + #self.emit(QtCore.SIGNAL("mousePressed(PyQt_PyObject)"), self.mouseTrail) + + def mouseReleaseEvent(self, ev): + QtGui.QGraphicsView.mouseReleaseEvent(self, ev) + if not self.mouseEnabled: + return + #self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) + self.emit(QtCore.SIGNAL("mouseReleased"), ev) + self.lastButtonReleased = ev.button() + return ## Everything below disabled for now.. + + ##self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) + #self.emit(QtCore.SIGNAL("mouseReleased(PyQt_PyObject)"), self.mouseTrail) + #if self.currentItem is not None: + #pev = self.graphicsSceneEvent(ev, self.pev, self.fev) + #self.pev = pev + #self.currentItem.mouseReleaseEvent(pev) + #self.currentItem = None + + def mouseMoveEvent(self, ev): + QtGui.QGraphicsView.mouseMoveEvent(self, ev) + if not self.mouseEnabled: + return + self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos())) + #print "moved. Grabber:", self.scene().mouseGrabberItem() + + if self.lastMousePos is None: + self.lastMousePos = Point(ev.pos()) + + if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it. + return + + delta = Point(ev.pos()) - self.lastMousePos + + self.lastMousePos = Point(ev.pos()) + + if ev.buttons() == QtCore.Qt.RightButton: + delta = Point(clip(delta[0], -50, 50), clip(-delta[1], -50, 50)) + scale = 1.01 ** delta + #if self.yInverted: + #scale[0] = 1. / scale[0] + self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos)) + self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) + + + elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button. + tr = -delta / self.currentScale + + self.translate(tr[0], tr[1]) + self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) + + #return ## Everything below disabled for now.. + + ##self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) + #if self.currentItem is not None: + #pev = self.graphicsSceneEvent(ev, self.pev, self.fev) + #self.pev = pev + #self.currentItem.mouseMoveEvent(pev) + + + + + def writeSvg(self, fileName=None): + if fileName is None: + fileName = str(QtGui.QFileDialog.getSaveFileName()) + self.svg = QtSvg.QSvgGenerator() + self.svg.setFileName(fileName) + self.svg.setSize(self.size()) + self.svg.setResolution(600) + painter = QtGui.QPainter(self.svg) + self.render(painter) + + def writeImage(self, fileName=None): + if fileName is None: + fileName = str(QtGui.QFileDialog.getSaveFileName()) + self.png = QtGui.QImage(self.size(), QtGui.QImage.Format_ARGB32) + painter = QtGui.QPainter(self.png) + rh = self.renderHints() + self.setRenderHints(QtGui.QPainter.Antialiasing) + self.render(painter) + self.setRenderHints(rh) + self.png.save(fileName) + + #def getFreehandLine(self): + + ## Wait for click + #self.clearMouse() + #while self.lastButtonReleased != QtCore.Qt.LeftButton: + #QtGui.qApp.sendPostedEvents() + #QtGui.qApp.processEvents() + #time.sleep(0.01) + #fl = vstack(self.mouseTrail) + #return fl + + #def getClick(self): + #fl = self.getFreehandLine() + #return fl[-1] + + +#class GraphicsSceneMouseEvent(QtGui.QGraphicsSceneMouseEvent): + #"""Stand-in class for QGraphicsSceneMouseEvent""" + #def __init__(self): + #QtGui.QGraphicsSceneMouseEvent.__init__(self) + + #def setPos(self, p): + #self.vpos = p + #def setButtons(self, p): + #self.vbuttons = p + #def setButton(self, p): + #self.vbutton = p + #def setModifiers(self, p): + #self.vmodifiers = p + #def setScenePos(self, p): + #self.vscenePos = p + #def setLastPos(self, p): + #self.vlastPos = p + #def setLastScenePos(self, p): + #self.vlastScenePos = p + #def setLastScreenPos(self, p): + #self.vlastScreenPos = p + #def setButtonDownPos(self, p): + #self.vbuttonDownPos = p + #def setButtonDownScenePos(self, p): + #self.vbuttonDownScenePos = p + #def setButtonDownScreenPos(self, p): + #self.vbuttonDownScreenPos = p + + #def pos(self): + #return self.vpos + #def buttons(self): + #return self.vbuttons + #def button(self): + #return self.vbutton + #def modifiers(self): + #return self.vmodifiers + #def scenePos(self): + #return self.vscenePos + #def lastPos(self): + #return self.vlastPos + #def lastScenePos(self): + #return self.vlastScenePos + #def lastScreenPos(self): + #return self.vlastScreenPos + #def buttonDownPos(self): + #return self.vbuttonDownPos + #def buttonDownScenePos(self): + #return self.vbuttonDownScenePos + #def buttonDownScreenPos(self): + #return self.vbuttonDownScreenPos + diff --git a/ImageView.py b/ImageView.py new file mode 100644 index 00000000..21be5819 --- /dev/null +++ b/ImageView.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +""" +ImageView.py - Widget for basic image dispay and analysis +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +Widget used for displaying 2D or 3D data. Features: + - float or int (including 16-bit int) image display via ImageItem + - zoom/pan via GraphicsView + - black/white level controls + - time slider for 3D data sets + - ROI plotting + - Image normalization through a variety of methods +""" + +from ImageViewTemplate import * +from graphicsItems import * +from widgets import ROI +from PyQt4 import QtCore, QtGui + +class PlotROI(ROI): + def __init__(self, size): + ROI.__init__(self, pos=[0,0], size=size, scaleSnap=True, translateSnap=True) + self.addScaleHandle([1, 1], [0, 0]) + + +class ImageView(QtGui.QWidget): + def __init__(self, parent=None, name="ImageView", *args): + QtGui.QWidget.__init__(self, parent, *args) + self.levelMax = 4096 + self.levelMin = 0 + self.name = name + self.image = None + self.imageDisp = None + self.ui = Ui_Form() + self.ui.setupUi(self) + self.scene = self.ui.graphicsView.sceneObj + self.ui.graphicsView.enableMouse(True) + self.ui.graphicsView.autoPixelRange = False + self.ui.graphicsView.setAspectLocked(True) + self.ui.graphicsView.invertY() + self.ui.graphicsView.enableMouse() + + self.imageItem = ImageItem() + self.scene.addItem(self.imageItem) + self.currentIndex = 0 + + self.ui.normGroup.hide() + + self.roi = PlotROI(10) + self.roi.setZValue(20) + self.scene.addItem(self.roi) + self.roi.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.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() + + for fn in ['addItem']: + 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.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) + QtCore.QObject.connect(self.ui.normDivideRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) + QtCore.QObject.connect(self.ui.normSubtractRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) + QtCore.QObject.connect(self.ui.normOffRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) + QtCore.QObject.connect(self.ui.normROICheck, QtCore.SIGNAL('clicked()'), self.updateNorm) + QtCore.QObject.connect(self.ui.normFrameCheck, QtCore.SIGNAL('clicked()'), self.updateNorm) + QtCore.QObject.connect(self.ui.normTimeRangeCheck, QtCore.SIGNAL('clicked()'), self.updateNorm) + QtCore.QObject.connect(self.ui.normStartSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm) + QtCore.QObject.connect(self.ui.normStopSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm) + + self.ui.roiPlot.registerPlot(self.name + '_ROI') + + 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() + + i, t = self.timeIndex(sl) + l.setPos(t) + + + self.imageDisp = None + self.updateImage() + self.roiChanged() + + def normToggled(self, b): + self.ui.normGroup.setVisible(b) + + def roiClicked(self): + if self.ui.roiBtn.isChecked(): + self.roi.show() + self.ui.roiPlot.show() + self.roiChanged() + else: + self.roi.hide() + self.ui.roiPlot.hide() + + def roiChanged(self): + if self.image is None: + return + + image = self.getProcessedImage() + if image.ndim == 2: + axes = (0, 1) + elif image.ndim == 3: + axes = (1, 2) + else: + return + data = self.roi.getArrayRegion(image.view(ndarray), self.imageItem, axes) + if data is not None: + while data.ndim > 1: + data = data.mean(axis=1) + self.roiCurve.setData(y=data, x=self.tVals) + #self.ui.roiPlot.replot() + + def setImage(self, img, autoRange=True, autoLevels=True, levels=None): + self.image = img + if hasattr(img, 'xvals'): + self.tVals = img.xvals(0) + else: + self.tVals = arange(img.shape[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} + + + self.imageDisp = None + if autoRange: + self.autoRange() + if autoLevels: + self.autoLevels() + if levels is not None: + self.levelMax = levels[1] + self.levelMin = levels[0] + self.updateImage() + if self.ui.roiBtn.isChecked(): + self.roiChanged() + + def autoLevels(self): + image = self.getProcessedImage() + + self.ui.whiteSlider.setValue(self.ui.whiteSlider.maximum()) + self.ui.blackSlider.setValue(0) + self.imageItem.setLevels(white=self.whiteLevel(), black=self.blackLevel()) + + def autoRange(self): + image = self.getProcessedImage() + + self.ui.graphicsView.setRange(QtCore.QRectF(0, 0, image.shape[self.axes['x']], image.shape[self.axes['y']]), padding=0., lockAspect=True) + + def getProcessedImage(self): + if self.imageDisp is None: + image = self.normalize(self.image) + self.imageDisp = image + self.levelMax = float(image.max()) + self.levelMin = float(image.min()) + return self.imageDisp + + def normalize(self, image): + + if self.ui.normOffRadio.isChecked(): + return image + + div = self.ui.normDivideRadio.isChecked() + norm = image.copy() + #if div: + #norm = ones(image.shape) + #else: + #norm = zeros(image.shape) + if div: + 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) + #print start, end, sind, eind + n = image[sind:eind+1].mean(axis=0) + n.shape = (1,) + n.shape + if div: + norm /= n + else: + norm -= n + + if self.ui.normFrameCheck.isChecked() and image.ndim == 3: + n = image.mean(axis=1).mean(axis=1) + n.shape = n.shape + (1, 1) + if div: + norm /= n + else: + norm -= n + + return norm + + + + def timeChanged(self): + (ind, time) = self.timeIndex(self.ui.timeSlider) + if ind != self.currentIndex: + self.currentIndex = ind + self.updateImage() + self.roiTimeLine.setPos(time) + #self.ui.roiPlot.replot() + self.emit(QtCore.SIGNAL('timeChanged'), ind, time) + + def updateImage(self): + ## Redraw image on screen + if self.image is None: + return + + 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.imageItem.updateImage(image, white=self.whiteLevel(), black=self.blackLevel()) + else: + 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 + #xv = self.image.xvals('Time') + xv = self.tVals + if xv is None: + 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 + inds = argwhere(xv < t) + if len(inds) < 1: + return (0,t) + ind = inds[-1,0] + #print ind + return ind, t + + def whiteLevel(self): + 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() + \ No newline at end of file diff --git a/ImageViewTemplate.py b/ImageViewTemplate.py new file mode 100644 index 00000000..3156ea9d --- /dev/null +++ b/ImageViewTemplate.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ImageViewTemplate.ui' +# +# Created: Fri Nov 20 08:22:10 2009 +# 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.normTBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) + self.normTBlurSpin.setObjectName("normTBlurSpin") + self.gridLayout_2.addWidget(self.normTBlurSpin, 4, 6, 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.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 diff --git a/ImageViewTemplate.ui b/ImageViewTemplate.ui new file mode 100644 index 00000000..eb2c3f03 --- /dev/null +++ b/ImageViewTemplate.ui @@ -0,0 +1,341 @@ + + + Form + + + + 0 + 0 + 757 + 495 + + + + Form + + + + 0 + + + 0 + + + + + Qt::Vertical + + + + + 0 + + + + + + 0 + 0 + + + normGroup + normGroup + + + + + + 4096 + + + Qt::Vertical + + + false + + + false + + + QSlider::TicksBelow + + + 410 + + + + + + + 4096 + + + 4096 + + + Qt::Vertical + + + + + + + B + + + + + + + W + + + + + + + + 40 + 16777215 + + + + ROI + + + true + + + + + + + 65535 + + + Qt::Horizontal + + + + + + + + 50 + 16777215 + + + + Norm + + + true + + + + + + + Normalization + + + + 0 + + + 0 + + + + + Subtract + + + + + + + Divide + + + false + + + + + + + + 75 + true + + + + Operation: + + + + + + + + 75 + true + + + + Mean: + + + + + + + + 75 + true + + + + Blur: + + + + + + + ROI + + + + + + + 65535 + + + Qt::Horizontal + + + + + + + 65535 + + + Qt::Horizontal + + + + + + + + + + X + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Y + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + T + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Stop + + + + + + + Start + + + + + + + Off + + + true + + + + + + + Time range + + + + + + + Frame + + + + + + + + + + + + + + + 0 + 40 + + + + + + + + + + GraphicsView + QWidget +
GraphicsView
+ 1 +
+ + PlotWidget + QWidget +
PlotWidget
+ 1 +
+
+ + +
diff --git a/MultiPlotItem.py b/MultiPlotItem.py new file mode 100644 index 00000000..137813db --- /dev/null +++ b/MultiPlotItem.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" +MultiPlotItem.py - Graphics item used for displaying an array of PlotItems +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from numpy import ndarray +from graphicsItems import * +from PlotItem import * + +try: + from metaarray import * + HAVE_METAARRAY = True +except: + raise + HAVE_METAARRAY = False + + +class MultiPlotItem(QtGui.QGraphicsWidget): + def __init__(self, parent=None): + QtGui.QGraphicsWidget.__init__(self, parent) + self.layout = QtGui.QGraphicsGridLayout() + self.layout.setContentsMargins(1,1,1,1) + self.setLayout(self.layout) + self.layout.setHorizontalSpacing(0) + self.layout.setVerticalSpacing(4) + self.plots = [] + + def plot(self, data): + #self.layout.clear() + self.plots = [] + + if HAVE_METAARRAY and isinstance(data, MetaArray): + if data.ndim != 2: + raise Exception("MultiPlot currently only accepts 2D MetaArray.") + ic = data.infoCopy() + ax = 0 + for i in [0, 1]: + if 'cols' in ic[i]: + ax = i + break + #print "Plotting using axis %d as columns (%d plots)" % (ax, data.shape[ax]) + for i in range(data.shape[ax]): + pi = PlotItem() + sl = [slice(None)] * 2 + sl[ax] = i + pi.plot(data[tuple(sl)]) + self.layout.addItem(pi, i, 0) + self.plots.append((pi, i, 0)) + title = None + units = None + info = ic[ax]['cols'][i] + if 'title' in info: + title = info['title'] + elif 'name' in info: + title = info['name'] + if 'units' in info: + units = info['units'] + + pi.setLabel('left', text=title, units=units) + + else: + raise Exception("Data type %s not (yet?) supported for MultiPlot." % type(data)) + + \ No newline at end of file diff --git a/MultiPlotWidget.py b/MultiPlotWidget.py new file mode 100644 index 00000000..2791de67 --- /dev/null +++ b/MultiPlotWidget.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" +MultiPlotWidget.py - Convenience class--GraphicsView widget displaying a MultiPlotItem +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from GraphicsView import * +from MultiPlotItem import * +import exceptions + +class MultiPlotWidget(GraphicsView): + """Widget implementing a graphicsView with a single PlotItem inside.""" + def __init__(self, parent=None): + GraphicsView.__init__(self, parent) + self.enableMouse(False) + self.mPlotItem = MultiPlotItem() + self.setCentralItem(self.mPlotItem) + ## Explicitly wrap methods from mPlotItem + #for m in ['setData']: + #setattr(self, m, getattr(self.mPlotItem, m)) + + def __getattr__(self, attr): ## implicitly wrap methods from plotItem + if hasattr(self.mPlotItem, attr): + m = getattr(self.mPlotItem, attr) + if hasattr(m, '__call__'): + return m + raise exceptions.NameError(attr) + + def widgetGroupInterface(self): + return (None, MultiPlotWidget.saveState, MultiPlotWidget.restoreState) + + def saveState(self): + return {} + #return self.plotItem.saveState() + + def restoreState(self, state): + pass + #return self.plotItem.restoreState(state) diff --git a/PIL_Fix/Image.py b/PIL_Fix/Image.py new file mode 100755 index 00000000..2b373059 --- /dev/null +++ b/PIL_Fix/Image.py @@ -0,0 +1,2099 @@ +# +# The Python Imaging Library. +# $Id: Image.py 2933 2006-12-03 12:08:22Z fredrik $ +# +# the Image class wrapper +# +# partial release history: +# 1995-09-09 fl Created +# 1996-03-11 fl PIL release 0.0 (proof of concept) +# 1996-04-30 fl PIL release 0.1b1 +# 1999-07-28 fl PIL release 1.0 final +# 2000-06-07 fl PIL release 1.1 +# 2000-10-20 fl PIL release 1.1.1 +# 2001-05-07 fl PIL release 1.1.2 +# 2002-03-15 fl PIL release 1.1.3 +# 2003-05-10 fl PIL release 1.1.4 +# 2005-03-28 fl PIL release 1.1.5 +# 2006-12-02 fl PIL release 1.1.6 +# +# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. +# Copyright (c) 1995-2006 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +VERSION = "1.1.6" + +try: + import warnings +except ImportError: + warnings = None + +class _imaging_not_installed: + # module placeholder + def __getattr__(self, id): + raise ImportError("The _imaging C module is not installed") + +try: + # give Tk a chance to set up the environment, in case we're + # using an _imaging module linked against libtcl/libtk (use + # __import__ to hide this from naive packagers; we don't really + # depend on Tk unless ImageTk is used, and that module already + # imports Tkinter) + __import__("FixTk") +except ImportError: + pass + +try: + # If the _imaging C module is not present, you can still use + # the "open" function to identify files, but you cannot load + # them. Note that other modules should not refer to _imaging + # directly; import Image and use the Image.core variable instead. + import _imaging + core = _imaging + del _imaging +except ImportError, v: + core = _imaging_not_installed() + if str(v)[:20] == "Module use of python" and warnings: + # The _imaging C module is present, but not compiled for + # the right version (windows only). Print a warning, if + # possible. + warnings.warn( + "The _imaging extension was built for another version " + "of Python; most PIL functions will be disabled", + RuntimeWarning + ) + +import ImageMode +import ImagePalette + +import os, string, sys + +# type stuff +from types import IntType, StringType, TupleType + +try: + UnicodeStringType = type(unicode("")) + ## + # (Internal) Checks if an object is a string. If the current + # Python version supports Unicode, this checks for both 8-bit + # and Unicode strings. + def isStringType(t): + return isinstance(t, StringType) or isinstance(t, UnicodeStringType) +except NameError: + def isStringType(t): + return isinstance(t, StringType) + +## +# (Internal) Checks if an object is a tuple. + +def isTupleType(t): + return isinstance(t, TupleType) + +## +# (Internal) Checks if an object is an image object. + +def isImageType(t): + return hasattr(t, "im") + +## +# (Internal) Checks if an object is a string, and that it points to a +# directory. + +def isDirectory(f): + return isStringType(f) and os.path.isdir(f) + +from operator import isNumberType, isSequenceType + +# +# Debug level + +DEBUG = 0 + +# +# Constants (also defined in _imagingmodule.c!) + +NONE = 0 + +# transpose +FLIP_LEFT_RIGHT = 0 +FLIP_TOP_BOTTOM = 1 +ROTATE_90 = 2 +ROTATE_180 = 3 +ROTATE_270 = 4 + +# transforms +AFFINE = 0 +EXTENT = 1 +PERSPECTIVE = 2 +QUAD = 3 +MESH = 4 + +# resampling filters +NONE = 0 +NEAREST = 0 +ANTIALIAS = 1 # 3-lobed lanczos +LINEAR = BILINEAR = 2 +CUBIC = BICUBIC = 3 + +# dithers +NONE = 0 +NEAREST = 0 +ORDERED = 1 # Not yet implemented +RASTERIZE = 2 # Not yet implemented +FLOYDSTEINBERG = 3 # default + +# palettes/quantizers +WEB = 0 +ADAPTIVE = 1 + +# categories +NORMAL = 0 +SEQUENCE = 1 +CONTAINER = 2 + +# -------------------------------------------------------------------- +# Registries + +ID = [] +OPEN = {} +MIME = {} +SAVE = {} +EXTENSION = {} + +# -------------------------------------------------------------------- +# Modes supported by this version + +_MODEINFO = { + # NOTE: this table will be removed in future versions. use + # getmode* functions or ImageMode descriptors instead. + + # official modes + "1": ("L", "L", ("1",)), + "L": ("L", "L", ("L",)), + "I": ("L", "I", ("I",)), + "F": ("L", "F", ("F",)), + "P": ("RGB", "L", ("P",)), + "RGB": ("RGB", "L", ("R", "G", "B")), + "RGBX": ("RGB", "L", ("R", "G", "B", "X")), + "RGBA": ("RGB", "L", ("R", "G", "B", "A")), + "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), + "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), + + # Experimental modes include I;16, I;16B, RGBa, BGR;15, + # and BGR;24. Use these modes only if you know exactly + # what you're doing... + +} + +if sys.byteorder == 'little': + _ENDIAN = '<' +else: + _ENDIAN = '>' + +_MODE_CONV = { + # official modes + "1": ('|b1', None), + "L": ('|u1', None), + "I": ('%si4' % _ENDIAN, None), # FIXME: is this correct? + "I;16": ('%su2' % _ENDIAN, None), # FIXME: is this correct? + "F": ('%sf4' % _ENDIAN, None), # FIXME: is this correct? + "P": ('|u1', None), + "RGB": ('|u1', 3), + "RGBX": ('|u1', 4), + "RGBA": ('|u1', 4), + "CMYK": ('|u1', 4), + "YCbCr": ('|u1', 4), +} + +def _conv_type_shape(im): + shape = im.size[::-1] + typ, extra = _MODE_CONV[im.mode] + if extra is None: + return shape, typ + else: + return shape+(extra,), typ + + +MODES = _MODEINFO.keys() +MODES.sort() + +# raw modes that may be memory mapped. NOTE: if you change this, you +# may have to modify the stride calculation in map.c too! +_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16B") + +## +# Gets the "base" mode for given mode. This function returns "L" for +# images that contain grayscale data, and "RGB" for images that +# contain color data. +# +# @param mode Input mode. +# @return "L" or "RGB". +# @exception KeyError If the input mode was not a standard mode. + +def getmodebase(mode): + return ImageMode.getmode(mode).basemode + +## +# Gets the storage type mode. Given a mode, this function returns a +# single-layer mode suitable for storing individual bands. +# +# @param mode Input mode. +# @return "L", "I", or "F". +# @exception KeyError If the input mode was not a standard mode. + +def getmodetype(mode): + return ImageMode.getmode(mode).basetype + +## +# Gets a list of individual band names. Given a mode, this function +# returns a tuple containing the names of individual bands (use +# {@link #getmodetype} to get the mode used to store each individual +# band. +# +# @param mode Input mode. +# @return A tuple containing band names. The length of the tuple +# gives the number of bands in an image of the given mode. +# @exception KeyError If the input mode was not a standard mode. + +def getmodebandnames(mode): + return ImageMode.getmode(mode).bands + +## +# Gets the number of individual bands for this mode. +# +# @param mode Input mode. +# @return The number of bands in this mode. +# @exception KeyError If the input mode was not a standard mode. + +def getmodebands(mode): + return len(ImageMode.getmode(mode).bands) + +# -------------------------------------------------------------------- +# Helpers + +_initialized = 0 + +## +# Explicitly loads standard file format drivers. + +def preinit(): + "Load standard file format drivers." + + global _initialized + if _initialized >= 1: + return + + try: + import BmpImagePlugin + except ImportError: + pass + try: + import GifImagePlugin + except ImportError: + pass + try: + import JpegImagePlugin + except ImportError: + pass + try: + import PpmImagePlugin + except ImportError: + pass + try: + import PngImagePlugin + except ImportError: + pass +# try: +# import TiffImagePlugin +# except ImportError: +# pass + + _initialized = 1 + +## +# Explicitly initializes the Python Imaging Library. This function +# loads all available file format drivers. + +def init(): + "Load all file format drivers." + + global _initialized + if _initialized >= 2: + return + + visited = {} + + directories = sys.path + + try: + directories = directories + [os.path.dirname(__file__)] + except NameError: + pass + + # only check directories (including current, if present in the path) + for directory in filter(isDirectory, directories): + fullpath = os.path.abspath(directory) + if visited.has_key(fullpath): + continue + for file in os.listdir(directory): + if file[-14:] == "ImagePlugin.py": + f, e = os.path.splitext(file) + try: + sys.path.insert(0, directory) + try: + __import__(f, globals(), locals(), []) + finally: + del sys.path[0] + except ImportError: + if DEBUG: + print "Image: failed to import", + print f, ":", sys.exc_value + visited[fullpath] = None + + if OPEN or SAVE: + _initialized = 2 + + +# -------------------------------------------------------------------- +# Codec factories (used by tostring/fromstring and ImageFile.load) + +def _getdecoder(mode, decoder_name, args, extra=()): + + # tweak arguments + if args is None: + args = () + elif not isTupleType(args): + args = (args,) + + try: + # get decoder + decoder = getattr(core, decoder_name + "_decoder") + # print decoder, (mode,) + args + extra + return apply(decoder, (mode,) + args + extra) + except AttributeError: + raise IOError("decoder %s not available" % decoder_name) + +def _getencoder(mode, encoder_name, args, extra=()): + + # tweak arguments + if args is None: + args = () + elif not isTupleType(args): + args = (args,) + + try: + # get encoder + encoder = getattr(core, encoder_name + "_encoder") + # print encoder, (mode,) + args + extra + return apply(encoder, (mode,) + args + extra) + except AttributeError: + raise IOError("encoder %s not available" % encoder_name) + + +# -------------------------------------------------------------------- +# Simple expression analyzer + +class _E: + def __init__(self, data): self.data = data + def __coerce__(self, other): return self, _E(other) + def __add__(self, other): return _E((self.data, "__add__", other.data)) + def __mul__(self, other): return _E((self.data, "__mul__", other.data)) + +def _getscaleoffset(expr): + stub = ["stub"] + data = expr(_E(stub)).data + try: + (a, b, c) = data # simplified syntax + if (a is stub and b == "__mul__" and isNumberType(c)): + return c, 0.0 + if (a is stub and b == "__add__" and isNumberType(c)): + return 1.0, c + except TypeError: pass + try: + ((a, b, c), d, e) = data # full syntax + if (a is stub and b == "__mul__" and isNumberType(c) and + d == "__add__" and isNumberType(e)): + return c, e + except TypeError: pass + raise ValueError("illegal expression") + + +# -------------------------------------------------------------------- +# Implementation wrapper + +## +# This class represents an image object. To create Image objects, use +# the appropriate factory functions. There's hardly ever any reason +# to call the Image constructor directly. +# +# @see #open +# @see #new +# @see #fromstring + +class Image: + + format = None + format_description = None + + def __init__(self): + self.im = None + self.mode = "" + self.size = (0, 0) + self.palette = None + self.info = {} + self.category = NORMAL + self.readonly = 0 + + def _new(self, im): + new = Image() + new.im = im + new.mode = im.mode + new.size = im.size + new.palette = self.palette + if im.mode == "P": + new.palette = ImagePalette.ImagePalette() + try: + new.info = self.info.copy() + except AttributeError: + # fallback (pre-1.5.2) + new.info = {} + for k, v in self.info: + new.info[k] = v + return new + + _makeself = _new # compatibility + + def _copy(self): + self.load() + self.im = self.im.copy() + self.readonly = 0 + + def _dump(self, file=None, format=None): + import tempfile + if not file: + file = tempfile.mktemp() + self.load() + if not format or format == "PPM": + self.im.save_ppm(file) + else: + file = file + "." + format + self.save(file, format) + return file + + def __getattr__(self, name): + if name == "__array_interface__": + # numpy array interface support + new = {} + shape, typestr = _conv_type_shape(self) + new['shape'] = shape + new['typestr'] = typestr + new['data'] = self.tostring() + return new + raise AttributeError(name) + + ## + # Returns a string containing pixel data. + # + # @param encoder_name What encoder to use. The default is to + # use the standard "raw" encoder. + # @param *args Extra arguments to the encoder. + # @return An 8-bit string. + + def tostring(self, encoder_name="raw", *args): + "Return image as a binary string" + + # may pass tuple instead of argument list + if len(args) == 1 and isTupleType(args[0]): + args = args[0] + + if encoder_name == "raw" and args == (): + args = self.mode + + self.load() + + # unpack data + e = _getencoder(self.mode, encoder_name, args) + e.setimage(self.im) + + bufsize = max(65536, self.size[0] * 4) # see RawEncode.c + + data = [] + while 1: + l, s, d = e.encode(bufsize) + data.append(d) + if s: + break + if s < 0: + raise RuntimeError("encoder error %d in tostring" % s) + + return string.join(data, "") + + ## + # Returns the image converted to an X11 bitmap. This method + # only works for mode "1" images. + # + # @param name The name prefix to use for the bitmap variables. + # @return A string containing an X11 bitmap. + # @exception ValueError If the mode is not "1" + + def tobitmap(self, name="image"): + "Return image as an XBM bitmap" + + self.load() + if self.mode != "1": + raise ValueError("not a bitmap") + data = self.tostring("xbm") + return string.join(["#define %s_width %d\n" % (name, self.size[0]), + "#define %s_height %d\n"% (name, self.size[1]), + "static char %s_bits[] = {\n" % name, data, "};"], "") + + ## + # Loads this image with pixel data from a string. + #

+ # This method is similar to the {@link #fromstring} function, but + # loads data into this image instead of creating a new image + # object. + + def fromstring(self, data, decoder_name="raw", *args): + "Load data to image from binary string" + + # may pass tuple instead of argument list + if len(args) == 1 and isTupleType(args[0]): + args = args[0] + + # default format + if decoder_name == "raw" and args == (): + args = self.mode + + # unpack data + d = _getdecoder(self.mode, decoder_name, args) + d.setimage(self.im) + s = d.decode(data) + + if s[0] >= 0: + raise ValueError("not enough image data") + if s[1] != 0: + raise ValueError("cannot decode image data") + + ## + # Allocates storage for the image and loads the pixel data. In + # normal cases, you don't need to call this method, since the + # Image class automatically loads an opened image when it is + # accessed for the first time. + # + # @return An image access object. + + def load(self): + "Explicitly load pixel data." + if self.im and self.palette and self.palette.dirty: + # realize palette + apply(self.im.putpalette, self.palette.getdata()) + self.palette.dirty = 0 + self.palette.mode = "RGB" + self.palette.rawmode = None + if self.info.has_key("transparency"): + self.im.putpalettealpha(self.info["transparency"], 0) + self.palette.mode = "RGBA" + if self.im: + return self.im.pixel_access(self.readonly) + + ## + # Verifies the contents of a file. For data read from a file, this + # method attempts to determine if the file is broken, without + # actually decoding the image data. If this method finds any + # problems, it raises suitable exceptions. If you need to load + # the image after using this method, you must reopen the image + # file. + + def verify(self): + "Verify file contents." + pass + + + ## + # Returns a converted copy of this image. For the "P" mode, this + # method translates pixels through the palette. If mode is + # omitted, a mode is chosen so that all information in the image + # and the palette can be represented without a palette. + #

+ # The current version supports all possible conversions between + # "L", "RGB" and "CMYK." + #

+ # When translating a colour image to black and white (mode "L"), + # the library uses the ITU-R 601-2 luma transform: + #

+ # L = R * 299/1000 + G * 587/1000 + B * 114/1000 + #

+ # When translating a greyscale image into a bilevel image (mode + # "1"), all non-zero values are set to 255 (white). To use other + # thresholds, use the {@link #Image.point} method. + # + # @def convert(mode, matrix=None) + # @param mode The requested mode. + # @param matrix An optional conversion matrix. If given, this + # should be 4- or 16-tuple containing floating point values. + # @return An Image object. + + def convert(self, mode=None, data=None, dither=None, + palette=WEB, colors=256): + "Convert to other pixel format" + + if not mode: + # determine default mode + if self.mode == "P": + self.load() + if self.palette: + mode = self.palette.mode + else: + mode = "RGB" + else: + return self.copy() + + self.load() + + if data: + # matrix conversion + if mode not in ("L", "RGB"): + raise ValueError("illegal conversion") + im = self.im.convert_matrix(mode, data) + return self._new(im) + + if mode == "P" and palette == ADAPTIVE: + im = self.im.quantize(colors) + return self._new(im) + + # colourspace conversion + if dither is None: + dither = FLOYDSTEINBERG + + try: + im = self.im.convert(mode, dither) + except ValueError: + try: + # normalize source image and try again + im = self.im.convert(getmodebase(self.mode)) + im = im.convert(mode, dither) + except KeyError: + raise ValueError("illegal conversion") + + return self._new(im) + + def quantize(self, colors=256, method=0, kmeans=0, palette=None): + + # methods: + # 0 = median cut + # 1 = maximum coverage + + # NOTE: this functionality will be moved to the extended + # quantizer interface in a later version of PIL. + + self.load() + + if palette: + # use palette from reference image + palette.load() + if palette.mode != "P": + raise ValueError("bad mode for palette image") + if self.mode != "RGB" and self.mode != "L": + raise ValueError( + "only RGB or L mode images can be quantized to a palette" + ) + im = self.im.convert("P", 1, palette.im) + return self._makeself(im) + + im = self.im.quantize(colors, method, kmeans) + return self._new(im) + + ## + # Copies this image. Use this method if you wish to paste things + # into an image, but still retain the original. + # + # @return An Image object. + + def copy(self): + "Copy raster data" + + self.load() + im = self.im.copy() + return self._new(im) + + ## + # Returns a rectangular region from this image. The box is a + # 4-tuple defining the left, upper, right, and lower pixel + # coordinate. + #

+ # This is a lazy operation. Changes to the source image may or + # may not be reflected in the cropped image. To break the + # connection, call the {@link #Image.load} method on the cropped + # copy. + # + # @param The crop rectangle, as a (left, upper, right, lower)-tuple. + # @return An Image object. + + def crop(self, box=None): + "Crop region from image" + + self.load() + if box is None: + return self.copy() + + # lazy operation + return _ImageCrop(self, box) + + ## + # Configures the image file loader so it returns a version of the + # image that as closely as possible matches the given mode and + # size. For example, you can use this method to convert a colour + # JPEG to greyscale while loading it, or to extract a 128x192 + # version from a PCD file. + #

+ # Note that this method modifies the Image object in place. If + # the image has already been loaded, this method has no effect. + # + # @param mode The requested mode. + # @param size The requested size. + + def draft(self, mode, size): + "Configure image decoder" + + pass + + def _expand(self, xmargin, ymargin=None): + if ymargin is None: + ymargin = xmargin + self.load() + return self._new(self.im.expand(xmargin, ymargin, 0)) + + ## + # Filters this image using the given filter. For a list of + # available filters, see the ImageFilter module. + # + # @param filter Filter kernel. + # @return An Image object. + # @see ImageFilter + + def filter(self, filter): + "Apply environment filter to image" + + self.load() + + from ImageFilter import Filter + if not isinstance(filter, Filter): + filter = filter() + + if self.im.bands == 1: + return self._new(filter.filter(self.im)) + # fix to handle multiband images since _imaging doesn't + ims = [] + for c in range(self.im.bands): + ims.append(self._new(filter.filter(self.im.getband(c)))) + return merge(self.mode, ims) + + ## + # Returns a tuple containing the name of each band in this image. + # For example, getbands on an RGB image returns ("R", "G", "B"). + # + # @return A tuple containing band names. + + def getbands(self): + "Get band names" + + return ImageMode.getmode(self.mode).bands + + ## + # Calculates the bounding box of the non-zero regions in the + # image. + # + # @return The bounding box is returned as a 4-tuple defining the + # left, upper, right, and lower pixel coordinate. If the image + # is completely empty, this method returns None. + + def getbbox(self): + "Get bounding box of actual data (non-zero pixels) in image" + + self.load() + return self.im.getbbox() + + ## + # Returns a list of colors used in this image. + # + # @param maxcolors Maximum number of colors. If this number is + # exceeded, this method returns None. The default limit is + # 256 colors. + # @return An unsorted list of (count, pixel) values. + + def getcolors(self, maxcolors=256): + "Get colors from image, up to given limit" + + self.load() + if self.mode in ("1", "L", "P"): + h = self.im.histogram() + out = [] + for i in range(256): + if h[i]: + out.append((h[i], i)) + if len(out) > maxcolors: + return None + return out + return self.im.getcolors(maxcolors) + + ## + # Returns the contents of this image as a sequence object + # containing pixel values. The sequence object is flattened, so + # that values for line one follow directly after the values of + # line zero, and so on. + #

+ # Note that the sequence object returned by this method is an + # internal PIL data type, which only supports certain sequence + # operations. To convert it to an ordinary sequence (e.g. for + # printing), use list(im.getdata()). + # + # @param band What band to return. The default is to return + # all bands. To return a single band, pass in the index + # value (e.g. 0 to get the "R" band from an "RGB" image). + # @return A sequence-like object. + + def getdata(self, band = None): + "Get image data as sequence object." + + self.load() + if band is not None: + return self.im.getband(band) + return self.im # could be abused + + ## + # Gets the the minimum and maximum pixel values for each band in + # the image. + # + # @return For a single-band image, a 2-tuple containing the + # minimum and maximum pixel value. For a multi-band image, + # a tuple containing one 2-tuple for each band. + + def getextrema(self): + "Get min/max value" + + self.load() + if self.im.bands > 1: + extrema = [] + for i in range(self.im.bands): + extrema.append(self.im.getband(i).getextrema()) + return tuple(extrema) + return self.im.getextrema() + + ## + # Returns a PyCObject that points to the internal image memory. + # + # @return A PyCObject object. + + def getim(self): + "Get PyCObject pointer to internal image memory" + + self.load() + return self.im.ptr + + + ## + # Returns the image palette as a list. + # + # @return A list of color values [r, g, b, ...], or None if the + # image has no palette. + + def getpalette(self): + "Get palette contents." + + self.load() + try: + return map(ord, self.im.getpalette()) + except ValueError: + return None # no palette + + + ## + # Returns the pixel value at a given position. + # + # @param xy The coordinate, given as (x, y). + # @return The pixel value. If the image is a multi-layer image, + # this method returns a tuple. + + def getpixel(self, xy): + "Get pixel value" + + self.load() + return self.im.getpixel(xy) + + ## + # Returns the horizontal and vertical projection. + # + # @return Two sequences, indicating where there are non-zero + # pixels along the X-axis and the Y-axis, respectively. + + def getprojection(self): + "Get projection to x and y axes" + + self.load() + x, y = self.im.getprojection() + return map(ord, x), map(ord, y) + + ## + # Returns a histogram for the image. The histogram is returned as + # a list of pixel counts, one for each pixel value in the source + # image. If the image has more than one band, the histograms for + # all bands are concatenated (for example, the histogram for an + # "RGB" image contains 768 values). + #

+ # A bilevel image (mode "1") is treated as a greyscale ("L") image + # by this method. + #

+ # If a mask is provided, the method returns a histogram for those + # parts of the image where the mask image is non-zero. The mask + # image must have the same size as the image, and be either a + # bi-level image (mode "1") or a greyscale image ("L"). + # + # @def histogram(mask=None) + # @param mask An optional mask. + # @return A list containing pixel counts. + + def histogram(self, mask=None, extrema=None): + "Take histogram of image" + + self.load() + if mask: + mask.load() + return self.im.histogram((0, 0), mask.im) + if self.mode in ("I", "F"): + if extrema is None: + extrema = self.getextrema() + return self.im.histogram(extrema) + return self.im.histogram() + + ## + # (Deprecated) Returns a copy of the image where the data has been + # offset by the given distances. Data wraps around the edges. If + # yoffset is omitted, it is assumed to be equal to xoffset. + #

+ # This method is deprecated. New code should use the offset + # function in the ImageChops module. + # + # @param xoffset The horizontal distance. + # @param yoffset The vertical distance. If omitted, both + # distances are set to the same value. + # @return An Image object. + + def offset(self, xoffset, yoffset=None): + "(deprecated) Offset image in horizontal and/or vertical direction" + if warnings: + warnings.warn( + "'offset' is deprecated; use 'ImageChops.offset' instead", + DeprecationWarning, stacklevel=2 + ) + import ImageChops + return ImageChops.offset(self, xoffset, yoffset) + + ## + # Pastes another image into this image. The box argument is either + # a 2-tuple giving the upper left corner, a 4-tuple defining the + # left, upper, right, and lower pixel coordinate, or None (same as + # (0, 0)). If a 4-tuple is given, the size of the pasted image + # must match the size of the region. + #

+ # If the modes don't match, the pasted image is converted to the + # mode of this image (see the {@link #Image.convert} method for + # details). + #

+ # Instead of an image, the source can be a integer or tuple + # containing pixel values. The method then fills the region + # with the given colour. When creating RGB images, you can + # also use colour strings as supported by the ImageColor module. + #

+ # If a mask is given, this method updates only the regions + # indicated by the mask. You can use either "1", "L" or "RGBA" + # images (in the latter case, the alpha band is used as mask). + # Where the mask is 255, the given image is copied as is. Where + # the mask is 0, the current value is preserved. Intermediate + # values can be used for transparency effects. + #

+ # Note that if you paste an "RGBA" image, the alpha band is + # ignored. You can work around this by using the same image as + # both source image and mask. + # + # @param im Source image or pixel value (integer or tuple). + # @param box An optional 4-tuple giving the region to paste into. + # If a 2-tuple is used instead, it's treated as the upper left + # corner. If omitted or None, the source is pasted into the + # upper left corner. + #

+ # If an image is given as the second argument and there is no + # third, the box defaults to (0, 0), and the second argument + # is interpreted as a mask image. + # @param mask An optional mask image. + # @return An Image object. + + def paste(self, im, box=None, mask=None): + "Paste other image into region" + + if isImageType(box) and mask is None: + # abbreviated paste(im, mask) syntax + mask = box; box = None + + if box is None: + # cover all of self + box = (0, 0) + self.size + + if len(box) == 2: + # lower left corner given; get size from image or mask + if isImageType(im): + size = im.size + elif isImageType(mask): + size = mask.size + else: + # FIXME: use self.size here? + raise ValueError( + "cannot determine region size; use 4-item box" + ) + box = box + (box[0]+size[0], box[1]+size[1]) + + if isStringType(im): + import ImageColor + im = ImageColor.getcolor(im, self.mode) + + elif isImageType(im): + im.load() + if self.mode != im.mode: + if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): + # should use an adapter for this! + im = im.convert(self.mode) + im = im.im + + self.load() + if self.readonly: + self._copy() + + if mask: + mask.load() + self.im.paste(im, box, mask.im) + else: + self.im.paste(im, box) + + ## + # Maps this image through a lookup table or function. + # + # @param lut A lookup table, containing 256 values per band in the + # image. A function can be used instead, it should take a single + # argument. The function is called once for each possible pixel + # value, and the resulting table is applied to all bands of the + # image. + # @param mode Output mode (default is same as input). In the + # current version, this can only be used if the source image + # has mode "L" or "P", and the output has mode "1". + # @return An Image object. + + def point(self, lut, mode=None): + "Map image through lookup table" + + self.load() + + if not isSequenceType(lut): + # if it isn't a list, it should be a function + if self.mode in ("I", "I;16", "F"): + # check if the function can be used with point_transform + scale, offset = _getscaleoffset(lut) + return self._new(self.im.point_transform(scale, offset)) + # for other modes, convert the function to a table + lut = map(lut, range(256)) * self.im.bands + + if self.mode == "F": + # FIXME: _imaging returns a confusing error message for this case + raise ValueError("point operation not supported for this mode") + + return self._new(self.im.point(lut, mode)) + + ## + # Adds or replaces the alpha layer in this image. If the image + # does not have an alpha layer, it's converted to "LA" or "RGBA". + # The new layer must be either "L" or "1". + # + # @param im The new alpha layer. This can either be an "L" or "1" + # image having the same size as this image, or an integer or + # other color value. + + def putalpha(self, alpha): + "Set alpha layer" + + self.load() + if self.readonly: + self._copy() + + if self.mode not in ("LA", "RGBA"): + # attempt to promote self to a matching alpha mode + try: + mode = getmodebase(self.mode) + "A" + try: + self.im.setmode(mode) + except (AttributeError, ValueError): + # do things the hard way + im = self.im.convert(mode) + if im.mode not in ("LA", "RGBA"): + raise ValueError # sanity check + self.im = im + self.mode = self.im.mode + except (KeyError, ValueError): + raise ValueError("illegal image mode") + + if self.mode == "LA": + band = 1 + else: + band = 3 + + if isImageType(alpha): + # alpha layer + if alpha.mode not in ("1", "L"): + raise ValueError("illegal image mode") + alpha.load() + if alpha.mode == "1": + alpha = alpha.convert("L") + else: + # constant alpha + try: + self.im.fillband(band, alpha) + except (AttributeError, ValueError): + # do things the hard way + alpha = new("L", self.size, alpha) + else: + return + + self.im.putband(alpha.im, band) + + ## + # Copies pixel data to this image. This method copies data from a + # sequence object into the image, starting at the upper left + # corner (0, 0), and continuing until either the image or the + # sequence ends. The scale and offset values are used to adjust + # the sequence values: pixel = value*scale + offset. + # + # @param data A sequence object. + # @param scale An optional scale value. The default is 1.0. + # @param offset An optional offset value. The default is 0.0. + + def putdata(self, data, scale=1.0, offset=0.0): + "Put data from a sequence object into an image." + + self.load() + if self.readonly: + self._copy() + + self.im.putdata(data, scale, offset) + + ## + # Attaches a palette to this image. The image must be a "P" or + # "L" image, and the palette sequence must contain 768 integer + # values, where each group of three values represent the red, + # green, and blue values for the corresponding pixel + # index. Instead of an integer sequence, you can use an 8-bit + # string. + # + # @def putpalette(data) + # @param data A palette sequence (either a list or a string). + + def putpalette(self, data, rawmode="RGB"): + "Put palette data into an image." + + self.load() + if self.mode not in ("L", "P"): + raise ValueError("illegal image mode") + if not isStringType(data): + data = string.join(map(chr, data), "") + self.mode = "P" + self.palette = ImagePalette.raw(rawmode, data) + self.palette.mode = "RGB" + self.load() # install new palette + + ## + # Modifies the pixel at the given position. The colour is given as + # a single numerical value for single-band images, and a tuple for + # multi-band images. + #

+ # Note that this method is relatively slow. For more extensive + # changes, use {@link #Image.paste} or the ImageDraw module + # instead. + # + # @param xy The pixel coordinate, given as (x, y). + # @param value The pixel value. + # @see #Image.paste + # @see #Image.putdata + # @see ImageDraw + + def putpixel(self, xy, value): + "Set pixel value" + + self.load() + if self.readonly: + self._copy() + + return self.im.putpixel(xy, value) + + ## + # Returns a resized copy of this image. + # + # @def resize(size, filter=NEAREST) + # @param size The requested size in pixels, as a 2-tuple: + # (width, height). + # @param filter An optional resampling filter. This can be + # one of NEAREST (use nearest neighbour), BILINEAR + # (linear interpolation in a 2x2 environment), BICUBIC + # (cubic spline interpolation in a 4x4 environment), or + # ANTIALIAS (a high-quality downsampling filter). + # If omitted, or if the image has mode "1" or "P", it is + # set NEAREST. + # @return An Image object. + + def resize(self, size, resample=NEAREST): + "Resize image" + + if resample not in (NEAREST, BILINEAR, BICUBIC, ANTIALIAS): + raise ValueError("unknown resampling filter") + + self.load() + + if self.mode in ("1", "P"): + resample = NEAREST + + if resample == ANTIALIAS: + # requires stretch support (imToolkit & PIL 1.1.3) + try: + im = self.im.stretch(size, resample) + except AttributeError: + raise ValueError("unsupported resampling filter") + else: + im = self.im.resize(size, resample) + + return self._new(im) + + ## + # Returns a rotated copy of this image. This method returns a + # copy of this image, rotated the given number of degrees counter + # clockwise around its centre. + # + # @def rotate(angle, filter=NEAREST) + # @param angle In degrees counter clockwise. + # @param filter An optional resampling filter. This can be + # one of NEAREST (use nearest neighbour), BILINEAR + # (linear interpolation in a 2x2 environment), or BICUBIC + # (cubic spline interpolation in a 4x4 environment). + # If omitted, or if the image has mode "1" or "P", it is + # set NEAREST. + # @param expand Optional expansion flag. If true, expands the output + # image to make it large enough to hold the entire rotated image. + # If false or omitted, make the output image the same size as the + # input image. + # @return An Image object. + + def rotate(self, angle, resample=NEAREST, expand=0): + "Rotate image. Angle given as degrees counter-clockwise." + + if expand: + import math + angle = -angle * math.pi / 180 + matrix = [ + math.cos(angle), math.sin(angle), 0.0, + -math.sin(angle), math.cos(angle), 0.0 + ] + def transform(x, y, (a, b, c, d, e, f)=matrix): + return a*x + b*y + c, d*x + e*y + f + + # calculate output size + w, h = self.size + xx = [] + yy = [] + for x, y in ((0, 0), (w, 0), (w, h), (0, h)): + x, y = transform(x, y) + xx.append(x) + yy.append(y) + w = int(math.ceil(max(xx)) - math.floor(min(xx))) + h = int(math.ceil(max(yy)) - math.floor(min(yy))) + + # adjust center + x, y = transform(w / 2.0, h / 2.0) + matrix[2] = self.size[0] / 2.0 - x + matrix[5] = self.size[1] / 2.0 - y + + return self.transform((w, h), AFFINE, matrix) + + if resample not in (NEAREST, BILINEAR, BICUBIC): + raise ValueError("unknown resampling filter") + + self.load() + + if self.mode in ("1", "P"): + resample = NEAREST + + return self._new(self.im.rotate(angle, resample)) + + ## + # Saves this image under the given filename. If no format is + # specified, the format to use is determined from the filename + # extension, if possible. + #

+ # Keyword options can be used to provide additional instructions + # to the writer. If a writer doesn't recognise an option, it is + # silently ignored. The available options are described later in + # this handbook. + #

+ # You can use a file object instead of a filename. In this case, + # you must always specify the format. The file object must + # implement the seek, tell, and write + # methods, and be opened in binary mode. + # + # @def save(file, format=None, **options) + # @param file File name or file object. + # @param format Optional format override. If omitted, the + # format to use is determined from the filename extension. + # If a file object was used instead of a filename, this + # parameter should always be used. + # @param **options Extra parameters to the image writer. + # @return None + # @exception KeyError If the output format could not be determined + # from the file name. Use the format option to solve this. + # @exception IOError If the file could not be written. The file + # may have been created, and may contain partial data. + + def save(self, fp, format=None, **params): + "Save image to file or stream" + + if isStringType(fp): + filename = fp + else: + if hasattr(fp, "name") and isStringType(fp.name): + filename = fp.name + else: + filename = "" + + # may mutate self! + self.load() + + self.encoderinfo = params + self.encoderconfig = () + + preinit() + + ext = string.lower(os.path.splitext(filename)[1]) + + if not format: + try: + format = EXTENSION[ext] + except KeyError: + init() + try: + format = EXTENSION[ext] + except KeyError: + raise KeyError(ext) # unknown extension + + try: + save_handler = SAVE[string.upper(format)] + except KeyError: + init() + save_handler = SAVE[string.upper(format)] # unknown format + + if isStringType(fp): + import __builtin__ + fp = __builtin__.open(fp, "wb") + close = 1 + else: + close = 0 + + try: + save_handler(self, fp, filename) + finally: + # do what we can to clean up + if close: + fp.close() + + ## + # Seeks to the given frame in this sequence file. If you seek + # beyond the end of the sequence, the method raises an + # EOFError exception. When a sequence file is opened, the + # library automatically seeks to frame 0. + #

+ # Note that in the current version of the library, most sequence + # formats only allows you to seek to the next frame. + # + # @param frame Frame number, starting at 0. + # @exception EOFError If the call attempts to seek beyond the end + # of the sequence. + # @see #Image.tell + + def seek(self, frame): + "Seek to given frame in sequence file" + + # overridden by file handlers + if frame != 0: + raise EOFError + + ## + # Displays this image. This method is mainly intended for + # debugging purposes. + #

+ # On Unix platforms, this method saves the image to a temporary + # PPM file, and calls the xv utility. + #

+ # On Windows, it saves the image to a temporary BMP file, and uses + # the standard BMP display utility to show it (usually Paint). + # + # @def show(title=None) + # @param title Optional title to use for the image window, + # where possible. + + def show(self, title=None, command=None): + "Display image (for debug purposes only)" + + _showxv(self, title, command) + + ## + # Split this image into individual bands. This method returns a + # tuple of individual image bands from an image. For example, + # splitting an "RGB" image creates three new images each + # containing a copy of one of the original bands (red, green, + # blue). + # + # @return A tuple containing bands. + + def split(self): + "Split image into bands" + + ims = [] + self.load() + for i in range(self.im.bands): + ims.append(self._new(self.im.getband(i))) + return tuple(ims) + + ## + # Returns the current frame number. + # + # @return Frame number, starting with 0. + # @see #Image.seek + + def tell(self): + "Return current frame number" + + return 0 + + ## + # Make this image into a thumbnail. This method modifies the + # image to contain a thumbnail version of itself, no larger than + # the given size. This method calculates an appropriate thumbnail + # size to preserve the aspect of the image, calls the {@link + # #Image.draft} method to configure the file reader (where + # applicable), and finally resizes the image. + #

+ # Note that the bilinear and bicubic filters in the current + # version of PIL are not well-suited for thumbnail generation. + # You should use ANTIALIAS unless speed is much more + # important than quality. + #

+ # Also note that this function modifies the Image object in place. + # If you need to use the full resolution image as well, apply this + # method to a {@link #Image.copy} of the original image. + # + # @param size Requested size. + # @param resample Optional resampling filter. This can be one + # of NEAREST, BILINEAR, BICUBIC, or + # ANTIALIAS (best quality). If omitted, it defaults + # to NEAREST (this will be changed to ANTIALIAS in a + # future version). + # @return None + + def thumbnail(self, size, resample=NEAREST): + "Create thumbnail representation (modifies image in place)" + + # FIXME: the default resampling filter will be changed + # to ANTIALIAS in future versions + + # preserve aspect ratio + x, y = self.size + if x > size[0]: y = max(y * size[0] / x, 1); x = size[0] + if y > size[1]: x = max(x * size[1] / y, 1); y = size[1] + size = x, y + + if size == self.size: + return + + self.draft(None, size) + + self.load() + + try: + im = self.resize(size, resample) + except ValueError: + if resample != ANTIALIAS: + raise + im = self.resize(size, NEAREST) # fallback + + self.im = im.im + self.mode = im.mode + self.size = size + + self.readonly = 0 + + # FIXME: the different tranform methods need further explanation + # instead of bloating the method docs, add a separate chapter. + + ## + # Transforms this image. This method creates a new image with the + # given size, and the same mode as the original, and copies data + # to the new image using the given transform. + #

+ # @def transform(size, method, data, resample=NEAREST) + # @param size The output size. + # @param method The transformation method. This is one of + # EXTENT (cut out a rectangular subregion), AFFINE + # (affine transform), PERSPECTIVE (perspective + # transform), QUAD (map a quadrilateral to a + # rectangle), or MESH (map a number of source quadrilaterals + # in one operation). + # @param data Extra data to the transformation method. + # @param resample Optional resampling filter. It can be one of + # NEAREST (use nearest neighbour), BILINEAR + # (linear interpolation in a 2x2 environment), or + # BICUBIC (cubic spline interpolation in a 4x4 + # environment). If omitted, or if the image has mode + # "1" or "P", it is set to NEAREST. + # @return An Image object. + + def transform(self, size, method, data=None, resample=NEAREST, fill=1): + "Transform image" + + import ImageTransform + if isinstance(method, ImageTransform.Transform): + method, data = method.getdata() + if data is None: + raise ValueError("missing method data") + im = new(self.mode, size, None) + if method == MESH: + # list of quads + for box, quad in data: + im.__transformer(box, self, QUAD, quad, resample, fill) + else: + im.__transformer((0, 0)+size, self, method, data, resample, fill) + + return im + + def __transformer(self, box, image, method, data, + resample=NEAREST, fill=1): + + # FIXME: this should be turned into a lazy operation (?) + + w = box[2]-box[0] + h = box[3]-box[1] + + if method == AFFINE: + # change argument order to match implementation + data = (data[2], data[0], data[1], + data[5], data[3], data[4]) + elif method == EXTENT: + # convert extent to an affine transform + x0, y0, x1, y1 = data + xs = float(x1 - x0) / w + ys = float(y1 - y0) / h + method = AFFINE + data = (x0 + xs/2, xs, 0, y0 + ys/2, 0, ys) + elif method == PERSPECTIVE: + # change argument order to match implementation + data = (data[2], data[0], data[1], + data[5], data[3], data[4], + data[6], data[7]) + elif method == QUAD: + # quadrilateral warp. data specifies the four corners + # given as NW, SW, SE, and NE. + nw = data[0:2]; sw = data[2:4]; se = data[4:6]; ne = data[6:8] + x0, y0 = nw; As = 1.0 / w; At = 1.0 / h + data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, + (se[0]-sw[0]-ne[0]+x0)*As*At, + y0, (ne[1]-y0)*As, (sw[1]-y0)*At, + (se[1]-sw[1]-ne[1]+y0)*As*At) + else: + raise ValueError("unknown transformation method") + + if resample not in (NEAREST, BILINEAR, BICUBIC): + raise ValueError("unknown resampling filter") + + image.load() + + self.load() + + if image.mode in ("1", "P"): + resample = NEAREST + + self.im.transform2(box, image.im, method, data, resample, fill) + + ## + # Returns a flipped or rotated copy of this image. + # + # @param method One of FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, + # ROTATE_90, ROTATE_180, or ROTATE_270. + + def transpose(self, method): + "Transpose image (flip or rotate in 90 degree steps)" + + self.load() + im = self.im.transpose(method) + return self._new(im) + +# -------------------------------------------------------------------- +# Lazy operations + +class _ImageCrop(Image): + + def __init__(self, im, box): + + Image.__init__(self) + + x0, y0, x1, y1 = box + if x1 < x0: + x1 = x0 + if y1 < y0: + y1 = y0 + + self.mode = im.mode + self.size = x1-x0, y1-y0 + + self.__crop = x0, y0, x1, y1 + + self.im = im.im + + def load(self): + + # lazy evaluation! + if self.__crop: + self.im = self.im.crop(self.__crop) + self.__crop = None + + # FIXME: future versions should optimize crop/paste + # sequences! + +# -------------------------------------------------------------------- +# Factories + +# +# Debugging + +def _wedge(): + "Create greyscale wedge (for debugging only)" + + return Image()._new(core.wedge("L")) + +## +# Creates a new image with the given mode and size. +# +# @param mode The mode to use for the new image. +# @param size A 2-tuple, containing (width, height) in pixels. +# @param color What colour to use for the image. Default is black. +# If given, this should be a single integer or floating point value +# for single-band modes, and a tuple for multi-band modes (one value +# per band). When creating RGB images, you can also use colour +# strings as supported by the ImageColor module. If the colour is +# None, the image is not initialised. +# @return An Image object. + +def new(mode, size, color=0): + "Create a new image" + + if color is None: + # don't initialize + return Image()._new(core.new(mode, size)) + + if isStringType(color): + # css3-style specifier + + import ImageColor + color = ImageColor.getcolor(color, mode) + + return Image()._new(core.fill(mode, size, color)) + +## +# Creates an image memory from pixel data in a string. +#

+# In its simplest form, this function takes three arguments +# (mode, size, and unpacked pixel data). +#

+# You can also use any pixel decoder supported by PIL. For more +# information on available decoders, see the section Writing Your Own File Decoder. +#

+# Note that this function decodes pixel data only, not entire images. +# If you have an entire image in a string, wrap it in a +# StringIO object, and use {@link #open} to load it. +# +# @param mode The image mode. +# @param size The image size. +# @param data An 8-bit string containing raw data for the given mode. +# @param decoder_name What decoder to use. +# @param *args Additional parameters for the given decoder. +# @return An Image object. + +def fromstring(mode, size, data, decoder_name="raw", *args): + "Load image from string" + + # may pass tuple instead of argument list + if len(args) == 1 and isTupleType(args[0]): + args = args[0] + + if decoder_name == "raw" and args == (): + args = mode + + im = new(mode, size) + im.fromstring(data, decoder_name, args) + return im + +## +# (New in 1.1.4) Creates an image memory from pixel data in a string +# or byte buffer. +#

+# This function is similar to {@link #fromstring}, but uses data in +# the byte buffer, where possible. This means that changes to the +# original buffer object are reflected in this image). Not all modes +# can share memory; supported modes include "L", "RGBX", "RGBA", and +# "CMYK". +#

+# Note that this function decodes pixel data only, not entire images. +# If you have an entire image file in a string, wrap it in a +# StringIO object, and use {@link #open} to load it. +#

+# In the current version, the default parameters used for the "raw" +# decoder differs from that used for {@link fromstring}. This is a +# bug, and will probably be fixed in a future release. The current +# release issues a warning if you do this; to disable the warning, +# you should provide the full set of parameters. See below for +# details. +# +# @param mode The image mode. +# @param size The image size. +# @param data An 8-bit string or other buffer object containing raw +# data for the given mode. +# @param decoder_name What decoder to use. +# @param *args Additional parameters for the given decoder. For the +# default encoder ("raw"), it's recommended that you provide the +# full set of parameters: +# frombuffer(mode, size, data, "raw", mode, 0, 1). +# @return An Image object. +# @since 1.1.4 + +def frombuffer(mode, size, data, decoder_name="raw", *args): + "Load image from string or buffer" + + # may pass tuple instead of argument list + if len(args) == 1 and isTupleType(args[0]): + args = args[0] + + if decoder_name == "raw": + if args == (): + if warnings: + warnings.warn( + "the frombuffer defaults may change in a future release; " + "for portability, change the call to read:\n" + " frombuffer(mode, size, data, 'raw', mode, 0, 1)", + RuntimeWarning, stacklevel=2 + ) + args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 + if args[0] in _MAPMODES: + im = new(mode, (1,1)) + im = im._new( + core.map_buffer(data, size, decoder_name, None, 0, args) + ) + im.readonly = 1 + return im + + return apply(fromstring, (mode, size, data, decoder_name, args)) + + +## +# (New in 1.1.6) Create an image memory from an object exporting +# the array interface (using the buffer protocol). +# +# If obj is not contiguous, then the tostring method is called +# and {@link frombuffer} is used. +# +# @param obj Object with array interface +# @param mode Mode to use (will be determined from type if None) +# @return An image memory. + +def fromarray(obj, mode=None): + arr = obj.__array_interface__ + shape = arr['shape'] + ndim = len(shape) + try: + strides = arr['strides'] + except KeyError: + strides = None + if mode is None: + typestr = arr['typestr'] + if not (typestr[0] == '|' or typestr[0] == _ENDIAN or + typestr[1:] not in ['u1', 'b1', 'i4', 'f4']): + raise TypeError("cannot handle data-type") + if typestr[0] == _ENDIAN: + typestr = typestr[1:3] + else: + typestr = typestr[:2] + if typestr == 'i4': + mode = 'I' + if typestr == 'u2': + mode = 'I;16' + elif typestr == 'f4': + mode = 'F' + elif typestr == 'b1': + mode = '1' + elif ndim == 2: + mode = 'L' + elif ndim == 3: + mode = 'RGB' + elif ndim == 4: + mode = 'RGBA' + else: + raise TypeError("Do not understand data.") + ndmax = 4 + bad_dims=0 + if mode in ['1','L','I','P','F']: + ndmax = 2 + elif mode == 'RGB': + ndmax = 3 + if ndim > ndmax: + raise ValueError("Too many dimensions.") + + size = shape[:2][::-1] + if strides is not None: + obj = obj.tostring() + + return frombuffer(mode, size, obj, "raw", mode, 0, 1) + +## +# Opens and identifies the given image file. +#

+# This is a lazy operation; this function identifies the file, but the +# actual image data is not read from the file until you try to process +# the data (or call the {@link #Image.load} method). +# +# @def open(file, mode="r") +# @param file A filename (string) or a file object. The file object +# must implement read, seek, and tell methods, +# and be opened in binary mode. +# @param mode The mode. If given, this argument must be "r". +# @return An Image object. +# @exception IOError If the file cannot be found, or the image cannot be +# opened and identified. +# @see #new + +def open(fp, mode="r"): + "Open an image file, without loading the raster data" + + if mode != "r": + raise ValueError("bad mode") + + if isStringType(fp): + import __builtin__ + filename = fp + fp = __builtin__.open(fp, "rb") + else: + filename = "" + + prefix = fp.read(16) + + preinit() + + for i in ID: + try: + factory, accept = OPEN[i] + if not accept or accept(prefix): + fp.seek(0) + return factory(fp, filename) + except (SyntaxError, IndexError, TypeError): + pass + + init() + + for i in ID: + try: + factory, accept = OPEN[i] + if not accept or accept(prefix): + fp.seek(0) + return factory(fp, filename) + except (SyntaxError, IndexError, TypeError): + pass + + raise IOError("cannot identify image file") + +# +# Image processing. + +## +# Creates a new image by interpolating between two input images, using +# a constant alpha. +# +#

+#    out = image1 * (1.0 - alpha) + image2 * alpha
+# 
+# +# @param im1 The first image. +# @param im2 The second image. Must have the same mode and size as +# the first image. +# @param alpha The interpolation alpha factor. If alpha is 0.0, a +# copy of the first image is returned. If alpha is 1.0, a copy of +# the second image is returned. There are no restrictions on the +# alpha value. If necessary, the result is clipped to fit into +# the allowed output range. +# @return An Image object. + +def blend(im1, im2, alpha): + "Interpolate between images." + + im1.load() + im2.load() + return im1._new(core.blend(im1.im, im2.im, alpha)) + +## +# Creates a new image by interpolating between two input images, +# using the mask as alpha. +# +# @param image1 The first image. +# @param image2 The second image. Must have the same mode and +# size as the first image. +# @param mask A mask image. This image can can have mode +# "1", "L", or "RGBA", and must have the same size as the +# other two images. + +def composite(image1, image2, mask): + "Create composite image by blending images using a transparency mask" + + image = image2.copy() + image.paste(image1, None, mask) + return image + +## +# Applies the function (which should take one argument) to each pixel +# in the given image. If the image has more than one band, the same +# function is applied to each band. Note that the function is +# evaluated once for each possible pixel value, so you cannot use +# random components or other generators. +# +# @def eval(image, function) +# @param image The input image. +# @param function A function object, taking one integer argument. +# @return An Image object. + +def eval(image, *args): + "Evaluate image expression" + + return image.point(args[0]) + +## +# Creates a new image from a number of single-band images. +# +# @param mode The mode to use for the output image. +# @param bands A sequence containing one single-band image for +# each band in the output image. All bands must have the +# same size. +# @return An Image object. + +def merge(mode, bands): + "Merge a set of single band images into a new multiband image." + + if getmodebands(mode) != len(bands) or "*" in mode: + raise ValueError("wrong number of bands") + for im in bands[1:]: + if im.mode != getmodetype(mode): + raise ValueError("mode mismatch") + if im.size != bands[0].size: + raise ValueError("size mismatch") + im = core.new(mode, bands[0].size) + for i in range(getmodebands(mode)): + bands[i].load() + im.putband(bands[i].im, i) + return bands[0]._new(im) + +# -------------------------------------------------------------------- +# Plugin registry + +## +# Register an image file plugin. This function should not be used +# in application code. +# +# @param id An image format identifier. +# @param factory An image file factory method. +# @param accept An optional function that can be used to quickly +# reject images having another format. + +def register_open(id, factory, accept=None): + id = string.upper(id) + ID.append(id) + OPEN[id] = factory, accept + +## +# Registers an image MIME type. This function should not be used +# in application code. +# +# @param id An image format identifier. +# @param mimetype The image MIME type for this format. + +def register_mime(id, mimetype): + MIME[string.upper(id)] = mimetype + +## +# Registers an image save function. This function should not be +# used in application code. +# +# @param id An image format identifier. +# @param driver A function to save images in this format. + +def register_save(id, driver): + SAVE[string.upper(id)] = driver + +## +# Registers an image extension. This function should not be +# used in application code. +# +# @param id An image format identifier. +# @param extension An extension used for this format. + +def register_extension(id, extension): + EXTENSION[string.lower(extension)] = string.upper(id) + + +# -------------------------------------------------------------------- +# Simple display support + +def _showxv(image, title=None, command=None): + + if os.name == "nt": + format = "BMP" + elif sys.platform == "darwin": + format = "JPEG" + if not command: + command = "open -a /Applications/Preview.app" + else: + format = None + if not command: + command = "xv" + if title: + command = command + " -name \"%s\"" % title + + if image.mode == "I;16": + # @PIL88 @PIL101 + # "I;16" isn't an 'official' mode, but we still want to + # provide a simple way to show 16-bit images. + base = "L" + else: + base = getmodebase(image.mode) + if base != image.mode and image.mode != "1": + file = image.convert(base)._dump(format=format) + else: + file = image._dump(format=format) + + if os.name == "nt": + command = "start /wait %s && del /f %s" % (file, file) + elif sys.platform == "darwin": + # on darwin open returns immediately resulting in the temp + # file removal while app is opening + command = "(%s %s; sleep 20; rm -f %s)&" % (command, file, file) + else: + command = "(%s %s; rm -f %s)&" % (command, file, file) + + os.system(command) diff --git a/PIL_Fix/README b/PIL_Fix/README new file mode 100644 index 00000000..3711e113 --- /dev/null +++ b/PIL_Fix/README @@ -0,0 +1,11 @@ +The file Image.py is a drop-in replacement for the same file in PIL 1.1.6. +It adds support for reading 16-bit TIFF files and converting then to numpy arrays. +(I submitted the changes to the PIL folks long ago, but to my knowledge the code +is not being used by them.) + +To use, copy this file into + /usr/lib/python2.6/dist-packages/PIL/ +or + C:\Python26\lib\site-packages\PIL\ + +..or wherever your system keeps its python modules. diff --git a/PlotItem.py b/PlotItem.py new file mode 100644 index 00000000..4cc0c33e --- /dev/null +++ b/PlotItem.py @@ -0,0 +1,957 @@ +# -*- coding: utf-8 -*- +""" +PlotItem.py - Graphics item implementing a scalable ViewBox with plotting powers. +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +This class is one of the workhorses of pyqtgraph. It implements a graphics item with +plots, labels, and scales which can be viewed inside a QGraphicsScene. If you want +a widget that can be added to your GUI, see PlotWidget instead. + +This class is very heavily featured: + - Automatically creates and manages PlotCurveItems + - Fast display and update of plots + - Manages zoom/pan ViewBox, scale, and label elements + - Automatic scaling when data changes + - Control panel with a huge feature set including averaging, decimation, + display, power spectrum, svg/png export, plot linking, and more. +""" + +from graphicsItems import * +from plotConfigTemplate import * +from PyQt4 import QtGui, QtCore, QtSvg +import weakref + +try: + from WidgetGroup import * + HAVE_WIDGETGROUP = True +except: + HAVE_WIDGETGROUP = False + +try: + from metaarray import * + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + + + + +class PlotItem(QtGui.QGraphicsWidget): + """Plot graphics item that can be added to any graphics scene. Implements axis titles, scales, interactive viewbox.""" + lastFileDir = None + managers = {} + + def __init__(self, parent=None): + QtGui.QGraphicsWidget.__init__(self, parent) + + ## Set up control buttons + + self.ctrlBtn = QtGui.QToolButton() + self.ctrlBtn.setText('?') + self.autoBtn = QtGui.QToolButton() + self.autoBtn.setText('A') + self.autoBtn.hide() + + for b in [self.ctrlBtn, self.autoBtn]: + proxy = QtGui.QGraphicsProxyWidget(self) + proxy.setWidget(b) + 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) + + + self.layout = QtGui.QGraphicsGridLayout() + self.layout.setContentsMargins(1,1,1,1) + self.setLayout(self.layout) + self.layout.setHorizontalSpacing(0) + self.layout.setVerticalSpacing(0) + + self.vb = ViewBox() + QtCore.QObject.connect(self.vb, QtCore.SIGNAL('xRangeChanged'), self.xRangeChanged) + QtCore.QObject.connect(self.vb, QtCore.SIGNAL('yRangeChanged'), self.yRangeChanged) + QtCore.QObject.connect(self.vb, QtCore.SIGNAL('rangeChangedManually'), self.enableManualScale) + + QtCore.QObject.connect(self.vb, QtCore.SIGNAL('viewChanged'), self.viewChanged) + + self.layout.addItem(self.vb, 2, 1) + self.alpha = 1.0 + self.autoAlpha = True + self.spectrumMode = False + + self.autoScale = [True, True] + + ## Create and place scale items + self.scales = { + 'top': {'item': ScaleItem(orientation='top', linkView=self.vb), 'pos': (1, 1)}, + 'bottom': {'item': ScaleItem(orientation='bottom', linkView=self.vb), 'pos': (3, 1)}, + 'left': {'item': ScaleItem(orientation='left', linkView=self.vb), 'pos': (2, 0)}, + 'right': {'item': ScaleItem(orientation='right', linkView=self.vb), 'pos': (2, 2)} + } + for k in self.scales: + self.layout.addItem(self.scales[k]['item'], *self.scales[k]['pos']) + + ## Create and place label items + #self.labels = { + #'title': {'item': LabelItem('title', size='11pt'), 'pos': (0, 2), 'text': ''}, + #'top': {'item': LabelItem('top'), 'pos': (1, 2), 'text': '', 'units': '', 'unitPrefix': ''}, + #'bottom': {'item': LabelItem('bottom'), 'pos': (5, 2), 'text': '', 'units': '', 'unitPrefix': ''}, + #'left': {'item': LabelItem('left'), 'pos': (3, 0), 'text': '', 'units': '', 'unitPrefix': ''}, + #'right': {'item': LabelItem('right'), 'pos': (3, 4), 'text': '', 'units': '', 'unitPrefix': ''} + #} + #self.labels['left']['item'].setAngle(-90) + #self.labels['right']['item'].setAngle(-90) + #for k in self.labels: + #self.layout.addItem(self.labels[k]['item'], *self.labels[k]['pos']) + self.titleLabel = LabelItem('', size='11pt') + self.layout.addItem(self.titleLabel, 0, 1) + self.setTitle(None) ## hide + + + for i in range(4): + self.layout.setRowPreferredHeight(i, 0) + self.layout.setRowMinimumHeight(i, 0) + self.layout.setRowSpacing(i, 0) + self.layout.setRowStretchFactor(i, 1) + + for i in range(3): + self.layout.setColumnPreferredWidth(i, 0) + self.layout.setColumnMinimumWidth(i, 0) + self.layout.setColumnSpacing(i, 0) + self.layout.setColumnStretchFactor(i, 1) + self.layout.setRowStretchFactor(2, 100) + self.layout.setColumnStretchFactor(1, 100) + + + ## Wrap a few methods from viewBox + for m in ['setXRange', 'setYRange', 'setRange', 'autoRange', 'viewRect']: + setattr(self, m, getattr(self.vb, m)) + + self.items = [] + self.curves = [] + self.paramList = {} + self.avgCurves = {} + + ### Set up context menu + + w = QtGui.QWidget() + self.ctrl = c = Ui_Form() + c.setupUi(w) + dv = QtGui.QDoubleValidator(self) + self.ctrlMenu = QtGui.QMenu() + ac = QtGui.QWidgetAction(self) + ac.setDefaultWidget(w) + self.ctrlMenu.addAction(ac) + + if HAVE_WIDGETGROUP: + self.stateGroup = WidgetGroup(self.ctrlMenu) + + self.fileDialog = None + + self.xLinkPlot = None + self.yLinkPlot = None + self.linksBlocked = False + + + #self.ctrlBtn.setFixedWidth(60) + self.setAcceptHoverEvents(True) + + ## Connect control widgets + QtCore.QObject.connect(c.xMinText, QtCore.SIGNAL('editingFinished()'), self.setManualXScale) + QtCore.QObject.connect(c.xMaxText, QtCore.SIGNAL('editingFinished()'), self.setManualXScale) + QtCore.QObject.connect(c.yMinText, QtCore.SIGNAL('editingFinished()'), self.setManualYScale) + QtCore.QObject.connect(c.yMaxText, QtCore.SIGNAL('editingFinished()'), self.setManualYScale) + + QtCore.QObject.connect(c.xManualRadio, QtCore.SIGNAL('clicked()'), self.updateXScale) + QtCore.QObject.connect(c.yManualRadio, QtCore.SIGNAL('clicked()'), self.updateYScale) + + QtCore.QObject.connect(c.xAutoRadio, QtCore.SIGNAL('clicked()'), self.updateXScale) + QtCore.QObject.connect(c.yAutoRadio, QtCore.SIGNAL('clicked()'), self.updateYScale) + + QtCore.QObject.connect(c.xAutoPercentSpin, QtCore.SIGNAL('valueChanged(int)'), self.replot) + QtCore.QObject.connect(c.yAutoPercentSpin, QtCore.SIGNAL('valueChanged(int)'), self.replot) + + #QtCore.QObject.connect(c.xLogCheck, QtCore.SIGNAL('toggled(bool)'), self.setXLog) + #QtCore.QObject.connect(c.yLogCheck, QtCore.SIGNAL('toggled(bool)'), self.setYLog) + + QtCore.QObject.connect(c.alphaGroup, QtCore.SIGNAL('toggled(bool)'), self.updateAlpha) + QtCore.QObject.connect(c.alphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateAlpha) + QtCore.QObject.connect(c.autoAlphaCheck, QtCore.SIGNAL('toggled(bool)'), self.updateAlpha) + + QtCore.QObject.connect(c.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.gridGroup, QtCore.SIGNAL('toggled(bool)'), self.updateGrid) + #QtCore.QObject.connect(c.gridAlphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateGrid) + + QtCore.QObject.connect(self.ctrl.xLinkCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.xLinkComboChanged) + QtCore.QObject.connect(self.ctrl.yLinkCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.yLinkComboChanged) + + QtCore.QObject.connect(self.ctrl.avgParamList, QtCore.SIGNAL('itemClicked(QListWidgetItem*)'), self.avgParamListClicked) + QtCore.QObject.connect(self.ctrl.averageGroup, QtCore.SIGNAL('toggled(bool)'), self.avgToggled) + + #QtCore.QObject.connect(self.ctrl.pointsGroup, QtCore.SIGNAL('toggled(bool)'), self.updatePointMode) + #QtCore.QObject.connect(self.ctrl.autoPointsCheck, QtCore.SIGNAL('toggled(bool)'), self.updatePointMode) + + QtCore.QObject.connect(self.ctrl.maxTracesCheck, QtCore.SIGNAL('toggled(bool)'), self.updateDecimation) + QtCore.QObject.connect(self.ctrl.maxTracesSpin, QtCore.SIGNAL('valueChanged(int)'), self.updateDecimation) + QtCore.QObject.connect(c.xMouseCheck, QtCore.SIGNAL('toggled(bool)'), self.mouseCheckChanged) + QtCore.QObject.connect(c.yMouseCheck, QtCore.SIGNAL('toggled(bool)'), self.mouseCheckChanged) + + self.xLinkPlot = None + self.yLinkPlot = None + self.linksBlocked = False + self.manager = None + + #self.showLabel('right', False) + #self.showLabel('top', False) + #self.showLabel('title', False) + #self.showLabel('left', False) + #self.showLabel('bottom', False) + self.showScale('right', False) + self.showScale('top', False) + self.showScale('left', True) + self.showScale('bottom', True) + + def __del__(self): + if self.manager is not None: + self.manager.removeWidget(self.name) + + def registerPlot(self, name): + self.name = name + win = str(self.window()) + #print "register", name, win + if win not in PlotItem.managers: + PlotItem.managers[win] = PlotWidgetManager() + self.manager = PlotItem.managers[win] + self.manager.addWidget(self, name) + QtCore.QObject.connect(self.manager, QtCore.SIGNAL('widgetListChanged'), self.updatePlotList) + self.updatePlotList() + + def updatePlotList(self): + """Update the list of all plotWidgets in the "link" combos""" + #print "update plot list", self + try: + for sc in [self.ctrl.xLinkCombo, self.ctrl.yLinkCombo]: + current = str(sc.currentText()) + sc.clear() + sc.addItem("") + if self.manager is not None: + for w in self.manager.listWidgets(): + #print w + if w == self.name: + continue + sc.addItem(w) + except: + import gc + refs= gc.get_referrers(self) + print " error during update. Referrers are:", refs + raise + + def viewGeometry(self): + """return the screen geometry of the viewbox""" + v = self.scene().views()[0] + b = self.vb.mapRectToScene(self.vb.boundingRect()) + wr = v.mapFromScene(b).boundingRect() + pos = v.pos() + wr.adjust(v.x(), v.y(), v.x(), v.y()) + return wr + + + def viewChanged(self, *args): + self.emit(QtCore.SIGNAL('viewChanged'), *args) + + def blockLink(self, b): + self.linksBlocked = b + + def xLinkComboChanged(self): + self.setXLink(str(self.ctrl.xLinkCombo.currentText())) + + 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) + 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) + self.yLinkPlot = plot + if plot is not None: + self.setManualYScale() + self.manager.linkY(self, plot) + + def linkXChanged(self, plot): + #print "update from", plot + if self.linksBlocked: + return + pr = plot.vb.viewRect() + pg = plot.viewGeometry() + if pg is None: + #print " return early" + return + sg = self.viewGeometry() + upp = float(pr.width()) / pg.width() + x1 = pr.left() + (sg.x()-pg.x()) * upp + x2 = x1 + sg.width() * upp + plot.blockLink(True) + self.setXRange(x1, x2, padding=0) + plot.blockLink(False) + self.replot() + + def linkYChanged(self, plot): + if self.linksBlocked: + return + pr = plot.vb.viewRect() + pg = plot.vb.boundingRect() + sg = self.vb.boundingRect() + upp = float(pr.height()) / pg.height() + y1 = pr.bottom() + (sg.y()-pg.y()) * upp + y2 = y1 + sg.height() * upp + plot.blockLink(True) + self.setYRange(y1, y2, padding=0) + plot.blockLink(False) + self.replot() + + def avgToggled(self, b): + if b: + self.recomputeAverages() + for k in self.avgCurves: + self.avgCurves[k][1].setVisible(b) + + def avgParamListClicked(self, item): + name = str(item.text()) + self.paramList[name] = (item.checkState() == QtCore.Qt.Checked) + self.recomputeAverages() + + def recomputeAverages(self): + for k in self.avgCurves: + self.removeItem(self.avgCurves[k][1]) + #Qwt.QwtPlotCurve.detach(self.avgCurves[k][1]) + self.avgCurves = {} + for c in self.curves: + self.addAvgCurve(c) + self.replot() + + def addAvgCurve(self, curve): + """Add a single curve into the pool of curves averaged together""" + + ### First determine the key of the curve to which this new data should be averaged + remKeys = [] + addKeys = [] + for i in range(self.ctrl.avgParamList.count()): + item = self.ctrl.avgParamList.item(i) + if item.checkState() == QtCore.Qt.Checked: + remKeys.append(str(item.text())) + else: + addKeys.append(str(item.text())) + + if len(remKeys) < 1: ## In this case, there would be 1 average plot for each data plot; not useful. + return + + p = curve.meta().copy() + for k in p: + if type(k) is tuple: + p['.'.join(k)] = p[k] + del p[k] + for rk in remKeys: + if rk in p: + del p[rk] + for ak in addKeys: + if ak not in p: + p[ak] = None + key = tuple(p.items()) + + ### Create a new curve if needed + if key not in self.avgCurves: + plot = PlotCurveItem() + plot.setPen(mkPen([0, 200, 0])) + plot.setShadowPen(mkPen([0, 0, 0, 100], 3)) + plot.setAlpha(1.0, False) + plot.setZValue(100) + self.addItem(plot) + #Qwt.QwtPlotCurve.attach(plot, self) + self.avgCurves[key] = [0, plot] + self.avgCurves[key][0] += 1 + (n, plot) = self.avgCurves[key] + + ### Average data together + (x, y) = curve.getData() + if plot.yData is not None: + newData = plot.yData * (n-1) / float(n) + y * 1.0 / float(n) + plot.setData(plot.xData, newData) + else: + plot.setData(x, y) + + + def mouseCheckChanged(self): + state = [self.ctrl.xMouseCheck.isChecked(), self.ctrl.yMouseCheck.isChecked()] + self.vb.setMouseEnabled(*state) + + def xRangeChanged(self, _, range): + self.ctrl.xMinText.setText('%0.5g' % range[0]) + self.ctrl.xMaxText.setText('%0.5g' % range[1]) + + ## automatically change unit scale + maxVal = max(abs(range[0]), abs(range[1])) + (scale, prefix) = siScale(maxVal) + #for l in ['top', 'bottom']: + #if self.getLabel(l).isVisible(): + #self.setLabel(l, unitPrefix=prefix) + #self.getScale(l).setScale(scale) + #else: + #self.setLabel(l, unitPrefix='') + #self.getScale(l).setScale(1.0) + + self.emit(QtCore.SIGNAL('xRangeChanged'), self, range) + + def yRangeChanged(self, _, range): + self.ctrl.yMinText.setText('%0.5g' % range[0]) + self.ctrl.yMaxText.setText('%0.5g' % range[1]) + + ## automatically change unit scale + maxVal = max(abs(range[0]), abs(range[1])) + (scale, prefix) = siScale(maxVal) + #for l in ['left', 'right']: + #if self.getLabel(l).isVisible(): + #self.setLabel(l, unitPrefix=prefix) + #self.getScale(l).setScale(scale) + #else: + #self.setLabel(l, unitPrefix='') + #self.getScale(l).setScale(1.0) + self.emit(QtCore.SIGNAL('yRangeChanged'), self, range) + + + def enableAutoScale(self): + self.ctrl.xAutoRadio.setChecked(True) + self.ctrl.yAutoRadio.setChecked(True) + self.autoBtn.hide() + self.replot() + + def updateXScale(self): + """Set plot to autoscale or not depending on state of radio buttons""" + if self.ctrl.xManualRadio.isChecked(): + self.setManualXScale() + else: + self.setAutoXScale() + self.replot() + + def updateYScale(self, b=False): + """Set plot to autoscale or not depending on state of radio buttons""" + if self.ctrl.yManualRadio.isChecked(): + self.setManualYScale() + else: + self.setAutoYScale() + self.replot() + + def enableManualScale(self, v=[True, True]): + if v[0]: + self.autoScale[0] = False + self.ctrl.xManualRadio.setChecked(True) + #self.setManualXScale() + if v[1]: + self.autoScale[1] = False + self.ctrl.yManualRadio.setChecked(True) + #self.setManualYScale() + self.autoBtn.show() + #self.replot() + + def setManualXScale(self): + self.autoScale[0] = False + x1 = float(self.ctrl.xMinText.text()) + x2 = float(self.ctrl.xMaxText.text()) + self.ctrl.xManualRadio.setChecked(True) + self.setXRange(x1, x2, padding=0) + self.autoBtn.show() + #self.replot() + + def setManualYScale(self): + self.autoScale[1] = False + y1 = float(self.ctrl.yMinText.text()) + y2 = float(self.ctrl.yMaxText.text()) + self.ctrl.yManualRadio.setChecked(True) + self.setYRange(y1, y2, padding=0) + self.autoBtn.show() + #self.replot() + + def setAutoXScale(self): + self.autoScale[0] = True + self.ctrl.xAutoRadio.setChecked(True) + #self.replot() + + def setAutoYScale(self): + self.autoScale[1] = True + self.ctrl.yAutoRadio.setChecked(True) + #self.replot() + + def addItem(self, item, *args): + self.items.append(item) + self.vb.addItem(item, *args) + + def removeItem(self, item): + if not item in self.items: + return + self.items.remove(item) + 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) + + def clear(self): + for i in self.items[:]: + self.removeItem(i) + self.avgCurves = {} + + def plot(self, data=None, x=None, clear=False, params=None, pen=None): + if clear: + self.clear() + if params is None: + params = {} + if HAVE_METAARRAY and isinstance(data, MetaArray): + curve = self._plotMetaArray(data, x=x) + elif isinstance(data, ndarray): + curve = self._plotArray(data, x=x) + elif isinstance(data, list): + if x is not None: + x = array(x) + curve = self._plotArray(array(data), x=x) + elif data is None: + curve = PlotCurveItem() + else: + raise Exception('Not sure how to plot object of type %s' % type(data)) + + #print data, curve + self.addCurve(curve, params) + if pen is not None: + curve.setPen(pen) + + return curve + + def addCurve(self, c, params=None): + c.setMeta(params) + self.curves.append(c) + #Qwt.QwtPlotCurve.attach(c, self) + self.addItem(c) + + ## configure curve for this plot + (alpha, auto) = self.alphaState() + c.setAlpha(alpha, auto) + c.setSpectrumMode(self.ctrl.powerSpectrumGroup.isChecked()) + c.setPointMode(self.pointMode()) + + ## Hide older plots if needed + self.updateDecimation() + + ## Add to average if needed + self.updateParamList() + if self.ctrl.averageGroup.isChecked(): + self.addAvgCurve(c) + + QtCore.QObject.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) + self.plotChanged() + + def plotChanged(self, curve=None): + ## Recompute auto range if needed + for ax in [0, 1]: + if self.autoScale[ax]: + percentScale = [self.ctrl.xAutoPercentSpin.value(), self.ctrl.yAutoPercentSpin.value()][ax] * 0.01 + mn = None + mx = None + for c in self.curves + [c[1] for c in self.avgCurves.values()]: + if not c.isVisible(): + continue + cmn, cmx = c.getRange(ax, percentScale) + if mn is None or cmn < mn: + mn = cmn + if mx is None or cmx > mx: + mx = cmx + if mn is None or mx is None: + continue + if mn == mx: + mn -= 1 + mx += 1 + self.setRange(ax, mn, mx) + + def replot(self): + self.plotChanged() + self.update() + + def updateParamList(self): + self.ctrl.avgParamList.clear() + ## Check to see that each parameter for each curve is present in the list + #print "\nUpdate param list", self + #print "paramList:", self.paramList + for c in self.curves: + #print " curve:", c + for p in c.meta().keys(): + #print " param:", p + if type(p) is tuple: + p = '.'.join(p) + + ## If the parameter is not in the list, add it. + matches = self.ctrl.avgParamList.findItems(p, QtCore.Qt.MatchExactly) + #print " matches:", matches + if len(matches) == 0: + i = QtGui.QListWidgetItem(p) + if p in self.paramList and self.paramList[p] is True: + #print " set checked" + i.setCheckState(QtCore.Qt.Checked) + else: + #print " set unchecked" + i.setCheckState(QtCore.Qt.Unchecked) + self.ctrl.avgParamList.addItem(i) + else: + i = matches[0] + + self.paramList[p] = (i.checkState() == QtCore.Qt.Checked) + #print "paramList:", self.paramList + + def writeSvg(self, fileName=None): + print "writeSvg" + if fileName is None: + fileName = QtGui.QFileDialog.getSaveFileName() + fileName = str(fileName) + self.svg = QtSvg.QSvgGenerator() + self.svg.setFileName(fileName) + self.svg.setSize(QtCore.QSize(self.size().width(), self.size().height())) + self.svg.setResolution(600) + painter = QtGui.QPainter(self.svg) + self.scene().render(painter, QtCore.QRectF(), self.mapRectToScene(self.boundingRect())) + + def writeImage(self, fileName=None): + if fileName is None: + fileName = QtGui.QFileDialog.getSaveFileName() + fileName = str(fileName) + self.png = QtGui.QImage(int(self.size().width()), int(self.size().height()), QtGui.QImage.Format_ARGB32) + painter = QtGui.QPainter(self.png) + painter.setRenderHints(painter.Antialiasing | painter.TextAntialiasing) + self.scene().render(painter, QtCore.QRectF(), self.mapRectToScene(self.boundingRect())) + painter.end() + self.png.save(fileName) + + + def saveState(self): + if not HAVE_WIDGETGROUP: + raise Exception("State save/restore requires WidgetGroup class.") + state = self.stateGroup.state() + state['paramList'] = self.paramList.copy() + #print "\nSAVE %s:\n" % str(self.name), state + #print "Saving state. averageGroup.isChecked(): %s state: %s" % (str(self.ctrl.averageGroup.isChecked()), str(state['averageGroup'])) + return state + + def restoreState(self, state): + if not HAVE_WIDGETGROUP: + raise Exception("State save/restore requires WidgetGroup class.") + if 'paramList' in state: + self.paramList = state['paramList'].copy() + self.stateGroup.setState(state) + self.updateParamList() + #print "\nRESTORE %s:\n" % str(self.name), state + #print "Restoring state. averageGroup.isChecked(): %s state: %s" % (str(self.ctrl.averageGroup.isChecked()), str(state['averageGroup'])) + #avg = self.ctrl.averageGroup.isChecked() + #if avg != state['averageGroup']: + #print " WARNING: avgGroup is %s, should be %s" % (str(avg), str(state['averageGroup'])) + + + def widgetGroupInterface(self): + return (None, PlotItem.saveState, PlotItem.restoreState) + + def updateSpectrumMode(self, b=None): + if b is None: + b = self.ctrl.powerSpectrumGroup.isChecked() + for c in self.curves: + c.setSpectrumMode(b) + self.enableAutoScale() + self.recomputeAverages() + + + def updateDecimation(self): + if self.ctrl.maxTracesCheck.isChecked(): + numCurves = self.ctrl.maxTracesSpin.value() + else: + numCurves = -1 + + curves = self.curves[:] + split = len(curves) - numCurves + for i in range(len(curves)): + if numCurves == -1 or i >= split: + curves[i].show() + else: + if self.ctrl.forgetTracesCheck.isChecked(): + curves[i].free() + self.removeItem(curves[i]) + else: + curves[i].hide() + + + def updateAlpha(self, *args): + (alpha, auto) = self.alphaState() + for c in self.curves: + c.setAlpha(alpha**2, auto) + + #self.replot(autoRange=False) + + def alphaState(self): + enabled = self.ctrl.alphaGroup.isChecked() + auto = self.ctrl.autoAlphaCheck.isChecked() + alpha = float(self.ctrl.alphaSlider.value()) / self.ctrl.alphaSlider.maximum() + if auto: + alpha = 1.0 ## should be 1/number of overlapping plots + if not enabled: + auto = False + alpha = 1.0 + return (alpha, auto) + + def pointMode(self): + if self.ctrl.pointsGroup.isChecked(): + if self.ctrl.autoPointsCheck.isChecked(): + mode = None + else: + mode = True + else: + mode = False + return mode + + + + #def mousePressEvent(self, ev): + #self.mousePos = array([ev.pos().x(), ev.pos().y()]) + #self.pressPos = self.mousePos.copy() + #QtGui.QGraphicsWidget.mousePressEvent(self, ev) + ## NOTE: we will only receive move/release events if we run ev.accept() + #print 'press' + + #def mouseReleaseEvent(self, ev): + #pos = array([ev.pos().x(), ev.pos().y()]) + #print 'release' + #if sum(abs(self.pressPos - pos)) < 3: ## Detect click + #if ev.button() == QtCore.Qt.RightButton: + #print 'popup' + #self.ctrlMenu.popup(self.mapToGlobal(ev.pos())) + #self.mousePos = pos + #QtGui.QGraphicsWidget.mouseReleaseEvent(self, ev) + + def resizeEvent(self, ev): + self.ctrlBtn.move(0, self.size().height() - self.ctrlBtn.size().height()) + self.autoBtn.move(self.ctrlBtn.width(), self.size().height() - self.autoBtn.size().height()) + + def hoverMoveEvent(self, ev): + self.mousePos = ev.pos() + self.mouseScreenPos = ev.screenPos() + + def ctrlBtnClicked(self): + #print self.mousePos + self.ctrlMenu.popup(self.mouseScreenPos) + + #def _checkLabelKey(self, key): + #if key not in self.labels: + #raise Exception("Label '%s' not found. Labels are: %s" % (key, str(self.labels.keys()))) + + def getLabel(self, key): + pass + #self._checkLabelKey(key) + #return self.labels[key]['item'] + + def _checkScaleKey(self, key): + if key not in self.scales: + raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(self.scales.keys()))) + + def getScale(self, key): + self._checkScaleKey(key) + return self.scales[key]['item'] + + def setLabel(self, key, text=None, units=None, unitPrefix=None, **args): + self.getScale(key).setLabel(text=text, units=units, unitPrefix=unitPrefix, **args) + #if text is not None: + #self.labels[key]['text'] = text + #if units != None: + #self.labels[key]['units'] = units + #if unitPrefix != None: + #self.labels[key]['unitPrefix'] = unitPrefix + + #text = self.labels[key]['text'] + #units = self.labels[key]['units'] + #unitPrefix = self.labels[key]['unitPrefix'] + + #if text is not '' or units is not '': + #l = self.getLabel(key) + #l.setText("%s (%s%s)" % (text, unitPrefix, units), **args) + #self.showLabel(key) + + + def showLabel(self, key, show=True): + self.getScale(key).showLabel(show) + #l = self.getLabel(key) + #p = self.labels[key]['pos'] + #if show: + #l.show() + #if key in ['left', 'right']: + #self.layout.setColumnFixedWidth(p[1], l.size().width()) + #l.setMaximumWidth(20) + #else: + #self.layout.setRowFixedHeight(p[0], l.size().height()) + #l.setMaximumHeight(20) + #else: + #l.hide() + #if key in ['left', 'right']: + #self.layout.setColumnFixedWidth(p[1], 0) + #l.setMaximumWidth(0) + #else: + #self.layout.setRowFixedHeight(p[0], 0) + #l.setMaximumHeight(0) + + def setTitle(self, title=None, **args): + if title is None: + self.titleLabel.setVisible(False) + self.layout.setRowFixedHeight(0, 0) + self.titleLabel.setMaximumHeight(0) + else: + self.titleLabel.setMaximumHeight(30) + self.layout.setRowFixedHeight(0, 30) + self.titleLabel.setVisible(True) + self.titleLabel.setText(title, **args) + + def showScale(self, key, show=True): + s = self.getScale(key) + p = self.scales[key]['pos'] + if show: + s.show() + #if key in ['left', 'right']: + #self.layout.setColumnFixedWidth(p[1], s.maximumWidth()) + ##s.setMaximumWidth(40) + #else: + #self.layout.setRowFixedHeight(p[0], s.maximumHeight()) + #s.setMaximumHeight(20) + else: + s.hide() + #if key in ['left', 'right']: + #self.layout.setColumnFixedWidth(p[1], 0) + ##s.setMaximumWidth(0) + #else: + #self.layout.setRowFixedHeight(p[0], 0) + #s.setMaximumHeight(0) + + def _plotArray(self, arr, x=None): + if arr.ndim != 1: + raise Exception("Array must be 1D to plot (shape is %s)" % arr.shape) + if x is None: + x = arange(arr.shape[0]) + if x.ndim != 1: + raise Exception("X array must be 1D to plot (shape is %s)" % x.shape) + c = PlotCurveItem(arr, x=x) + return c + + + + def _plotMetaArray(self, arr, x=None, autoLabel=True): + inf = arr.infoCopy() + if arr.ndim != 1: + raise Exception('can only automatically plot 1 dimensional arrays.') + ## create curve + try: + xv = arr.xvals(0) + #print 'xvals:', xv + except: + if x is None: + xv = arange(arr.shape[0]) + else: + xv = x + c = PlotCurveItem() + c.setData(x=xv, y=arr.view(ndarray)) + + if autoLabel: + name = arr._info[0].get('name', None) + units = arr._info[0].get('units', None) + self.setLabel('bottom', text=name, units=units) + + name = arr._info[1].get('name', None) + units = arr._info[1].get('units', None) + self.setLabel('left', text=name, units=units) + + return c + + def saveSvgClicked(self): + self.fileDialog = QtGui.QFileDialog() + #if PlotItem.lastFileDir is not None: + #self.fileDialog.setDirectory(PlotItem.lastFileDir) + self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + self.fileDialog.show() + QtCore.QObject.connect(self.fileDialog, QtCore.SIGNAL('fileSelected(const QString)'), self.writeSvg) + + #def svgFileSelected(self, fileName): + ##PlotWidget.lastFileDir = os.path.split(fileName)[0] + #self.writeSvg(str(fileName)) + + def saveImgClicked(self): + self.fileDialog = QtGui.QFileDialog() + #if PlotItem.lastFileDir is not None: + #self.fileDialog.setDirectory(PlotItem.lastFileDir) + self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + self.fileDialog.show() + QtCore.QObject.connect(self.fileDialog, QtCore.SIGNAL('fileSelected(const QString)'), self.writeImage) + + #def imgFileSelected(self, fileName): + ##PlotWidget.lastFileDir = os.path.split(fileName)[0] + #self.writeImage(str(fileName)) + + +class PlotWidgetManager(QtCore.QObject): + """Used for managing communication between PlotWidgets""" + def __init__(self): + QtCore.QObject.__init__(self) + self.widgets = weakref.WeakValueDictionary() # Don't keep PlotWidgets around just because they are listed here + + def addWidget(self, w, name): + self.widgets[name] = w + self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) + + def removeWidget(self, name): + if name in self.widgets: + del self.widgets[name] + self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) + + + def listWidgets(self): + return self.widgets.keys() + + def getWidget(self, name): + if name not in self.widgets: + return None + else: + return self.widgets[name] + + def linkX(self, p1, p2): + QtCore.QObject.connect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) + QtCore.QObject.connect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) + p1.linkXChanged(p2) + #p2.setManualXScale() + + def unlinkX(self, p1, p2): + QtCore.QObject.disconnect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) + QtCore.QObject.disconnect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) + + def linkY(self, p1, p2): + QtCore.QObject.connect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) + QtCore.QObject.connect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) + p1.linkYChanged(p2) + #p2.setManualYScale() + + def unlinkY(self, p1, p2): + QtCore.QObject.disconnect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) + QtCore.QObject.disconnect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) diff --git a/PlotWidget.py b/PlotWidget.py new file mode 100644 index 00000000..03f8e934 --- /dev/null +++ b/PlotWidget.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" +PlotWidget.py - Convenience class--GraphicsView widget displaying a single PlotItem +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from GraphicsView import * +from PlotItem import * +import exceptions + +class PlotWidget(GraphicsView): + """Widget implementing a graphicsView with a single PlotItem inside.""" + def __init__(self, parent=None): + GraphicsView.__init__(self, parent) + self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + self.enableMouse(False) + self.plotItem = PlotItem() + self.setCentralItem(self.plotItem) + ## Explicitly wrap methods from plotItem + for m in ['addItem', 'autoRange', 'clear']: + setattr(self, m, getattr(self.plotItem, m)) + QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) + + def __getattr__(self, attr): ## implicitly wrap methods from plotItem + if hasattr(self.plotItem, attr): + m = getattr(self.plotItem, attr) + if hasattr(m, '__call__'): + return m + raise exceptions.NameError(attr) + + def viewChanged(self, *args): + self.emit(QtCore.SIGNAL('viewChanged'), *args) + + def widgetGroupInterface(self): + return (None, PlotWidget.saveState, PlotWidget.restoreState) + + def saveState(self): + return self.plotItem.saveState() + + def restoreState(self, state): + return self.plotItem.restoreState(state) + diff --git a/Point.py b/Point.py new file mode 100644 index 00000000..1532db5a --- /dev/null +++ b/Point.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +""" +Point.py - Extension of QPointF which adds a few missing methods. +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from PyQt4 import QtCore +from math import acos + +def clip(x, mn, mx): + if x > mx: + return mx + if x < mn: + return mn + return x + +class Point(QtCore.QPointF): + """Extension of QPointF which adds a few missing methods.""" + + def __init__(self, *args): + if len(args) == 1: + if hasattr(args[0], '__getitem__'): + QtCore.QPointF.__init__(self, float(args[0][0]), float(args[0][1])) + return + elif type(args[0]) in [float, int]: + QtCore.QPointF.__init__(self, float(args[0]), float(args[0])) + return + elif len(args) == 2: + QtCore.QPointF.__init__(self, args[0], args[1]) + return + QtCore.QPointF.__init__(self, *args) + + def __getitem__(self, i): + if i == 0: + return self.x() + elif i == 1: + return self.y() + else: + raise IndexError("Point has no index %d" % i) + + def __setitem__(self, i, x): + if i == 0: + return self.setX(x) + elif i == 1: + return self.setY(x) + else: + raise IndexError("Point has no index %d" % i) + + def __radd__(self, a): + return self._math_('__radd__', a) + + def __add__(self, a): + return self._math_('__add__', a) + + def __rsub__(self, a): + return self._math_('__rsub__', a) + + def __sub__(self, a): + return self._math_('__sub__', a) + + def __rmul__(self, a): + return self._math_('__rmul__', a) + + def __mul__(self, a): + return self._math_('__mul__', a) + + def __rdiv__(self, a): + return self._math_('__rdiv__', a) + + def __div__(self, a): + return self._math_('__div__', a) + + def __rpow__(self, a): + return self._math_('__rpow__', a) + + def __pow__(self, a): + return self._math_('__pow__', a) + + def _math_(self, op, x): + #print "point math:", op + try: + return Point(getattr(QtCore.QPointF, op)(self, x)) + except: + x = Point(x) + return Point(getattr(self[0], op)(x[0]), getattr(self[1], op)(x[1])) + + def length(self): + return (self[0]**2 + self[1]**2) ** 0.5 + + def angle(self, a): + n1 = self.length() + n2 = a.length() + if n1 == 0. or n2 == 0.: + return None + ang = acos(clip(self.dot(a) / (n1 * n2), -1.0, 1.0)) + c = self.cross(a) + if c > 0: + ang *= -1. + return ang + + def dot(self, a): + a = Point(a) + return self[0]*a[0] + self[1]*a[1] + + def cross(self, a): + a = Point(a) + return self[0]*a[1] - self[1]*a[0] + + def __repr__(self): + return "Point(%f, %f)" % (self[0], self[1]) + + + def min(self): + return min(self[0], self[1]) + + def max(self): + return max(self[0], self[1]) \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/functions.py b/functions.py new file mode 100644 index 00000000..025e4b86 --- /dev/null +++ b/functions.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +""" +functions.py - Miscellaneous functions with no other home +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from PyQt4 import QtGui +from numpy import clip, floor, log + +## Copied from acq4/lib/util/functions +SI_PREFIXES = u'yzafpnµm kMGTPEZY' +def siScale(x, minVal=1e-25): + """Return the recommended scale factor and SI prefix string for x.""" + if abs(x) < minVal: + m = 0 + x = 0 + else: + m = int(clip(floor(log(abs(x))/log(1000)), -9.0, 9.0)) + if m == 0: + pref = '' + elif m < -8 or m > 8: + pref = 'e%d' % (m*3) + else: + pref = SI_PREFIXES[m+8] + p = .001**m + return (p, pref) + + + +def mkPen(color=None, hsv=None, width=1, style=None, cosmetic=True): + if color is None: + color = [255, 255, 255] + if hsv is not None: + color = hsvColor(*hsv) + else: + color = mkColor(color) + + pen = QtGui.QPen(QtGui.QBrush(color), width) + pen.setCosmetic(cosmetic) + if style is not None: + pen.setStyle(style) + return pen + +def hsvColor(h, s=1.0, v=1.0, a=1.0): + c = QtGui.QColor() + c.setHsvF(h, s, v, a) + return c + +def mkColor(*args): + """make a QColor from a variety of argument types""" + err = 'Not sure how to make a color from "%s"' % str(args) + if len(args) == 1: + if isinstance(args[0], QtGui.QColor): + return QtGui.QColor(args[0]) + elif hasattr(args[0], '__len__'): + if len(args[0]) == 3: + (r, g, b) = args[0] + a = 255 + elif len(args[0]) == 4: + (r, g, b, a) = args[0] + else: + raise Exception(err) + else: + raise Exception(err) + elif len(args) == 3: + (r, g, b) = args + a = 255 + elif len(args) == 4: + (r, g, b, a) = args + else: + raise Exception(err) + return QtGui.QColor(r, g, b, a) + +def colorStr(c): + """Generate a hex string code from a QColor""" + return ('%02x'*4) % (c.red(), c.blue(), c.green(), c.alpha()) + +def intColor(ind, colors=9, values=3, maxValue=255, minValue=150, sat=255): + """Creates a QColor from a single index. Useful for stepping through a predefined list of colors.""" + colors = int(colors) + values = int(values) + ind = int(ind) % (colors * values) + indh = ind % colors + indv = ind / colors + v = minValue + indv * ((maxValue-minValue) / (values-1)) + h = (indh * 360) / colors + + c = QtGui.QColor() + c.setHsv(h, sat, v) + return c \ No newline at end of file diff --git a/graphicsItems.py b/graphicsItems.py new file mode 100644 index 00000000..7e914ae5 --- /dev/null +++ b/graphicsItems.py @@ -0,0 +1,1481 @@ +# -*- coding: utf-8 -*- +""" +graphicsItems.py - Defines several graphics item classes for use in Qt graphics/view framework +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +Provides ImageItem, PlotCurveItem, and ViewBox, amongst others. +""" + + +from PyQt4 import QtGui, QtCore +from numpy import * +import scipy.weave as weave +from scipy.weave import converters +from scipy.fftpack import fft +#from metaarray import MetaArray +from Point import * +from functions import * +import types, sys, struct + + +class ItemGroup(QtGui.QGraphicsItem): + def boundingRect(self): + return QtCore.QRectF() + + def paint(self, *args): + pass + + def addItem(self, item): + item.setParentItem(self) + +## 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) + def emit(self, *args): + return QtCore.QObject.emit(self._qObj_, *args) + + +class ImageItem(QtGui.QGraphicsPixmapItem): + def __init__(self, image=None, copy=True, parent=None, *args): + self.qimage = QtGui.QImage() + self.pixmap = None + self.useWeave = False + self.blackLevel = None + self.whiteLevel = None + self.alpha = 1.0 + self.image = None + self.clipLevel = None + QtGui.QGraphicsPixmapItem.__init__(self, parent, *args) + #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) + if image is not None: + self.updateImage(image, copy, autoRange=True) + #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) + + def setAlpha(self, alpha): + self.alpha = alpha + self.updateImage() + + #def boundingRect(self): + #return self.pixmapItem.boundingRect() + #return QtCore.QRectF(0, 0, self.qimage.width(), self.qimage.height()) + + def width(self): + if self.pixmap is None: + return None + return self.pixmap.width() + + def height(self): + if self.pixmap is None: + return None + return self.pixmap.height() + + def setClipLevel(self, level=None): + self.clipLevel = level + + #def paint(self, p, opt, widget): + #pass + #if self.pixmap is not None: + #p.drawPixmap(0, 0, self.pixmap) + #print "paint" + + def setLevels(self, white=None, black=None): + if white is not None: + self.whiteLevel = white + if black is not None: + self.blackLevel = black + self.updateImage() + + def updateImage(self, image=None, copy=True, autoRange=False, clipMask=None, white=None, black=None): + axh = {'x': 0, 'y': 1, 'c': 2} + #print "Update image", black, white + if white is not None: + self.whiteLevel = white + if black is not None: + self.blackLevel = black + + + if image is None: + if self.image is None: + return + else: + if copy: + self.image = image.copy() + else: + self.image = image + #print " image max:", self.image.max(), "min:", self.image.min() + + # Determine scale factors + if autoRange or self.blackLevel is None: + self.blackLevel = self.image.min() + self.whiteLevel = self.image.max() + #print "Image item using", self.blackLevel, self.whiteLevel + + if self.blackLevel != self.whiteLevel: + scale = 255. / (self.whiteLevel - self.blackLevel) + else: + scale = 0. + + + ## Recolor and convert to 8 bit per channel + # Try using weave, then fall back to python + shape = self.image.shape + black = float(self.blackLevel) + try: + if not self.useWeave: + raise Exception('Skipping weave compile') + sim = ascontiguousarray(self.image) + sim.shape = sim.size + im = zeros(sim.shape, dtype=ubyte) + n = im.size + + code = """ + for( int i=0; i 255.0 ) + a = 255.0; + else if( a < 0.0 ) + a = 0.0; + im(i) = a; + } + """ + + weave.inline(code, ['sim', 'im', 'n', 'black', 'scale'], type_converters=converters.blitz, compiler = 'gcc') + sim.shape = shape + im.shape = shape + except: + if self.useWeave: + self.useWeave = False + sys.excepthook(*sys.exc_info()) + print "==============================================================================" + print "Weave compile failed, falling back to slower version. Original error is above." + self.image.shape = shape + im = ((self.image - black) * scale).clip(0.,255.).astype(ubyte) + + + try: + im1 = empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=ubyte) + except: + print im.shape, axh + raise + alpha = clip(int(255 * self.alpha), 0, 255) + # Fill image + if im.ndim == 2: + im2 = im.transpose(axh['y'], axh['x']) + im1[..., 0] = im2 + im1[..., 1] = im2 + im1[..., 2] = im2 + im1[..., 3] = alpha + elif im.ndim == 3: + im2 = im.transpose(axh['y'], axh['x'], axh['c']) + + for i in range(0, im.shape[axh['c']]): + im1[..., i] = im2[..., i] + + for i in range(im.shape[axh['c']], 3): + im1[..., i] = 0 + if im.shape[axh['c']] < 4: + im1[..., 3] = alpha + + else: + raise Exception("Image must be 2 or 3 dimensions") + #self.im1 = im1 + # Display image + + if self.clipLevel is not None or clipMask is not None: + if clipMask is not None: + mask = clipMask.transpose() + else: + mask = (self.image < self.clipLevel).transpose() + im1[..., 0][mask] *= 0.5 + im1[..., 1][mask] *= 0.5 + im1[..., 2][mask] = 255 + #print "Final image:", im1.dtype, im1.min(), im1.max(), im1.shape + self.ims = im1.tostring() ## Must be held in memory here because qImage won't do it for us :( + qimage = QtGui.QImage(self.ims, im1.shape[1], im1.shape[0], QtGui.QImage.Format_ARGB32) + self.pixmap = QtGui.QPixmap.fromImage(qimage) + ##del self.ims + self.setPixmap(self.pixmap) + self.update() + + def getPixmap(self): + return self.pixmap.copy() + + + +class PlotCurveItem(QtGui.QGraphicsWidget): + """Class representing a single plot curve.""" + def __init__(self, y=None, x=None, copy=False, pen=None, shadow=None, parent=None): + QtGui.QGraphicsWidget.__init__(self, parent) + self.free() + #self.dispPath = None + + if pen is None: + pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) + self.pen = pen + + self.shadow = shadow + if y is not None: + self.updateData(y, x, copy) + #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) + + self.metaDict = {} + self.opts = { + 'spectrumMode': False, + 'logMode': [False, False], + 'pointMode': False, + 'pointStyle': None, + 'decimation': False, + 'alphaHint': 1.0, + 'alphaMode': False + } + + #self.fps = None + + def getData(self): + if self.xData is None: + return (None, None) + if self.xDisp is None: + x = self.xData + y = self.yData + if self.opts['spectrumMode']: + f = fft(y) / len(y) + y = abs(f[1:len(f)/2]) + dt = x[-1] - x[0] + x = linspace(0, 0.5*len(x)/dt, len(y)) + if self.opts['logMode'][0]: + x = log10(x) + if self.opts['logMode'][1]: + y = log10(y) + self.xDisp = x + self.yDisp = y + #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() + #print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() + return self.xDisp, self.yDisp + + #def generateSpecData(self): + #f = fft(self.yData) / len(self.yData) + #self.ySpec = abs(f[1:len(f)/2]) + #dt = self.xData[-1] - self.xData[0] + #self.xSpec = linspace(0, 0.5*len(self.xData)/dt, len(self.ySpec)) + + def getRange(self, ax, frac=1.0): + #print "getRange", ax, frac + (x, y) = self.getData() + if x is None or len(x) == 0: + return (0, 1) + + if ax == 0: + d = x + elif ax == 1: + d = y + + if frac >= 1.0: + return (d.min(), d.max()) + elif frac <= 0.0: + raise Exception("Value for parameter 'frac' must be > 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]) + + + + + def setMeta(self, data): + self.metaData = data + + def meta(self): + return self.metaData + + def setPen(self, pen): + self.pen = pen + self.update() + + def setColor(self, color): + self.pen.setColor(color) + self.update() + + def setAlpha(self, alpha, auto): + self.opts['alphaHint'] = alpha + self.opts['alphaMode'] = auto + self.update() + + def setSpectrumMode(self, mode): + self.opts['spectrumMode'] = mode + self.xDisp = self.yDisp = None + self.path = None + self.update() + + def setLogMode(self, mode): + self.opts['logMode'] = mode + self.xDisp = self.yDisp = None + self.path = None + self.update() + + def setPointMode(self, mode): + self.opts['pointMode'] = mode + self.update() + + def setShadowPen(self, pen): + self.shadow = pen + self.update() + + def setData(self, x, y, copy=False): + """For Qwt compatibility""" + self.updateData(y, x, copy) + + def updateData(self, data, x=None, copy=False): + if isinstance(data, list): + data = array(data) + if isinstance(x, list): + x = array(x) + if not isinstance(data, ndarray) or data.ndim > 2: + raise Exception("Plot data must be 1 or 2D ndarray (data shape is %s)" % str(data.shape)) + if data.ndim == 2: ### If data is 2D array, then assume x and y values are in first two columns or rows. + if x is not None: + raise Exception("Plot data may be 2D only if no x argument is supplied.") + ax = 0 + if data.shape[0] > 2 and data.shape[1] == 2: + ax = 1 + ind = [slice(None), slice(None)] + ind[ax] = 0 + y = data[tuple(ind)] + ind[ax] = 1 + x = data[tuple(ind)] + elif data.ndim == 1: + y = data + + if x.shape != y.shape: + raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) + self.prepareGeometryChange() + if copy: + self.yData = y.copy() + else: + self.yData = y + + if copy and x is not None: + self.xData = x.copy() + else: + self.xData = x + + if x is None: + self.xData = arange(0, self.y.shape[0]) + + self.path = None + #self.specPath = None + self.xDisp = self.yDisp = None + self.update() + self.emit(QtCore.SIGNAL('plotChanged'), self) + + def generatePath(self, x, y): + path = QtGui.QPainterPath() + + ## Create all vertices in path. The method used below creates a binary format so that all + ## vertices can be read in at once. This binary format may change in future versions of Qt, + ## so the original (slower) method is left here for emergencies: + #self.path.moveTo(x[0], y[0]) + #for i in range(1, y.shape[0]): + #self.path.lineTo(x[i], y[i]) + + ## Speed this up using >> operator + ## Format is: + ## numVerts(i4) 0(i4) + ## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect + ## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex + ## ... + ## 0(i4) + ## + ## All values are big endian--pack using struct.pack('>d') or struct.pack('>i') + # + n = x.shape[0] + # create empty array, pad with extra space on either end + arr = empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')]) + # write first two integers + arr.data[12:20] = struct.pack('>ii', n, 0) + # Fill array with vertex values + arr[1:-1]['x'] = x + arr[1:-1]['y'] = y + arr[1:-1]['c'] = 1 + # write last 0 + lastInd = 20*(n+1) + arr.data[lastInd:lastInd+4] = struct.pack('>i', 0) + + # create datastream object and stream into path + buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here + ds = QtCore.QDataStream(buf) + ds >> path + + return path + + def boundingRect(self): + (x, y) = self.getData() + 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() + return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) + + def paint(self, p, opt, widget): + if self.xData is None: + return + #if self.opts['spectrumMode']: + #if self.specPath is None: + + #self.specPath = self.generatePath(*self.getData()) + #path = self.specPath + #else: + if self.path is None: + self.path = self.generatePath(*self.getData()) + path = self.path + + if self.shadow is not None: + sp = QtGui.QPen(self.shadow) + else: + sp = None + + ## Copy pens and apply alpha adjustment + cp = QtGui.QPen(self.pen) + for pen in [sp, cp]: + if pen is None: + continue + c = pen.color() + c.setAlpha(c.alpha() * self.opts['alphaHint']) + pen.setColor(c) + #pen.setCosmetic(True) + + if self.shadow is not None: + p.setPen(sp) + p.drawPath(path) + p.setPen(cp) + p.drawPath(path) + + def free(self): + self.xData = None ## raw values + self.yData = None + self.xDisp = None ## display values (after log / fft) + self.yDisp = None + self.path = None + #del self.xData, self.yData, self.xDisp, self.yDisp, self.path + + +class ROIPlotItem(PlotCurveItem): + def __init__(self, roi, data, img, axes=(0,1), xVals=None, color=None): + self.roi = roi + self.roiData = data + self.roiImg = img + self.axes = axes + self.xVals = xVals + PlotCurveItem.__init__(self, self.getRoiData(), x=self.xVals, color=color) + roi.connect(QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) + #self.roiChangedEvent() + + def getRoiData(self): + d = self.roi.getArrayRegion(self.roiData, self.roiImg, axes=self.axes) + if d is None: + return + while d.ndim > 1: + d = d.mean(axis=1) + return d + + def roiChangedEvent(self): + d = self.getRoiData() + self.updateData(d, self.xVals) + + + + +class UIGraphicsItem(QtGui.QGraphicsItem): + """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 + if bounds is None: + self._bounds = QtCore.QRectF(0, 0, 1, 1) + else: + self._bounds = bounds + self._viewRect = self._view.rect() + self._viewTransform = self.viewTransform() + self.setNewBounds() + + def viewRect(self): + """Return the viewport widget rect""" + return self._view.rect() + + def viewTransform(self): + """Returns a matrix that maps viewport coordinates onto scene coordinates""" + if self._view is None: + return QtGui.QTransform() + else: + return self._view.viewportTransform() + + def boundingRect(self): + if self._view is None: + self.bounds = self._bounds + else: + vr = self._view.rect() + tr = self.viewTransform() + if vr != self._viewRect or tr != self._viewTransform: + self.viewChangedEvent(vr, self._viewRect) + self._viewRect = vr + self._viewTransform = tr + self.setNewBounds() + #print "viewRect", self._viewRect.x(), self._viewRect.y(), self._viewRect.width(), self._viewRect.height() + #print "bounds", self.bounds.x(), self.bounds.y(), self.bounds.width(), self.bounds.height() + return self.bounds + + def setNewBounds(self): + bounds = QtCore.QRectF( + QtCore.QPointF(self._bounds.left()*self._viewRect.width(), self._bounds.top()*self._viewRect.height()), + QtCore.QPointF(self._bounds.right()*self._viewRect.width(), self._bounds.bottom()*self._viewRect.height()) + ) + bounds.adjust(0.5, 0.5, 0.5, 0.5) + self.bounds = self.viewTransform().inverted()[0].mapRect(bounds) + self.prepareGeometryChange() + + def viewChangedEvent(self, newRect, oldRect): + """Called when the view widget is resized""" + pass + + def unitRect(self): + return self.viewTransform().inverted()[0].mapRect(QtCore.QRectF(0, 0, 1, 1)) + + def paint(self, *args): + pass + + + + +class LabelItem(QtGui.QGraphicsWidget): + def __init__(self, text, parent=None, **args): + QtGui.QGraphicsWidget.__init__(self, parent) + self.item = QtGui.QGraphicsTextItem(self) + self.opts = args + if 'color' not in args: + self.opts['color'] = 'CCC' + else: + if isinstance(args['color'], QtGui.QColor): + self.opts['color'] = colorStr(args['color'])[:6] + self.sizeHint = {} + self.setText(text) + + + def setAttr(self, attr, value): + """Set default text properties. See setText() for accepted parameters.""" + self.opts[attr] = value + + def setText(self, text, **args): + """Set the text and text properties in the label. Accepts optional arguments for auto-generating + a CSS style string: + color: string (example: 'CCFF00') + size: string (example: '8pt') + bold: boolean + italic: boolean + """ + self.text = text + opts = self.opts.copy() + for k in args: + opts[k] = args[k] + + optlist = [] + if 'color' in opts: + optlist.append('color: #' + opts['color']) + if 'size' in opts: + optlist.append('font-size: ' + opts['size']) + if 'bold' in opts and opts['bold'] in [True, False]: + optlist.append('font-weight: ' + {True:'bold', False:'normal'}[opts['bold']]) + if 'italic' in opts and opts['italic'] in [True, False]: + optlist.append('font-style: ' + {True:'italic', False:'normal'}[opts['italic']]) + full = "%s" % ('; '.join(optlist), text) + #print full + self.item.setHtml(full) + self.updateMin() + + def resizeEvent(self, ev): + c1 = self.boundingRect().center() + c2 = self.item.mapToParent(self.item.boundingRect().center()) # + self.item.pos() + dif = c1 - c2 + self.item.moveBy(dif.x(), dif.y()) + #print c1, c2, dif, self.item.pos() + + def setAngle(self, angle): + self.angle = angle + self.item.resetMatrix() + self.item.rotate(angle) + self.updateMin() + + def updateMin(self): + bounds = self.item.mapRectToParent(self.item.boundingRect()) + self.setMinimumWidth(bounds.width()) + self.setMinimumHeight(bounds.height()) + #print self.text, bounds.width(), bounds.height() + + #self.sizeHint = { + #QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()), + #QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()), + #QtCore.Qt.MaximumSize: (bounds.width()*2, bounds.height()*2), + #QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this? + #} + + + #def sizeHint(self, hint, constraint): + #return self.sizeHint[hint] + + + + + +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.""" + QtGui.QGraphicsWidget.__init__(self, parent) + self.label = QtGui.QGraphicsTextItem(self) + self.orientation = orientation + if orientation not in ['left', 'right', 'top', 'bottom']: + raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") + if orientation in ['left', 'right']: + #self.setMinimumWidth(25) + #self.setSizePolicy(QtGui.QSizePolicy( + #QtGui.QSizePolicy.Minimum, + #QtGui.QSizePolicy.Expanding + #)) + self.label.rotate(-90) + #else: + #self.setMinimumHeight(50) + #self.setSizePolicy(QtGui.QSizePolicy( + #QtGui.QSizePolicy.Expanding, + #QtGui.QSizePolicy.Minimum + #)) + #self.drawLabel = False + + self.labelText = '' + self.labelUnits = '' + self.labelUnitPrefix='' + self.labelStyle = {'color': '#CCC'} + + self.textHeight = 18 + self.tickLength = 10 + self.scale = 1.0 + self.autoScale = True + + self.setRange(0, 1) + + if pen is None: + pen = QtGui.QPen(QtGui.QColor(100, 100, 100)) + self.setPen(pen) + + self.linkedView = None + if linkView is not None: + self.linkToView(linkView) + + self.showLabel(False) + + + def resizeEvent(self, ev=None): + #s = self.size() + + ## Set the position of the label + nudge = 5 + br = self.label.boundingRect() + p = QtCore.QPointF(0, 0) + if self.orientation == 'left': + p.setY(int(self.size().height()/2 + br.width()/2)) + p.setX(-nudge) + #s.setWidth(10) + elif self.orientation == 'right': + #s.setWidth(10) + p.setY(int(self.size().height()/2 + br.width()/2)) + p.setX(int(self.size().width()-br.height()+nudge)) + elif self.orientation == 'top': + #s.setHeight(10) + p.setY(-nudge) + p.setX(int(self.size().width()/2. - br.width()/2.)) + elif self.orientation == 'bottom': + p.setX(int(self.size().width()/2. - br.width()/2.)) + #s.setHeight(10) + p.setY(int(self.size().height()-br.height()+nudge)) + #self.label.resize(s) + self.label.setPos(p) + + def showLabel(self, show=True): + #self.drawLabel = show + self.label.setVisible(show) + if self.orientation in ['left', 'right']: + self.setWidth() + else: + self.setHeight() + if self.autoScale: + self.setScale() + + def setLabel(self, text=None, units=None, unitPrefix=None, **args): + if text is not None: + self.labelText = text + self.showLabel() + if units is not None: + self.labelUnits = units + self.showLabel() + if unitPrefix is not None: + self.labelUnitPrefix = unitPrefix + if len(args) > 0: + self.labelStyle = args + self.label.setHtml(self.labelString()) + self.resizeEvent() + self.update() + + def labelString(self): + if self.labelUnits == '': + if self.scale == 1.0: + units = '' + else: + units = u'(x%g)' % (1.0/self.scale) + else: + #print repr(self.labelUnitPrefix), repr(self.labelUnits) + units = u'(%s%s)' % (self.labelUnitPrefix, self.labelUnits) + + s = u'%s %s' % (self.labelText, units) + + style = ';'.join(['%s: "%s"' % (k, self.labelStyle[k]) for k in self.labelStyle]) + + return u"%s" % (style, s) + + def setHeight(self, h=None): + if h is None: + h = self.textHeight + self.tickLength + if self.label.isVisible(): + h += self.textHeight + self.setMaximumHeight(h) + self.setMinimumHeight(h) + + + def setWidth(self, w=None): + if w is None: + w = self.tickLength + 40 + if self.label.isVisible(): + w += self.textHeight + self.setMaximumWidth(w) + self.setMinimumWidth(w) + + def setPen(self, pen): + self.pen = pen + self.update() + + def setScale(self, scale=None): + if scale is None: + #if self.drawLabel: ## If there is a label, then we are free to rescale the values + if self.label.isVisible(): + d = self.range[1] - self.range[0] + #pl = 1-int(log10(d)) + #scale = 10 ** pl + (scale, prefix) = siScale(d / 2.) + if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. + scale = 1.0 + prefix = '' + self.setLabel(unitPrefix=prefix) + else: + scale = 1.0 + + + if scale != self.scale: + self.scale = scale + self.setLabel() + self.update() + + def setRange(self, mn, mx): + self.range = [mn, mx] + if self.autoScale: + self.setScale() + self.update() + + def linkToView(self, view): + if self.orientation in ['right', 'left']: + signal = QtCore.SIGNAL('yRangeChanged') + else: + signal = QtCore.SIGNAL('xRangeChanged') + + if self.linkedView is not None: + QtCore.QObject.disconnect(view, signal, self.linkedViewChanged) + self.linkedView = view + QtCore.QObject.connect(view, signal, self.linkedViewChanged) + + def linkedViewChanged(self, _, newRange): + self.setRange(*newRange) + + def boundingRect(self): + return self.mapRectFromParent(self.geometry()) + + 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) + + if self.orientation == 'left': + p.drawLine(bounds.topRight(), bounds.bottomRight()) + tickStart = bounds.right() + tickDir = -1 + axis = 0 + elif self.orientation == 'right': + p.drawLine(bounds.topLeft(), bounds.bottomLeft()) + tickStart = bounds.left() + tickDir = 1 + axis = 0 + elif self.orientation == 'top': + p.drawLine(bounds.bottomLeft(), bounds.bottomRight()) + tickStart = bounds.bottom() + tickDir = -1 + axis = 1 + elif self.orientation == 'bottom': + p.drawLine(bounds.topLeft(), bounds.topRight()) + tickStart = bounds.top() + tickDir = 1 + axis = 1 + + ## Determine optimal tick spacing + intervals = [1., 2., 5., 10., 20., 50.] + dif = abs(self.range[1] - self.range[0]) + if dif == 0.0: + return + #print "dif:", dif + pw = 10 ** (floor(log10(dif))-1) + for i in range(len(intervals)): + i1 = i + if dif / (pw*intervals[i]) < 10: + break + + + #print "range: %s dif: %f power: %f interval: %f spacing: %f" % (str(self.range), dif, pw, intervals[i1], sp) + + #print " start at %f, %d ticks" % (start, num) + + + if axis == 0: + xs = -bounds.height() / dif + else: + xs = bounds.width() / dif + + ## draw ticks and text + for i in [i1, i1+1, i1+2]: ## draw three different intervals + ## spacing for this interval + sp = pw*intervals[i] + + ## determine starting tick + start = ceil(self.range[0] / sp) * sp + + ## determine number of ticks + num = int(dif / sp) + 1 + + ## last tick value + last = start + sp * num + + ## Number of decimal places to print + maxVal = max(abs(start), abs(last)) + places = max(0, 1-int(log10(sp*self.scale))) + + ## length of tick + h = min(self.tickLength, (self.tickLength*3 / num) - 1.) + + ## alpha + a = min(255, (765. / num) - 1.) + + if axis == 0: + offset = self.range[0] * xs - bounds.height() + else: + offset = self.range[0] * xs + + for j in range(num): + v = start + sp * j + x = (v * xs) - offset + p1 = [0, 0] + p2 = [0, 0] + p1[axis] = tickStart + p2[axis] = tickStart + h*tickDir + p1[1-axis] = p2[1-axis] = x + + if p1[1-axis] > [bounds.width(), bounds.height()][1-axis]: + continue + if p1[1-axis] < 0: + continue + p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100, a))) + p.drawLine(Point(p1), Point(p2)) + if i == i1+1: + if abs(v) < .001 or abs(v) >= 10000: + vstr = "%g" % (v * self.scale) + else: + vstr = ("%%0.%df" % places) % (v * self.scale) + + textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) + height = textRect.height() + 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) + elif self.orientation == 'right': + textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStart+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) + elif self.orientation == 'bottom': + textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop + rect = QtCore.QRectF(x-100, tickStart+self.tickLength, 200, height) + + p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100))) + p.drawText(rect, textFlags, vstr) + #p.drawRect(rect) + + ## Draw label + #if self.drawLabel: + #height = self.size().height() + #width = self.size().width() + #if self.orientation == 'left': + #p.translate(0, height) + #p.rotate(-90) + #rect = QtCore.QRectF(0, 0, height, self.textHeight) + #textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop + #elif self.orientation == 'right': + #p.rotate(10) + #rect = QtCore.QRectF(0, 0, height, width) + #textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom + ##rect = QtCore.QRectF(tickStart+self.tickLength, x-(height/2), 100-self.tickLength, height) + #elif self.orientation == 'top': + #rect = QtCore.QRectF(0, 0, width, height) + #textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop + ##rect = QtCore.QRectF(x-100, tickStart-self.tickLength-height, 200, height) + #elif self.orientation == 'bottom': + #rect = QtCore.QRectF(0, 0, width, height) + #textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom + ##rect = QtCore.QRectF(x-100, tickStart+self.tickLength, 200, height) + #p.drawText(rect, textFlags, self.labelString()) + ##p.drawRect(rect) + + def show(self): + + if self.orientation in ['left', 'right']: + self.setWidth() + else: + self.setHeight() + QtGui.QGraphicsWidget.show(self) + + def hide(self): + if self.orientation in ['left', 'right']: + self.setWidth(0) + else: + self.setHeight(0) + QtGui.QGraphicsWidget.hide(self) + + + + + + + +#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.currentScale = Point(1, 1) + + self.yInverted = False + #self.invertY() + self.setZValue(-100) + #self.picture = None + self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) + + self.drawFrame = True + + self.mouseEnabled = [True, True] + + def setMouseEnabled(self, x, y): + self.mouseEnabled = [x, y] + + def addItem(self, item): + if item.zValue() < self.zValue(): + item.setZValue(self.zValue()+1) + item.setParentItem(self.childGroup) + #print "addItem:", item, item.boundingRect() + + def removeItem(self, item): + self.scene().removeItem(item) + + def resizeEvent(self, ev): + #self.setRange(self.range, padding=0) + self.updateMatrix() + + + def viewRect(self): + try: + return QtCore.QRectF(self.range[0][0], self.range[1][0], self.range[0][1]-self.range[0][0], self.range[1][1] - self.range[1][0]) + except: + print "make qrectf failed:", self.range + raise + + def updateMatrix(self): + #print "udpateMatrix:" + #print " range:", self.range + vr = self.viewRect() + translate = Point(vr.center()) + bounds = self.boundingRect() + #print " bounds:", bounds + if vr.height() == 0 or vr.width() == 0: + return + scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height()) + #print " scale:", scale + m = QtGui.QMatrix() + + ## First center the viewport at 0 + self.childGroup.resetMatrix() + center = self.transform().inverted()[0].map(bounds.center()) + #print " transform to center:", center + if self.yInverted: + m.translate(center.x(), -center.y()) + #print " inverted; translate", center.x(), center.y() + else: + m.translate(center.x(), center.y()) + #print " not inverted; translate", center.x(), -center.y() + + ## Now scale and translate properly + if self.aspectLocked: + scale = Point(scale.min()) + if not self.yInverted: + scale = scale * Point(1, -1) + m.scale(scale[0], scale[1]) + #print " scale:", scale + st = translate + m.translate(-st[0], -st[1]) + #print " translate:", st + self.childGroup.setMatrix(m) + self.currentScale = scale + + def invertY(self, b=True): + self.yInverted = b + self.updateMatrix() + + def childTransform(self): + m = self.childGroup.transform() + m1 = QtGui.QTransform() + m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y()) + return m*m1 + + def setAspectLocked(self, s): + self.aspectLocked = s + + def viewScale(self): + pr = self.range + #print "viewScale:", self.range + xd = pr[0][1] - pr[0][0] + yd = pr[1][1] - pr[1][0] + if xd == 0 or yd == 0: + print "Warning: 0 range in view:", xd, yd + return array([1,1]) + + #cs = self.canvas().size() + cs = self.boundingRect() + scale = array([cs.width() / xd, cs.height() / yd]) + #print "view scale:", scale + return scale + + def scaleBy(self, s, center=None): + #print "scaleBy", s, center + xr, yr = self.range + if center is None: + xc = (xr[1] + xr[0]) * 0.5 + yc = (yr[1] + yr[0]) * 0.5 + else: + (xc, yc) = center + + x1 = xc + (xr[0]-xc) * s[0] + x2 = xc + (xr[1]-xc) * s[0] + y1 = yc + (yr[0]-yc) * s[1] + y2 = yc + (yr[1]-yc) * s[1] + + #print xr, xc, s, (xr[0]-xc) * s[0], (xr[1]-xc) * s[0] + #print [[x1, x2], [y1, y2]] + + + + self.setXRange(x1, x2, update=False, padding=0) + self.setYRange(y1, y2, padding=0) + #print self.range + + def translateBy(self, t, viewCoords=False): + t = t.astype(float) + #print "translate:", t, self.viewScale() + if viewCoords: ## scale from pixels + t /= self.viewScale() + xr, yr = self.range + #self.setAxisScale(self.xBottom, xr[0] + t[0], xr[1] + t[0]) + #self.setAxisScale(self.yLeft, yr[0] + t[1], yr[1] + t[1]) + #print xr, yr, t + self.setXRange(xr[0] + t[0], xr[1] + t[0], update=False, padding=0) + self.setYRange(yr[0] + t[1], yr[1] + t[1], padding=0) + #self.replot(autoRange=False) + #self.updateMatrix() + + + def mouseMoveEvent(self, ev): + pos = array([ev.pos().x(), ev.pos().y()]) + dif = pos - self.mousePos + dif *= -1 + self.mousePos = pos + + ## Ignore axes if mouse is disabled + mask = array(self.mouseEnabled, dtype=float) + + ## Scale or translate based on mouse button + if ev.buttons() & QtCore.Qt.LeftButton: + if not self.yInverted: + mask *= array([1, -1]) + tr = dif*mask + self.translateBy(tr, viewCoords=True) + self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) + elif ev.buttons() & QtCore.Qt.RightButton: + dif = ev.screenPos() - ev.lastScreenPos() + dif = array([dif.x(), dif.y()]) + dif[0] *= -1 + s = ((mask * 0.02) + 1) ** dif + #print mask, dif, s + center = Point(self.childGroup.transform().inverted()[0].map(ev.buttonDownPos(QtCore.Qt.RightButton))) + self.scaleBy(s, center) + self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) + + def mousePressEvent(self, ev): + self.mousePos = array([ev.pos().x(), ev.pos().y()]) + self.pressPos = self.mousePos.copy() + #Qwt.QwtPlot.mousePressEvent(self, ev) + + def mouseReleaseEvent(self, ev): + pos = array([ev.pos().x(), ev.pos().y()]) + #if sum(abs(self.pressPos - pos)) < 3: ## Detect click + #if ev.button() == QtCore.Qt.RightButton: + #self.ctrlMenu.popup(self.mapToGlobal(ev.pos())) + self.mousePos = pos + #Qwt.QwtPlot.mouseReleaseEvent(self, ev) + + def setRange(self, ax, min, max, padding=0.02, update=True): + if ax == 0: + self.setXRange(min, max, update=update, padding=padding) + else: + self.setYRange(min, max, update=update, padding=padding) + + 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.") + padding = (max-min) * padding + min -= padding + max += padding + if self.range[1] != [min, max]: + #self.setAxisScale(self.yLeft, min, max) + self.range[1] = [min, max] + #self.ctrl.yMinText.setText('%g' % min) + #self.ctrl.yMaxText.setText('%g' % max) + self.emit(QtCore.SIGNAL('yRangeChanged'), self, (min, max)) + self.emit(QtCore.SIGNAL('viewChanged'), self) + if update: + self.updateMatrix() + + 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." + #raise Exception("Tried to set range with 0 width.") + padding = (max-min) * padding + min -= padding + max += padding + if self.range[0] != [min, max]: + #self.setAxisScale(self.xBottom, min, max) + self.range[0] = [min, max] + #self.ctrl.xMinText.setText('%g' % min) + #self.ctrl.xMaxText.setText('%g' % max) + self.emit(QtCore.SIGNAL('xRangeChanged'), self, (min, max)) + self.emit(QtCore.SIGNAL('viewChanged'), self) + if update: + self.updateMatrix() + + def autoRange(self, padding=0.02): + br = self.childGroup.childrenBoundingRect() + #print br + #px = br.width() * padding + #py = br.height() * padding + self.setXRange(br.left(), br.right(), padding=padding, update=False) + self.setYRange(br.top(), br.bottom(), padding=padding) + + def boundingRect(self): + return QtCore.QRectF(0, 0, self.size().width(), self.size().height()) + + def paint(self, p, opt, widget): + if self.drawFrame: + bounds = self.boundingRect() + p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100))) + #p.fillRect(bounds, QtGui.QColor(0, 0, 0)) + p.drawRect(bounds) + + +class InfiniteLine(QtGui.QGraphicsItem): + def __init__(self, view, pos, angle=90, pen=None): + QtGui.QGraphicsItem.__init__(self) + self.view = view + self.p = [0, 0] + self.setAngle(angle) + self.setPos(pos) + + 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) + + def setPen(self, pen): + self.pen = pen + + def setAngle(self, angle): + self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 + self.updateLine() + + def setPos(self, pos): + if type(pos) in [list, tuple]: + self.p = pos + elif isinstance(pos, QtCore.QPointF): + self.p = [pos.x(), pos.y()] + else: + if self.angle == 90: + self.p = [pos, 0] + elif self.angle == 0: + self.p = [0, pos] + else: + raise Exception("Must specify 2D coordinate for non-orthogonal lines.") + self.updateLine() + + def updateLine(self): + vr = self.view.viewRect() + + if self.angle > 45: + m = tan((90-self.angle) * pi / 180.) + y1 = vr.bottom() + y2 = 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 + #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) + #QtGui.QGraphicsLineItem.setLine(self, x1, y1, x2, y2) + + def boundingRect(self): + #self.updateLine() + #return QtGui.QGraphicsLineItem.boundingRect(self) + #print "bounds", self.bounds + return self.bounds + + def paint(self, p, *args): + p.setPen(self.pen) + #print "paint", self.line + p.drawLine(self.line[0], self.line[1]) + + +class VTickGroup(QtGui.QGraphicsPathItem): + def __init__(self, xvals=None, yrange=None, pen=None, relative=False, view=None): + QtGui.QGraphicsPathItem.__init__(self) + if yrange is None: + yrange = [0, 1] + if xvals is None: + xvals = [] + if pen is None: + pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) + self.ticks = [] + self.xvals = [] + self.view = view + self.yrange = [0,1] + self.setPen(pen) + self.setYRange(yrange, relative) + self.setXVals(xvals) + self.valid = False + + + #def setPen(self, pen=None): + #if pen is None: + #pen = self.pen + #self.pen = pen + #for t in self.ticks: + #t.setPen(pen) + ##self.update() + + def setXVals(self, vals): + self.xvals = vals + #self.rebuildTicks() + self.valid = False + + def setYRange(self, vals, relative=False): + self.yrange = vals + self.relative = relative + if self.view is not None: + if relative: + QtCore.QObject.connect(self.view, QtCore.SIGNAL('viewChanged'), self.rebuildTicks) + else: + try: + QtCore.QObject.disconnect(self.view, QtCore.SIGNAL('viewChanged'), self.rebuildTicks) + except: + pass + #self.rebuildTicks() + self.valid = False + + 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 + + def rebuildTicks(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) + self.valid = True + + def paint(self, *args): + if not self.valid: + self.rebuildTicks() + QtGui.QGraphicsPathItem.paint(self, *args) + + +class GridItem(UIGraphicsItem): + def __init__(self, view, bounds=None, *args): + UIGraphicsItem.__init__(self, view, bounds) + #QtGui.QGraphicsItem.__init__(self, *args) + self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape) + #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) + + self.picture = None + + + def viewChangedEvent(self, newRect, oldRect): + self.picture = None + + def paint(self, p, opt, widget): + #p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100))) + #p.drawRect(self.boundingRect()) + + ## draw picture + if self.picture is None: + #print "no pic, draw.." + self.generatePicture() + p.drawPicture(0, 0, self.picture) + #print "draw" + + + def generatePicture(self): + self.picture = QtGui.QPicture() + p = QtGui.QPainter() + p.begin(self.picture) + + dt = self.viewTransform().inverted()[0] + vr = self.viewRect() + unit = self.unitRect() + dim = [vr.width(), vr.height()] + lvr = self.boundingRect() + ul = array([lvr.left(), lvr.top()]) + br = array([lvr.right(), lvr.bottom()]) + + texts = [] + + if ul[1] > br[1]: + x = ul[1] + ul[1] = br[1] + br[1] = x + + for i in range(2, -1, -1): ## Draw three different scales of grid + + dist = br-ul + nlTarget = 10.**i + d = 10. ** floor(log10(abs(dist/nlTarget))+0.5) + ul1 = floor(ul / d) * d + br1 = ceil(br / d) * d + dist = br1-ul1 + nl = (dist / d) + 0.5 + for ax in range(0,2): ## Draw grid for both axes + ppl = dim[ax] / nl[ax] + c = clip(3.*(ppl-3), 0., 30.) + linePen = QtGui.QPen(QtGui.QColor(255, 255, 255, c)) + textPen = QtGui.QPen(QtGui.QColor(255, 255, 255, c*2)) + + bx = (ax+1) % 2 + for x in range(0, int(nl[ax])): + p.setPen(linePen) + p1 = array([0.,0.]) + p2 = array([0.,0.]) + p1[ax] = ul1[ax] + x * d[ax] + p2[ax] = p1[ax] + p1[bx] = ul[bx] + p2[bx] = br[bx] + p.drawLine(QtCore.QPointF(p1[0], p1[1]), QtCore.QPointF(p2[0], p2[1])) + if i < 2: + p.setPen(textPen) + if ax == 0: + x = p1[0] + unit.width() + y = ul[1] + unit.height() * 8. + else: + x = ul[0] + unit.width()*3 + y = p1[1] + unit.height() + texts.append((QtCore.QPointF(x, y), "%g"%p1[ax])) + tr = self.viewTransform() + tr.scale(1.5, 1.5) + p.setWorldTransform(tr.inverted()[0]) + for t in texts: + x = tr.map(t[0]) + p.drawText(x, t[1]) + p.end() diff --git a/graphicsWindows.py b/graphicsWindows.py new file mode 100644 index 00000000..79faf1cc --- /dev/null +++ b/graphicsWindows.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +""" +graphicsWindows.py - Convenience classes which create a new window with PlotWidget or ImageView. +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from PyQt4 import QtCore, QtGui +from PlotWidget import * +from ImageView import * + +class PlotWindow(QtGui.QMainWindow): + def __init__(self, title=None): + QtGui.QMainWindow.__init__(self) + self.cw = PlotWidget() + self.setCentralWidget(self.cw) + for m in ['plot', 'autoRange', 'addItem', 'setLabel', 'clear']: + setattr(self, m, getattr(self.cw, m)) + if title is not None: + self.setWindowTitle(title) + self.show() + +class ImageWindow(QtGui.QMainWindow): + def __init__(self, title=None): + QtGui.QMainWindow.__init__(self) + self.cw = ImageView() + self.setCentralWidget(self.cw) + for m in ['setImage', 'autoRange', 'addItem']: + setattr(self, m, getattr(self.cw, m)) + if title is not None: + self.setWindowTitle(title) + self.show() diff --git a/license.txt b/license.txt new file mode 100644 index 00000000..3d04b87e --- /dev/null +++ b/license.txt @@ -0,0 +1,7 @@ +Copyright (c) 2010 Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/plotConfigTemplate.py b/plotConfigTemplate.py new file mode 100644 index 00000000..dd1c5e3d --- /dev/null +++ b/plotConfigTemplate.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'plotConfigTemplate.ui' +# +# Created: Tue Jan 12 14:23:16 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(210, 320) + Form.setMaximumSize(QtCore.QSize(250, 350)) + self.gridLayout_3 = QtGui.QGridLayout(Form) + self.gridLayout_3.setMargin(0) + self.gridLayout_3.setSpacing(0) + self.gridLayout_3.setObjectName("gridLayout_3") + self.tabWidget = QtGui.QTabWidget(Form) + self.tabWidget.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.tabWidget.setObjectName("tabWidget") + self.tab = QtGui.QWidget() + self.tab.setObjectName("tab") + self.verticalLayout = QtGui.QVBoxLayout(self.tab) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setMargin(0) + self.verticalLayout.setObjectName("verticalLayout") + self.groupBox = QtGui.QGroupBox(self.tab) + self.groupBox.setObjectName("groupBox") + self.gridLayout = QtGui.QGridLayout(self.groupBox) + self.gridLayout.setMargin(0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.xManualRadio = QtGui.QRadioButton(self.groupBox) + self.xManualRadio.setObjectName("xManualRadio") + self.gridLayout.addWidget(self.xManualRadio, 0, 0, 1, 1) + self.xMinText = QtGui.QLineEdit(self.groupBox) + self.xMinText.setObjectName("xMinText") + self.gridLayout.addWidget(self.xMinText, 0, 1, 1, 1) + self.xMaxText = QtGui.QLineEdit(self.groupBox) + self.xMaxText.setObjectName("xMaxText") + self.gridLayout.addWidget(self.xMaxText, 0, 2, 1, 1) + self.xAutoRadio = QtGui.QRadioButton(self.groupBox) + self.xAutoRadio.setChecked(True) + self.xAutoRadio.setObjectName("xAutoRadio") + self.gridLayout.addWidget(self.xAutoRadio, 1, 0, 1, 1) + self.xAutoPercentSpin = QtGui.QSpinBox(self.groupBox) + self.xAutoPercentSpin.setEnabled(True) + self.xAutoPercentSpin.setMinimum(1) + self.xAutoPercentSpin.setMaximum(100) + self.xAutoPercentSpin.setSingleStep(1) + self.xAutoPercentSpin.setProperty("value", QtCore.QVariant(100)) + self.xAutoPercentSpin.setObjectName("xAutoPercentSpin") + self.gridLayout.addWidget(self.xAutoPercentSpin, 1, 1, 1, 2) + self.xLinkCombo = QtGui.QComboBox(self.groupBox) + self.xLinkCombo.setObjectName("xLinkCombo") + self.gridLayout.addWidget(self.xLinkCombo, 2, 1, 1, 2) + self.xMouseCheck = QtGui.QCheckBox(self.groupBox) + self.xMouseCheck.setChecked(True) + self.xMouseCheck.setObjectName("xMouseCheck") + self.gridLayout.addWidget(self.xMouseCheck, 3, 1, 1, 1) + self.xLogCheck = QtGui.QCheckBox(self.groupBox) + self.xLogCheck.setObjectName("xLogCheck") + self.gridLayout.addWidget(self.xLogCheck, 3, 0, 1, 1) + self.label = QtGui.QLabel(self.groupBox) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 2, 0, 1, 1) + self.verticalLayout.addWidget(self.groupBox) + self.groupBox_2 = QtGui.QGroupBox(self.tab) + self.groupBox_2.setObjectName("groupBox_2") + self.gridLayout_2 = QtGui.QGridLayout(self.groupBox_2) + self.gridLayout_2.setMargin(0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.yManualRadio = QtGui.QRadioButton(self.groupBox_2) + self.yManualRadio.setObjectName("yManualRadio") + self.gridLayout_2.addWidget(self.yManualRadio, 0, 0, 1, 1) + self.yMinText = QtGui.QLineEdit(self.groupBox_2) + self.yMinText.setObjectName("yMinText") + self.gridLayout_2.addWidget(self.yMinText, 0, 1, 1, 1) + self.yMaxText = QtGui.QLineEdit(self.groupBox_2) + self.yMaxText.setObjectName("yMaxText") + self.gridLayout_2.addWidget(self.yMaxText, 0, 2, 1, 1) + self.yAutoRadio = QtGui.QRadioButton(self.groupBox_2) + self.yAutoRadio.setChecked(True) + self.yAutoRadio.setObjectName("yAutoRadio") + self.gridLayout_2.addWidget(self.yAutoRadio, 1, 0, 1, 1) + self.yAutoPercentSpin = QtGui.QSpinBox(self.groupBox_2) + self.yAutoPercentSpin.setEnabled(True) + self.yAutoPercentSpin.setMinimum(1) + self.yAutoPercentSpin.setMaximum(100) + self.yAutoPercentSpin.setSingleStep(1) + self.yAutoPercentSpin.setProperty("value", QtCore.QVariant(100)) + self.yAutoPercentSpin.setObjectName("yAutoPercentSpin") + self.gridLayout_2.addWidget(self.yAutoPercentSpin, 1, 1, 1, 2) + self.yLinkCombo = QtGui.QComboBox(self.groupBox_2) + self.yLinkCombo.setObjectName("yLinkCombo") + self.gridLayout_2.addWidget(self.yLinkCombo, 2, 1, 1, 2) + self.yMouseCheck = QtGui.QCheckBox(self.groupBox_2) + self.yMouseCheck.setChecked(True) + self.yMouseCheck.setObjectName("yMouseCheck") + self.gridLayout_2.addWidget(self.yMouseCheck, 3, 1, 1, 1) + self.yLogCheck = QtGui.QCheckBox(self.groupBox_2) + self.yLogCheck.setObjectName("yLogCheck") + self.gridLayout_2.addWidget(self.yLogCheck, 3, 0, 1, 1) + self.label_2 = QtGui.QLabel(self.groupBox_2) + self.label_2.setObjectName("label_2") + self.gridLayout_2.addWidget(self.label_2, 2, 0, 1, 1) + self.verticalLayout.addWidget(self.groupBox_2) + self.tabWidget.addTab(self.tab, "") + self.tab_2 = QtGui.QWidget() + self.tab_2.setObjectName("tab_2") + self.verticalLayout_2 = QtGui.QVBoxLayout(self.tab_2) + self.verticalLayout_2.setSpacing(0) + self.verticalLayout_2.setMargin(0) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.powerSpectrumGroup = QtGui.QGroupBox(self.tab_2) + self.powerSpectrumGroup.setCheckable(True) + self.powerSpectrumGroup.setChecked(False) + self.powerSpectrumGroup.setObjectName("powerSpectrumGroup") + self.verticalLayout_2.addWidget(self.powerSpectrumGroup) + self.decimateGroup = QtGui.QGroupBox(self.tab_2) + self.decimateGroup.setCheckable(True) + self.decimateGroup.setObjectName("decimateGroup") + self.gridLayout_4 = QtGui.QGridLayout(self.decimateGroup) + self.gridLayout_4.setMargin(0) + self.gridLayout_4.setSpacing(0) + self.gridLayout_4.setObjectName("gridLayout_4") + self.manualDecimateRadio = QtGui.QRadioButton(self.decimateGroup) + self.manualDecimateRadio.setObjectName("manualDecimateRadio") + self.gridLayout_4.addWidget(self.manualDecimateRadio, 0, 0, 1, 1) + self.decimateSpin = QtGui.QSpinBox(self.decimateGroup) + self.decimateSpin.setObjectName("decimateSpin") + self.gridLayout_4.addWidget(self.decimateSpin, 0, 1, 1, 1) + self.autoDecimateRadio = QtGui.QRadioButton(self.decimateGroup) + self.autoDecimateRadio.setChecked(True) + self.autoDecimateRadio.setObjectName("autoDecimateRadio") + self.gridLayout_4.addWidget(self.autoDecimateRadio, 1, 0, 1, 1) + self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.maxTracesCheck.setObjectName("maxTracesCheck") + self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1) + self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) + self.maxTracesSpin.setObjectName("maxTracesSpin") + self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 1, 1) + self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.forgetTracesCheck.setObjectName("forgetTracesCheck") + self.gridLayout_4.addWidget(self.forgetTracesCheck, 3, 0, 1, 2) + self.verticalLayout_2.addWidget(self.decimateGroup) + self.averageGroup = QtGui.QGroupBox(self.tab_2) + self.averageGroup.setCheckable(True) + self.averageGroup.setChecked(False) + self.averageGroup.setObjectName("averageGroup") + self.gridLayout_5 = QtGui.QGridLayout(self.averageGroup) + self.gridLayout_5.setMargin(0) + self.gridLayout_5.setSpacing(0) + self.gridLayout_5.setObjectName("gridLayout_5") + self.avgParamList = QtGui.QListWidget(self.averageGroup) + self.avgParamList.setObjectName("avgParamList") + self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1) + self.verticalLayout_2.addWidget(self.averageGroup) + self.tabWidget.addTab(self.tab_2, "") + self.tab_3 = QtGui.QWidget() + self.tab_3.setObjectName("tab_3") + self.verticalLayout_3 = QtGui.QVBoxLayout(self.tab_3) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.alphaGroup = QtGui.QGroupBox(self.tab_3) + self.alphaGroup.setCheckable(True) + self.alphaGroup.setObjectName("alphaGroup") + self.horizontalLayout = QtGui.QHBoxLayout(self.alphaGroup) + self.horizontalLayout.setObjectName("horizontalLayout") + self.autoAlphaCheck = QtGui.QCheckBox(self.alphaGroup) + self.autoAlphaCheck.setChecked(False) + self.autoAlphaCheck.setObjectName("autoAlphaCheck") + self.horizontalLayout.addWidget(self.autoAlphaCheck) + self.alphaSlider = QtGui.QSlider(self.alphaGroup) + self.alphaSlider.setMaximum(1000) + self.alphaSlider.setProperty("value", QtCore.QVariant(1000)) + self.alphaSlider.setOrientation(QtCore.Qt.Horizontal) + self.alphaSlider.setObjectName("alphaSlider") + self.horizontalLayout.addWidget(self.alphaSlider) + self.verticalLayout_3.addWidget(self.alphaGroup) + self.gridGroup = QtGui.QGroupBox(self.tab_3) + self.gridGroup.setCheckable(True) + self.gridGroup.setObjectName("gridGroup") + self.verticalLayout_4 = QtGui.QVBoxLayout(self.gridGroup) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.gridAlphaSlider = QtGui.QSlider(self.gridGroup) + self.gridAlphaSlider.setMaximum(255) + self.gridAlphaSlider.setProperty("value", QtCore.QVariant(70)) + self.gridAlphaSlider.setOrientation(QtCore.Qt.Horizontal) + self.gridAlphaSlider.setObjectName("gridAlphaSlider") + self.verticalLayout_4.addWidget(self.gridAlphaSlider) + self.verticalLayout_3.addWidget(self.gridGroup) + self.pointsGroup = QtGui.QGroupBox(self.tab_3) + self.pointsGroup.setCheckable(True) + self.pointsGroup.setObjectName("pointsGroup") + self.verticalLayout_5 = QtGui.QVBoxLayout(self.pointsGroup) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.autoPointsCheck = QtGui.QCheckBox(self.pointsGroup) + self.autoPointsCheck.setChecked(True) + self.autoPointsCheck.setObjectName("autoPointsCheck") + self.verticalLayout_5.addWidget(self.autoPointsCheck) + self.verticalLayout_3.addWidget(self.pointsGroup) + spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.verticalLayout_3.addItem(spacerItem) + self.tabWidget.addTab(self.tab_3, "") + self.tab_4 = QtGui.QWidget() + self.tab_4.setObjectName("tab_4") + self.gridLayout_7 = QtGui.QGridLayout(self.tab_4) + self.gridLayout_7.setObjectName("gridLayout_7") + spacerItem1 = QtGui.QSpacerItem(59, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_7.addItem(spacerItem1, 0, 0, 1, 1) + self.gridLayout_6 = QtGui.QGridLayout() + self.gridLayout_6.setObjectName("gridLayout_6") + self.saveSvgBtn = QtGui.QPushButton(self.tab_4) + self.saveSvgBtn.setObjectName("saveSvgBtn") + self.gridLayout_6.addWidget(self.saveSvgBtn, 0, 0, 1, 1) + self.saveImgBtn = QtGui.QPushButton(self.tab_4) + self.saveImgBtn.setObjectName("saveImgBtn") + self.gridLayout_6.addWidget(self.saveImgBtn, 1, 0, 1, 1) + self.saveMaBtn = QtGui.QPushButton(self.tab_4) + self.saveMaBtn.setObjectName("saveMaBtn") + self.gridLayout_6.addWidget(self.saveMaBtn, 2, 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) + spacerItem3 = QtGui.QSpacerItem(20, 211, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.gridLayout_7.addItem(spacerItem3, 1, 1, 1, 1) + self.tabWidget.addTab(self.tab_4, "") + self.gridLayout_3.addWidget(self.tabWidget, 0, 0, 1, 1) + + self.retranslateUi(Form) + self.tabWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.groupBox.setTitle(QtGui.QApplication.translate("Form", "X Axis", None, QtGui.QApplication.UnicodeUTF8)) + self.xManualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) + self.xMinText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) + self.xMaxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) + self.xAutoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.xAutoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) + self.xMouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse", None, QtGui.QApplication.UnicodeUTF8)) + self.xLogCheck.setText(QtGui.QApplication.translate("Form", "Log", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "Link with:", None, QtGui.QApplication.UnicodeUTF8)) + self.groupBox_2.setTitle(QtGui.QApplication.translate("Form", "Y Axis", None, QtGui.QApplication.UnicodeUTF8)) + self.yManualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) + self.yMinText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) + self.yMaxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) + self.yAutoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.yAutoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) + self.yMouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse", None, QtGui.QApplication.UnicodeUTF8)) + self.yLogCheck.setText(QtGui.QApplication.translate("Form", "Log", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("Form", "Link with:", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QtGui.QApplication.translate("Form", "Scale", None, QtGui.QApplication.UnicodeUTF8)) + self.powerSpectrumGroup.setTitle(QtGui.QApplication.translate("Form", "Power Spectrum", None, QtGui.QApplication.UnicodeUTF8)) + self.decimateGroup.setTitle(QtGui.QApplication.translate("Form", "Decimate", None, QtGui.QApplication.UnicodeUTF8)) + self.manualDecimateRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) + self.autoDecimateRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8)) + self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", None, QtGui.QApplication.UnicodeUTF8)) + self.averageGroup.setTitle(QtGui.QApplication.translate("Form", "Average", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QtGui.QApplication.translate("Form", "Data", None, QtGui.QApplication.UnicodeUTF8)) + self.alphaGroup.setTitle(QtGui.QApplication.translate("Form", "Alpha", None, QtGui.QApplication.UnicodeUTF8)) + self.autoAlphaCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.gridGroup.setTitle(QtGui.QApplication.translate("Form", "Grid", None, QtGui.QApplication.UnicodeUTF8)) + self.pointsGroup.setTitle(QtGui.QApplication.translate("Form", "Points", None, QtGui.QApplication.UnicodeUTF8)) + self.autoPointsCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), QtGui.QApplication.translate("Form", "Display", None, QtGui.QApplication.UnicodeUTF8)) + 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.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 new file mode 100644 index 00000000..9434d8d3 --- /dev/null +++ b/plotConfigTemplate.ui @@ -0,0 +1,527 @@ + + + Form + + + + 0 + 0 + 210 + 320 + + + + + 250 + 350 + + + + Form + + + + 0 + + + 0 + + + + + + 16777215 + 16777215 + + + + 0 + + + + Scale + + + + 0 + + + 0 + + + + + X Axis + + + + 0 + + + 0 + + + + + Manual + + + + + + + 0 + + + + + + + 0 + + + + + + + Auto + + + true + + + + + + + true + + + % + + + 1 + + + 100 + + + 1 + + + 100 + + + + + + + + + + Mouse + + + true + + + + + + + Log + + + + + + + Link with: + + + + + + + + + + Y Axis + + + + 0 + + + 0 + + + + + Manual + + + + + + + 0 + + + + + + + 0 + + + + + + + Auto + + + true + + + + + + + true + + + % + + + 1 + + + 100 + + + 1 + + + 100 + + + + + + + + + + Mouse + + + true + + + + + + + Log + + + + + + + Link with: + + + + + + + + + + + Data + + + + 0 + + + 0 + + + + + Power Spectrum + + + true + + + false + + + + + + + Decimate + + + true + + + + 0 + + + 0 + + + + + Manual + + + + + + + + + + Auto + + + true + + + + + + + Max Traces: + + + + + + + + + + Forget hidden traces + + + + + + + + + + Average + + + true + + + false + + + + 0 + + + 0 + + + + + + + + + + + + Display + + + + + + Alpha + + + true + + + + + + Auto + + + false + + + + + + + 1000 + + + 1000 + + + Qt::Horizontal + + + + + + + + + + Grid + + + true + + + + + + 255 + + + 70 + + + Qt::Horizontal + + + + + + + + + + Points + + + true + + + + + + Auto + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Save + + + + + + Qt::Horizontal + + + + 59 + 20 + + + + + + + + + + SVG + + + + + + + Image + + + + + + + MetaArray + + + + + + + + + Qt::Horizontal + + + + 59 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 211 + + + + + + + + + + + + + diff --git a/test_ImageView.py b/test_ImageView.py new file mode 100644 index 00000000..ea197f95 --- /dev/null +++ b/test_ImageView.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +from ImageView import * +from numpy import random +from PyQt4 import QtCore, QtGui +from scipy.ndimage import * + +app = QtGui.QApplication([]) +win = QtGui.QMainWindow() +imv = ImageView() +win.setCentralWidget(imv) +win.show() + +img = gaussian_filter(random.random((200, 200)), (5, 5)) * 5 +data = random.random((100, 200, 200)) +data += img + +for i in range(data.shape[0]): + data[i] += exp(-(2.*i)/data.shape[0]) +data += 10 +imv.setImage(data) \ No newline at end of file diff --git a/test_MultiPlotWidget.py b/test_MultiPlotWidget.py new file mode 100755 index 00000000..ab29f115 --- /dev/null +++ b/test_MultiPlotWidget.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from scipy import random +from numpy import linspace +from PyQt4 import QtGui, QtCore +from MultiPlotWidget import * +from metaarray import * + +app = QtGui.QApplication([]) +mw = QtGui.QMainWindow() +pw = MultiPlotWidget() +mw.setCentralWidget(pw) +mw.show() + +ma = MetaArray(random.random((3, 1000)), info=[{'name': 'Signal', 'cols': [{'name': 'Col1'}, {'name': 'Col2'}, {'name': 'Col3'}]}, {'name': 'Time', 'vals': linspace(0., 1., 1000)}]) +pw.plot(ma) diff --git a/test_PlotWidget.py b/test_PlotWidget.py new file mode 100755 index 00000000..d022c716 --- /dev/null +++ b/test_PlotWidget.py @@ -0,0 +1,80 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from scipy import random +from PyQt4 import QtGui, QtCore +from PlotWidget import * +from graphicsItems import * + + +app = QtGui.QApplication([]) +mw = QtGui.QMainWindow() +cw = QtGui.QWidget() +mw.setCentralWidget(cw) +l = QtGui.QVBoxLayout() +cw.setLayout(l) + +pw = PlotWidget() +l.addWidget(pw) +pw2 = PlotWidget() +l.addWidget(pw2) +pw3 = PlotWidget() +l.addWidget(pw3) + +pw.registerPlot('Plot1') +pw2.registerPlot('Plot2') + +#p1 = PlotCurveItem() +#pw.addItem(p1) +p1 = pw.plot() +rect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, 0, 1, 1)) +rect.setPen(QtGui.QPen(QtGui.QColor(100, 200, 100))) +pw.addItem(rect) + +pen = QtGui.QPen(QtGui.QBrush(QtGui.QColor(255, 255, 255, 10)), 5) +pen.setCosmetic(True) +#pen.setJoinStyle(QtCore.Qt.MiterJoin) +p1.setShadowPen(pen) +p1.setPen(QtGui.QPen(QtGui.QColor(255, 255, 255, 50))) + +#l1 = QtGui.QGraphicsLineItem(0, 2, 2, 3) +#l1.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) +l2 = InfiniteLine(pw, 1.5, 0) +#l3 = InfiniteLine(pw, [1.5, 1.5], 45) +#pw.addItem(l1) +pw.addItem(l2) +#pw.addItem(l3) + +pw3.plot(array([100000]*100)) + + +mw.show() + + +def rand(n): + data = random.random(n) + data[int(n*0.1):int(n*0.13)] += .5 + data[int(n*0.18)] += 2 + data[int(n*0.1):int(n*0.13)] *= 5 + data[int(n*0.18)] *= 20 + return data, arange(n, n+len(data)) / float(n) + + +def updateData(): + yd, xd = rand(10000) + p1.updateData(yd, x=xd) + +yd, xd = rand(10000) +updateData() +pw.autoRange() + +t = QtCore.QTimer() +QtCore.QObject.connect(t, QtCore.SIGNAL('timeout()'), updateData) +t.start(50) + +for i in range(0, 5): + for j in range(0, 3): + yd, xd = rand(10000) + pw2.plot(yd*(j+1), xd, params={'iter': i, 'val': j}) + +#app.exec_() \ No newline at end of file diff --git a/test_ROItypes.py b/test_ROItypes.py new file mode 100755 index 00000000..1d50f7fb --- /dev/null +++ b/test_ROItypes.py @@ -0,0 +1,88 @@ +#!/usr/bin/python -i +# -*- coding: utf-8 -*- +from scipy import zeros +from graphicsWindows import * +from graphicsItems import * +from widgets import * +from PlotWidget import * +from PyQt4 import QtCore, QtGui + +qapp = QtGui.QApplication([]) + +#i = PlotWindow(array([0,1,2,1,2]), parent=None, title='') + +class Win(QtGui.QMainWindow): + pass + +w = Win() +v = GraphicsView() +v.invertY(True) +v.setAspectLocked(True) +v.enableMouse(True) +v.autoPixelScale = False + +w.setCentralWidget(v) +#s = QtGui.QGraphicsScene() +#v.setScene(s) +s = v.scene() + +#p = Plot(array([0,2,1,3,4]), copy=False) +#s.addItem(p) + +arr = ones((100, 100), dtype=float) +arr[45:55, 45:55] = 0 +arr[25, :] = 5 +arr[:, 25] = 5 +arr[75, :] = 5 +arr[:, 75] = 5 +arr[50, :] = 10 +arr[:, 50] = 10 + +im1 = ImageItem(arr) +im2 = ImageItem(arr) +s.addItem(im1) +s.addItem(im2) +im2.moveBy(110, 20) +im3 = ImageItem() +s.addItem(im3) +im3.moveBy(0, 130) +im3.setZValue(10) +im4 = ImageItem() +s.addItem(im4) +im4.moveBy(110, 130) +im4.setZValue(10) + + +#g = Grid(view=v, bounds=QtCore.QRectF(0.1, 0.1, 0.8, 0.8)) +#g = Grid(view=v) +#s.addItem(g) + +#wid = RectROI([0, 0], [2, 2], maxBounds=QtCore.QRectF(-1, -1, 5, 5)) +roi = TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140)) +s.addItem(roi) +roi2 = LineROI([0, 0], [20, 20], width=5) +s.addItem(roi2) +mlroi = MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5) +s.addItem(mlroi) +elroi = EllipseROI([110, 10], [30, 20]) +s.addItem(elroi) +croi = CircleROI([110, 50], [20, 20]) +s.addItem(croi) + +def updateImg(roi): + global im1, im2, im3, im4, arr + arr1 = roi.getArrayRegion(arr, img=im1) + im3.updateImage(arr1, autoRange=True) + arr2 = roi.getArrayRegion(arr, img=im2) + im4.updateImage(arr2, autoRange=True) + +roi.connect(QtCore.SIGNAL('regionChanged'), lambda: updateImg(roi)) +roi2.connect(QtCore.SIGNAL('regionChanged'), lambda: updateImg(roi2)) +croi.connect(QtCore.SIGNAL('regionChanged'), lambda: updateImg(croi)) +elroi.connect(QtCore.SIGNAL('regionChanged'), lambda: updateImg(elroi)) +mlroi.connect(QtCore.SIGNAL('regionChanged'), lambda: updateImg(mlroi)) + + +v.setRange(QtCore.QRect(-2, -2, 220, 220)) + +w.show() \ No newline at end of file diff --git a/test_viewBox.py b/test_viewBox.py new file mode 100755 index 00000000..0c51963b --- /dev/null +++ b/test_viewBox.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from scipy import random +from PyQt4 import QtGui, QtCore +from GraphicsView import * +from graphicsItems import * + + +app = QtGui.QApplication([]) +mw = QtGui.QMainWindow() +cw = QtGui.QWidget() +vl = QtGui.QVBoxLayout() +cw.setLayout(vl) +mw.setCentralWidget(cw) +mw.show() +mw.resize(800, 600) + + +gv = GraphicsView(cw) +gv.enableMouse(False) +#w1 = QtGui.QGraphicsWidget() +l = QtGui.QGraphicsGridLayout() +l.setHorizontalSpacing(0) +l.setVerticalSpacing(0) + + +vb = ViewBox() +p1 = PlotCurveItem() +#gv.scene().addItem(vb) +vb.addItem(p1) +vl.addWidget(gv) +rect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, 0, 1, 1)) +rect.setPen(QtGui.QPen(QtGui.QColor(100, 200, 100))) +vb.addItem(rect) + +l.addItem(vb, 0, 2) +gv.centralWidget.setLayout(l) + + +xScale = ScaleItem(orientation='bottom', linkView=vb) +l.addItem(xScale, 1, 2) +yScale = ScaleItem(orientation='left', linkView=vb) +l.addItem(yScale, 0, 1) + +xLabel = LabelItem(u"X Axis (μV)", html=True, color='CCC') +l.setRowFixedHeight(2, 20) +l.setRowFixedHeight(1, 40) +l.addItem(xLabel, 2, 2) +yLabel = LabelItem("Y Axis", color=QtGui.QColor(200, 200, 200)) +yLabel.setAngle(90) +l.setColumnFixedWidth(0, 20) +l.setColumnFixedWidth(1, 60) +l.addItem(yLabel, 0, 0) + + +#grid = GridItem(gv) +#vb.addItem(grid) + +#gv.scene().addItem(w1) +#w1.setGeometry(0, 0, 1, 1) + + +#c1 = Qwt.QwtPlotCurve() +#c1.setData(range(len(data)), data) +#c1.attach(p1) +#c2 = PlotCurve() +#c2.setData([1,2,3,4,5,6,7,8], [1,2,10,4,3,2,4,1]) +#c2.attach(p2) + +def rand(n): + data = random.random(n) + data[int(n*0.1):int(n*0.13)] += .5 + data[int(n*0.18)] += 2 + data[int(n*0.1):int(n*0.13)] *= 5 + data[int(n*0.18)] *= 20 + #c1.setData(range(len(data)), data) + return data, arange(n, n+len(data)) / float(n) + + +def updateData(): + yd, xd = rand(10000) + p1.updateData(yd, x=xd) + + #vb.setRange(p1.boundingRect()) + #p1.plot(yd, x=xd, clear=True) + +yd, xd = rand(10000) +#p2.plot(yd * 1000, x=xd) +#for i in [1,2]: + #for j in range(3): + #yd, xd = rand(1000) + #p3.plot(yd * 100000 * i, x=xd, params={'repetitions': j, 'scale': i}) +updateData() +vb.autoRange() + +t = QtCore.QTimer() +QtCore.QObject.connect(t, QtCore.SIGNAL('timeout()'), updateData) +t.start(50) + diff --git a/widgets.py b/widgets.py new file mode 100644 index 00000000..8939c4ee --- /dev/null +++ b/widgets.py @@ -0,0 +1,837 @@ +# -*- 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]) +