From 4f6f25efe9d7d1e3167856a0c3736af0c4af55ec Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 1 Nov 2012 21:48:12 -0400 Subject: [PATCH] ENH: Add canvastools subpckages with base class and line tools --- skimage/viewer/canvastools/__init__.py | 1 + skimage/viewer/canvastools/base.py | 109 +++++++++++++++ skimage/viewer/canvastools/line_tool.py | 169 ++++++++++++++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 skimage/viewer/canvastools/__init__.py create mode 100644 skimage/viewer/canvastools/base.py create mode 100644 skimage/viewer/canvastools/line_tool.py diff --git a/skimage/viewer/canvastools/__init__.py b/skimage/viewer/canvastools/__init__.py new file mode 100644 index 00000000..a04d4188 --- /dev/null +++ b/skimage/viewer/canvastools/__init__.py @@ -0,0 +1 @@ +from line_tool import LineTool, ThickLineTool diff --git a/skimage/viewer/canvastools/base.py b/skimage/viewer/canvastools/base.py new file mode 100644 index 00000000..f14b431e --- /dev/null +++ b/skimage/viewer/canvastools/base.py @@ -0,0 +1,109 @@ +import numpy as np +import matplotlib as mpl +from matplotlib import lines + + +__all__ = ['CanvasToolBase', 'ToolHandles'] + + +class CanvasToolBase(object): + """Base canvas tool for matplotlib axes. + + Parameters + ---------- + """ + def __init__(self, ax, useblit=None): + self.ax = ax + self.canvas = ax.figure.canvas + self.cids = [] + self._artists = [] + self.active = True + + if useblit is None: + useblit = True if mpl.backends.backend.endswith('Agg') else False + self.useblit = useblit + if useblit: + bbox = self.ax.bbox + self.img_background = self.canvas.copy_from_bbox(bbox) + + 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. + + This method (or a version of it) should be called at the beginning + of any event callback. + """ + return not self.active + + def set_visible(self, val): + for a in self._artists: + a.set_visible(val) + + +class ToolHandles(object): + """Control handles for canvas tools. + + Parameters + ---------- + ax : :class:`matplotlib.axes.Axes` + Matplotlib axes where tool handles are displayed. + x, y : 1D arrays + Coordinates of control handles. + marker : str + Shape of marker used to display handle. See `matplotlib.pyplot.plot`. + marker_props : see :class:`matplotlib.lines.Line2D`. + """ + def __init__(self, ax, x, y, marker='o', markerprops=None): + self.ax = ax + + props = dict(mfc='w', ls='none', alpha=0.5, visible=False) + props.update(markerprops if markerprops is not None else {}) + self._markers = lines.Line2D(x, y, marker=marker, **props) + self.ax.add_line(self._markers) + self.artist = self._markers + + @property + def x(self): + return self._markers.get_xdata() + + @property + def y(self): + return self._markers.get_ydata() + + def set_data(self, pts, y=None): + """Set x and y positions of handles""" + if y is not None: + x = pts + pts = np.array([x, y]) + self._markers.set_data(pts) + + def set_visible(self, val): + self._markers.set_visible(val) + + 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)) + # Transform data coordinates to pixel coordinates. + pts = self.ax.transData.transform(pts) + diff = pts - ((x, y)) + dist = np.sqrt(np.sum(diff**2, axis=1)) + return np.argmin(dist), np.min(dist) diff --git a/skimage/viewer/canvastools/line_tool.py b/skimage/viewer/canvastools/line_tool.py new file mode 100644 index 00000000..7dd3d064 --- /dev/null +++ b/skimage/viewer/canvastools/line_tool.py @@ -0,0 +1,169 @@ +import numpy as np +from matplotlib import lines + +from base import CanvasToolBase, ToolHandles + + +__all__ = ['LineTool', 'ThickLineTool'] + + +class LineTool(CanvasToolBase): + """ + Parameters + ---------- + on_update : function + Function accepting end points of line as the only argument. + + Attributes + ---------- + end_pts : 2D array + End points of line ((x1, y1), (x2, y2)). + """ + def __init__(self, ax, x, y, on_update=None, on_enter=None, maxdist=10, + lineprops=None): + super(LineTool, self).__init__(ax) + + #TODO: Figure out how to cleanly restore image background for useblit + self.useblit = False + + props = dict(color='r', linewidth=1, alpha=0.4, solid_capstyle='butt') + props.update(lineprops if lineprops is not None else {}) + self.linewidth = props['linewidth'] + self.maxdist = maxdist + self._active_pt = None + + if on_update is None: + on_update = lambda *args: None + self.on_update = on_update + + if on_enter is None: + on_enter = lambda *args: None + self.on_enter = on_enter + + self._init_end_pts = np.transpose([x, y]) + self.end_pts = self._init_end_pts.copy() + + self._line = lines.Line2D(x, y, **props) + ax.add_line(self._line) + + self._handles = ToolHandles(ax, x, y) + self._handles.set_visible(True) + self._artists = [self._line, self._handles.artist] + + 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) + + def on_mouse_press(self, event): + if event.button != 1: + return + if event.inaxes == None: + return + idx, px_dist = self._handles.closest(event.x, event.y) + if px_dist < self.maxdist: + self._active_pt = idx + + def on_mouse_release(self, event): + if event.button != 1: + return + self._active_pt = None + + def on_move(self, event): + if event.button != 1: + return + if self._active_pt is None: + return + if not self.ax.in_axes(event): + return + x, y = event.xdata, event.ydata + self.update(x, y) + + def on_key_press(self, event): + if event.key == 'enter': + self.on_enter(self.end_pts) + self.set_visible(False) + self.redraw() + + def reset(self): + self.end_pts = self._init_end_pts.copy() + self._line.set_data(np.transpose(self.end_pts)) + self._handles.set_data(np.transpose(self.end_pts)) + self.update(None, None) + + def update(self, x, y): + if x is not None: + self.end_pts[self._active_pt, :] = x, y + self._line.set_data(np.transpose(self.end_pts)) + self._handles.set_data(np.transpose(self.end_pts)) + self._line.set_linewidth(self.linewidth) + + self.ax.relim() + self.redraw() + + self.on_update(self.end_pts) + + def redraw(self): + if self.useblit: + # self.canvas.restore_region(self.img_background) + self.ax.draw_artist(self._line) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw_idle() + + +class ThickLineTool(LineTool): + + def __init__(self, ax, x, y, on_update=None, on_enter=None, maxdist=10, + lineprops=None): + super(ThickLineTool, self).__init__(ax, x, y, on_update=on_update, + on_enter=on_enter, maxdist=maxdist, + lineprops=lineprops) + + self.connect_event('key_press_event', self.on_key_press) + self.connect_event('scroll_event', self.on_scroll) + + def on_scroll(self, event): + if not event.inaxes: + return + if event.button == 'up': + self._thicken_scan_line() + elif event.button == 'down': + self._shrink_scan_line() + + def on_key_press(self, event): + super(ThickLineTool, self).on_key_press(event) + + if event.key == '+': + self._thicken_scan_line() + elif event.key == '-': + self._shrink_scan_line() + elif event.key == 'r': + self.reset() + + def _thicken_scan_line(self): + self.linewidth += 1 + self.update(None, None) + + def _shrink_scan_line(self): + if self.linewidth > 1: + self.linewidth -= 1 + self.update(None, None) + + +if __name__ == '__main__': + import matplotlib.pyplot as plt + from skimage import data + + image = data.camera() + + f, ax = plt.subplots() + ax.imshow(image, interpolation='nearest') + h, w = image.shape + + def printer(pts): + x, y = np.transpose(pts) + print "length = %0.2f" % np.sqrt(np.diff(x)**2 + np.diff(y)**2) + + # line_tool = LineTool(ax, [w/3, 2*w/3], [h/2, h/2]) + line_tool = ThickLineTool(ax, [w/3, 2*w/3], [h/2, h/2], on_enter=printer) + plt.show()