From 631e97d9735ea6fd5e956c8255e5d9f99f9c9ad2 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 12 Dec 2012 23:43:46 -0500 Subject: [PATCH] ENH: Add rectangle tool and crop plugin example --- skimage/viewer/canvastools/__init__.py | 1 + skimage/viewer/canvastools/recttool.py | 195 +++++++++++++++++++++++++ skimage/viewer/plugins/crop.py | 34 +++++ skimage/viewer/viewers/core.py | 11 ++ viewer_examples/plugins/croptool.py | 9 ++ 5 files changed, 250 insertions(+) create mode 100644 skimage/viewer/canvastools/recttool.py create mode 100644 skimage/viewer/plugins/crop.py create mode 100644 viewer_examples/plugins/croptool.py diff --git a/skimage/viewer/canvastools/__init__.py b/skimage/viewer/canvastools/__init__.py index 326ce780..5a50b8a5 100644 --- a/skimage/viewer/canvastools/__init__.py +++ b/skimage/viewer/canvastools/__init__.py @@ -1 +1,2 @@ from linetool import LineTool, ThickLineTool +from recttool import RectangleTool diff --git a/skimage/viewer/canvastools/recttool.py b/skimage/viewer/canvastools/recttool.py new file mode 100644 index 00000000..ba189b4b --- /dev/null +++ b/skimage/viewer/canvastools/recttool.py @@ -0,0 +1,195 @@ +import matplotlib.widgets as mwidgets + +from skimage.viewer.canvastools.base import CanvasToolBase +from skimage.viewer.canvastools.base import ToolHandles + + +__all__ = ['RectangleTool'] + + +class RectangleTool(mwidgets.RectangleSelector, CanvasToolBase): + """Widget for selecting a rectangular region in a plot. + + After making the desired selection, press "Enter" to accept the selection + and call the `on_enter` callback function. + + Parameters + ---------- + ax : :class:`matplotlib.axes.Axes + on_enter : function + Function accepting rectangle extents as the only argument; called + whenever "Enter" key is pressed. If None, print extents of rectangle. + maxdist : float + Maximum distance in pixels for selection of a control handle + (i.e. corner or edge) handle. + rectprops : dict + Properties for :class:`matplotlib.patches.Rectangle`. This class + redefines defaults in :class:`matplotlib.widgets.RectangleSelector`. + kwargs : see :class:`matplotlib.widgets.RectangleSelector`. + + Attributes + ---------- + extents : tuple + Rectangle extents: (xmin, xmax, ymin, ymax). + """ + + def __init__(self, ax, on_update=None, on_enter=None, rectprops=None, + maxdist=10, **kwargs): + CanvasToolBase.__init__(self, ax, on_update=on_update, + on_enter=on_enter) + + props = dict(edgecolor=None, facecolor='r', alpha=0.15) + props.update(rectprops if rectprops is not None else {}) + if props['edgecolor'] is None: + props['edgecolor'] = props['facecolor'] + mwidgets.RectangleSelector.__init__(self, ax, lambda *args: None, + rectprops=props, + useblit=self.useblit) + # Alias rectangle attribute, which is initialized in RectangleSelector. + self._rect = self.to_draw + self._rect.set_animated(True) + + self.maxdist = maxdist + self.active_handle = None + self._extents_on_press = None + + if on_enter is None: + def on_enter(extents): + print "(xmin=%.3g, xmax=%.3g, ymin=%.3g, ymax=%.3g)" % extents + self.on_enter = on_enter + + props = dict(mec=props['edgecolor']) + self._corner_order = ['NW', 'NE', 'SE', 'SW'] + xc, yc = self.corners + self._corner_handles = ToolHandles(ax, xc, yc, marker_props=props) + + self._edge_order = ['W', 'N', 'E', 'S'] + xe, ye = self.edge_centers + self._edge_handles = ToolHandles(ax, xe, ye, marker='s', + marker_props=props) + + self._artists = [self._rect, + self._corner_handles.artist, + self._edge_handles.artist] + + @property + def _rect_bbox(self): + x0 = self._rect.get_x() + y0 = self._rect.get_y() + width = self._rect.get_width() + height = self._rect.get_height() + return x0, y0, width, height + + @property + def corners(self): + """Corners of rectangle from lower left, moving clockwise.""" + x0, y0, width, height = self._rect_bbox + xc = x0, x0 + width, x0 + width, x0 + yc = y0, y0, y0 + height, y0 + height + return xc, yc + + @property + def edge_centers(self): + """Midpoint of rectangle edges from left, moving clockwise.""" + x0, y0, width, height = self._rect_bbox + w = width / 2. + h = height / 2. + xe = x0, x0 + w, x0 + width, x0 + w + ye = y0 + h, y0, y0 + h, y0 + height + return xe, ye + + @property + def extents(self): + """Return (xmin, xmax, ymin, ymax).""" + x0, y0, width, height = self._rect_bbox + xmin, xmax = sorted([x0, x0 + width]) + ymin, ymax = sorted([y0, y0 + height]) + return xmin, xmax, ymin, ymax + + def release(self, event): + mwidgets.RectangleSelector.release(self, event) + self._extents_on_press = None + # Undo hiding of rectangle and redraw. + self.set_visible(True) + self.redraw() + + def press(self, event): + self._set_active_handle(event) + if self.active_handle is None: + # Clear previous rectangle before drawing new rectangle. + self.set_visible(False) + self.redraw() + self.set_visible(True) + mwidgets.RectangleSelector.press(self, event) + + def _set_active_handle(self, event): + """Set active handle based on the location of the mouse event""" + # Note: event.xdata/ydata in data coordinates, event.x/y in pixels + c_idx, c_dist = self._corner_handles.closest(event.x, event.y) + e_idx, e_dist = self._edge_handles.closest(event.x, event.y) + + # Set active handle as closest handle, if mouse click is close enough. + if c_dist > self.maxdist and e_dist > self.maxdist: + self.active_handle = None + return + elif c_dist < e_dist: + self.active_handle = self._corner_order[c_idx] + else: + self.active_handle = self._edge_order[e_idx] + + # Save coordinates of rectangle at the start of handle movement. + x1, x2, y1, y2 = self.extents + # Switch variables so that only x2 and/or y2 are updated on move. + if self.active_handle in ['W', 'SW', 'NW']: + x1, x2 = x2, event.xdata + if self.active_handle in ['N', 'NW', 'NE']: + y1, y2 = y2, event.ydata + self._extents_on_press = x1, x2, y1, y2 + + def onmove(self, event): + + if self.eventpress is None or self.ignore(event): + return + + if self.active_handle is None: + # New rectangle + x1 = self.eventpress.xdata + y1 = self.eventpress.ydata + x2, y2 = event.xdata, event.ydata + else: + x1, x2, y1, y2 = self._extents_on_press + if self.active_handle in ['E', 'W'] + self._corner_order: + x2 = event.xdata + if self.active_handle in ['N', 'S'] + self._corner_order: + y2 = event.ydata + xmin, xmax = sorted([x1, x2]) + ymin, ymax = sorted([y1, y2]) + + # Update displayed rectangle + self._rect.set_x(xmin) + self._rect.set_y(ymin) + self._rect.set_width(xmax - xmin) + self._rect.set_height(ymax - ymin) + + # Update displayed handles + self._corner_handles.set_data(*self.corners) + self._edge_handles.set_data(*self.edge_centers) + + self.redraw() + + @property + def geometry(self): + return self.extents + + +if __name__ == '__main__': + import matplotlib.pyplot as plt + from skimage import data + + f, ax = plt.subplots() + ax.imshow(data.camera(), interpolation='nearest') + + rect_tool = RectangleTool(ax) + plt.show() + print "Final selection:", + rect_tool.on_enter(rect_tool.extents) diff --git a/skimage/viewer/plugins/crop.py b/skimage/viewer/plugins/crop.py new file mode 100644 index 00000000..49f6014b --- /dev/null +++ b/skimage/viewer/plugins/crop.py @@ -0,0 +1,34 @@ +from .base import Plugin +from ..canvastools import RectangleTool +from skimage.viewer.widgets.history import SaveButtons + + +__all__ = ['Crop'] + + +class Crop(Plugin): + name = 'Crop' + + def __init__(self, maxdist=10, **kwargs): + super(Crop, self).__init__(**kwargs) + self.maxdist = maxdist + self.add_widget(SaveButtons()) + print self.help() + + def attach(self, image_viewer): + super(Crop, self).attach(image_viewer) + + self.rect_tool = RectangleTool(self.image_viewer.ax, + maxdist=self.maxdist, + on_enter=self.crop) + + def help(self): + helpstr = ("Crop tool", + "Select rectangular region and press enter to crop.") + return '\n'.join(helpstr) + + def crop(self, extents): + xmin, xmax, ymin, ymax = extents + image = self.image_viewer.image[ymin:ymax+1, xmin:xmax+1] + self.image_viewer.image = image + self.image_viewer.ax.relim() diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index f740eb81..4bd70f64 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -137,10 +137,21 @@ class ImageViewer(QMainWindow): def image(self, image): self._img = image self._image_plot.set_array(image) + + # Adjust size if new image shape doesn't match the original + h, w = image.shape + # update data coordinates (otherwise pixel coordinates are off) + self._image_plot.set_extent((0, w, h, 0)) + # update display (otherwise image doesn't fill the canvas) + self.ax.set_xlim(0, w) + self.ax.set_ylim(h, 0) + + # update color range clim = dtype_range[image.dtype.type] if clim[0] < 0 and image.min() >= 0: clim = (0, clim[1]) self._image_plot.set_clim(clim) + self.redraw() def reset_image(self): diff --git a/viewer_examples/plugins/croptool.py b/viewer_examples/plugins/croptool.py new file mode 100644 index 00000000..0eb44903 --- /dev/null +++ b/viewer_examples/plugins/croptool.py @@ -0,0 +1,9 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.crop import Crop + + +image = data.camera() +viewer = ImageViewer(image) +viewer += Crop() +viewer.show()