From 4eadccdcc19214d161781c8880077e3fa2901510 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 18 Apr 2012 00:02:15 -0400 Subject: [PATCH] documentation updates --- GraphicsScene/GraphicsScene.py | 53 +-- GraphicsScene/mouseEvents.py | 121 ++++++- __init__.py | 16 +- documentation/source/apireference.rst | 1 + .../source/graphicsscene/graphicsscene.rst | 8 + .../source/graphicsscene/hoverevent.rst | 5 + documentation/source/graphicsscene/index.rst | 12 + .../source/graphicsscene/mouseclickevent.rst | 5 + .../source/graphicsscene/mousedragevent.rst | 5 + documentation/source/how_to_use.rst | 4 +- documentation/source/introduction.rst | 4 +- graphicsItems/PlotItem/PlotItem.py | 6 + imageview/ImageView.py | 327 ++++++++++-------- widgets/PlotWidget.py | 30 +- 14 files changed, 409 insertions(+), 188 deletions(-) create mode 100644 documentation/source/graphicsscene/graphicsscene.rst create mode 100644 documentation/source/graphicsscene/hoverevent.rst create mode 100644 documentation/source/graphicsscene/index.rst create mode 100644 documentation/source/graphicsscene/mouseclickevent.rst create mode 100644 documentation/source/graphicsscene/mousedragevent.rst diff --git a/GraphicsScene/GraphicsScene.py b/GraphicsScene/GraphicsScene.py index 2bcf504e..cd3c2350 100644 --- a/GraphicsScene/GraphicsScene.py +++ b/GraphicsScene/GraphicsScene.py @@ -23,36 +23,37 @@ class GraphicsScene(QtGui.QGraphicsScene): events, but this turned out to be impossible because the constructor for QGraphicsMouseEvent is private) - - Generates MouseClicked events in addition to the usual press/move/release events. + * Generates MouseClicked events in addition to the usual press/move/release events. (This works around a problem where it is impossible to have one item respond to a drag if another is watching for a click.) - - Adjustable radius around click that will catch objects so you don't have to click *exactly* over small/thin objects - - Global context menu--if an item implements a context menu, then its parent(s) may also add items to the menu. - - Allows items to decide _before_ a mouse click which item will be the recipient of mouse events. + * Adjustable radius around click that will catch objects so you don't have to click *exactly* over small/thin objects + * Global context menu--if an item implements a context menu, then its parent(s) may also add items to the menu. + * Allows items to decide _before_ a mouse click which item will be the recipient of mouse events. This lets us indicate unambiguously to the user which item they are about to click/drag on - - Eats mouseMove events that occur too soon after a mouse press. - - Reimplements items() and itemAt() to circumvent PyQt bug + * Eats mouseMove events that occur too soon after a mouse press. + * Reimplements items() and itemAt() to circumvent PyQt bug Mouse interaction is as follows: + 1) Every time the mouse moves, the scene delivers both the standard hoverEnter/Move/LeaveEvents as well as custom HoverEvents. 2) Items are sent HoverEvents in Z-order and each item may optionally call event.acceptClicks(button), acceptDrags(button) or both. If this method call returns True, this informs the item that _if_ the user clicks/drags the specified mouse button, the item is guaranteed to be the recipient of click/drag events (the item may wish to change its appearance to indicate this). - If the call to acceptClicks/Drags returns False, then the item is guaranteed to NOT receive + If the call to acceptClicks/Drags returns False, then the item is guaranteed to *not* receive the requested event (because another item has already accepted it). 3) If the mouse is clicked, a mousePressEvent is generated as usual. If any items accept this press event, then No click/drag events will be generated and mouse interaction proceeds as defined by Qt. This allows items to function properly if they are expecting the usual press/move/release sequence of events. (It is recommended that items do NOT accept press events, and instead use click/drag events) - Note: The default implementation of QGraphicsItem.mousePressEvent will ACCEPT the event if the + Note: The default implementation of QGraphicsItem.mousePressEvent will *accept* the event if the item is has its Selectable or Movable flags enabled. You may need to override this behavior. - 3) If no item accepts the mousePressEvent, then the scene will begin delivering mouseDrag and/or mouseClick events. + 4) If no item accepts the mousePressEvent, then the scene will begin delivering mouseDrag and/or mouseClick events. If the mouse is moved a sufficient distance (or moved slowly enough) before the button is released, then a mouseDragEvent is generated. If no drag events are generated before the button is released, then a mouseClickEvent is generated. - 4) Click/drag events are delivered to the item that called acceptClicks/acceptDrags on the HoverEvent + 5) Click/drag events are delivered to the item that called acceptClicks/acceptDrags on the HoverEvent in step 1. If no such items exist, then the scene attempts to deliver the events to items near the event. ClickEvents may be delivered in this way even if no item originally claimed it could accept the click. DragEvents may only be delivered this way if it is the initial @@ -470,23 +471,25 @@ class GraphicsScene(QtGui.QGraphicsScene): The final menu will look like: - Original Item 1 - Original Item 2 - ... - Original Item N - ------------------ - Parent Item 1 - Parent Item 2 - ... - Grandparent Item 1 - ... + | Original Item 1 + | Original Item 2 + | ... + | Original Item N + | ------------------ + | Parent Item 1 + | Parent Item 2 + | ... + | Grandparent Item 1 + | ... - Arguments: - item - The item that initially created the context menu - (This is probably the item making the call to this function) - menu - The context menu being shown by the item - event - The original event that triggered the menu to appear. + ============== ================================================== + **Arguments:** + item The item that initially created the context menu + (This is probably the item making the call to this function) + menu The context menu being shown by the item + event The original event that triggered the menu to appear. + ============== ================================================== """ #items = self.itemsNearEvent(ev) diff --git a/GraphicsScene/mouseEvents.py b/GraphicsScene/mouseEvents.py index eb21229a..ce991c84 100644 --- a/GraphicsScene/mouseEvents.py +++ b/GraphicsScene/mouseEvents.py @@ -4,6 +4,13 @@ import weakref import pyqtgraph.ptime as ptime class MouseDragEvent: + """ + Instances of this class are delivered to items in a :class:`GraphicsScene ` via their mouseDragEvent() method when the item is being mouse-dragged. + + """ + + + def __init__(self, moveEvent, pressEvent, lastEvent, start=False, finish=False): self.start = start self.finish = finish @@ -27,59 +34,99 @@ class MouseDragEvent: self._modifiers = moveEvent.modifiers() def accept(self): + """An item should call this method if it can handle the event. This will prevent the event being delivered to any other items.""" self.accepted = True self.acceptedItem = self.currentItem def ignore(self): + """An item should call this method if it cannot handle the event. This will allow the event to be delivered to other items.""" self.accepted = False def isAccepted(self): return self.accepted def scenePos(self): + """Return the current scene position of the mouse.""" return Point(self._scenePos) def screenPos(self): + """Return the current screen position (pixels relative to widget) of the mouse.""" return Point(self._screenPos) def buttonDownScenePos(self, btn=None): + """ + Return the scene position of the mouse at the time *btn* was pressed. + If *btn* is omitted, then the button that initiated the drag is assumed. + """ if btn is None: btn = self.button() return Point(self._buttonDownScenePos[int(btn)]) def buttonDownScreenPos(self, btn=None): + """ + Return the screen position (pixels relative to widget) of the mouse at the time *btn* was pressed. + If *btn* is omitted, then the button that initiated the drag is assumed. + """ if btn is None: btn = self.button() return Point(self._buttonDownScreenPos[int(btn)]) def lastScenePos(self): + """ + Return the scene position of the mouse immediately prior to this event. + """ return Point(self._lastScenePos) def lastScreenPos(self): + """ + Return the screen position of the mouse immediately prior to this event. + """ return Point(self._lastScreenPos) def buttons(self): + """ + Return the buttons currently pressed on the mouse. + (see QGraphicsSceneMouseEvent::buttons in the Qt documentation) + """ return self._buttons def button(self): - """Return the button that initiated the drag (may be different from the buttons currently pressed)""" + """Return the button that initiated the drag (may be different from the buttons currently pressed) + (see QGraphicsSceneMouseEvent::button in the Qt documentation) + + """ return self._button def pos(self): + """ + Return the current position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._scenePos)) def lastPos(self): + """ + Return the previous position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._lastScenePos)) def buttonDownPos(self, btn=None): + """ + Return the position of the mouse at the time the drag was initiated + in the coordinate system of the item that the event was delivered to. + """ if btn is None: btn = self.button() return Point(self.currentItem.mapFromScene(self._buttonDownScenePos[int(btn)])) def isStart(self): + """Returns True if this event is the first since a drag was initiated.""" return self.start def isFinish(self): + """Returns False if this is the last event in a drag. Note that this + event will have the same position as the previous one.""" return self.finish def __repr__(self): @@ -88,11 +135,21 @@ class MouseDragEvent: return "(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish())) def modifiers(self): + """Return any keyboard modifiers currently pressed. + (see QGraphicsSceneMouseEvent::modifiers in the Qt documentation) + + """ return self._modifiers class MouseClickEvent: + """ + Instances of this class are delivered to items in a :class:`GraphicsScene ` via their mouseClickEvent() method when the item is clicked. + + + """ + def __init__(self, pressEvent, double=False): self.accepted = False self.currentItem = None @@ -106,37 +163,60 @@ class MouseClickEvent: def accept(self): + """An item should call this method if it can handle the event. This will prevent the event being delivered to any other items.""" self.accepted = True self.acceptedItem = self.currentItem def ignore(self): + """An item should call this method if it cannot handle the event. This will allow the event to be delivered to other items.""" self.accepted = False def isAccepted(self): return self.accepted def scenePos(self): + """Return the current scene position of the mouse.""" return Point(self._scenePos) def screenPos(self): + """Return the current screen position (pixels relative to widget) of the mouse.""" return Point(self._screenPos) def buttons(self): + """ + Return the buttons currently pressed on the mouse. + (see QGraphicsSceneMouseEvent::buttons in the Qt documentation) + """ return self._buttons def button(self): + """Return the mouse button that generated the click event. + (see QGraphicsSceneMouseEvent::button in the Qt documentation) + """ return self._button def double(self): + """Return True if this is a double-click.""" return self._double def pos(self): + """ + Return the current position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._scenePos)) def lastPos(self): + """ + Return the previous position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._lastScenePos)) def modifiers(self): + """Return any keyboard modifiers currently pressed. + (see QGraphicsSceneMouseEvent::modifiers in the Qt documentation) + """ return self._modifiers def __repr__(self): @@ -150,8 +230,9 @@ class MouseClickEvent: class HoverEvent: """ + Instances of this class are delivered to items in a :class:`GraphicsScene ` via their hoverEvent() method when the mouse is hovering over the item. This event class both informs items that the mouse cursor is nearby and allows items to - communicate with one another about whether each item will accept _potential_ mouse events. + communicate with one another about whether each item will accept *potential* mouse events. It is common for multiple overlapping items to receive hover events and respond by changing their appearance. This can be misleading to the user since, in general, only one item will @@ -188,13 +269,21 @@ class HoverEvent: def isEnter(self): + """Returns True if the mouse has just entered the item's shape""" return self.enter def isExit(self): + """Returns True if the mouse has just exited the item's shape""" return self.exit def acceptClicks(self, button): - """""" + """Inform the scene that the item (that the event was delivered to) + would accept a mouse click event if the user were to click before + moving the mouse again. + + Returns True if the request is successful, otherwise returns False (indicating + that some other item would receive an incoming click). + """ if not self.acceptable: return False if button not in self.__clickItems: @@ -203,6 +292,13 @@ class HoverEvent: return False def acceptDrags(self, button): + """Inform the scene that the item (that the event was delivered to) + would accept a mouse drag event if the user were to drag before + the next hover event. + + Returns True if the request is successful, otherwise returns False (indicating + that some other item would receive an incoming drag event). + """ if not self.acceptable: return False if button not in self.__dragItems: @@ -211,24 +307,40 @@ class HoverEvent: return False def scenePos(self): + """Return the current scene position of the mouse.""" return Point(self._scenePos) def screenPos(self): + """Return the current screen position of the mouse.""" return Point(self._screenPos) def lastScenePos(self): + """Return the previous scene position of the mouse.""" return Point(self._lastScenePos) def lastScreenPos(self): + """Return the previous screen position of the mouse.""" return Point(self._lastScreenPos) def buttons(self): + """ + Return the buttons currently pressed on the mouse. + (see QGraphicsSceneMouseEvent::buttons in the Qt documentation) + """ return self._buttons def pos(self): + """ + Return the current position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._scenePos)) def lastPos(self): + """ + Return the previous position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._lastScenePos)) def __repr__(self): @@ -237,6 +349,9 @@ class HoverEvent: return "(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit())) def modifiers(self): + """Return any keyboard modifiers currently pressed. + (see QGraphicsSceneMouseEvent::modifiers in the Qt documentation) + """ return self._modifiers def clickItems(self): diff --git a/__init__.py b/__init__.py index ddfe7d8e..9e22f53b 100644 --- a/__init__.py +++ b/__init__.py @@ -119,9 +119,10 @@ QAPP = None def plot(*args, **kargs): """ - | Create and return a PlotWindow (this is just a window with PlotWidget inside), plot data in it. - | Accepts a *title* argument to set the title of the window. - | All other arguments are used to plot data. (see :func:`PlotItem.plot() `) + Create and return a :class:`PlotWindow ` + (this is just a window with :class:`PlotWidget ` inside), plot data in it. + Accepts a *title* argument to set the title of the window. + All other arguments are used to plot data. (see :func:`PlotItem.plot() `) """ mkQApp() #if 'title' in kargs: @@ -149,10 +150,11 @@ def plot(*args, **kargs): def image(*args, **kargs): """ - | Create and return an ImageWindow (this is just a window with ImageView widget inside), show image data inside. - | Will show 2D or 3D image data. - | Accepts a *title* argument to set the title of the window. - | All other arguments are used to show data. (see :func:`ImageView.setImage() `) + Create and return an :class:`ImageWindow ` + (this is just a window with :class:`ImageView ` widget inside), show image data inside. + Will show 2D or 3D image data. + Accepts a *title* argument to set the title of the window. + All other arguments are used to show data. (see :func:`ImageView.setImage() `) """ mkQApp() w = ImageWindow(*args, **kargs) diff --git a/documentation/source/apireference.rst b/documentation/source/apireference.rst index ab4ec666..ec303140 100644 --- a/documentation/source/apireference.rst +++ b/documentation/source/apireference.rst @@ -9,3 +9,4 @@ Contents: functions graphicsItems/index widgets/index + graphicsscene/index \ No newline at end of file diff --git a/documentation/source/graphicsscene/graphicsscene.rst b/documentation/source/graphicsscene/graphicsscene.rst new file mode 100644 index 00000000..334a282b --- /dev/null +++ b/documentation/source/graphicsscene/graphicsscene.rst @@ -0,0 +1,8 @@ +GraphicsScene +============= + +.. autoclass:: pyqtgraph.GraphicsScene + :members: + + .. automethod:: pyqtgraph.GraphicsScene.__init__ + diff --git a/documentation/source/graphicsscene/hoverevent.rst b/documentation/source/graphicsscene/hoverevent.rst new file mode 100644 index 00000000..46007f91 --- /dev/null +++ b/documentation/source/graphicsscene/hoverevent.rst @@ -0,0 +1,5 @@ +HoverEvent +========== + +.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.HoverEvent + :members: diff --git a/documentation/source/graphicsscene/index.rst b/documentation/source/graphicsscene/index.rst new file mode 100644 index 00000000..189bde6c --- /dev/null +++ b/documentation/source/graphicsscene/index.rst @@ -0,0 +1,12 @@ +GraphicsScene and Mouse Events +============================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + graphicsscene + hoverevent + mouseclickevent + mousedragevent diff --git a/documentation/source/graphicsscene/mouseclickevent.rst b/documentation/source/graphicsscene/mouseclickevent.rst new file mode 100644 index 00000000..f0c94e16 --- /dev/null +++ b/documentation/source/graphicsscene/mouseclickevent.rst @@ -0,0 +1,5 @@ +MouseClickEvent +=============== + +.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent + :members: diff --git a/documentation/source/graphicsscene/mousedragevent.rst b/documentation/source/graphicsscene/mousedragevent.rst new file mode 100644 index 00000000..05c3aa6c --- /dev/null +++ b/documentation/source/graphicsscene/mousedragevent.rst @@ -0,0 +1,5 @@ +MouseDragEvent +============== + +.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.MouseDragEvent + :members: diff --git a/documentation/source/how_to_use.rst b/documentation/source/how_to_use.rst index 74e901d0..76c2d72b 100644 --- a/documentation/source/how_to_use.rst +++ b/documentation/source/how_to_use.rst @@ -17,7 +17,7 @@ Pyqtgraph makes it very easy to visualize data from the command line. Observe:: import pyqtgraph as pg pg.plot(data) # data can be a list of values or a numpy array -The example above would open a window displaying a line plot of the data given. I don't think it could reasonably be any simpler than that. The call to pg.plot returns a handle to the plot widget that is created, allowing more data to be added to the same window. +The example above would open a window displaying a line plot of the data given. The call to :func:`pg.plot ` returns a handle to the :class:`plot widget ` that is created, allowing more data to be added to the same window. Further examples:: @@ -43,5 +43,5 @@ While I consider this approach somewhat lazy, it is often the case that 'lazy' i Embedding widgets inside PyQt applications ------------------------------------------ -For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: PlotWidget, ImageView, GraphicsView, GraphicsLayoutWidget. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality. +For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: :class:`PlotWidget `, :class:`ImageView `, :class:`GraphicsLayoutWidget `, and :class:`GraphicsView `. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality. diff --git a/documentation/source/introduction.rst b/documentation/source/introduction.rst index c5c1dfab..44a498bc 100644 --- a/documentation/source/introduction.rst +++ b/documentation/source/introduction.rst @@ -44,8 +44,8 @@ This will start a launcher with a list of available examples. Select an item fro How does it compare to... ------------------------- -* matplotlib: For plotting and making publication-quality graphics, matplotlib is far more mature than pyqtgraph. However, matplotlib is also much slower and not suitable for applications requiring realtime update of plots/video or rapid interactivity. It also does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph. +* matplotlib: For plotting, pyqtgraph is not nearly as complete/mature as matplotlib, but runs much faster. Matplotlib is more aimed toward making publication-quality graphics, whereas pyqtgraph is intended for use in data acquisition and analysis applications. Matplotlib is more intuitive for matlab programmers; pyqtgraph is more intuitive for python/qt programmers. Matplotlib (to my knowledge) does not include many of pyqtgraph's features such as image interaction, volumetric rendering, parameter trees, flowcharts, etc. -* pyqwt5: pyqwt is generally more mature than pyqtgraph for plotting and is about as fast. The major differences are 1) pyqtgraph is written in pure python, so it is somewhat more portable than pyqwt, which often lags behind pyqt in development (and can be a pain to install on some platforms) and 2) like matplotlib, pyqwt does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph. +* pyqwt5: About as fast as pyqwt5, but not quite as complete for plotting functionality. Image handling in pyqtgraph is much more complete (again, no ROI widgets in qwt). Also, pyqtgraph is written in pure python, so it is more portable than pyqwt, which often lags behind pyqt in development (I originally used pyqwt, but decided it was too much trouble to rely on it as a dependency in my projects). Like matplotlib, pyqwt (to my knowledge) does not include many of pyqtgraph's features such as image interaction, volumetric rendering, parameter trees, flowcharts, etc. (My experience with these libraries is somewhat outdated; please correct me if I am wrong here) diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index f1bd6313..ebc21c34 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -780,6 +780,9 @@ class PlotItem(GraphicsWidget): def removeItem(self, item): + """ + Remove an item from the internal ViewBox. + """ if not item in self.items: return self.items.remove(item) @@ -796,6 +799,9 @@ class PlotItem(GraphicsWidget): #item.sigPlotChanged.connect(self.plotChanged) def clear(self): + """ + Remove all items from the ViewBox. + """ for i in self.items[:]: self.removeItem(i) self.avgCurves = {} diff --git a/imageview/ImageView.py b/imageview/ImageView.py index 3ff75d9c..26fbfdb8 100644 --- a/imageview/ImageView.py +++ b/imageview/ImageView.py @@ -37,7 +37,26 @@ class PlotROI(ROI): class ImageView(QtGui.QWidget): + """ + Widget used for display and analysis of image data. + Implements many features: + * Displays 2D and 3D image data. For 3D data, a z-axis + slider is displayed allowing the user to select which frame is displayed. + * Displays histogram of image data with movable region defining the dark/light levels + * Editable gradient provides a color lookup table + * Frame slider may also be moved using left/right arrow keys as well as pgup, pgdn, home, and end. + * Basic analysis features including: + + * ROI and embedded plot for measuring image values across frames + * Image normalization / background subtraction + + Basic Usage:: + + imv = pg.ImageView() + imv.show() + imv.setImage(data) + """ sigTimeChanged = QtCore.Signal(object, object) sigProcessingChanged = QtCore.Signal(object) @@ -149,7 +168,168 @@ class ImageView(QtGui.QWidget): self.roiClicked() ## initialize roi plot to correct shape / visibility + def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None): + """ + Set the image to be displayed in the widget. + + ============== ======================================================================= + **Arguments:** + *img* (numpy array) the image to be displayed. + *xvals* (numpy array) 1D array of z-axis values corresponding to the third axis + in a 3D image. For video, this array should contain the time of each frame. + *autoRange* (bool) whether to scale/pan the view to fit the image. + *autoLevels* (bool) whether to update the white/black levels to fit the image. + *levels* (min, max); the white and black level values to use. + *axes* Dictionary indicating the interpretation for each axis. + This is only needed to override the default guess. Format is:: + + {'t':0, 'x':1, 'y':2, 'c':3}; + ============== ======================================================================= + """ + prof = debug.Profiler('ImageView.setImage', disabled=True) + + if not isinstance(img, np.ndarray): + raise Exception("Image must be specified as ndarray.") + self.image = img + + if xvals is not None: + self.tVals = xvals + elif hasattr(img, 'xvals'): + try: + self.tVals = img.xvals(0) + except: + self.tVals = np.arange(img.shape[0]) + else: + self.tVals = np.arange(img.shape[0]) + #self.ui.timeSlider.setValue(0) + #self.ui.normStartSlider.setValue(0) + #self.ui.timeSlider.setMaximum(img.shape[0]-1) + prof.mark('1') + + if axes is None: + if img.ndim == 2: + self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None} + elif img.ndim == 3: + if img.shape[2] <= 4: + self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2} + else: + 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.shape))) + 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) + prof.mark('2') + + self.imageDisp = None + + + prof.mark('3') + + self.currentIndex = 0 + self.updateImage() + if levels is None and autoLevels: + self.autoLevels() + if levels is not None: ## this does nothing since getProcessedImage sets these values again. + self.levelMax = levels[1] + self.levelMin = levels[0] + + if self.ui.roiBtn.isChecked(): + self.roiChanged() + prof.mark('4') + + + if self.axes['t'] is not None: + #self.ui.roiPlot.show() + self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) + self.timeLine.setValue(0) + #self.ui.roiPlot.setMouseEnabled(False, False) + if len(self.tVals) > 1: + start = self.tVals.min() + stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02 + elif len(self.tVals) == 1: + start = self.tVals[0] - 0.5 + stop = self.tVals[0] + 0.5 + else: + start = 0 + stop = 1 + for s in [self.timeLine, self.normRgn]: + s.setBounds([start, stop]) + #else: + #self.ui.roiPlot.hide() + prof.mark('5') + + self.imageItem.resetTransform() + if scale is not None: + self.imageItem.scale(*scale) + if pos is not None: + self.imageItem.setPos(*pos) + prof.mark('6') + + if autoRange: + self.autoRange() + self.roiClicked() + prof.mark('7') + prof.finish() + + + def play(self, rate): + """Begin automatically stepping frames forward at the given rate (in fps). + This can also be accessed by pressing the spacebar.""" + #print "play:", rate + self.playRate = rate + if rate == 0: + self.playTimer.stop() + return + + self.lastPlayTime = ptime.time() + if not self.playTimer.isActive(): + self.playTimer.start(16) + + + + def autoLevels(self): + """Set the min/max levels automatically to match the image data.""" + #image = self.getProcessedImage() + self.setLevels(self.levelMin, self.levelMax) + + #self.ui.histogram.imageChanged(autoLevel=True) + + + def setLevels(self, min, max): + """Set the min/max (bright and dark) levels.""" + self.ui.histogram.setLevels(min, max) + + def autoRange(self): + """Auto scale and pan the view around the image.""" + image = self.getProcessedImage() + + #self.ui.graphicsView.setRange(QtCore.QRectF(0, 0, image.shape[self.axes['x']], image.shape[self.axes['y']]), padding=0., lockAspect=True) + self.view.setRange(self.imageItem.boundingRect(), padding=0.) + + def getProcessedImage(self): + """Returns the image data after it has been processed by any normalization options in use.""" + if self.imageDisp is None: + image = self.normalize(self.image) + self.imageDisp = image + self.levelMin, self.levelMax = map(float, ImageView.quickMinMax(self.imageDisp)) + self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) + + return self.imageDisp + + def close(self): + """Closes the widget nicely, making sure to clear the graphics scene and release memory.""" self.ui.roiPlot.close() self.ui.graphicsView.close() #self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage) @@ -224,17 +404,6 @@ class ImageView(QtGui.QWidget): else: self.play(0) - def play(self, rate): - #print "play:", rate - self.playRate = rate - if rate == 0: - self.playTimer.stop() - return - - self.lastPlayTime = ptime.time() - if not self.playTimer.isActive(): - self.playTimer.start(16) - def timeout(self): now = ptime.time() @@ -251,6 +420,7 @@ class ImageView(QtGui.QWidget): self.jumpFrames(n) def setCurrentIndex(self, ind): + """Set the currently displayed frame index.""" self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[0]-1) self.updateImage() self.ignoreTimeLine = True @@ -258,7 +428,7 @@ class ImageView(QtGui.QWidget): self.ignoreTimeLine = False def jumpFrames(self, n): - """If this is a video, move ahead n frames""" + """Move video frame ahead n frames (may be negative)""" if self.axes['t'] is not None: self.setCurrentIndex(self.currentIndex + n) @@ -360,137 +530,6 @@ class ImageView(QtGui.QWidget): #self.ui.roiPlot.replot() - def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None): - """Set the image to be displayed in the widget. - Options are: - img: ndarray; the image to be displayed. - autoRange: bool; whether to scale/pan the view to fit the image. - autoLevels: bool; whether to update the white/black levels to fit the image. - levels: (min, max); the white and black level values to use. - axes: {'t':0, 'x':1, 'y':2, 'c':3}; Dictionary indicating the interpretation for each axis. - This is only needed to override the default guess. - """ - prof = debug.Profiler('ImageView.setImage', disabled=True) - - if not isinstance(img, np.ndarray): - raise Exception("Image must be specified as ndarray.") - self.image = img - - if xvals is not None: - self.tVals = xvals - elif hasattr(img, 'xvals'): - try: - self.tVals = img.xvals(0) - except: - self.tVals = np.arange(img.shape[0]) - else: - self.tVals = np.arange(img.shape[0]) - #self.ui.timeSlider.setValue(0) - #self.ui.normStartSlider.setValue(0) - #self.ui.timeSlider.setMaximum(img.shape[0]-1) - prof.mark('1') - - if axes is None: - if img.ndim == 2: - self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None} - elif img.ndim == 3: - if img.shape[2] <= 4: - self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2} - else: - 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.shape))) - 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) - prof.mark('2') - - self.imageDisp = None - - - prof.mark('3') - - self.currentIndex = 0 - self.updateImage() - if levels is None and autoLevels: - self.autoLevels() - if levels is not None: ## this does nothing since getProcessedImage sets these values again. - self.levelMax = levels[1] - self.levelMin = levels[0] - - if self.ui.roiBtn.isChecked(): - self.roiChanged() - prof.mark('4') - - - if self.axes['t'] is not None: - #self.ui.roiPlot.show() - self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) - self.timeLine.setValue(0) - #self.ui.roiPlot.setMouseEnabled(False, False) - if len(self.tVals) > 1: - start = self.tVals.min() - stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02 - elif len(self.tVals) == 1: - start = self.tVals[0] - 0.5 - stop = self.tVals[0] + 0.5 - else: - start = 0 - stop = 1 - for s in [self.timeLine, self.normRgn]: - s.setBounds([start, stop]) - #else: - #self.ui.roiPlot.hide() - prof.mark('5') - - self.imageItem.resetTransform() - if scale is not None: - self.imageItem.scale(*scale) - if pos is not None: - self.imageItem.setPos(*pos) - prof.mark('6') - - if autoRange: - self.autoRange() - self.roiClicked() - prof.mark('7') - prof.finish() - - - def autoLevels(self): - #image = self.getProcessedImage() - self.setLevels(self.levelMin, self.levelMax) - - #self.ui.histogram.imageChanged(autoLevel=True) - - - def setLevels(self, min, max): - self.ui.histogram.setLevels(min, max) - - def autoRange(self): - image = self.getProcessedImage() - - #self.ui.graphicsView.setRange(QtCore.QRectF(0, 0, image.shape[self.axes['x']], image.shape[self.axes['y']]), padding=0., lockAspect=True) - self.view.setRange(self.imageItem.boundingRect(), padding=0.) - - def getProcessedImage(self): - if self.imageDisp is None: - image = self.normalize(self.image) - self.imageDisp = image - self.levelMin, self.levelMax = map(float, ImageView.quickMinMax(self.imageDisp)) - self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) - - return self.imageDisp @staticmethod def quickMinMax(data): @@ -578,7 +617,7 @@ class ImageView(QtGui.QWidget): def timeIndex(self, slider): - """Return the time and frame index indicated by a slider""" + ## Return the time and frame index indicated by a slider if self.image is None: return (0,0) #v = slider.value() diff --git a/widgets/PlotWidget.py b/widgets/PlotWidget.py index fa9fcf1a..3bb1f636 100644 --- a/widgets/PlotWidget.py +++ b/widgets/PlotWidget.py @@ -16,11 +16,30 @@ class PlotWidget(GraphicsView): #sigRangeChanged = QtCore.Signal(object, object) ## already defined in GraphicsView """ - Widget implementing a graphicsView with a single PlotItem inside. + :class:`GraphicsView ` widget with a single + :class:`PlotItem ` inside. - The following methods are wrapped directly from PlotItem: addItem, removeItem, - clear, setXRange, setYRange, setRange, setAspectLocked, setMouseEnabled. For all - other methods, use getPlotItem. + The following methods are wrapped directly from PlotItem: + :func:`addItem `, + :func:`removeItem `, + :func:`clear `, + :func:`setXRange `, + :func:`setYRange `, + :func:`setRange `, + :func:`autoRange `, + :func:`setXLink `, + :func:`setYLink `, + :func:`viewRect `, + :func:`setMouseEnabled `, + :func:`enableAutoRange `, + :func:`disableAutoRange `, + :func:`setAspectLocked `, + :func:`register `, + :func:`unregister ` + + + For all + other methods, use :func:`getPlotItem `. """ def __init__(self, parent=None, **kargs): GraphicsView.__init__(self, parent) @@ -29,7 +48,8 @@ class PlotWidget(GraphicsView): self.plotItem = PlotItem(**kargs) self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem - for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled']: + ## NOTE: If you change this list, update the documentation above as well. + for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', 'register', 'unregister', 'viewRect']: setattr(self, m, getattr(self.plotItem, m)) #QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged)