From 2d9d8bb94ce8f32ff8d07feab60a509c16d21cdb Mon Sep 17 00:00:00 2001 From: blink1073 Date: Sun, 20 Jul 2014 18:39:49 -0500 Subject: [PATCH] Refactor blit manager and event manager to viewer --- skimage/viewer/canvastools/base.py | 115 +++++------------------- skimage/viewer/canvastools/linetool.py | 83 +++-------------- skimage/viewer/canvastools/painttool.py | 20 ++--- skimage/viewer/canvastools/recttool.py | 25 +++--- skimage/viewer/viewers/core.py | 102 ++++++++++++++++++++- 5 files changed, 153 insertions(+), 192 deletions(-) diff --git a/skimage/viewer/canvastools/base.py b/skimage/viewer/canvastools/base.py index fce163f3..a1a06360 100644 --- a/skimage/viewer/canvastools/base.py +++ b/skimage/viewer/canvastools/base.py @@ -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)) diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index 4b53ebad..e1b48c05 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -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 diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index 9b94e0a2..8d3c9449 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -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 diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py index 929fe939..d3724bf9 100644 --- a/skimage/viewer/canvastools/recttool.py +++ b/skimage/viewer/canvastools/recttool.py @@ -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 diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 52ac073a..afd3ff2b 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -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)