Merge from ACQ4:

- Lots of bug fixes
 - API change in PlotItem.plot(...)
 - Started replacing QObjectWorkaround with QWidget
 - Made plotCurveItems clickable
 - Added curve-following arrows
This commit is contained in:
Luke Campagnola 2011-02-07 19:40:38 -05:00
parent 661e4411e2
commit c55392965f
13 changed files with 789 additions and 177 deletions

View File

@ -66,8 +66,14 @@ class GraphicsView(QtGui.QGraphicsView):
self.updateMatrix()
self.sceneObj = QtGui.QGraphicsScene()
self.setScene(self.sceneObj)
## by default we set up a central widget with a grid layout.
## this can be replaced if needed.
self.centralWidget = None
self.setCentralItem(QtGui.QGraphicsWidget())
self.centralLayout = QtGui.QGraphicsGridLayout()
self.centralWidget.setLayout(self.centralLayout)
self.mouseEnabled = False
self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False)
self.clickAccepted = False
@ -85,6 +91,7 @@ class GraphicsView(QtGui.QGraphicsView):
ev.ignore()
def setCentralItem(self, item):
"""Sets a QGraphicsWidget to automatically fill the entire view."""
if self.centralWidget is not None:
self.scene().removeItem(self.centralWidget)
self.centralWidget = item
@ -305,21 +312,24 @@ class GraphicsView(QtGui.QGraphicsView):
#self.currentItem = None
def mouseMoveEvent(self, ev):
#if self.lastMousePos is None:
#self.lastMousePos = Point(ev.pos())
#delta = Point(ev.pos()) - self.lastMousePos
#if abs(delta[0]) > 100 or abs(delta[1]) > 100: ## Weird bug generating extra events..
#return
#self.lastMousePos = Point(ev.pos())
#print "move", delta
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))
@ -377,6 +387,10 @@ class GraphicsView(QtGui.QGraphicsView):
self.render(painter)
painter.end()
def dragEnterEvent(self, ev):
ev.ignore() ## not sure why, but for some reason this class likes to consume drag events
#def getFreehandLine(self):

View File

@ -106,12 +106,12 @@ class ImageView(QtGui.QWidget):
setattr(self, fn, getattr(self.ui.graphicsView, fn))
#QtCore.QObject.connect(self.ui.timeSlider, QtCore.SIGNAL('valueChanged(int)'), self.timeChanged)
self.timeLine.connect(QtCore.SIGNAL('positionChanged'), self.timeLineChanged)
self.timeLine.connect(self.timeLine, QtCore.SIGNAL('positionChanged'), self.timeLineChanged)
#QtCore.QObject.connect(self.ui.whiteSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateImage)
#QtCore.QObject.connect(self.ui.blackSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateImage)
QtCore.QObject.connect(self.ui.gradientWidget, QtCore.SIGNAL('gradientChanged'), self.updateImage)
QtCore.QObject.connect(self.ui.roiBtn, QtCore.SIGNAL('clicked()'), self.roiClicked)
self.roi.connect(QtCore.SIGNAL('regionChanged'), self.roiChanged)
self.roi.connect(self.roi, QtCore.SIGNAL('regionChanged'), self.roiChanged)
QtCore.QObject.connect(self.ui.normBtn, QtCore.SIGNAL('toggled(bool)'), self.normToggled)
QtCore.QObject.connect(self.ui.normDivideRadio, QtCore.SIGNAL('clicked()'), self.updateNorm)
QtCore.QObject.connect(self.ui.normSubtractRadio, QtCore.SIGNAL('clicked()'), self.updateNorm)
@ -124,7 +124,7 @@ class ImageView(QtGui.QWidget):
##QtCore.QObject.connect(self.ui.normStartSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm)
#QtCore.QObject.connect(self.ui.normStopSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm)
self.normProxy = proxyConnect(self.normRgn, QtCore.SIGNAL('regionChanged'), self.updateNorm)
self.normRoi.connect(QtCore.SIGNAL('regionChangeFinished'), self.updateNorm)
self.normRoi.connect(self.normRoi, QtCore.SIGNAL('regionChangeFinished'), self.updateNorm)
self.ui.roiPlot.registerPlot(self.name + '_ROI')
@ -347,7 +347,19 @@ class ImageView(QtGui.QWidget):
self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None}
elif img.ndim == 4:
self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3}
else:
raise Exception("Can not interpret image with dimensions %s" % (str(img)))
elif isinstance(axes, dict):
self.axes = axes.copy()
elif isinstance(axes, list) or isinstance(axes, tuple):
self.axes = {}
for i in range(len(axes)):
self.axes[axes[i]] = i
else:
raise Exception("Can not interpret axis specification %s. Must be like {'t': 2, 'x': 0, 'y': 1} or ('t', 'x', 'y', 'c')" % (str(axes)))
for x in ['t', 'x', 'y', 'c']:
self.axes[x] = self.axes.get(x, None)
self.imageDisp = None
if autoLevels:

View File

@ -1,9 +1,17 @@
# -*- coding: utf-8 -*-
from PyQt4 import QtGui, QtCore
"""For circumventing PyQt's lack of multiple inheritance (just until PySide becomes stable)"""
class Obj(QtCore.QObject):
def event(self, ev):
self.emit(QtCore.SIGNAL('event'), ev)
return QtCore.QObject.event(self, ev)
class QObjectWorkaround:
def __init__(self):
self._qObj_ = QtCore.QObject()
self._qObj_ = Obj()
self.connect(QtCore.SIGNAL('event'), self.event)
def connect(self, *args):
if args[0] is self:
return QtCore.QObject.connect(self._qObj_, *args[1:])
@ -15,8 +23,20 @@ class QObjectWorkaround:
return QtCore.QObject.emit(self._qObj_, *args)
def blockSignals(self, b):
return self._qObj_.blockSignals(b)
def setProperty(self, prop, val):
return self._qObj_.setProperty(prop, val)
def property(self, prop):
return self._qObj_.property(prop)
def event(self, ev):
pass
class QGraphicsObject(QtGui.QGraphicsItem, QObjectWorkaround):
def __init__(self, *args):
QtGui.QGraphicsItem.__init__(self, *args)
QObjectWorkaround.__init__(self)
#class QGraphicsObject(QtGui.QGraphicsItem, QObjectWorkaround):
#def __init__(self, *args):
#QtGui.QGraphicsItem.__init__(self, *args)
#QObjectWorkaround.__init__(self)
class QGraphicsObject(QtGui.QGraphicsWidget):
def shape(self):
return QtGui.QGraphicsItem.shape(self)
#QGraphicsObject = QtGui.QGraphicsObject

View File

@ -20,6 +20,7 @@ This class is very heavily featured:
from graphicsItems import *
from plotConfigTemplate import *
from PyQt4 import QtGui, QtCore, QtSvg
from functions import *
#from ObjectWorkaround import *
#tryWorkaround(QtCore, QtGui)
import weakref
@ -43,7 +44,7 @@ class PlotItem(QtGui.QGraphicsWidget):
lastFileDir = None
managers = {}
def __init__(self, parent=None, name=None):
def __init__(self, parent=None, name=None, labels=None, **kargs):
QtGui.QGraphicsWidget.__init__(self, parent)
## Set up control buttons
@ -226,6 +227,15 @@ class PlotItem(QtGui.QGraphicsWidget):
if name is not None:
self.registerPlot(name)
if labels is not None:
for k in labels:
if isinstance(labels[k], basestring):
labels[k] = (labels[k],)
self.setLabel(k, *labels[k])
if len(kargs) > 0:
self.plot(**kargs)
def __del__(self):
if self.manager is not None:
@ -274,8 +284,8 @@ class PlotItem(QtGui.QGraphicsWidget):
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())
pos = v.mapToGlobal(v.pos())
wr.adjust(pos.x(), pos.y(), pos.x(), pos.y())
return wr
@ -324,6 +334,7 @@ class PlotItem(QtGui.QGraphicsWidget):
self.manager.linkY(self, plot)
def linkXChanged(self, plot):
"""Called when a linked plot has changed its X scale"""
#print "update from", plot
if self.linksBlocked:
return
@ -337,11 +348,13 @@ class PlotItem(QtGui.QGraphicsWidget):
x1 = pr.left() + (sg.x()-pg.x()) * upp
x2 = x1 + sg.width() * upp
plot.blockLink(True)
self.setManualXScale()
self.setXRange(x1, x2, padding=0)
plot.blockLink(False)
self.replot()
def linkYChanged(self, plot):
"""Called when a linked plot has changed its Y scale"""
if self.linksBlocked:
return
pr = plot.vb.viewRect()
@ -351,6 +364,7 @@ class PlotItem(QtGui.QGraphicsWidget):
y1 = pr.bottom() + (sg.y()-pg.y()) * upp
y2 = y1 + sg.height() * upp
plot.blockLink(True)
self.setManualYScale()
self.setYRange(y1, y2, padding=0)
plot.blockLink(False)
self.replot()
@ -554,7 +568,7 @@ class PlotItem(QtGui.QGraphicsWidget):
self.curves.remove(item)
self.updateDecimation()
self.updateParamList()
item.connect(QtCore.SIGNAL('plotChanged'), self.plotChanged)
item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged)
def clear(self):
for i in self.items[:]:
@ -567,7 +581,18 @@ class PlotItem(QtGui.QGraphicsWidget):
self.avgCurves = {}
def plot(self, data=None, x=None, clear=False, params=None, pen=None):
def plot(self, data=None, data2=None, x=None, y=None, clear=False, params=None, pen=None):
"""Add a new plot curve. Data may be specified a few ways:
plot(yVals) # x vals will be integers
plot(xVals, yVals)
plot(y=yVals, x=xVals)
"""
if y is not None:
data = y
if data2 is not None:
x = data
data = data2
if clear:
self.clear()
if params is None:
@ -588,7 +613,7 @@ class PlotItem(QtGui.QGraphicsWidget):
#print data, curve
self.addCurve(curve, params)
if pen is not None:
curve.setPen(pen)
curve.setPen(mkPen(pen))
return curve
@ -619,7 +644,7 @@ class PlotItem(QtGui.QGraphicsWidget):
if self.ctrl.averageGroup.isChecked():
self.addAvgCurve(c)
c.connect(QtCore.SIGNAL('plotChanged'), self.plotChanged)
c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged)
self.plotChanged()
def plotChanged(self, curve=None):
@ -643,6 +668,7 @@ class PlotItem(QtGui.QGraphicsWidget):
mn -= 1
mx += 1
self.setRange(ax, mn, mx)
#print "Auto range:", ax, mn, mx
def replot(self):
self.plotChanged()
@ -876,25 +902,6 @@ class PlotItem(QtGui.QGraphicsWidget):
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())
@ -906,14 +913,8 @@ class PlotItem(QtGui.QGraphicsWidget):
def ctrlBtnClicked(self):
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:
@ -925,43 +926,9 @@ class PlotItem(QtGui.QGraphicsWidget):
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:
@ -979,20 +946,8 @@ class PlotItem(QtGui.QGraphicsWidget):
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:

View File

@ -51,4 +51,7 @@ class PlotWidget(GraphicsView):
return self.plotItem.restoreState(state)
def getPlotItem(self):
return self.plotItem
return self.plotItem

View File

@ -92,20 +92,24 @@ class Point(QtCore.QPointF):
return Point(getattr(self[0], op)(x[0]), getattr(self[1], op)(x[1]))
def length(self):
"""Returns the vector length of this Point."""
return (self[0]**2 + self[1]**2) ** 0.5
def angle(self, a):
"""Returns the angle between this vector and the vector 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))
## Probably this should be done with arctan2 instead..
ang = acos(clip(self.dot(a) / (n1 * n2), -1.0, 1.0)) ### in radians
c = self.cross(a)
if c > 0:
ang *= -1.
return ang
def dot(self, a):
"""Returns the dot product of a and this Point."""
a = Point(a)
return self[0]*a[0] + self[1]*a[1]

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
### import all the goodies and add some helper functions for easy CLI use
from functions import *
from graphicsItems import *
from graphicsWindows import *
#import PlotWidget
#import ImageView
from PyQt4 import QtGui
plots = []
images = []
QAPP = None
def plot(*args, **kargs):
mkQApp()
if 'title' in kargs:
w = PlotWindow(title=kargs['title'])
del kargs['title']
else:
w = PlotWindow()
w.plot(*args, **kargs)
plots.append(w)
w.show()
return w
def show(*args, **kargs):
mkQApp()
w = ImageWindow(*args, **kargs)
images.append(w)
w.show()
return w
def mkQApp():
if QtGui.QApplication.instance() is None:
global QAPP
QAPP = QtGui.QApplication([])

View File

@ -88,6 +88,6 @@ 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})
pw2.plot(y=yd*(j+1), x=xd, params={'iter': i, 'val': j})
#app.exec_()

View File

@ -4,7 +4,8 @@
import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from scipy import zeros
from numpy import random
from scipy import zeros, ones
from pyqtgraph.graphicsWindows import *
from pyqtgraph.graphicsItems import *
from pyqtgraph.widgets import *
@ -84,6 +85,7 @@ rois.append(MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=mkPen(2)))
rois.append(EllipseROI([110, 10], [30, 20], pen=mkPen(3)))
rois.append(CircleROI([110, 50], [20, 20], pen=mkPen(4)))
rois.append(PolygonROI([[2,0], [2.1,0], [2,.1]], pen=mkPen(5)))
#rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0)))
for r in rois:
s.addItem(r)
c = pi1.plot(pen=r.pen)

View File

@ -5,6 +5,18 @@ Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
"""
colorAbbrev = {
'b': (0,0,255,255),
'g': (0,255,0,255),
'r': (255,0,0,255),
'c': (0,255,255,255),
'm': (255,0,255,255),
'y': (255,255,0,255),
'k': (0,0,0,255),
'w': (255,255,255,255),
}
from PyQt4 import QtGui
from numpy import clip, floor, log
@ -26,11 +38,25 @@ def siScale(x, minVal=1e-25):
p = .001**m
return (p, pref)
def mkBrush(color):
return QtGui.QBrush(mkColor(color))
def mkPen(color=None, width=1, style=None, cosmetic=True, hsv=None, ):
def mkPen(arg=None, color=None, width=1, style=None, cosmetic=True, hsv=None, ):
"""Convenience function for making pens. Examples:
mkPen(color)
mkPen(color, width=2)
mkPen(cosmetic=False, width=4.5, color='r')
mkPen({'color': "FF0", width: 2})
"""
if isinstance(arg, dict):
return mkPen(**arg)
elif arg is not None:
if isinstance(arg, QtGui.QPen):
return arg
color = arg
if color is None:
color = [255, 255, 255]
color = mkColor(200, 200, 200)
if hsv is not None:
color = hsvColor(*hsv)
else:
@ -48,20 +74,64 @@ def hsvColor(h, s=1.0, v=1.0, a=1.0):
return c
def mkColor(*args):
"""make a QColor from a variety of argument types"""
"""make a QColor from a variety of argument types
accepted types are:
r, g, b, [a]
(r, g, b, [a])
float (greyscale, 0.0-1.0)
int (uses intColor)
(int, hues) (uses intColor)
QColor
"c" (see colorAbbrev dictionary)
"RGB" (strings may optionally begin with "#")
"RGBA"
"RRGGBB"
"RRGGBBAA"
"""
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 isinstance(args[0], float):
r = g = b = int(args[0] * 255)
a = 255
elif isinstance(args[0], basestring):
c = args[0]
if c[0] == '#':
c = c[1:]
if len(c) == 1:
(r, g, b, a) = colorAbbrev[c]
if len(c) == 3:
r = int(c[0]*2, 16)
g = int(c[1]*2, 16)
b = int(c[2]*2, 16)
a = 255
elif len(c) == 4:
r = int(c[0]*2, 16)
g = int(c[1]*2, 16)
b = int(c[2]*2, 16)
a = int(c[3]*2, 16)
elif len(c) == 6:
r = int(c[0:2], 16)
g = int(c[2:4], 16)
b = int(c[4:6], 16)
a = 255
elif len(c) == 8:
r = int(c[0:2], 16)
g = int(c[2:4], 16)
b = int(c[4:6], 16)
a = int(c[6:8], 16)
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]
elif len(args[0]) == 2:
return intColor(*args[0])
else:
raise Exception(err)
if type(args[0]) == int:
elif type(args[0]) == int:
return intColor(args[0])
else:
raise Exception(err)
@ -74,19 +144,27 @@ def mkColor(*args):
raise Exception(err)
return QtGui.QColor(r, g, b, a)
def colorTuple(c):
return (c.red(), c.blue(), c.green(), c.alpha())
def colorStr(c):
"""Generate a hex string code from a QColor"""
return ('%02x'*4) % (c.red(), c.blue(), c.green(), c.alpha())
return ('%02x'*4) % colorTuple(c)
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)
def intColor(index, hues=9, values=3, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255):
"""Creates a QColor from a single index. Useful for stepping through a predefined list of colors.
- The argument "index" determines which color from the set will be returned
- All other arguments determine what the set of predefined colors will be
Colors are chosen by cycling across hues while varying the value (brightness). By default, there
are 9 hues and 3 values for a total of 27 different colors. """
hues = int(hues)
values = int(values)
ind = int(ind) % (colors * values)
indh = ind % colors
indv = ind / colors
ind = int(index) % (hues * values)
indh = ind % hues
indv = ind / hues
v = minValue + indv * ((maxValue-minValue) / (values-1))
h = (indh * 360) / colors
h = minHue + (indh * (maxHue-minHue)) / hues
c = QtGui.QColor()
c.setHsv(h, sat, v)

View File

@ -9,6 +9,8 @@ Provides ImageItem, PlotCurveItem, and ViewBox, amongst others.
from PyQt4 import QtGui, QtCore
if not hasattr(QtCore, 'Signal'):
QtCore.Signal = QtCore.pyqtSignal
from ObjectWorkaround import *
#tryWorkaround(QtCore, QtGui)
#from numpy import *
@ -178,7 +180,7 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround):
else:
useWeave = False
def __init__(self, image=None, copy=True, parent=None, *args):
def __init__(self, image=None, copy=True, parent=None, border=None, *args):
QObjectWorkaround.__init__(self)
self.qimage = QtGui.QImage()
self.pixmap = None
@ -189,6 +191,9 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround):
self.image = None
self.clipLevel = None
self.drawKernel = None
if border is not None:
border = mkPen(border)
self.border = border
QtGui.QGraphicsPixmapItem.__init__(self, parent, *args)
#self.pixmapItem = QtGui.QGraphicsPixmapItem(self)
@ -248,9 +253,9 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround):
else:
gotNewData = True
if copy:
self.image = image.copy()
self.image = image.view(np.ndarray).copy()
else:
self.image = image
self.image = image.view(np.ndarray)
#print " image max:", self.image.max(), "min:", self.image.min()
# Determine scale factors
@ -381,22 +386,36 @@ class ImageItem(QtGui.QGraphicsPixmapItem, QObjectWorkaround):
def setDrawKernel(self, kernel=None):
self.drawKernel = kernel
def paint(self, p, *args):
#QtGui.QGraphicsPixmapItem.paint(self, p, *args)
if self.pixmap is None:
return
p.drawPixmap(self.boundingRect(), self.pixmap, QtCore.QRectF(0, 0, self.pixmap.width(), self.pixmap.height()))
if self.border is not None:
p.setPen(self.border)
p.drawRect(self.boundingRect())
class PlotCurveItem(GraphicsObject):
"""Class representing a single plot curve."""
def __init__(self, y=None, x=None, copy=False, pen=None, shadow=None, parent=None, color=None):
sigClicked = QtCore.Signal(object)
def __init__(self, y=None, x=None, copy=False, pen=None, shadow=None, parent=None, color=None, clickable=False):
GraphicsObject.__init__(self, parent)
#GraphicsWidget.__init__(self, parent)
self.free()
#self.dispPath = None
if pen is None:
if color is None:
pen = QtGui.QPen(QtGui.QColor(200, 200, 200))
self.setPen((200,200,200))
else:
pen = QtGui.QPen(color)
self.pen = pen
self.setPen(color)
else:
self.setPen(pen)
self.shadow = shadow
if y is not None:
@ -414,8 +433,13 @@ class PlotCurveItem(GraphicsObject):
'alphaMode': False
}
self.setClickable(clickable)
#self.fps = None
def setClickable(self, s):
self.clickable = s
def getData(self):
if self.xData is None:
return (None, None)
@ -497,7 +521,7 @@ class PlotCurveItem(GraphicsObject):
return self.metaData
def setPen(self, pen):
self.pen = pen
self.pen = mkPen(pen)
self.update()
def setColor(self, color):
@ -547,6 +571,13 @@ class PlotCurveItem(GraphicsObject):
x = np.array(x)
if not isinstance(data, np.ndarray) or data.ndim > 2:
raise Exception("Plot data must be 1 or 2D ndarray (data shape is %s)" % str(data.shape))
if x == None:
if 'complex' in str(data.dtype):
raise Exception("Can not plot complex data types.")
else:
if 'complex' in str(data.dtype)+str(x.dtype):
raise Exception("Can not plot complex data types.")
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.")
@ -573,7 +604,7 @@ class PlotCurveItem(GraphicsObject):
self.xData = x
if x is None:
self.xData = arange(0, self.yData.shape[0])
self.xData = np.arange(0, self.yData.shape[0])
if self.xData.shape != self.yData.shape:
raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape)))
@ -691,10 +722,181 @@ class PlotCurveItem(GraphicsObject):
self.path = None
#del self.xData, self.yData, self.xDisp, self.yDisp, self.path
class ScatterPlotItem(QtGui.QGraphicsItem):
def __init__(self, spots=None, pxMode=True, pen=None, brush=None, size=5):
def mousePressEvent(self, ev):
#GraphicsObject.mousePressEvent(self, ev)
if not self.clickable:
ev.ignore()
if ev.button() != QtCore.Qt.LeftButton:
ev.ignore()
self.mousePressPos = ev.pos()
self.mouseMoved = False
def mouseMoveEvent(self, ev):
#GraphicsObject.mouseMoveEvent(self, ev)
self.mouseMoved = True
print "move"
def mouseReleaseEvent(self, ev):
#GraphicsObject.mouseReleaseEvent(self, ev)
if not self.mouseMoved:
self.sigClicked.emit(self)
class CurvePoint(QtGui.QGraphicsItem, QObjectWorkaround):
"""A GraphicsItem that sets its location to a point on a PlotCurveItem.
The position along the curve is a property, and thus can be easily animated."""
def __init__(self, curve, index=0, pos=None):
"""Position can be set either as an index referring to the sample number or
the position 0.0 - 1.0"""
QtGui.QGraphicsItem.__init__(self)
QObjectWorkaround.__init__(self)
self.curve = None
self.setProperty('position', 0.0)
self.setProperty('index', 0)
if hasattr(self, 'ItemHasNoContents'):
self.setFlags(self.flags() | self.ItemHasNoContents)
self.curve = curve
self.setParentItem(curve)
if pos is not None:
self.setPos(pos)
else:
self.setIndex(index)
def setPos(self, pos):
self.setProperty('position', pos)
def setIndex(self, index):
self.setProperty('index', index)
def event(self, ev):
if not isinstance(ev, QtCore.QDynamicPropertyChangeEvent) or self.curve is None:
return
if ev.propertyName() == 'index':
index = self.property('index').toInt()[0]
elif ev.propertyName() == 'position':
index = None
else:
return
(x, y) = self.curve.getData()
if index is None:
#print self.property('position').toDouble()[0], self.property('position').typeName()
index = (len(x)-1) * clip(self.property('position').toDouble()[0], 0.0, 1.0)
if index != int(index):
i1 = int(index)
i2 = clip(i1+1, 0, len(x)-1)
s2 = index-i1
s1 = 1.0-s2
newPos = (x[i1]*s1+x[i2]*s2, y[i1]*s1+y[i2]*s2)
else:
index = int(index)
i1 = clip(index-1, 0, len(x)-1)
i2 = clip(index+1, 0, len(x)-1)
newPos = (x[index], y[index])
p1 = self.parentItem().mapToScene(QtCore.QPointF(x[i1], y[i1]))
p2 = self.parentItem().mapToScene(QtCore.QPointF(x[i2], y[i2]))
ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x())
self.resetTransform()
self.rotate(180+ ang * 180 / np.pi)
QtGui.QGraphicsItem.setPos(self, *newPos)
def boundingRect(self):
return QtCore.QRectF()
def paint(self, *args):
pass
def makeAnimation(self, prop='position', start=0.0, end=1.0, duration=10000, loop=1):
anim = QtCore.QPropertyAnimation(self._qObj_, prop)
anim.setDuration(duration)
anim.setStartValue(start)
anim.setEndValue(end)
anim.setLoopCount(loop)
return anim
class ArrowItem(QtGui.QGraphicsPolygonItem):
def __init__(self, **opts):
QtGui.QGraphicsPolygonItem.__init__(self)
defOpts = {
'style': 'tri',
'pxMode': True,
'size': 20,
'angle': -150,
'pos': (0,0),
'width': 8,
'tipAngle': 25,
'baseAngle': 90,
'pen': (200,200,200),
'brush': (50,50,200),
}
defOpts.update(opts)
self.setStyle(**defOpts)
self.setPen(mkPen(defOpts['pen']))
self.setBrush(mkBrush(defOpts['brush']))
self.rotate(self.opts['angle'])
self.moveBy(*self.opts['pos'])
def setStyle(self, **opts):
self.opts = opts
if opts['style'] == 'tri':
points = [
QtCore.QPointF(0,0),
QtCore.QPointF(opts['size'],-opts['width']/2.),
QtCore.QPointF(opts['size'],opts['width']/2.),
]
poly = QtGui.QPolygonF(points)
else:
raise Exception("Unrecognized arrow style '%s'" % opts['style'])
self.setPolygon(poly)
if opts['pxMode']:
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
else:
self.setFlags(self.flags() & ~self.ItemIgnoresTransformations)
def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing)
QtGui.QGraphicsPolygonItem.paint(self, p, *args)
class CurveArrow(CurvePoint):
"""Provides an arrow that points to any specific sample on a PlotCurveItem.
Provides properties that can be animated."""
def __init__(self, curve, index=0, pos=None, **opts):
CurvePoint.__init__(self, curve, index=index, pos=pos)
if opts.get('pxMode', True):
opts['pxMode'] = False
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
opts['angle'] = 0
self.arrow = ArrowItem(**opts)
self.arrow.setParentItem(self)
def setStyle(**opts):
return self.arrow.setStyle(**opts)
class ScatterPlotItem(QtGui.QGraphicsWidget):
sigPointClicked = QtCore.Signal(object)
def __init__(self, spots=None, pxMode=True, pen=None, brush=None, size=5):
QtGui.QGraphicsWidget.__init__(self)
self.spots = []
self.range = [[0,0], [0,0]]
@ -740,7 +942,8 @@ class ScatterPlotItem(QtGui.QGraphicsItem):
brush = s.get('brush', self.brush)
pen = s.get('pen', self.pen)
pen.setCosmetic(True)
item = self.mkSpot(pos, size, self.pxMode, brush, pen)
data = s.get('data', None)
item = self.mkSpot(pos, size, self.pxMode, brush, pen, data)
self.spots.append(item)
if xmn is None:
xmn = pos[0]-size
@ -755,10 +958,11 @@ class ScatterPlotItem(QtGui.QGraphicsItem):
self.range = [[xmn, xmx], [ymn, ymx]]
def mkSpot(self, pos, size, pxMode, brush, pen):
item = SpotItem(size, pxMode, brush, pen)
def mkSpot(self, pos, size, pxMode, brush, pen, data):
item = SpotItem(size, pxMode, brush, pen, data)
item.setParentItem(self)
item.setPos(pos)
item.sigClicked.connect(self.pointClicked)
return item
def boundingRect(self):
@ -769,11 +973,17 @@ class ScatterPlotItem(QtGui.QGraphicsItem):
def paint(self, p, *args):
pass
def pointClicked(self, point):
self.sigPointClicked.emit(point)
def points(self):
return self.spots[:]
class SpotItem(QtGui.QGraphicsItem):
def __init__(self, size, pxMode, brush, pen):
QtGui.QGraphicsItem.__init__(self)
class SpotItem(QtGui.QGraphicsWidget):
sigClicked = QtCore.Signal(object)
def __init__(self, size, pxMode, brush, pen, data):
QtGui.QGraphicsWidget.__init__(self)
if pxMode:
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
#self.setCacheMode(self.DeviceCoordinateCache) ## causes crash on linux
@ -782,6 +992,15 @@ class SpotItem(QtGui.QGraphicsItem):
self.path = QtGui.QPainterPath()
s2 = size/2.
self.path.addEllipse(QtCore.QRectF(-s2, -s2, size, size))
self.data = data
def setBrush(self, brush):
self.brush = mkBrush(brush)
self.update()
def setPen(self, pen):
self.pen = mkPen(pen)
self.update()
def boundingRect(self):
return self.path.boundingRect()
@ -794,8 +1013,26 @@ class SpotItem(QtGui.QGraphicsItem):
p.setBrush(self.brush)
p.drawPath(self.path)
def mousePressEvent(self, ev):
QtGui.QGraphicsItem.mousePressEvent(self, ev)
if ev.button() == QtCore.Qt.LeftButton:
self.mouseMoved = False
ev.accept()
else:
ev.ignore()
def mouseMoveEvent(self, ev):
QtGui.QGraphicsItem.mouseMoveEvent(self, ev)
self.mouseMoved = True
pass
def mouseReleaseEvent(self, ev):
QtGui.QGraphicsItem.mouseReleaseEvent(self, ev)
if not self.mouseMoved:
self.sigClicked.emit(self)
class ROIPlotItem(PlotCurveItem):
@ -806,7 +1043,7 @@ class ROIPlotItem(PlotCurveItem):
self.axes = axes
self.xVals = xVals
PlotCurveItem.__init__(self, self.getRoiData(), x=self.xVals, color=color)
roi.connect(QtCore.SIGNAL('regionChanged'), self.roiChangedEvent)
roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent)
#self.roiChangedEvent()
def getRoiData(self):
@ -1361,7 +1598,7 @@ class ViewBox(QtGui.QGraphicsWidget):
#self.picture = None
self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding))
self.drawFrame = True
self.drawFrame = False
self.mouseEnabled = [True, True]
@ -1493,6 +1730,7 @@ class ViewBox(QtGui.QGraphicsWidget):
def mouseMoveEvent(self, ev):
QtGui.QGraphicsWidget.mouseMoveEvent(self, ev)
pos = np.array([ev.pos().x(), ev.pos().y()])
dif = pos - self.mousePos
dif *= -1
@ -1523,11 +1761,14 @@ class ViewBox(QtGui.QGraphicsWidget):
ev.ignore()
def mousePressEvent(self, ev):
QtGui.QGraphicsWidget.mousePressEvent(self, ev)
self.mousePos = np.array([ev.pos().x(), ev.pos().y()])
self.pressPos = self.mousePos.copy()
ev.accept()
def mouseReleaseEvent(self, ev):
QtGui.QGraphicsWidget.mouseReleaseEvent(self, ev)
pos = np.array([ev.pos().x(), ev.pos().y()])
#if sum(abs(self.pressPos - pos)) < 3: ## Detect click
#if ev.button() == QtCore.Qt.RightButton:
@ -1619,14 +1860,12 @@ class InfiniteLine(GraphicsObject):
self.maxRange = [None, None]
else:
self.maxRange = bounds
self.movable = movable
self.setMovable(movable)
self.view = weakref.ref(view)
self.p = [0, 0]
self.setAngle(angle)
self.setPos(pos)
if movable:
self.setAcceptHoverEvents(True)
self.hasMoved = False
@ -1639,7 +1878,12 @@ class InfiniteLine(GraphicsObject):
#for p in self.getBoundingParents():
#QtCore.QObject.connect(p, QtCore.SIGNAL('viewChanged'), self.updateLine)
QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.updateLine)
def setMovable(self, m):
self.movable = m
self.setAcceptHoverEvents(m)
def setBounds(self, bounds):
self.maxRange = bounds
self.setValue(self.value())
@ -1825,7 +2069,6 @@ class LinearRegionItem(GraphicsObject):
self.rect.setParentItem(self)
self.bounds = QtCore.QRectF()
self.view = weakref.ref(view)
self.setBrush = self.rect.setBrush
self.brush = self.rect.brush
@ -1841,18 +2084,23 @@ class LinearRegionItem(GraphicsObject):
for l in self.lines:
l.setParentItem(self)
l.connect(QtCore.SIGNAL('positionChangeFinished'), self.lineMoveFinished)
l.connect(QtCore.SIGNAL('positionChanged'), self.lineMoved)
l.connect(l, QtCore.SIGNAL('positionChangeFinished'), self.lineMoveFinished)
l.connect(l, QtCore.SIGNAL('positionChanged'), self.lineMoved)
if brush is None:
brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50))
self.setBrush(brush)
self.setMovable(movable)
def setBounds(self, bounds):
for l in self.lines:
l.setBounds(bounds)
def setMovable(self, m):
for l in self.lines:
l.setMovable(m)
self.movable = m
def boundingRect(self):
return self.rect.boundingRect()
@ -1878,6 +2126,9 @@ class LinearRegionItem(GraphicsObject):
self.rect.setRect(vb)
def mousePressEvent(self, ev):
if not self.movable:
ev.ignore()
return
for l in self.lines:
l.mousePressEvent(ev) ## pass event to both lines so they move together
#if self.movable and ev.button() == QtCore.Qt.LeftButton:
@ -1892,6 +2143,8 @@ class LinearRegionItem(GraphicsObject):
def mouseMoveEvent(self, ev):
#print "move", ev.pos()
if not self.movable:
return
self.lines[0].blockSignals(True) # only want to update once
for l in self.lines:
l.mouseMoveEvent(ev)
@ -1919,7 +2172,7 @@ class VTickGroup(QtGui.QGraphicsPathItem):
if xvals is None:
xvals = []
if pen is None:
pen = QtGui.QPen(QtGui.QColor(200, 200, 200))
pen = (200, 200, 200)
self.ticks = []
self.xvals = []
if view is None:
@ -1932,14 +2185,9 @@ class VTickGroup(QtGui.QGraphicsPathItem):
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 setPen(self, pen):
pen = mkPen(pen)
QtGui.QGraphicsPathItem.setPen(self, pen)
def setXVals(self, vals):
self.xvals = vals
@ -2065,7 +2313,6 @@ class GridItem(UIGraphicsItem):
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

View File

@ -9,30 +9,119 @@ from PyQt4 import QtCore, QtGui
from PlotWidget import *
from ImageView import *
QAPP = None
class PlotWindow(QtGui.QMainWindow):
def __init__(self, title=None):
if QtGui.QApplication.instance() is None:
global QAPP
QAPP = QtGui.QApplication([])
QtGui.QMainWindow.__init__(self)
self.cw = PlotWidget()
self.setCentralWidget(self.cw)
for m in ['plot', 'autoRange', 'addItem', 'removeItem', '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):
if QtGui.QApplication.instance() is None:
global QAPP
QAPP = QtGui.QApplication([])
def mkQApp():
if QtGui.QApplication.instance() is None:
global QAPP
QAPP = QtGui.QApplication([])
class GraphicsLayoutWidget(GraphicsView):
def __init__(self):
GraphicsView.__init__(self)
self.items = {}
self.currentRow = 0
self.currentCol = 0
def nextRow(self):
"""Advance to next row for automatic item placement"""
self.currentRow += 1
self.currentCol = 0
def nextCol(self, colspan=1):
"""Advance to next column, while returning the current column number
(generally only for internal use)"""
self.currentCol += colspan
return self.currentCol-colspan
def addPlot(self, row=None, col=None, rowspan=1, colspan=1, **kargs):
plot = PlotItem(**kargs)
self.addItem(plot, row, col, rowspan, colspan)
return plot
def addItem(self, item, row=None, col=None, rowspan=1, colspan=1):
if row not in self.items:
self.items[row] = {}
self.items[row][col] = item
if row is None:
row = self.currentRow
if col is None:
col = self.nextCol(colspan)
self.centralLayout.addItem(item, row, col, rowspan, colspan)
def getItem(self, row, col):
return self.items[row][col]
class GraphicsWindow(GraphicsLayoutWidget):
def __init__(self, title=None, size=(800,600)):
mkQApp()
self.win = QtGui.QMainWindow()
GraphicsLayoutWidget.__init__(self)
self.win.setCentralWidget(self)
self.win.resize(*size)
if title is not None:
self.win.setWindowTitle(title)
self.win.show()
class TabWindow(QtGui.QMainWindow):
def __init__(self, title=None, size=(800,600)):
mkQApp()
QtGui.QMainWindow.__init__(self)
self.cw = ImageView()
self.resize(*size)
self.cw = QtGui.QTabWidget()
self.setCentralWidget(self.cw)
for m in ['setImage', 'autoRange', 'addItem', 'removeItem', 'blackLevel', 'whiteLevel', 'imageItem']:
setattr(self, m, getattr(self.cw, m))
if title is not None:
self.setWindowTitle(title)
self.show()
def __getattr__(self, attr):
if hasattr(self.cw, attr):
return getattr(self.cw, attr)
else:
raise NameError(attr)
#class PlotWindow(QtGui.QMainWindow):
#def __init__(self, title=None, **kargs):
#mkQApp()
#QtGui.QMainWindow.__init__(self)
#self.cw = PlotWidget(**kargs)
#self.setCentralWidget(self.cw)
#for m in ['plot', 'autoRange', 'addItem', 'removeItem', 'setLabel', 'clear', 'viewRect']:
#setattr(self, m, getattr(self.cw, m))
#if title is not None:
#self.setWindowTitle(title)
#self.show()
class PlotWindow(PlotWidget):
def __init__(self, title=None, **kargs):
mkQApp()
self.win = QtGui.QMainWindow()
PlotWidget.__init__(self, **kargs)
self.win.setCentralWidget(self)
for m in ['resize']:
setattr(self, m, getattr(self.win, m))
if title is not None:
self.win.setWindowTitle(title)
self.win.show()
class ImageWindow(ImageView):
def __init__(self, *args, **kargs):
mkQApp()
self.win = QtGui.QMainWindow()
if 'title' in kargs:
self.win.setWindowTitle(kargs['title'])
del kargs['title']
ImageView.__init__(self, self.win)
if len(args) > 0 or len(kargs) > 0:
self.setImage(*args, **kargs)
self.win.setCentralWidget(self)
for m in ['resize']:
setattr(self, m, getattr(self.win, m))
#for m in ['setImage', 'autoRange', 'addItem', 'removeItem', 'blackLevel', 'whiteLevel', 'imageItem']:
#setattr(self, m, getattr(self.cw, m))
self.win.show()

View File

@ -144,6 +144,11 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
raise Exception("Scale/rotate handles must have either the same x or y coordinate as their center point.")
return self.addHandle({'name': name, 'type': 'sr', 'center': center, 'pos': pos, 'item': item})
def addRotateFreeHandle(self, pos, center, axes=None, item=None, name=None):
pos = Point(pos)
center = Point(center)
return self.addHandle({'name': name, 'type': 'rf', '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()
@ -262,14 +267,20 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
def movePoint(self, pt, pos, modifiers=QtCore.Qt.KeyboardModifier()):
#print "movePoint() called."
## pos is the new position of the handle in scene coords, as requested by the handle.
newState = self.stateCopy()
h = self.handles[pt]
#p0 = self.mapToScene(h['item'].pos())
## p0 is current (before move) position of handle in scene coords
p0 = self.mapToScene(h['pos'] * self.state['size'])
p1 = Point(pos)
## transform p0 and p1 into parent's coordinates (same as scene coords if there is no parent). I forget why.
p0 = self.mapSceneToParent(p0)
p1 = self.mapSceneToParent(p1)
## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1)
if h.has_key('center'):
c = h['center']
cs = c * self.state['size']
@ -296,15 +307,18 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
#cs = c * self.state['size']
#p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs
## If a handle and its center have the same x or y value, we can't scale across that axis.
if h['center'][0] == h['pos'][0]:
lp1[0] = 0
if h['center'][1] == h['pos'][1]:
lp1[1] = 0
## snap
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
## determine scale factors and new size of ROI
hs = h['pos'] - c
if hs[0] == 0:
hs[0] = 1
@ -312,6 +326,7 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
hs[1] = 1
newSize = lp1 / hs
## Perform some corrections and limit checks
if newSize[0] == 0:
newSize[0] = newState['size'][0]
if newSize[1] == 0:
@ -324,10 +339,12 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
if self.aspectLocked:
newSize[0] = newSize[1]
## Move ROI so the center point occupies the same scene location after the scale
s0 = c * self.state['size']
s1 = c * newSize
cc = self.mapToParent(s0 - s1) - self.mapToParent(Point(0, 0))
## update state, do more boundary checks
newState['size'] = newSize
newState['pos'] = newState['pos'] + cc
if self.maxBounds is not None:
@ -339,30 +356,31 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
self.prepareGeometryChange()
self.state = newState
## move handles to their new locations
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
elif h['type'] in ['r', 'rf']:
## If the handle is directly over its center point, we can't compute an angle.
if lp1.length() == 0 or lp0.length() == 0:
return
## determine new rotation angle, constrained if necessary
ang = newState['angle'] + lp0.angle(lp1)
if ang is None:
if ang is None: ## this should never happen..
return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
ang = round(ang / (np.pi/12.)) * (np.pi/12.)
## create rotation transform
tr = QtGui.QTransform()
tr.rotate(-ang * 180. / np.pi)
## mvoe ROI so that center point remains stationary after rotate
cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos'])
newState['angle'] = ang
newState['pos'] = newState['pos'] + cc
## check boundaries, update
if self.maxBounds is not None:
r = self.stateRect(newState)
if not self.maxBounds.contains(r):
@ -370,6 +388,45 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
self.setTransform(tr)
self.setPos(newState['pos'], update=False)
self.state = newState
## If this is a free-rotate handle, its distance from the center may change.
if h['type'] == 'rf':
h['item'].setPos(self.mapFromScene(p1)) ## changes ROI coordinates of handle
#elif h['type'] == 'rf':
### If the handle is directly over its center point, we can't compute an angle.
#if lp1.length() == 0 or lp0.length() == 0:
#return
### determine new rotation angle, constrained if necessary
#pos = Point(pos)
#ang = newState['angle'] + lp0.angle(lp1)
#if ang is None:
##h['item'].setPos(self.mapFromScene(Point(pos[0], 0.0))) ## changes ROI coordinates of handle
#h['item'].setPos(self.mapFromScene(pos))
#return
#if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
#ang = round(ang / (np.pi/12.)) * (np.pi/12.)
#tr = QtGui.QTransform()
#tr.rotate(-ang * 180. / np.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
#h['item'].setPos(self.mapFromScene(pos)) ## changes ROI coordinates of handle
##self.emit(QtCore.SIGNAL('regionChanged'), self)
elif h['type'] == 'sr':
#newState = self.stateCopy()
@ -419,6 +476,7 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
self.handleChange()
def handleChange(self):
"""The state of the ROI has changed; redraw if needed."""
#print "handleChange() called."
changed = False
#print "self.lastState:", self.lastState
@ -446,7 +504,8 @@ class ROI(QtGui.QGraphicsItem, QObjectWorkaround):
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:
@ -687,6 +746,9 @@ class Handle(QtGui.QGraphicsItem):
elif typ == 'sr':
self.sides = 12
self.startAng = 0
elif typ == 'rf':
self.sides = 12
self.startAng = 0
else:
self.sides = 4
self.startAng = np.pi/4
@ -723,6 +785,7 @@ class Handle(QtGui.QGraphicsItem):
if not r[0].checkPointMove(r[1], pos, modifiers):
return
#print "point moved; inform %d ROIs" % len(self.roi)
# A handle can be used by multiple ROIs; tell each to update its handle position
for r in self.roi:
r[0].movePoint(r[1], pos, modifiers)
@ -915,6 +978,7 @@ class PolygonROI(ROI):
#ROI.__init__(self, positions[0])
for p in positions:
self.addFreeHandle(p)
self.setZValue(1000)
def listPoints(self):
return [p['item'].pos() for p in self.handles]
@ -922,6 +986,8 @@ class PolygonROI(ROI):
def movePoint(self, *args, **kargs):
ROI.movePoint(self, *args, **kargs)
self.prepareGeometryChange()
for h in self.handles:
h['pos'] = h['item'].pos()
def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing)
@ -934,7 +1000,7 @@ class PolygonROI(ROI):
def boundingRect(self):
r = QtCore.QRectF()
for h in self.handles:
r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect()
r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs
return r
def shape(self):
@ -943,4 +1009,89 @@ class PolygonROI(ROI):
for i in range(len(self.handles)):
p.lineTo(self.handles[i]['item'].pos())
return p
def stateCopy(self):
sc = {}
sc['pos'] = Point(self.state['pos'])
sc['size'] = Point(self.state['size'])
sc['angle'] = self.state['angle']
#sc['handles'] = self.handles
return sc
class SpiralROI(ROI):
def __init__(self, pos=None, size=None, **args):
if size == None:
size = [100e-6,100e-6]
if pos == None:
pos = [0,0]
ROI.__init__(self, pos, size, **args)
self.translateSnap = False
self.addFreeHandle([0.25,0], name='a')
self.addRotateFreeHandle([1,0], [0,0], name='r')
#self.getRadius()
#QtCore.connect(self, QtCore.SIGNAL('regionChanged'), self.
def getRadius(self):
radius = Point(self.handles[1]['item'].pos()).length()
#r2 = radius[1]
#r3 = r2[0]
return radius
def boundingRect(self):
r = self.getRadius()
return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r)
#return self.bounds
def movePoint(self, *args, **kargs):
ROI.movePoint(self, *args, **kargs)
self.prepareGeometryChange()
for h in self.handles:
h['pos'] = h['item'].pos()/self.state['size'][0]
def handleChange(self):
ROI.handleChange(self)
if len(self.handles) > 1:
self.path = QtGui.QPainterPath()
h0 = Point(self.handles[0]['item'].pos()).length()
a = h0/(2.0*np.pi)
theta = 30.0*(2.0*np.pi)/360.0
self.path.moveTo(QtCore.QPointF(a*theta*cos(theta), a*theta*sin(theta)))
x0 = a*theta*cos(theta)
y0 = a*theta*sin(theta)
radius = self.getRadius()
theta += 20.0*(2.0*np.pi)/360.0
i = 0
while Point(x0, y0).length() < radius and i < 1000:
x1 = a*theta*cos(theta)
y1 = a*theta*sin(theta)
self.path.lineTo(QtCore.QPointF(x1,y1))
theta += 20.0*(2.0*np.pi)/360.0
x0 = x1
y0 = y1
i += 1
return self.path
def shape(self):
p = QtGui.QPainterPath()
p.addEllipse(self.boundingRect())
return p
def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing)
#path = self.shape()
p.setPen(self.pen)
p.drawPath(self.path)
p.setPen(QtGui.QPen(QtGui.QColor(255,0,0)))
p.drawPath(self.shape())
p.setPen(QtGui.QPen(QtGui.QColor(0,0,255)))
p.drawRect(self.boundingRect())