Skip to content

Commit

Permalink
ENH: Add rectangle tool and crop plugin example
Browse files Browse the repository at this point in the history
  • Loading branch information
tonysyu committed Dec 13, 2012
1 parent 36bc6da commit 631e97d
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 0 deletions.
1 change: 1 addition & 0 deletions skimage/viewer/canvastools/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from linetool import LineTool, ThickLineTool
from recttool import RectangleTool
195 changes: 195 additions & 0 deletions skimage/viewer/canvastools/recttool.py
Original file line number Diff line number Diff line change
@@ -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)
34 changes: 34 additions & 0 deletions skimage/viewer/plugins/crop.py
Original file line number Diff line number Diff line change
@@ -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()
11 changes: 11 additions & 0 deletions skimage/viewer/viewers/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,21 @@ def image(self):
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):
Expand Down
9 changes: 9 additions & 0 deletions viewer_examples/plugins/croptool.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 631e97d

Please sign in to comment.