Refactor blit manager and event manager to viewer

This commit is contained in:
blink1073
2014-07-20 18:39:49 -05:00
committed by Steven Silvester
parent 1799846be9
commit 2d9d8bb94c
5 changed files with 153 additions and 192 deletions
+22 -93
View File
@@ -11,38 +11,13 @@ def _pass(*args):
pass
class BlitManager(object):
"""Object that manages blits on an axes"""
def __init__(self, ax):
self.ax = ax
self.canvas = ax.figure.canvas
self.canvas.mpl_connect('draw_event', self.on_draw_event)
self.ax = ax
self.background = None
self.artists = []
def on_draw_event(self, event=None):
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.draw_artists()
def redraw(self):
if self.background is not None:
self.canvas.restore_region(self.background)
self.draw_artists()
self.canvas.blit(self.ax.bbox)
def draw_artists(self):
for artist in self.artists:
self.ax.draw_artist(artist)
class CanvasToolBase(object):
"""Base canvas tool for matplotlib axes.
Parameters
----------
ax : :class:`matplotlib.axes.Axes`
Matplotlib axes where tool is displayed.
viewer : :class:`skimage.viewer.Viewer`
Skimage viewer object.
on_move : function
Function called whenever a control handle is moved.
This function must accept the end points of line as the only argument.
@@ -50,45 +25,19 @@ class CanvasToolBase(object):
Function called whenever the control handle is released.
on_enter : function
Function called whenever the "enter" key is pressed.
useblit : bool
If True, update canvas by blitting, which is much faster than normal
redrawing (turn off for debugging purposes).
"""
def __init__(self, ax, on_move=None, on_enter=None, on_release=None,
def __init__(self, viewer, on_move=None, on_enter=None, on_release=None,
useblit=True):
self.ax = ax
self.canvas = ax.figure.canvas
self.cids = []
self._artists = []
self.viewer = viewer
self.ax = viewer.ax
self.artists = []
self.active = True
self.connect_event('draw_event', self._on_draw_event)
if useblit:
if not hasattr(ax, 'blit_manager'):
ax.blit_manager = BlitManager(ax)
ax.blit_manager.artists.extend(self._artists)
self.useblit = useblit
self.callback_on_move = _pass if on_move is None else on_move
self.callback_on_enter = _pass if on_enter is None else on_enter
self.callback_on_release = _pass if on_release is None else on_release
def connect_event(self, event, callback):
"""Connect callback with an event.
This should be used in lieu of `figure.canvas.mpl_connect` since this
function stores call back ids for later clean up.
"""
cid = self.canvas.mpl_connect(event, callback)
self.cids.append(cid)
def disconnect_events(self):
"""Disconnect all events created by this widget."""
for c in self.cids:
self.canvas.mpl_disconnect(c)
def ignore(self, event):
"""Return True if event should be ignored.
@@ -97,47 +46,30 @@ class CanvasToolBase(object):
"""
return not self.active
def redraw(self):
self.viewer.redraw()
def set_visible(self, val):
for artist in self._artists:
artist.set_visible(val)
def _on_draw_event(self, event=None):
if self.useblit:
for artist in self._artists:
if not artist in self.ax.blit_manager.artists:
self.ax.blit_manager.artists.append(artist)
else:
self._draw_artists()
def _draw_artists(self):
for artist in self._artists:
self.ax.draw_artist(artist)
def remove(self):
"""Remove artists and events from axes.
Note that the naming here mimics the interface of Matplotlib artists.
"""
#TODO: For some reason, RectangleTool doesn't get properly removed
self.disconnect_events()
for a in self._artists:
a.remove()
def redraw(self):
"""Redraw image and canvas artists.
This method should be called by subclasses when artists are updated.
"""
if not self.useblit:
self.canvas.draw()
else:
self.ax.blit_manager.redraw()
def on_key_press(self, event):
if event.key == 'enter':
self.callback_on_enter(self.geometry)
self.set_visible(False)
self.redraw()
self.viewer.redraw()
def on_mouse_press(self, event):
pass
def on_mouse_release(self, event):
pass
def on_move(self, event):
pass
def on_scroll(self, event):
pass
@property
def geometry(self):
@@ -190,9 +122,6 @@ class ToolHandles(object):
def set_animated(self, val):
self._markers.set_animated(val)
def draw(self):
self.ax.draw_artist(self._markers)
def closest(self, x, y):
"""Return index and pixel distance to closest index."""
pts = np.transpose((self.x, self.y))
+10 -73
View File
@@ -10,70 +10,13 @@ from skimage.viewer.canvastools.base import CanvasToolBase, ToolHandles
__all__ = ['LineTool', 'ThickLineTool']
class EventManager(object):
"""Object that manages events on a canvas"""
def __init__(self, ax):
self.canvas = ax.figure.canvas
self.connect_event('button_press_event', self.on_mouse_press)
self.connect_event('key_press_event', self.on_key_press)
self.connect_event('button_release_event', self.on_mouse_release)
self.connect_event('motion_notify_event', self.on_move)
self.tools = []
self.active_tool = None
def connect_event(self, name, handler):
self.canvas.mpl_connect(name, handler)
def attach(self, tool):
self.tools.append(tool)
self.active_tool = tool
def on_mouse_press(self, event):
for tool in self.tools:
if not tool.ignore(event) and tool.hit_test(event):
self.active_tool = tool
tool.on_mouse_press(event)
return
if self.active_tool and not self.active_tool.ignore(event):
self.active_tool.on_mouse_press(event)
return
for tool in self.tools:
if not tool.ignore(event):
self.active_tool = tool
tool.on_mouse_press(event)
return
def on_key_press(self, event):
tool = self.get_tool()
if not tool is None and not tool.ignore(event):
tool.on_key_press(event)
def get_tool(self):
if not self.tools:
return
if self.active_tool is None:
self.active_tool = self.tools[0]
return self.active_tool
def on_mouse_release(self, event):
tool = self.get_tool()
if not tool is None and not tool.ignore(event):
tool.on_mouse_release(event)
def on_move(self, event):
tool = self.get_tool()
if not tool is None and not tool.ignore(event):
tool.on_move(event)
class LineTool(CanvasToolBase):
"""Widget for line selection in a plot.
Parameters
----------
ax : :class:`matplotlib.axes.Axes`
Matplotlib axes where tool is displayed.
viewer : :class:`skimage.viewer.Viewer`
Skimage viewer object.
on_move : function
Function called whenever a control handle is moved.
This function must accept the end points of line as the only argument.
@@ -91,7 +34,7 @@ class LineTool(CanvasToolBase):
end_points : 2D array
End points of line ((x1, y1), (x2, y2)).
"""
def __init__(self, ax, on_move=None, on_release=None, on_enter=None,
def __init__(self, viewer, on_move=None, on_release=None, on_enter=None,
maxdist=10, line_props=None,
**kwargs):
super(LineTool, self).__init__(ax, on_move=on_move, on_enter=on_enter,
@@ -108,21 +51,18 @@ class LineTool(CanvasToolBase):
self._end_pts = np.transpose([x, y])
self._line = lines.Line2D(x, y, visible=False, animated=True, **props)
ax.add_line(self._line)
self.ax.add_line(self._line)
self._handles = ToolHandles(ax, x, y)
self._handles = ToolHandles(self.ax, x, y)
self._handles.set_visible(False)
self._artists = [self._line, self._handles.artist]
if not hasattr(ax, 'event_manager'):
ax.event_manager = EventManager(ax)
ax.event_manager.attach(self)
self.artists = [self._line, self._handles.artist]
if on_enter is None:
def on_enter(pts):
x, y = np.transpose(pts)
print("length = %0.2f" % np.sqrt(np.diff(x)**2 + np.diff(y)**2))
self.callback_on_enter = on_enter
viewer.add_tool(self)
@property
def end_points(self):
@@ -189,8 +129,8 @@ class ThickLineTool(LineTool):
Parameters
----------
ax : :class:`matplotlib.axes.Axes`
Matplotlib axes where tool is displayed.
viewer : :class:`skimage.viewer.Viewer`
Skimage viewer object.
on_move : function
Function called whenever a control handle is moved.
This function must accept the end points of line as the only argument.
@@ -211,7 +151,7 @@ class ThickLineTool(LineTool):
End points of line ((x1, y1), (x2, y2)).
"""
def __init__(self, ax, on_move=None, on_enter=None, on_release=None,
def __init__(self, viewer, on_move=None, on_enter=None, on_release=None,
on_change=None, maxdist=10, line_props=None):
super(ThickLineTool, self).__init__(ax,
on_move=on_move,
@@ -225,9 +165,6 @@ class ThickLineTool(LineTool):
pass
self.callback_on_change = on_change
self.connect_event('scroll_event', self.on_scroll)
self.connect_event('key_press_event', self.on_key_press)
def on_scroll(self, event):
if not event.inaxes:
return
+9 -11
View File
@@ -17,8 +17,8 @@ class PaintTool(CanvasToolBase):
Parameters
----------
ax : :class:`matplotlib.axes.Axes`
Matplotlib axes where tool is displayed.
viewer : :class:`skimage.viewer.Viewer`
Skimage viewer object.
overlay_shape : shape tuple
2D shape tuple used to initialize overlay image.
alpha : float (between [0, 1])
@@ -41,8 +41,9 @@ class PaintTool(CanvasToolBase):
label : int
Current paint color.
"""
def __init__(self, ax, overlay_shape, radius=5, alpha=0.3, on_move=None,
on_release=None, on_enter=None, rect_props=None):
def __init__(self, viewer, overlay_shape, radius=5, alpha=0.3,
on_move=None, on_release=None, on_enter=None,
rect_props=None):
super(PaintTool, self).__init__(ax, on_move=on_move, on_enter=on_enter,
on_release=on_release)
@@ -63,11 +64,8 @@ class PaintTool(CanvasToolBase):
self.radius = radius
# Note that the order is important: Redraw cursor *after* overlay
self._artists = [self._overlay_plot, self._cursor]
self.connect_event('button_press_event', self.on_mouse_press)
self.connect_event('button_release_event', self.on_mouse_release)
self.connect_event('motion_notify_event', self.on_move)
self.artists = [self._overlay_plot, self._cursor]
view.add_tool(self)
@property
def label(self):
@@ -123,7 +121,7 @@ class PaintTool(CanvasToolBase):
self.radius = self._radius
self.overlay = np.zeros(shape, dtype='uint8')
def _on_key_press(self, event):
def on_key_press(self, event):
if event.key == 'enter':
self.callback_on_enter(self.geometry)
self.redraw()
@@ -140,7 +138,7 @@ class PaintTool(CanvasToolBase):
self.callback_on_release(self.geometry)
def on_move(self, event):
if not self.ax.in_axes(event):
if not self.viewer.ax.in_axes(event):
self._cursor.set_visible(False)
self.redraw() # make sure cursor is not visible
return
+13 -12
View File
@@ -18,8 +18,8 @@ class RectangleTool(CanvasToolBase, RectangleSelector):
Parameters
----------
ax : :class:`matplotlib.axes.Axes`
Matplotlib axes where tool is displayed.
viewer : :class:`skimage.viewer.Viewer`
Skimage viewer object.
on_move : function
Function called whenever a control handle is moved.
This function must accept the rectangle extents as the only argument.
@@ -39,7 +39,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector):
Rectangle extents: (xmin, xmax, ymin, ymax).
"""
def __init__(self, ax, on_move=None, on_release=None, on_enter=None,
def __init__(self, viewer, on_move=None, on_release=None, on_enter=None,
maxdist=10, rect_props=None):
CanvasToolBase.__init__(self, ax, on_move=on_move,
on_enter=on_enter, on_release=on_release)
@@ -48,9 +48,9 @@ class RectangleTool(CanvasToolBase, RectangleSelector):
props.update(rect_props if rect_props is not None else {})
if props['edgecolor'] is None:
props['edgecolor'] = props['facecolor']
RectangleSelector.__init__(self, ax, lambda *args: None,
rectprops=props,
useblit=self.useblit)
RectangleSelector.__init__(self, self.ax, lambda *args: None,
rectprops=props)
self.disconnect_events() # events are handled by the viewer
# Alias rectangle attribute, which is initialized in RectangleSelector.
self._rect = self.to_draw
self._rect.set_animated(True)
@@ -74,9 +74,10 @@ class RectangleTool(CanvasToolBase, RectangleSelector):
self._edge_handles = ToolHandles(ax, xe, ye, marker='s',
marker_props=props)
self._artists = [self._rect,
self._corner_handles.artist,
self._edge_handles.artist]
self.artists = [self._rect,
self._corner_handles.artist,
self._edge_handles.artist]
viewer.add_tool(self)
@property
def _rect_bbox(self):
@@ -129,7 +130,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector):
self.set_visible(True)
self.redraw()
def release(self, event):
def on_mouse_release(self, event):
if event.button != 1:
return
if not self.ax.in_axes(event):
@@ -142,7 +143,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector):
self.redraw()
self.callback_on_release(self.geometry)
def press(self, event):
def on_mouse_press(self, event):
if event.button != 1 or not self.ax.in_axes(event):
return
self._set_active_handle(event)
@@ -177,7 +178,7 @@ class RectangleTool(CanvasToolBase, RectangleSelector):
y1, y2 = y2, event.ydata
self._extents_on_press = x1, x2, y1, y2
def onmove(self, event):
def on_move(self, event):
if self.eventpress is None or not self.ax.in_axes(event):
return
+99 -3
View File
@@ -12,7 +12,6 @@ else:
from skimage import io, img_as_float
from skimage.util.dtype import dtype_range
from skimage.exposure import rescale_intensity
from skimage._shared.testing import doctest_skip_parser
import numpy as np
from .. import utils
from ..widgets import Slider
@@ -51,6 +50,88 @@ def mpl_image_to_rgba(mpl_image):
return img_as_float(image)
class BlitManager(object):
"""Object that manages blits on an axes"""
def __init__(self, ax):
self.ax = ax
self.canvas = ax.figure.canvas
self.canvas.mpl_connect('draw_event', self.on_draw_event)
self.ax = ax
self.background = None
self.artists = []
def on_draw_event(self, event=None):
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.draw_artists()
def redraw(self):
if self.background is not None:
self.canvas.restore_region(self.background)
self.draw_artists()
self.canvas.blit(self.ax.bbox)
def draw_artists(self):
for artist in self.artists:
self.ax.draw_artist(artist)
class EventManager(object):
"""Object that manages events on a canvas"""
def __init__(self, ax):
self.canvas = ax.figure.canvas
self.connect_event('button_press_event', self.on_mouse_press)
self.connect_event('key_press_event', self.on_key_press)
self.connect_event('button_release_event', self.on_mouse_release)
self.connect_event('motion_notify_event', self.on_move)
self.tools = []
self.active_tool = None
def connect_event(self, name, handler):
self.canvas.mpl_connect(name, handler)
def attach(self, tool):
self.tools.append(tool)
self.active_tool = tool
def on_mouse_press(self, event):
for tool in self.tools:
if not tool.ignore(event) and tool.hit_test(event):
self.active_tool = tool
tool.on_mouse_press(event)
return
if self.active_tool and not self.active_tool.ignore(event):
self.active_tool.on_mouse_press(event)
return
for tool in reversed(self.tools):
if not tool.ignore(event):
self.active_tool = tool
tool.on_mouse_press(event)
return
def on_key_press(self, event):
tool = self.get_tool()
if not tool is None and not tool.ignore(event):
tool.on_key_press(event)
def get_tool(self):
if not self.tools:
return
if self.active_tool is None:
self.active_tool = self.tools[0]
return self.active_tool
def on_mouse_release(self, event):
tool = self.get_tool()
if not tool is None and not tool.ignore(event):
tool.on_mouse_release(event)
def on_move(self, event):
tool = self.get_tool()
if not tool is None and not tool.ignore(event):
tool.on_move(event)
class ImageViewer(QtGui.QMainWindow):
"""Viewer for displaying images.
@@ -91,7 +172,7 @@ class ImageViewer(QtGui.QMainWindow):
# Signal that the original image has been changed
original_image_changed = Signal(np.ndarray)
def __init__(self, image):
def __init__(self, image, useblit=True):
# Start main loop
utils.init_qtapp()
super(ImageViewer, self).__init__()
@@ -125,6 +206,12 @@ class ImageViewer(QtGui.QMainWindow):
self.canvas.setParent(self)
self.ax.autoscale(enable=False)
self._tools = []
self.useblit = useblit
if useblit:
self._blit_manager = BlitManager(self.ax)
self._event_manager = EventManager(self.ax)
self._image_plot = self.ax.images[0]
self._update_original_image(image)
self.plugins = []
@@ -238,7 +325,10 @@ class ImageViewer(QtGui.QMainWindow):
return [p.output() for p in self.plugins]
def redraw(self):
self.canvas.draw_idle()
if self.useblit:
self._blit_manager.redraw()
else:
self.canvas.draw_idle()
@property
def image(self):
@@ -280,6 +370,12 @@ class ImageViewer(QtGui.QMainWindow):
else:
self.status_message('')
def add_tool(self, tool):
if self.blit:
self._blit_manager.artists.extend(tool.artists)
self._tools.append(tool)
self._event_manager.attach(tool)
def _format_coord(self, x, y):
# callback function to format coordinate display in status bar
x = int(x + 0.5)