Merge pull request #589 from acq4/roi-updates

Roi updates
This commit is contained in:
Luke Campagnola 2017-10-04 10:13:43 -07:00 committed by GitHub
commit 54f41bae8b

View File

@ -26,7 +26,8 @@ from .. import getConfigOption
__all__ = [ __all__ = [
'ROI', 'ROI',
'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI',
'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', 'CrosshairROI', 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI',
'CrosshairROI',
] ]
@ -231,6 +232,9 @@ class ROI(GraphicsObject):
multiple change functions to be called sequentially while minimizing processing overhead multiple change functions to be called sequentially while minimizing processing overhead
and repeated signals. Setting ``update=False`` also forces ``finish=False``. 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: if y is None:
pos = Point(pos) pos = Point(pos)
else: else:
@ -238,6 +242,7 @@ class ROI(GraphicsObject):
if isinstance(y, bool): if isinstance(y, bool):
raise TypeError("Positional arguments to setPos() must be numerical.") raise TypeError("Positional arguments to setPos() must be numerical.")
pos = Point(pos, y) pos = Point(pos, y)
self.state['pos'] = pos self.state['pos'] = pos
QtGui.QGraphicsItem.setPos(self, pos) QtGui.QGraphicsItem.setPos(self, pos)
if update: if update:
@ -247,16 +252,20 @@ class ROI(GraphicsObject):
"""Set the size of the ROI. May be specified as a QPoint, Point, or list of two values. """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. 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) size = Point(size)
self.prepareGeometryChange() self.prepareGeometryChange()
self.state['size'] = size self.state['size'] = size
if update: if update:
self.stateChanged(finish=finish) self.stateChanged(finish=finish)
def setAngle(self, angle, update=True, finish=True): def setAngle(self, angle, update=True, finish=True):
"""Set the angle of rotation (in degrees) for this ROI. """Set the angle of rotation (in degrees) for this ROI.
See setPos() for an explanation of the update and finish arguments. 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 self.state['angle'] = angle
tr = QtGui.QTransform() tr = QtGui.QTransform()
#tr.rotate(-angle * 180 / np.pi) #tr.rotate(-angle * 180 / np.pi)
@ -756,11 +765,6 @@ class ROI(GraphicsObject):
else: else:
raise Exception("New point location must be given in either 'parent' or 'scene' coordinates.") 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) ## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1)
if 'center' in h: if 'center' in h:
c = h['center'] c = h['center']
@ -770,8 +774,6 @@ class ROI(GraphicsObject):
if h['type'] == 't': if h['type'] == 't':
snap = True if (modifiers & QtCore.Qt.ControlModifier) else None 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) self.translate(p1-p0, snap=snap, update=False)
elif h['type'] == 'f': elif h['type'] == 'f':
@ -779,7 +781,6 @@ class ROI(GraphicsObject):
h['item'].setPos(newPos) h['item'].setPos(newPos)
h['pos'] = newPos h['pos'] = newPos
self.freeHandleMoved = True self.freeHandleMoved = True
#self.sigRegionChanged.emit(self) ## should be taken care of by call to stateChanged()
elif h['type'] == 's': elif h['type'] == 's':
## If a handle and its center have the same x or y value, we can't scale across that axis. ## If a handle and its center have the same x or y value, we can't scale across that axis.
@ -921,10 +922,7 @@ class ROI(GraphicsObject):
r = self.stateRect(newState) r = self.stateRect(newState)
if not self.maxBounds.contains(r): if not self.maxBounds.contains(r):
return return
#self.setTransform(tr)
#self.setPos(newState['pos'], update=False)
#self.prepareGeometryChange()
#self.state = newState
self.setState(newState, update=False) self.setState(newState, update=False)
self.stateChanged(finish=finish) self.stateChanged(finish=finish)
@ -1734,10 +1732,18 @@ class EllipseROI(ROI):
""" """
def __init__(self, pos, size, **args): def __init__(self, pos, size, **args):
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
self.path = None
ROI.__init__(self, pos, size, **args) 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.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]) 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): def paint(self, p, opt, widget):
r = self.boundingRect() r = self.boundingRect()
p.setRenderHint(QtGui.QPainter.Antialiasing) p.setRenderHint(QtGui.QPainter.Antialiasing)
@ -1760,6 +1766,7 @@ class EllipseROI(ROI):
return arr return arr
w = arr.shape[axes[0]] w = arr.shape[axes[0]]
h = arr.shape[axes[1]] h = arr.shape[axes[1]]
## generate an ellipsoidal mask ## 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)) 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))
@ -1772,8 +1779,27 @@ class EllipseROI(ROI):
return arr * mask return arr * mask
def shape(self): def shape(self):
self.path = QtGui.QPainterPath() if self.path is None:
self.path.addEllipse(self.boundingRect()) 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 return self.path
@ -1790,10 +1816,15 @@ class CircleROI(EllipseROI):
============== ============================================================= ============== =============================================================
""" """
def __init__(self, pos, size, **args): def __init__(self, pos, size=None, radius=None, **args):
ROI.__init__(self, pos, size, **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.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]) self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5])
@ -2157,79 +2188,6 @@ class _PolyLineSegment(LineSegmentROI):
return LineSegmentROI.hoverEvent(self, ev) 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): class CrosshairROI(ROI):
"""A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable.""" """A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable."""
@ -2251,16 +2209,8 @@ class CrosshairROI(ROI):
self.prepareGeometryChange() self.prepareGeometryChange()
def boundingRect(self): def boundingRect(self):
#size = self.size()
#return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized()
return self.shape().boundingRect() 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): def shape(self):
if self._shape is None: if self._shape is None:
radius = self.getState()['size'][1] radius = self.getState()['size'][1]
@ -2274,58 +2224,43 @@ class CrosshairROI(ROI):
stroker.setWidth(10) stroker.setWidth(10)
outline = stroker.createStroke(p) outline = stroker.createStroke(p)
self._shape = self.mapFromDevice(outline) 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 return self._shape
def paint(self, p, *args): def paint(self, p, *args):
#p.save()
#r = self.getRect()
radius = self.getState()['size'][1] radius = self.getState()['size'][1]
p.setRenderHint(QtGui.QPainter.Antialiasing) p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen) 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(0, -radius), Point(0, radius))
p.drawLine(Point(-radius, 0), Point(radius, 0)) 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)