From 3c2c970a6b74594b274d27c26b81130840a91ef3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:00:57 -0700 Subject: [PATCH 1/5] Remove spiral ROI --- pyqtgraph/graphicsItems/ROI.py | 76 +--------------------------------- 1 file changed, 2 insertions(+), 74 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 963ecb05..bc77e1c3 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -26,7 +26,8 @@ from .. import getConfigOption __all__ = [ 'ROI', 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', - 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', 'CrosshairROI', + 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', + 'CrosshairROI', ] @@ -2157,79 +2158,6 @@ class _PolyLineSegment(LineSegmentROI): return LineSegmentROI.hoverEvent(self, ev) -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 stateChanged(self, finish=True): - ROI.stateChanged(self, finish=finish) - 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.currentPen) - 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()) - - class CrosshairROI(ROI): """A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable.""" From 4d0f3b5821aac9e0ee96cb645973d3f217e65728 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:03:24 -0700 Subject: [PATCH 2/5] Code cleanup --- pyqtgraph/graphicsItems/ROI.py | 68 ++-------------------------------- 1 file changed, 4 insertions(+), 64 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index bc77e1c3..6f0a46a4 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -239,6 +239,7 @@ class ROI(GraphicsObject): if isinstance(y, bool): raise TypeError("Positional arguments to setPos() must be numerical.") pos = Point(pos, y) + self.state['pos'] = pos QtGui.QGraphicsItem.setPos(self, pos) if update: @@ -253,7 +254,7 @@ class ROI(GraphicsObject): self.state['size'] = size if update: self.stateChanged(finish=finish) - + def setAngle(self, angle, update=True, finish=True): """Set the angle of rotation (in degrees) for this ROI. See setPos() for an explanation of the update and finish arguments. @@ -757,11 +758,6 @@ class ROI(GraphicsObject): else: raise Exception("New point location must be given in either 'parent' or 'scene' coordinates.") - - ## 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 'center' in h: c = h['center'] @@ -771,8 +767,6 @@ class ROI(GraphicsObject): if h['type'] == 't': snap = True if (modifiers & QtCore.Qt.ControlModifier) else None - #if self.translateSnap or (): - #snap = Point(self.snapSize, self.snapSize) self.translate(p1-p0, snap=snap, update=False) elif h['type'] == 'f': @@ -780,7 +774,6 @@ class ROI(GraphicsObject): h['item'].setPos(newPos) h['pos'] = newPos self.freeHandleMoved = True - #self.sigRegionChanged.emit(self) ## should be taken care of by call to stateChanged() elif h['type'] == 's': ## If a handle and its center have the same x or y value, we can't scale across that axis. @@ -922,10 +915,7 @@ class ROI(GraphicsObject): 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.setState(newState, update=False) self.stateChanged(finish=finish) @@ -1761,6 +1751,7 @@ class EllipseROI(ROI): return arr w = arr.shape[axes[0]] h = arr.shape[axes[1]] + ## generate an ellipsoidal mask mask = np.fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h)) @@ -2179,16 +2170,8 @@ class CrosshairROI(ROI): self.prepareGeometryChange() def boundingRect(self): - #size = self.size() - #return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized() return self.shape().boundingRect() - #def getRect(self): - ### same as boundingRect -- for internal use so that boundingRect can be re-implemented in subclasses - #size = self.size() - #return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized() - - def shape(self): if self._shape is None: radius = self.getState()['size'][1] @@ -2202,56 +2185,13 @@ class CrosshairROI(ROI): stroker.setWidth(10) outline = stroker.createStroke(p) self._shape = self.mapFromDevice(outline) - - - ##h1 = self.handles[0]['item'].pos() - ##h2 = self.handles[1]['item'].pos() - #w1 = Point(-0.5, 0)*self.size() - #w2 = Point(0.5, 0)*self.size() - #h1 = Point(0, -0.5)*self.size() - #h2 = Point(0, 0.5)*self.size() - - #dh = h2-h1 - #dw = w2-w1 - #if dh.length() == 0 or dw.length() == 0: - #return p - #pxv = self.pixelVectors(dh)[1] - #if pxv is None: - #return p - - #pxv *= 4 - - #p.moveTo(h1+pxv) - #p.lineTo(h2+pxv) - #p.lineTo(h2-pxv) - #p.lineTo(h1-pxv) - #p.lineTo(h1+pxv) - - #pxv = self.pixelVectors(dw)[1] - #if pxv is None: - #return p - - #pxv *= 4 - - #p.moveTo(w1+pxv) - #p.lineTo(w2+pxv) - #p.lineTo(w2-pxv) - #p.lineTo(w1-pxv) - #p.lineTo(w1+pxv) return self._shape def paint(self, p, *args): - #p.save() - #r = self.getRect() radius = self.getState()['size'][1] p.setRenderHint(QtGui.QPainter.Antialiasing) p.setPen(self.currentPen) - #p.translate(r.left(), r.top()) - #p.scale(r.width()/10., r.height()/10.) ## need to scale up a little because drawLine has trouble dealing with 0.5 - #p.drawLine(0,5, 10,5) - #p.drawLine(5,0, 5,10) - #p.restore() p.drawLine(Point(0, -radius), Point(0, radius)) p.drawLine(Point(-radius, 0), Point(radius, 0)) From 97b71a2b284664ed8db052fb3799434a3973f09f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:03:47 -0700 Subject: [PATCH 3/5] Add RulerROI --- pyqtgraph/graphicsItems/ROI.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 6f0a46a4..473506cf 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -2197,3 +2197,31 @@ class CrosshairROI(ROI): p.drawLine(Point(-radius, 0), Point(radius, 0)) +class RulerROI(LineSegmentROI): + def paint(self, p, *args): + LineSegmentROI.paint(self, p, *args) + h1 = self.handles[0]['item'].pos() + h2 = self.handles[1]['item'].pos() + p1 = p.transform().map(h1) + p2 = p.transform().map(h2) + + vec = Point(h2) - Point(h1) + length = vec.length() + angle = vec.angle(Point(1, 0)) + + pvec = p2 - p1 + pvecT = Point(pvec.y(), -pvec.x()) + pos = 0.5 * (p1 + p2) + pvecT * 40 / pvecT.length() + + p.resetTransform() + + txt = fn.siFormat(length, suffix='m') + '\n%0.1f deg' % angle + p.drawText(QtCore.QRectF(pos.x()-50, pos.y()-50, 100, 100), QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter, txt) + + def boundingRect(self): + r = LineSegmentROI.boundingRect(self) + pxl = self.pixelLength(Point([1, 0])) + if pxl is None: + return r + pxw = 50 * pxl + return r.adjusted(-50, -50, 50, 50) From 0de0bf4c44e5600303c5fdf317bf498d120ac947 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:05:08 -0700 Subject: [PATCH 4/5] Fix: very small ellipse/circle ROIs have bad click areas --- pyqtgraph/graphicsItems/ROI.py | 42 ++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 473506cf..850104c0 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1725,10 +1725,18 @@ class EllipseROI(ROI): """ def __init__(self, pos, size, **args): #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) + self.path = None ROI.__init__(self, pos, size, **args) + self.sigRegionChanged.connect(self._clearPath) + self._addHandles() + + def _addHandles(self): 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 _clearPath(self): + self.path = None + def paint(self, p, opt, widget): r = self.boundingRect() p.setRenderHint(QtGui.QPainter.Antialiasing) @@ -1764,8 +1772,27 @@ class EllipseROI(ROI): return arr * mask def shape(self): - self.path = QtGui.QPainterPath() - self.path.addEllipse(self.boundingRect()) + if self.path is None: + path = QtGui.QPainterPath() + + # Note: Qt has a bug where very small ellipses (radius <0.001) do + # not correctly intersect with mouse position (upper-left and + # lower-right quadrants are not clickable). + #path.addEllipse(self.boundingRect()) + + # Workaround: manually draw the path. + br = self.boundingRect() + center = br.center() + r1 = br.width() / 2. + r2 = br.height() / 2. + theta = np.linspace(0, 2*np.pi, 24) + x = center.x() + r1 * np.cos(theta) + y = center.y() + r2 * np.sin(theta) + path.moveTo(x[0], y[0]) + for i in range(1, len(x)): + path.lineTo(x[i], y[i]) + self.path = path + return self.path @@ -1782,10 +1809,15 @@ class CircleROI(EllipseROI): ============== ============================================================= """ - def __init__(self, pos, size, **args): - ROI.__init__(self, pos, size, **args) + def __init__(self, pos, size=None, radius=None, **args): + if size is None: + if radius is None: + raise TypeError("Must provide either size or radius.") + size = (radius*2, radius*2) + EllipseROI.__init__(self, pos, size, **args) self.aspectLocked = True - #self.addTranslateHandle([0.5, 0.5]) + + def _addHandles(self): self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) From 60ce541df6aa0bda66bc07a3894ab0e34f556a57 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:05:31 -0700 Subject: [PATCH 5/5] minor argument type checking --- pyqtgraph/graphicsItems/ROI.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 850104c0..9f3dae6f 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -232,6 +232,9 @@ class ROI(GraphicsObject): multiple change functions to be called sequentially while minimizing processing overhead and repeated signals. Setting ``update=False`` also forces ``finish=False``. """ + if update not in (True, False): + raise TypeError("update argument must be bool") + if y is None: pos = Point(pos) else: @@ -249,6 +252,8 @@ class ROI(GraphicsObject): """Set the size of the ROI. May be specified as a QPoint, Point, or list of two values. See setPos() for an explanation of the update and finish arguments. """ + if update not in (True, False): + raise TypeError("update argument must be bool") size = Point(size) self.prepareGeometryChange() self.state['size'] = size @@ -259,6 +264,8 @@ class ROI(GraphicsObject): """Set the angle of rotation (in degrees) for this ROI. See setPos() for an explanation of the update and finish arguments. """ + if update not in (True, False): + raise TypeError("update argument must be bool") self.state['angle'] = angle tr = QtGui.QTransform() #tr.rotate(-angle * 180 / np.pi)