mirror of
https://github.com/wassname/scikit-image.git
synced 2026-07-01 03:59:53 +08:00
Refactor blit manager and event manager to viewer
This commit is contained in:
committed by
Steven Silvester
parent
1799846be9
commit
2d9d8bb94c
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user