documentation updates

This commit is contained in:
Luke Campagnola 2012-04-18 00:02:15 -04:00
parent 44f2a0ecc4
commit 4eadccdcc1
14 changed files with 409 additions and 188 deletions

View File

@ -23,36 +23,37 @@ class GraphicsScene(QtGui.QGraphicsScene):
events, but this turned out to be impossible because the constructor for QGraphicsMouseEvent events, but this turned out to be impossible because the constructor for QGraphicsMouseEvent
is private) 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 (This works around a problem where it is impossible to have one item respond to a
drag if another is watching for a click.) 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 * 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. * 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. * 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 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. * Eats mouseMove events that occur too soon after a mouse press.
- Reimplements items() and itemAt() to circumvent PyQt bug * Reimplements items() and itemAt() to circumvent PyQt bug
Mouse interaction is as follows: Mouse interaction is as follows:
1) Every time the mouse moves, the scene delivers both the standard hoverEnter/Move/LeaveEvents 1) Every time the mouse moves, the scene delivers both the standard hoverEnter/Move/LeaveEvents
as well as custom HoverEvents. as well as custom HoverEvents.
2) Items are sent HoverEvents in Z-order and each item may optionally call event.acceptClicks(button), 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_ 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 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). 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). 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 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 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. 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) (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. 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, If the mouse is moved a sufficient distance (or moved slowly enough) before the button is released,
then a mouseDragEvent is generated. then a mouseDragEvent is generated.
If no drag events are generated before the button is released, then a mouseClickEvent 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. 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 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 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: The final menu will look like:
Original Item 1 | Original Item 1
Original Item 2 | Original Item 2
... | ...
Original Item N | Original Item N
------------------ | ------------------
Parent Item 1 | Parent Item 1
Parent Item 2 | Parent Item 2
... | ...
Grandparent Item 1 | Grandparent Item 1
... | ...
Arguments: ============== ==================================================
item - The item that initially created the context menu **Arguments:**
(This is probably the item making the call to this function) item The item that initially created the context menu
menu - The context menu being shown by the item (This is probably the item making the call to this function)
event - The original event that triggered the menu to appear. menu The context menu being shown by the item
event The original event that triggered the menu to appear.
============== ==================================================
""" """
#items = self.itemsNearEvent(ev) #items = self.itemsNearEvent(ev)

View File

@ -4,6 +4,13 @@ import weakref
import pyqtgraph.ptime as ptime import pyqtgraph.ptime as ptime
class MouseDragEvent: class MouseDragEvent:
"""
Instances of this class are delivered to items in a :class:`GraphicsScene <pyqtgraph.GraphicsScene>` via their mouseDragEvent() method when the item is being mouse-dragged.
"""
def __init__(self, moveEvent, pressEvent, lastEvent, start=False, finish=False): def __init__(self, moveEvent, pressEvent, lastEvent, start=False, finish=False):
self.start = start self.start = start
self.finish = finish self.finish = finish
@ -27,59 +34,99 @@ class MouseDragEvent:
self._modifiers = moveEvent.modifiers() self._modifiers = moveEvent.modifiers()
def accept(self): 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.accepted = True
self.acceptedItem = self.currentItem self.acceptedItem = self.currentItem
def ignore(self): 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 self.accepted = False
def isAccepted(self): def isAccepted(self):
return self.accepted return self.accepted
def scenePos(self): def scenePos(self):
"""Return the current scene position of the mouse."""
return Point(self._scenePos) return Point(self._scenePos)
def screenPos(self): def screenPos(self):
"""Return the current screen position (pixels relative to widget) of the mouse."""
return Point(self._screenPos) return Point(self._screenPos)
def buttonDownScenePos(self, btn=None): 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: if btn is None:
btn = self.button() btn = self.button()
return Point(self._buttonDownScenePos[int(btn)]) return Point(self._buttonDownScenePos[int(btn)])
def buttonDownScreenPos(self, btn=None): 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: if btn is None:
btn = self.button() btn = self.button()
return Point(self._buttonDownScreenPos[int(btn)]) return Point(self._buttonDownScreenPos[int(btn)])
def lastScenePos(self): def lastScenePos(self):
"""
Return the scene position of the mouse immediately prior to this event.
"""
return Point(self._lastScenePos) return Point(self._lastScenePos)
def lastScreenPos(self): def lastScreenPos(self):
"""
Return the screen position of the mouse immediately prior to this event.
"""
return Point(self._lastScreenPos) return Point(self._lastScreenPos)
def buttons(self): def buttons(self):
"""
Return the buttons currently pressed on the mouse.
(see QGraphicsSceneMouseEvent::buttons in the Qt documentation)
"""
return self._buttons return self._buttons
def button(self): 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 return self._button
def pos(self): 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)) return Point(self.currentItem.mapFromScene(self._scenePos))
def lastPos(self): 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)) return Point(self.currentItem.mapFromScene(self._lastScenePos))
def buttonDownPos(self, btn=None): 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: if btn is None:
btn = self.button() btn = self.button()
return Point(self.currentItem.mapFromScene(self._buttonDownScenePos[int(btn)])) return Point(self.currentItem.mapFromScene(self._buttonDownScenePos[int(btn)]))
def isStart(self): def isStart(self):
"""Returns True if this event is the first since a drag was initiated."""
return self.start return self.start
def isFinish(self): 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 return self.finish
def __repr__(self): def __repr__(self):
@ -88,11 +135,21 @@ class MouseDragEvent:
return "<MouseDragEvent (%g,%g)->(%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())) return "<MouseDragEvent (%g,%g)->(%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): def modifiers(self):
"""Return any keyboard modifiers currently pressed.
(see QGraphicsSceneMouseEvent::modifiers in the Qt documentation)
"""
return self._modifiers return self._modifiers
class MouseClickEvent: class MouseClickEvent:
"""
Instances of this class are delivered to items in a :class:`GraphicsScene <pyqtgraph.GraphicsScene>` via their mouseClickEvent() method when the item is clicked.
"""
def __init__(self, pressEvent, double=False): def __init__(self, pressEvent, double=False):
self.accepted = False self.accepted = False
self.currentItem = None self.currentItem = None
@ -106,37 +163,60 @@ class MouseClickEvent:
def accept(self): 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.accepted = True
self.acceptedItem = self.currentItem self.acceptedItem = self.currentItem
def ignore(self): 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 self.accepted = False
def isAccepted(self): def isAccepted(self):
return self.accepted return self.accepted
def scenePos(self): def scenePos(self):
"""Return the current scene position of the mouse."""
return Point(self._scenePos) return Point(self._scenePos)
def screenPos(self): def screenPos(self):
"""Return the current screen position (pixels relative to widget) of the mouse."""
return Point(self._screenPos) return Point(self._screenPos)
def buttons(self): def buttons(self):
"""
Return the buttons currently pressed on the mouse.
(see QGraphicsSceneMouseEvent::buttons in the Qt documentation)
"""
return self._buttons return self._buttons
def button(self): def button(self):
"""Return the mouse button that generated the click event.
(see QGraphicsSceneMouseEvent::button in the Qt documentation)
"""
return self._button return self._button
def double(self): def double(self):
"""Return True if this is a double-click."""
return self._double return self._double
def pos(self): 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)) return Point(self.currentItem.mapFromScene(self._scenePos))
def lastPos(self): 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)) return Point(self.currentItem.mapFromScene(self._lastScenePos))
def modifiers(self): def modifiers(self):
"""Return any keyboard modifiers currently pressed.
(see QGraphicsSceneMouseEvent::modifiers in the Qt documentation)
"""
return self._modifiers return self._modifiers
def __repr__(self): def __repr__(self):
@ -150,8 +230,9 @@ class MouseClickEvent:
class HoverEvent: class HoverEvent:
""" """
Instances of this class are delivered to items in a :class:`GraphicsScene <pyqtgraph.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 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 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 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): def isEnter(self):
"""Returns True if the mouse has just entered the item's shape"""
return self.enter return self.enter
def isExit(self): def isExit(self):
"""Returns True if the mouse has just exited the item's shape"""
return self.exit return self.exit
def acceptClicks(self, button): 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: if not self.acceptable:
return False return False
if button not in self.__clickItems: if button not in self.__clickItems:
@ -203,6 +292,13 @@ class HoverEvent:
return False return False
def acceptDrags(self, button): 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: if not self.acceptable:
return False return False
if button not in self.__dragItems: if button not in self.__dragItems:
@ -211,24 +307,40 @@ class HoverEvent:
return False return False
def scenePos(self): def scenePos(self):
"""Return the current scene position of the mouse."""
return Point(self._scenePos) return Point(self._scenePos)
def screenPos(self): def screenPos(self):
"""Return the current screen position of the mouse."""
return Point(self._screenPos) return Point(self._screenPos)
def lastScenePos(self): def lastScenePos(self):
"""Return the previous scene position of the mouse."""
return Point(self._lastScenePos) return Point(self._lastScenePos)
def lastScreenPos(self): def lastScreenPos(self):
"""Return the previous screen position of the mouse."""
return Point(self._lastScreenPos) return Point(self._lastScreenPos)
def buttons(self): def buttons(self):
"""
Return the buttons currently pressed on the mouse.
(see QGraphicsSceneMouseEvent::buttons in the Qt documentation)
"""
return self._buttons return self._buttons
def pos(self): 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)) return Point(self.currentItem.mapFromScene(self._scenePos))
def lastPos(self): 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)) return Point(self.currentItem.mapFromScene(self._lastScenePos))
def __repr__(self): def __repr__(self):
@ -237,6 +349,9 @@ class HoverEvent:
return "<HoverEvent (%g,%g)->(%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())) return "<HoverEvent (%g,%g)->(%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): def modifiers(self):
"""Return any keyboard modifiers currently pressed.
(see QGraphicsSceneMouseEvent::modifiers in the Qt documentation)
"""
return self._modifiers return self._modifiers
def clickItems(self): def clickItems(self):

View File

@ -119,9 +119,10 @@ QAPP = None
def plot(*args, **kargs): def plot(*args, **kargs):
""" """
| Create and return a PlotWindow (this is just a window with PlotWidget inside), plot data in it. Create and return a :class:`PlotWindow <pyqtgraph.PlotWindow>`
| Accepts a *title* argument to set the title of the window. (this is just a window with :class:`PlotWidget <pyqtgraph.PlotWidget>` inside), plot data in it.
| All other arguments are used to plot data. (see :func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`) Accepts a *title* argument to set the title of the window.
All other arguments are used to plot data. (see :func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`)
""" """
mkQApp() mkQApp()
#if 'title' in kargs: #if 'title' in kargs:
@ -149,10 +150,11 @@ def plot(*args, **kargs):
def image(*args, **kargs): def image(*args, **kargs):
""" """
| Create and return an ImageWindow (this is just a window with ImageView widget inside), show image data inside. Create and return an :class:`ImageWindow <pyqtgraph.ImageWindow>`
| Will show 2D or 3D image data. (this is just a window with :class:`ImageView <pyqtgraph.ImageView>` widget inside), show image data inside.
| Accepts a *title* argument to set the title of the window. Will show 2D or 3D image data.
| All other arguments are used to show data. (see :func:`ImageView.setImage() <pyqtgraph.ImageView.setImage>`) Accepts a *title* argument to set the title of the window.
All other arguments are used to show data. (see :func:`ImageView.setImage() <pyqtgraph.ImageView.setImage>`)
""" """
mkQApp() mkQApp()
w = ImageWindow(*args, **kargs) w = ImageWindow(*args, **kargs)

View File

@ -9,3 +9,4 @@ Contents:
functions functions
graphicsItems/index graphicsItems/index
widgets/index widgets/index
graphicsscene/index

View File

@ -0,0 +1,8 @@
GraphicsScene
=============
.. autoclass:: pyqtgraph.GraphicsScene
:members:
.. automethod:: pyqtgraph.GraphicsScene.__init__

View File

@ -0,0 +1,5 @@
HoverEvent
==========
.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.HoverEvent
:members:

View File

@ -0,0 +1,12 @@
GraphicsScene and Mouse Events
==============================
Contents:
.. toctree::
:maxdepth: 2
graphicsscene
hoverevent
mouseclickevent
mousedragevent

View File

@ -0,0 +1,5 @@
MouseClickEvent
===============
.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent
:members:

View File

@ -0,0 +1,5 @@
MouseDragEvent
==============
.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.MouseDragEvent
:members:

View File

@ -17,7 +17,7 @@ Pyqtgraph makes it very easy to visualize data from the command line. Observe::
import pyqtgraph as pg import pyqtgraph as pg
pg.plot(data) # data can be a list of values or a numpy array 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 <pyqtgraph.plot>` returns a handle to the :class:`plot widget <pyqtgraph.PlotWidget>` that is created, allowing more data to be added to the same window.
Further examples:: 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 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 <pyqtgraph.PlotWidget>`, :class:`ImageView <pyqtgraph.ImageView>`, :class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>`, and :class:`GraphicsView <pyqtgraph.GraphicsView>`. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality.

View File

@ -44,8 +44,8 @@ This will start a launcher with a list of available examples. Select an item fro
How does it compare to... 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) (My experience with these libraries is somewhat outdated; please correct me if I am wrong here)

View File

@ -780,6 +780,9 @@ class PlotItem(GraphicsWidget):
def removeItem(self, item): def removeItem(self, item):
"""
Remove an item from the internal ViewBox.
"""
if not item in self.items: if not item in self.items:
return return
self.items.remove(item) self.items.remove(item)
@ -796,6 +799,9 @@ class PlotItem(GraphicsWidget):
#item.sigPlotChanged.connect(self.plotChanged) #item.sigPlotChanged.connect(self.plotChanged)
def clear(self): def clear(self):
"""
Remove all items from the ViewBox.
"""
for i in self.items[:]: for i in self.items[:]:
self.removeItem(i) self.removeItem(i)
self.avgCurves = {} self.avgCurves = {}

View File

@ -37,7 +37,26 @@ class PlotROI(ROI):
class ImageView(QtGui.QWidget): 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) sigTimeChanged = QtCore.Signal(object, object)
sigProcessingChanged = QtCore.Signal(object) sigProcessingChanged = QtCore.Signal(object)
@ -149,7 +168,168 @@ class ImageView(QtGui.QWidget):
self.roiClicked() ## initialize roi plot to correct shape / visibility 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): def close(self):
"""Closes the widget nicely, making sure to clear the graphics scene and release memory."""
self.ui.roiPlot.close() self.ui.roiPlot.close()
self.ui.graphicsView.close() self.ui.graphicsView.close()
#self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage) #self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage)
@ -224,17 +404,6 @@ class ImageView(QtGui.QWidget):
else: else:
self.play(0) 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): def timeout(self):
now = ptime.time() now = ptime.time()
@ -251,6 +420,7 @@ class ImageView(QtGui.QWidget):
self.jumpFrames(n) self.jumpFrames(n)
def setCurrentIndex(self, ind): def setCurrentIndex(self, ind):
"""Set the currently displayed frame index."""
self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[0]-1) self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[0]-1)
self.updateImage() self.updateImage()
self.ignoreTimeLine = True self.ignoreTimeLine = True
@ -258,7 +428,7 @@ class ImageView(QtGui.QWidget):
self.ignoreTimeLine = False self.ignoreTimeLine = False
def jumpFrames(self, n): 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: if self.axes['t'] is not None:
self.setCurrentIndex(self.currentIndex + n) self.setCurrentIndex(self.currentIndex + n)
@ -360,137 +530,6 @@ class ImageView(QtGui.QWidget):
#self.ui.roiPlot.replot() #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 @staticmethod
def quickMinMax(data): def quickMinMax(data):
@ -578,7 +617,7 @@ class ImageView(QtGui.QWidget):
def timeIndex(self, slider): 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: if self.image is None:
return (0,0) return (0,0)
#v = slider.value() #v = slider.value()

View File

@ -16,11 +16,30 @@ class PlotWidget(GraphicsView):
#sigRangeChanged = QtCore.Signal(object, object) ## already defined in GraphicsView #sigRangeChanged = QtCore.Signal(object, object) ## already defined in GraphicsView
""" """
Widget implementing a graphicsView with a single PlotItem inside. :class:`GraphicsView <pyqtgraph.GraphicsView>` widget with a single
:class:`PlotItem <pyqtgraph.PlotItem>` inside.
The following methods are wrapped directly from PlotItem: addItem, removeItem, The following methods are wrapped directly from PlotItem:
clear, setXRange, setYRange, setRange, setAspectLocked, setMouseEnabled. For all :func:`addItem <pyqtgraph.PlotItem.addItem>`,
other methods, use getPlotItem. :func:`removeItem <pyqtgraph.PlotItem.removeItem>`,
:func:`clear <pyqtgraph.PlotItem.clear>`,
:func:`setXRange <pyqtgraph.ViewBox.setXRange>`,
:func:`setYRange <pyqtgraph.ViewBox.setYRange>`,
:func:`setRange <pyqtgraph.ViewBox.setRange>`,
:func:`autoRange <pyqtgraph.ViewBox.autoRange>`,
:func:`setXLink <pyqtgraph.ViewBox.setXLink>`,
:func:`setYLink <pyqtgraph.ViewBox.setYLink>`,
:func:`viewRect <pyqtgraph.ViewBox.viewRect>`,
:func:`setMouseEnabled <pyqtgraph.ViewBox.setMouseEnabled>`,
:func:`enableAutoRange <pyqtgraph.ViewBox.enableAutoRange>`,
:func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`,
:func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`,
:func:`register <pyqtgraph.ViewBox.register>`,
:func:`unregister <pyqtgraph.ViewBox.unregister>`
For all
other methods, use :func:`getPlotItem <pyqtgraph.PlotWidget.getPlotItem>`.
""" """
def __init__(self, parent=None, **kargs): def __init__(self, parent=None, **kargs):
GraphicsView.__init__(self, parent) GraphicsView.__init__(self, parent)
@ -29,7 +48,8 @@ class PlotWidget(GraphicsView):
self.plotItem = PlotItem(**kargs) self.plotItem = PlotItem(**kargs)
self.setCentralItem(self.plotItem) self.setCentralItem(self.plotItem)
## Explicitly wrap methods from 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)) setattr(self, m, getattr(self.plotItem, m))
#QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) #QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged)
self.plotItem.sigRangeChanged.connect(self.viewRangeChanged) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged)