ROI updates:

- ROI.movePoint now expects parent coordinates by default
   - Added ROI.getHandles()
   - Renamed MultiLineROI to MultiRectROI
   - Reorganized MultiRectROI, added addSegment and removeSegment methods (thanks Martin!)
This commit is contained in:
Luke Campagnola 2012-10-31 02:01:55 -04:00
parent 2c679dfbcc
commit eab1d75592
2 changed files with 130 additions and 125 deletions

View File

@ -36,12 +36,16 @@ img1a = pg.ImageItem(arr)
v1a.addItem(img1a) v1a.addItem(img1a)
img1b = pg.ImageItem() img1b = pg.ImageItem()
v1b.addItem(img1b) v1b.addItem(img1b)
v1a.disableAutoRange('xy')
v1b.disableAutoRange('xy')
v1a.autoRange()
v1b.autoRange()
rois = [] rois = []
rois.append(pg.RectROI([20, 20], [20, 20], pen=(0,9))) rois.append(pg.RectROI([20, 20], [20, 20], pen=(0,9)))
rois[-1].addRotateHandle([1,0], [0.5, 0.5]) rois[-1].addRotateHandle([1,0], [0.5, 0.5])
rois.append(pg.LineROI([0, 60], [20, 80], width=5, pen=(1,9))) rois.append(pg.LineROI([0, 60], [20, 80], width=5, pen=(1,9)))
rois.append(pg.MultiLineROI([[20, 90], [50, 60], [60, 90]], width=5, pen=(2,9))) rois.append(pg.MultiRectROI([[20, 90], [50, 60], [60, 90]], width=5, pen=(2,9)))
rois.append(pg.EllipseROI([60, 10], [30, 20], pen=(3,9))) rois.append(pg.EllipseROI([60, 10], [30, 20], pen=(3,9)))
rois.append(pg.CircleROI([80, 50], [20, 20], pen=(4,9))) rois.append(pg.CircleROI([80, 50], [20, 20], pen=(4,9)))
#rois.append(pg.LineSegmentROI([[110, 50], [20, 20]], pen=(5,9))) #rois.append(pg.LineSegmentROI([[110, 50], [20, 20]], pen=(5,9)))
@ -70,6 +74,10 @@ r2a = pg.PolyLineROI([[0,0], [10,10], [10,30], [30,10]], closed=True)
v2a.addItem(r2a) v2a.addItem(r2a)
r2b = pg.PolyLineROI([[0,-20], [10,-10], [10,-30]], closed=False) r2b = pg.PolyLineROI([[0,-20], [10,-10], [10,-30]], closed=False)
v2a.addItem(r2b) v2a.addItem(r2b)
v2a.disableAutoRange('xy')
#v2b.disableAutoRange('xy')
v2a.autoRange()
#v2b.autoRange()
text = """Building custom ROI types<Br> text = """Building custom ROI types<Br>
ROIs can be built with a variety of different handle types<br> ROIs can be built with a variety of different handle types<br>
@ -107,6 +115,9 @@ r3b.addRotateHandle([0, 1], [1, 0])
r3b.addScaleRotateHandle([0, 0.5], [0.5, 0.5]) r3b.addScaleRotateHandle([0, 0.5], [0.5, 0.5])
r3b.addScaleRotateHandle([1, 0.5], [0.5, 0.5]) r3b.addScaleRotateHandle([1, 0.5], [0.5, 0.5])
v3.disableAutoRange('xy')
v3.autoRange()
text = """Transforming objects with ROI""" text = """Transforming objects with ROI"""
w4 = w.addLayout(row=1, col=1) w4 = w.addLayout(row=1, col=1)
@ -121,6 +132,9 @@ img4 = pg.ImageItem(arr)
v4.addItem(r4) v4.addItem(r4)
img4.setParentItem(r4) img4.setParentItem(r4)
v4.disableAutoRange('xy')
v4.autoRange()

View File

@ -28,7 +28,7 @@ from .UIGraphicsItem import UIGraphicsItem
__all__ = [ __all__ = [
'ROI', 'ROI',
'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI',
'LineROI', 'MultiLineROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI',
] ]
@ -370,6 +370,8 @@ class ROI(GraphicsObject):
else: else:
return (self.handles[index]['name'], self.handles[index]['item'].scenePos()) return (self.handles[index]['name'], self.handles[index]['item'].scenePos())
def getHandles(self):
return [h['item'] for h in self.handles]
def mapSceneToParent(self, pt): def mapSceneToParent(self, pt):
return self.mapToParent(self.mapFromScene(pt)) return self.mapToParent(self.mapFromScene(pt))
@ -538,19 +540,27 @@ class ROI(GraphicsObject):
return True return True
def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True): def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True, coords='parent'):
## called by Handles when they are moved. ## called by Handles when they are moved.
## pos is the new position of the handle in scene coords, as requested by the handle. ## pos is the new position of the handle in scene coords, as requested by the handle.
newState = self.stateCopy() newState = self.stateCopy()
index = self.indexOfHandle(handle) index = self.indexOfHandle(handle)
h = self.handles[index] h = self.handles[index]
p0 = self.mapToScene(h['pos'] * self.state['size']) p0 = self.mapToParent(h['pos'] * self.state['size'])
p1 = Point(pos) p1 = Point(pos)
## transform p0 and p1 into parent's coordinates (same as scene coords if there is no parent). I forget why. if coords == 'parent':
p0 = self.mapSceneToParent(p0) pass
elif coords == 'scene':
p1 = self.mapSceneToParent(p1) p1 = self.mapSceneToParent(p1)
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) ## 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:
@ -566,8 +576,8 @@ class ROI(GraphicsObject):
self.translate(p1-p0, snap=snap, update=False) self.translate(p1-p0, snap=snap, update=False)
elif h['type'] == 'f': elif h['type'] == 'f':
newPos = self.mapFromScene(pos) newPos = self.mapFromParent(p1)
h['item'].setPos(self.mapFromScene(pos)) 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() #self.sigRegionChanged.emit(self) ## should be taken care of by call to stateChanged()
@ -1212,7 +1222,7 @@ class Handle(UIGraphicsItem):
#print "point moved; inform %d ROIs" % len(self.roi) #print "point moved; inform %d ROIs" % len(self.roi)
# A handle can be used by multiple ROIs; tell each to update its handle position # A handle can be used by multiple ROIs; tell each to update its handle position
for r in self.rois: for r in self.rois:
r.movePoint(self, pos, modifiers, finish=finish) r.movePoint(self, pos, modifiers, finish=finish, coords='scene')
def buildPath(self): def buildPath(self):
size = self.radius size = self.radius
@ -1264,9 +1274,9 @@ class Handle(UIGraphicsItem):
if self._shape is None: if self._shape is None:
s = self.generateShape() s = self.generateShape()
if s is None: if s is None:
return self.shape return self.path
self._shape = s self._shape = s
self.prepareGeometryChange() self.prepareGeometryChange() ## beware--this can cause the view to adjust, which would immediately invalidate the shape.
return self._shape return self._shape
def boundingRect(self): def boundingRect(self):
@ -1358,8 +1368,15 @@ class LineROI(ROI):
self.addScaleHandle([0.5, 1], [0.5, 0.5]) self.addScaleHandle([0.5, 1], [0.5, 0.5])
class MultiLineROI(QtGui.QGraphicsObject):
class MultiRectROI(QtGui.QGraphicsObject):
"""
Chain of rectangular ROIs connected by handles.
This is generally used to mark a curved path through
an image similarly to PolyLineROI. It differs in that each segment
of the chain is rectangular instead of linear and thus has width.
"""
sigRegionChangeFinished = QtCore.Signal(object) sigRegionChangeFinished = QtCore.Signal(object)
sigRegionChangeStarted = QtCore.Signal(object) sigRegionChangeStarted = QtCore.Signal(object)
sigRegionChanged = QtCore.Signal(object) sigRegionChanged = QtCore.Signal(object)
@ -1368,27 +1385,17 @@ class MultiLineROI(QtGui.QGraphicsObject):
QtGui.QGraphicsObject.__init__(self) QtGui.QGraphicsObject.__init__(self)
self.pen = pen self.pen = pen
self.roiArgs = args self.roiArgs = args
self.lines = []
if len(points) < 2: if len(points) < 2:
raise Exception("Must start with at least 2 points") raise Exception("Must start with at least 2 points")
self.lines = []
self.lines.append(ROI([0, 0], [1, 5], parent=self, pen=pen, **args))
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, pen=pen, **args))
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: ## create first segment
l.translatable = False self.addSegment(points[1], connectTo=points[0], scaleHandle=True)
l.sigRegionChanged.connect(self.roiChangedEvent)
l.sigRegionChangeStarted.connect(self.roiChangeStartedEvent) ## create remaining segments
l.sigRegionChangeFinished.connect(self.roiChangeFinishedEvent) for p in points[2:]:
self.addSegment(p)
def paint(self, *args): def paint(self, *args):
pass pass
@ -1411,6 +1418,12 @@ class MultiLineROI(QtGui.QGraphicsObject):
def roiChangeFinishedEvent(self): def roiChangeFinishedEvent(self):
self.sigRegionChangeFinished.emit(self) self.sigRegionChangeFinished.emit(self)
def getHandlePositions(self):
"""Return the positions of all handles in local coordinates."""
pos = [self.mapFromScene(self.lines[0].getHandles()[0].scenePos())]
for l in self.lines:
pos.append(self.mapFromScene(l.getHandles()[1].scenePos()))
return pos
def getArrayRegion(self, arr, img=None, axes=(0,1)): def getArrayRegion(self, arr, img=None, axes=(0,1)):
rgns = [] rgns = []
@ -1432,6 +1445,59 @@ class MultiLineROI(QtGui.QGraphicsObject):
return np.concatenate(rgns, axis=axes[0]) return np.concatenate(rgns, axis=axes[0])
def addSegment(self, pos=(0,0), scaleHandle=False, connectTo=None):
"""
Add a new segment to the ROI connecting from the previous endpoint to *pos*.
(pos is specified in the parent coordinate system of the MultiRectROI)
"""
## by default, connect to the previous endpoint
if connectTo is None:
connectTo = self.lines[-1].getHandles()[1]
## create new ROI
newRoi = ROI((0,0), [1, 5], parent=self, pen=self.pen, **self.roiArgs)
self.lines.append(newRoi)
## Add first SR handle
if isinstance(connectTo, Handle):
self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5], item=connectTo)
newRoi.movePoint(connectTo, connectTo.scenePos(), coords='scene')
else:
h = self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5])
newRoi.movePoint(h, connectTo, coords='scene')
## add second SR handle
h = self.lines[-1].addScaleRotateHandle([1, 0.5], [0, 0.5])
newRoi.movePoint(h, pos)
## optionally add scale handle (this MUST come after the two SR handles)
if scaleHandle:
newRoi.addScaleHandle([0.5, 1], [0.5, 0.5])
newRoi.translatable = False
newRoi.sigRegionChanged.connect(self.roiChangedEvent)
newRoi.sigRegionChangeStarted.connect(self.roiChangeStartedEvent)
newRoi.sigRegionChangeFinished.connect(self.roiChangeFinishedEvent)
self.sigRegionChanged.emit(self)
def removeSegment(self, index=-1):
"""Remove a segment from the ROI."""
roi = self.lines[index]
self.lines.pop(index)
self.scene().removeItem(roi)
roi.sigRegionChanged.disconnect(self.roiChangedEvent)
roi.sigRegionChangeStarted.disconnect(self.roiChangeStartedEvent)
roi.sigRegionChangeFinished.disconnect(self.roiChangeFinishedEvent)
self.sigRegionChanged.emit(self)
class MultiLineROI(MultiRectROI):
def __init__(self, *args, **kwds):
MultiRectROI.__init__(self, *args, **kwds)
print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)")
class EllipseROI(ROI): class EllipseROI(ROI):
def __init__(self, pos, size, **args): def __init__(self, pos, size, **args):
@ -1475,6 +1541,8 @@ class CircleROI(EllipseROI):
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])
class PolygonROI(ROI): class PolygonROI(ROI):
## deprecated. Use PloyLineROI instead.
def __init__(self, positions, pos=None, **args): def __init__(self, positions, pos=None, **args):
if pos is None: if pos is None:
pos = [0,0] pos = [0,0]
@ -1483,16 +1551,17 @@ class PolygonROI(ROI):
for p in positions: for p in positions:
self.addFreeHandle(p) self.addFreeHandle(p)
self.setZValue(1000) self.setZValue(1000)
print("Warning: PolygonROI is deprecated. Use PolyLineROI instead.")
def listPoints(self): def listPoints(self):
return [p['item'].pos() for p in self.handles] return [p['item'].pos() for p in self.handles]
def movePoint(self, *args, **kargs): #def movePoint(self, *args, **kargs):
ROI.movePoint(self, *args, **kargs) #ROI.movePoint(self, *args, **kargs)
self.prepareGeometryChange() #self.prepareGeometryChange()
for h in self.handles: #for h in self.handles:
h['pos'] = h['item'].pos() #h['pos'] = h['item'].pos()
def paint(self, p, *args): def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing) p.setRenderHint(QtGui.QPainter.Antialiasing)
@ -1687,103 +1756,33 @@ class LineSegmentROI(ROI):
ROI.__init__(self, pos, [1,1], **args) ROI.__init__(self, pos, [1,1], **args)
#ROI.__init__(self, positions[0]) #ROI.__init__(self, positions[0])
if len(positions) > 2: if len(positions) > 2:
raise Exception("LineSegmentROI can only be defined by 2 positions. This is an API change.") raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.")
for i, p in enumerate(positions): for i, p in enumerate(positions):
self.addFreeHandle(p, item=handles[i]) self.addFreeHandle(p, item=handles[i])
#self.setZValue(1000)
#self.parentROI = None
#self.hasParentROI = False
#self.setAcceptsHandles(acceptsHandles)
#def setParentROI(self, parent):
#self.parentROI = parent
#if parent != None:
#self.hasParentROI = True
#else:
#self.hasParentROI = False
#def setAcceptsHandles(self, b):
#if b:
#self.setAcceptedMouseButtons(QtCore.Qt.LeftButton)
#else:
#self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
#def close(self):
##for h in self.handles:
##if len(h['item'].roi) == 1:
##h['item'].scene().removeItem(h['item'])
##elif h['item'].parentItem() == self:
##h['item'].setParentItem(self.parentItem())
#self.scene().removeItem(self)
#def handleRemoved(self, handle):
#self.parentROI.handleRemoved(self, handle)
#def hoverEvent(self, ev):
#if (self.translatable or self.acceptsHandles) and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
##print " setHover: True"
#self.setMouseHover(True)
#self.sigHoverEvent.emit(self)
#else:
##print " setHover: False"
#self.setMouseHover(False)
#def mouseClickEvent(self, ev):
#ROI.mouseClickEvent(self, ev) ## only checks for Right-clicks (for now anyway)
#if ev.button() == QtCore.Qt.LeftButton:
#if self.acceptsHandles:
#ev.accept()
#self.newHandleRequested(ev.pos()) ## ev.pos is the position in this item's coordinates
#else:
#ev.ignore()
#def newHandleRequested(self, evPos):
#print "newHandleRequested"
#if evPos - self.handles[0].pos() == Point(0.,0.) or evPos-handles[1].pos() == Point(0.,0.):
# return
#self.parentROI.newHandleRequested(self, self.mapToParent(evPos)) ## so now evPos should be passed in in the parents coordinate system
def listPoints(self): def listPoints(self):
return [p['item'].pos() for p in self.handles] return [p['item'].pos() for p in self.handles]
#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): def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing) p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen) p.setPen(self.currentPen)
h1 = self.handles[0]['item'].pos() h1 = self.handles[0]['item'].pos()
h2 = self.handles[1]['item'].pos() h2 = self.handles[1]['item'].pos()
p.drawLine(h1, h2) p.drawLine(h1, h2)
#p.setPen(fn.mkPen('w'))
#p.drawPath(self.shape())
#for i in range(len(self.handles)-1):
#h1 = self.handles[i]['item'].pos()
#h2 = self.handles[i+1]['item'].pos()
#p.drawLine(h1, h2)
def boundingRect(self): def boundingRect(self):
return self.shape().boundingRect() return self.shape().boundingRect()
#r = QtCore.QRectF()
#for h in self.handles:
#r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs
#return r
def shape(self): def shape(self):
p = QtGui.QPainterPath() p = QtGui.QPainterPath()
#pw, ph = self.pixelSize()
#pHyp = 4 * (pw**2 + ph**2)**0.5
h1 = self.handles[0]['item'].pos() h1 = self.handles[0]['item'].pos()
h2 = self.handles[1]['item'].pos() h2 = self.handles[1]['item'].pos()
dh = h2-h1
if dh.length() == 0:
return p
pxv = self.pixelVectors(h2-h1)[1] pxv = self.pixelVectors(h2-h1)[1]
if pxv is None: if pxv is None:
@ -1799,14 +1798,6 @@ class LineSegmentROI(ROI):
return p 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
def getArrayRegion(self, data, img, axes=(0,1)): def getArrayRegion(self, data, img, axes=(0,1)):
""" """
Use the position of this ROI relative to an imageItem to pull a slice from an array. Use the position of this ROI relative to an imageItem to pull a slice from an array.
@ -1849,11 +1840,11 @@ class SpiralROI(ROI):
return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r) return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r)
#return self.bounds #return self.bounds
def movePoint(self, *args, **kargs): #def movePoint(self, *args, **kargs):
ROI.movePoint(self, *args, **kargs) #ROI.movePoint(self, *args, **kargs)
self.prepareGeometryChange() #self.prepareGeometryChange()
for h in self.handles: #for h in self.handles:
h['pos'] = h['item'].pos()/self.state['size'][0] #h['pos'] = h['item'].pos()/self.state['size'][0]
def stateChanged(self): def stateChanged(self):
ROI.stateChanged(self) ROI.stateChanged(self)