diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index b99465b5..ef2f329b 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -36,8 +36,15 @@ def rectStr(r): return "[%f, %f] + [%f, %f]" % (r.x(), r.y(), r.width(), r.height()) class ROI(GraphicsObject): - """Generic region-of-interest widget. - Can be used for implementing many types of selection box with rotate/translate/scale handles. + """ + Generic region-of-interest widget. + + Can be used for implementing many types of selection box with + rotate/translate/scale handles. + ROIs can be customized to have a variety of shapes (by subclassing or using + any of the built-in subclasses) and any combination of draggable handles + that allow the user to manibulate the ROI. + Signals ----------------------- ---------------------------------------------------- @@ -59,6 +66,42 @@ class ROI(GraphicsObject): sigRemoveRequested Emitted when the user selects 'remove' from the ROI's context menu (if available). ----------------------- ---------------------------------------------------- + + + Arguments + ---------------- ----------------------------------------------------------- + pos (length-2 sequence) Indicates the position of the ROI's + origin. For most ROIs, this is the lower-left corner of + its bounding rectangle. + size (length-2 sequence) Indicates the width and height of the + ROI. + angle (float) The rotation of the ROI in degrees. Default is 0. + invertible (bool) If True, the user may resize the ROI to have + negative width or height (assuming the ROI has scale + handles). Default is False. + maxBounds (QRect, QRectF, or None) Specifies boundaries that the ROI + cannot be dragged outside of by the user. Default is None. + snapSize (float) The spacing of snap positions used when *scaleSnap* + or *translateSnap* are enabled. Default is 1.0. + scaleSnap (bool) If True, the width and height of the ROI are forced + to be integer multiples of *snapSize* when being resized + by the user. Default is False. + translateSnap (bool) If True, the x and y positions of the ROI are forced + to be integer multiples of *snapSize* when being resized + by the user. Default is False. + rotateSnap (bool) If True, the ROI angle is forced to a multiple of + 15 degrees when rotated by the user. Default is False. + parent (QGraphicsItem) The graphics item parent of this ROI. It + is generally not necessary to specify the parent. + pen (QPen or argument to pg.mkPen) The pen to use when drawing + the shape of the ROI. + movable (bool) If True, the ROI can be moved by dragging anywhere + inside the ROI. Default is True. + removable (bool) If True, the ROI will be given a context menu with + an option to remove the ROI. The ROI emits + sigRemoveRequested when this menu action is selected. + Default is False. + ---------------- ----------------------------------------------------------- """ sigRegionChangeFinished = QtCore.Signal(object) @@ -117,7 +160,11 @@ class ROI(GraphicsObject): return sc def saveState(self): - """Return the state of the widget in a format suitable for storing to disk. (Points are converted to tuple)""" + """Return the state of the widget in a format suitable for storing to + disk. (Points are converted to tuple) + + Combined with setState(), this allows ROIs to be easily saved and + restored.""" state = {} state['pos'] = tuple(self.state['pos']) state['size'] = tuple(self.state['size']) @@ -125,6 +172,10 @@ class ROI(GraphicsObject): return state def setState(self, state, update=True): + """ + Set the state of the ROI from a structure generated by saveState() or + getState(). + """ self.setPos(state['pos'], update=False) self.setSize(state['size'], update=False) self.setAngle(state['angle'], update=update) @@ -135,20 +186,31 @@ class ROI(GraphicsObject): h['item'].setZValue(z+1) def parentBounds(self): + """ + Return the bounding rectangle of this ROI in the coordinate system + of its parent. + """ return self.mapToParent(self.boundingRect()).boundingRect() def setPen(self, pen): + """ + Set the pen to use when drawing the ROI shape. + """ self.pen = fn.mkPen(pen) self.currentPen = self.pen self.update() def size(self): + """Return the size (w,h) of the ROI.""" return self.getState()['size'] def pos(self): + """Return the position (x,y) of the ROI's origin. + For most ROIs, this will be the lower-left corner.""" return self.getState()['pos'] def angle(self): + """Return the angle of the ROI in degrees.""" return self.getState()['angle'] def setPos(self, pos, update=True, finish=True): @@ -264,21 +326,86 @@ class ROI(GraphicsObject): #self.stateChanged() def rotate(self, angle, update=True, finish=True): + """ + Rotate the ROI by *angle* degrees. + + Also accepts *update* and *finish* arguments (see setPos() for a + description of these). + """ self.setAngle(self.angle()+angle, update=update, finish=finish) def handleMoveStarted(self): self.preMoveState = self.getState() def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None): + """ + Add a new translation handle to the ROI. Dragging the handle will move + the entire ROI without changing its angle or shape. + + Note that, by default, ROIs may be moved by dragging anywhere inside the + ROI. However, for larger ROIs it may be desirable to disable this and + instead provide one or more translation handles. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ pos = Point(pos) return self.addHandle({'name': name, 'type': 't', 'pos': pos, 'item': item}, index=index) def addFreeHandle(self, pos=None, axes=None, item=None, name=None, index=None): + """ + Add a new free handle to the ROI. Dragging free handles has no effect + on the position or shape of the ROI. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ if pos is not None: pos = Point(pos) return self.addHandle({'name': name, 'type': 'f', 'pos': pos, 'item': item}, index=index) def addScaleHandle(self, pos, center, axes=None, item=None, name=None, lockAspect=False, index=None): + """ + Add a new scale handle to the ROI. Dragging a scale handle allows the + user to change the height and/or width of the ROI. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + center (length-2 sequence) The center point around which + scaling takes place. If the center point has the + same x or y value as the handle position, then + scaling will be disabled for that axis. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ pos = Point(pos) center = Point(center) info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item, 'lockAspect': lockAspect} @@ -289,11 +416,51 @@ class ROI(GraphicsObject): return self.addHandle(info, index=index) def addRotateHandle(self, pos, center, item=None, name=None, index=None): + """ + Add a new rotation handle to the ROI. Dragging a rotation handle allows + the user to change the angle of the ROI. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + center (length-2 sequence) The center point around which + rotation takes place. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ pos = Point(pos) center = Point(center) return self.addHandle({'name': name, 'type': 'r', 'center': center, 'pos': pos, 'item': item}, index=index) def addScaleRotateHandle(self, pos, center, item=None, name=None, index=None): + """ + Add a new scale+rotation handle to the ROI. When dragging a handle of + this type, the user can simultaneously rotate the ROI around an + arbitrary center point as well as scale the ROI by dragging the handle + toward or away from the center point. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + center (length-2 sequence) The center point around which + scaling and rotation take place. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ pos = Point(pos) center = Point(center) if pos[0] != center[0] and pos[1] != center[1]: @@ -301,6 +468,27 @@ class ROI(GraphicsObject): return self.addHandle({'name': name, 'type': 'sr', 'center': center, 'pos': pos, 'item': item}, index=index) def addRotateFreeHandle(self, pos, center, axes=None, item=None, name=None, index=None): + """ + Add a new rotation+free handle to the ROI. When dragging a handle of + this type, the user can rotate the ROI around an + arbitrary center point, while moving toward or away from the center + point has no effect on the shape of the ROI. + + Arguments: + ------------------- ---------------------------------------------------- + pos (length-2 sequence) The position of the handle + relative to the shape of the ROI. A value of (0,0) + indicates the origin, whereas (1, 1) indicates the + upper-right corner, regardless of the ROI's size. + center (length-2 sequence) The center point around which + rotation takes place. + item The Handle instance to add. If None, a new handle + will be created. + name The name of this handle (optional). Handles are + identified by name when calling + getLocalHandlePositions and getSceneHandlePositions. + ------------------- ---------------------------------------------------- + """ pos = Point(pos) center = Point(center) return self.addHandle({'name': name, 'type': 'rf', 'center': center, 'pos': pos, 'item': item}, index=index) @@ -329,6 +517,9 @@ class ROI(GraphicsObject): return h def indexOfHandle(self, handle): + """ + Return the index of *handle* in the list of this ROI's handles. + """ if isinstance(handle, Handle): index = [i for i, info in enumerate(self.handles) if info['item'] is handle] if len(index) == 0: @@ -338,7 +529,8 @@ class ROI(GraphicsObject): return handle def removeHandle(self, handle): - """Remove a handle from this ROI. Argument may be either a Handle instance or the integer index of the handle.""" + """Remove a handle from this ROI. Argument may be either a Handle + instance or the integer index of the handle.""" index = self.indexOfHandle(handle) handle = self.handles[index]['item'] @@ -349,20 +541,17 @@ class ROI(GraphicsObject): self.stateChanged() def replaceHandle(self, oldHandle, newHandle): - """Replace one handle in the ROI for another. This is useful when connecting multiple ROIs together. - *oldHandle* may be a Handle instance or the index of a handle.""" - #print "=========================" - #print "replace", oldHandle, newHandle - #print self - #print self.handles - #print "-----------------" + """Replace one handle in the ROI for another. This is useful when + connecting multiple ROIs together. + + *oldHandle* may be a Handle instance or the index of a handle to be + replaced.""" index = self.indexOfHandle(oldHandle) info = self.handles[index] self.removeHandle(index) info['item'] = newHandle info['pos'] = newHandle.pos() self.addHandle(info, index=index) - #print self.handles def checkRemoveHandle(self, handle): ## This is used when displaying a Handle's context menu to determine @@ -373,7 +562,10 @@ class ROI(GraphicsObject): def getLocalHandlePositions(self, index=None): - """Returns the position of a handle in ROI coordinates""" + """Returns the position of handles in the ROI's coordinate system. + + The format returned is a list of (name, pos) tuples. + """ if index == None: positions = [] for h in self.handles: @@ -383,6 +575,10 @@ class ROI(GraphicsObject): return (self.handles[index]['name'], self.handles[index]['pos']) def getSceneHandlePositions(self, index=None): + """Returns the position of handles in the scene coordinate system. + + The format returned is a list of (name, pos) tuples. + """ if index == None: positions = [] for h in self.handles: @@ -392,6 +588,9 @@ class ROI(GraphicsObject): return (self.handles[index]['name'], self.handles[index]['item'].scenePos()) def getHandles(self): + """ + Return a list of this ROI's Handles. + """ return [h['item'] for h in self.handles] def mapSceneToParent(self, pt): @@ -467,8 +666,6 @@ class ROI(GraphicsObject): self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self)) self.removeTimer.start(0) - - def mouseDragEvent(self, ev): if ev.isStart(): #p = ev.pos() @@ -510,56 +707,16 @@ class ROI(GraphicsObject): self.sigClicked.emit(self, ev) else: ev.ignore() - - - def cancelMove(self): self.isMoving = False self.setState(self.preMoveState) - - #def pointDragEvent(self, pt, ev): - ### just for handling drag start/stop. - ### drag moves are handled through movePoint() - - #if ev.isStart(): - #self.isMoving = True - #self.preMoveState = self.getState() - - #self.sigRegionChangeStarted.emit(self) - #elif ev.isFinish(): - #self.isMoving = False - #self.sigRegionChangeFinished.emit(self) - #return - - - #def pointPressEvent(self, pt, ev): - ##print "press" - #self.isMoving = True - #self.preMoveState = self.getState() - - ##self.emit(QtCore.SIGNAL('regionChangeStarted'), self) - #self.sigRegionChangeStarted.emit(self) - ##self.pressPos = self.mapFromScene(ev.scenePos()) - ##self.pressHandlePos = self.handles[pt]['item'].pos() - - #def pointReleaseEvent(self, pt, ev): - ##print "release" - #self.isMoving = False - ##self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - #self.sigRegionChangeFinished.emit(self) - - #def pointMoveEvent(self, pt, ev): - #self.movePoint(pt, ev.scenePos(), ev.modifiers()) - - def checkPointMove(self, handle, pos, modifiers): """When handles move, they must ask the ROI if the move is acceptable. By default, this always returns True. Subclasses may wish override. """ return True - def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True, coords='parent'): ## called by Handles when they are moved. @@ -804,7 +961,6 @@ class ROI(GraphicsObject): round(pos[1] / snap[1]) * snap[1] ) - def boundingRect(self): return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() @@ -871,7 +1027,25 @@ class ROI(GraphicsObject): return bounds, tr def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): - """Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array. + """Use the position and orientation of this ROI relative to an imageItem + to pull a slice from an array. + + **Arguments** + ------------------- ---------------------------------------------------- + data The array to slice from. Note that this array does + *not* have to be the same data that is represented + in *img*. + img (ImageItem or other suitable QGraphicsItem) + Used to determine the relationship between the + ROI and the boundaries of *data*. + axes (length-2 tuple) Specifies the axes in *data* that + correspond to the x and y axes of *img*. + returnMappedCoords (bool) If True, the array slice is returned along + with a corresponding array of coordinates that were + used to extract data from the original array. + **kwds All keyword arguments are passed to + :func:`affineSlice `. + ------------------- ---------------------------------------------------- This method uses :func:`affineSlice ` to generate the slice from *data* and uses :func:`getAffineSliceParams ` to determine the parameters to @@ -1088,7 +1262,18 @@ class ROI(GraphicsObject): class Handle(UIGraphicsItem): + """ + Handle represents a single user-interactable point attached to an ROI. They + are usually created by a call to one of the ROI.add___Handle() methods. + Handles are represented as a square, diamond, or circle, and are drawn with + fixed pixel size regardless of the scaling of the view they are displayed in. + + Handles may be dragged to change the position, size, orientation, or other + properties of the ROI they are attached to. + + + """ types = { ## defines number of sides, start angle for each handle type 't': (4, np.pi/4), 'f': (4, np.pi/4), @@ -1360,6 +1545,22 @@ class TestROI(ROI): class RectROI(ROI): + """ + Rectangular ROI subclass with a single scale handle at the top-right corner. + + **Arguments** + -------------- ------------------------------------------------------------- + pos (length-2 sequence) The position of the ROI origin. + See ROI(). + size (length-2 sequence) The size of the ROI. See ROI(). + centered (bool) If True, scale handles affect the ROI relative to its + center, rather than its origin. + sideScalers (bool) If True, extra scale handles are added at the top and + right edges. + **args All extra keyword arguments are passed to 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) @@ -1375,6 +1576,22 @@ class RectROI(ROI): self.addScaleHandle([0.5, 1], [0.5, center[1]]) class LineROI(ROI): + """ + Rectangular ROI subclass with scale-rotate handles on either side. This + allows the ROI to be positioned as if moving the ends of a line segment. + A third handle controls the width of the ROI orthogonal to its "line" axis. + + **Arguments** + -------------- ------------------------------------------------------------- + pos1 (length-2 sequence) The position of the center of the ROI's + left edge. + pos2 (length-2 sequence) The position of the center of the ROI's + right edge. + width (float) The width of the ROI. + **args All extra keyword arguments are passed to ROI() + -------------- ------------------------------------------------------------- + + """ def __init__(self, pos1, pos2, width, **args): pos1 = Point(pos1) pos2 = Point(pos2) @@ -1399,6 +1616,13 @@ class MultiRectROI(QtGui.QGraphicsObject): 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. + + **Arguments** + -------------- ------------------------------------------------------------- + points (list of length-2 sequences) The list of points in the path. + width (float) The width of the ROIs orthogonal to the path. + **args All extra keyword arguments are passed to ROI() + -------------- ------------------------------------------------------------- """ sigRegionChangeFinished = QtCore.Signal(object) sigRegionChangeStarted = QtCore.Signal(object) @@ -1523,6 +1747,18 @@ class MultiLineROI(MultiRectROI): print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)") class EllipseROI(ROI): + """ + Elliptical ROI subclass with one scale handle and one rotation handle. + + + **Arguments** + -------------- ------------------------------------------------------------- + pos (length-2 sequence) The position of the ROI's origin. + size (length-2 sequence) The size of the ROI's bounding rectangle. + **args All extra keyword arguments are passed to ROI() + -------------- ------------------------------------------------------------- + + """ def __init__(self, pos, size, **args): #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) ROI.__init__(self, pos, size, **args) @@ -1540,6 +1776,10 @@ class EllipseROI(ROI): p.drawEllipse(r) def getArrayRegion(self, arr, img=None): + """ + Return the result of ROI.getArrayRegion() masked by the elliptical shape + of the ROI. Regions outside the ellipse are set to 0. + """ arr = ROI.getArrayRegion(self, arr, img) if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0: return None @@ -1557,12 +1797,25 @@ class EllipseROI(ROI): class CircleROI(EllipseROI): + """ + Circular ROI subclass. Behaves exactly as EllipseROI, but may only be scaled + proportionally to maintain its aspect ratio. + + **Arguments** + -------------- ------------------------------------------------------------- + pos (length-2 sequence) The position of the ROI's origin. + size (length-2 sequence) The size of the ROI's bounding rectangle. + **args All extra keyword arguments are passed to ROI() + -------------- ------------------------------------------------------------- + + """ 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]) - + + class PolygonROI(ROI): ## deprecated. Use PloyLineROI instead. @@ -1616,8 +1869,24 @@ class PolygonROI(ROI): return sc class PolyLineROI(ROI): - """Container class for multiple connected LineSegmentROIs. Responsible for adding new - line segments, and for translation/(rotation?) of multiple lines together.""" + """ + Container class for multiple connected LineSegmentROIs. + + This class allows the user to draw paths of multiple line segments. + + **Arguments** + -------------- ------------------------------------------------------------- + positions (list of length-2 sequences) The list of points in the path. + Note that, unlike the handle positions specified in other + ROIs, these positions must be expressed in the normal + coordinate system of the ROI, rather than (0 to 1) relative + to the size of the ROI. + closed (bool) if True, an extra LineSegmentROI is added connecting + the beginning and end points. + **args All extra keyword arguments are passed to ROI() + -------------- ------------------------------------------------------------- + + """ def __init__(self, positions, closed=False, pos=None, **args): if pos is None: @@ -1730,6 +1999,10 @@ class PolyLineROI(ROI): return p def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): + """ + Return the result of ROI.getArrayRegion(), masked by the shape of the + ROI. Values outside the ROI shape are set to 0. + """ sl = self.getArraySlice(data, img, axes=(0,1)) if sl is None: return None @@ -1758,6 +2031,14 @@ class PolyLineROI(ROI): class LineSegmentROI(ROI): """ ROI subclass with two freely-moving handles defining a line. + + **Arguments** + -------------- ------------------------------------------------------------- + positions (list of two length-2 sequences) The endpoints of the line + segment. Note that, unlike the handle positions specified in + other ROIs, these positions must be expressed in the normal + coordinate system of the ROI, rather than (0 to 1) relative + to the size of the ROI. """ def __init__(self, positions=(None, None), pos=None, handles=(None,None), **args): @@ -1810,8 +2091,13 @@ class LineSegmentROI(ROI): 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. - Since this pulls 1D data from a 2D coordinate system, the return value will have ndim = data.ndim-1 + Use the position of this ROI relative to an imageItem to pull a slice + from an array. + + Since this pulls 1D data from a 2D coordinate system, the return value + will have ndim = data.ndim-1 + + See ROI.getArrayRegion() for a description of the arguments. """ imgPts = [self.mapToItem(img, h['item'].pos()) for h in self.handles]