diff --git a/skimage/viewer/canvastools/__init__.py b/skimage/viewer/canvastools/__init__.py index 5a50b8a5..22a2d205 100644 --- a/skimage/viewer/canvastools/__init__.py +++ b/skimage/viewer/canvastools/__init__.py @@ -1,2 +1,3 @@ from linetool import LineTool, ThickLineTool from recttool import RectangleTool +from painttool import PaintTool diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py new file mode 100644 index 00000000..3fbab153 --- /dev/null +++ b/skimage/viewer/canvastools/painttool.py @@ -0,0 +1,203 @@ +import numpy as np + +try: + import matplotlib.pyplot as plt + import matplotlib.colors as mcolors + LABELS_CMAP = mcolors.ListedColormap(['white', 'red', 'dodgerblue', 'gold', + 'greenyellow', 'blueviolet']) +except ImportError: + print("Could not import matplotlib -- skimage.viewer not available.") + +from base import CanvasToolBase + + +__all__ = ['PaintTool'] + + +class PaintTool(CanvasToolBase): + """Widget for painting on top of a plot. + + Parameters + ---------- + ax : :class:`matplotlib.axes.Axes` + Matplotlib axes where tool is displayed. + overlay_shape : shape tuple + 2D shape tuple used to initialize overlay image. + alpha : float (between [0, 1]) + Opacity of overlay + on_move : function + Function called whenever a control handle is moved. + This function must accept the end points of line as the only argument. + on_release : function + Function called whenever the control handle is released. + on_enter : function + Function called whenever the "enter" key is pressed. + rect_props : dict + Properties for :class:`matplotlib.patches.Rectangle`. This class + redefines defaults in :class:`matplotlib.widgets.RectangleSelector`. + + Attributes + ---------- + overlay : array + Overlay of painted labels displayed on top of image. + 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): + super(PaintTool, self).__init__(ax, on_move=on_move, on_enter=on_enter, + on_release=on_release) + + props = dict(edgecolor='r', facecolor='0.7', alpha=0.5, animated=True) + props.update(rect_props if rect_props is not None else {}) + + self.alpha = alpha + self.cmap = LABELS_CMAP + self._overlay_plot = None + self._shape = overlay_shape + self.overlay = np.zeros(overlay_shape, dtype='uint8') + + self._cursor = plt.Rectangle((0, 0), 0, 0, **props) + self._cursor.set_visible(False) + self.ax.add_patch(self._cursor) + + # `label` and `radius` can only be set after initializing `_cursor` + self.label = 1 + 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) + + @property + def label(self): + return self._label + + @label.setter + def label(self, value): + if value >= self.cmap.N: + raise ValueError('Maximum label value = %s' % len(self.cmap - 1)) + self._label = value + self._cursor.set_edgecolor(self.cmap(value)) + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, r): + self._radius = r + self._width = 2 * r + 1 + self._cursor.set_width(self._width) + self._cursor.set_height(self._width) + self.window = CenteredWindow(r, self._shape) + + @property + def overlay(self): + return self._overlay + + @overlay.setter + def overlay(self, image): + self._overlay = image + if image is None: + self.ax.images.remove(self._overlay_plot) + self._overlay_plot = None + elif self._overlay_plot is None: + props = dict(cmap=self.cmap, alpha=self.alpha, + norm=mcolors.no_norm(), animated=True) + self._overlay_plot = self.ax.imshow(image, **props) + else: + self._overlay_plot.set_data(image) + self.redraw() + + def _on_key_press(self, event): + if event.key == 'enter': + self.callback_on_enter(self.geometry) + self.redraw() + + def on_mouse_press(self, event): + if event.button != 1 or not self.ax.in_axes(event): + return + self.update_cursor(event.xdata, event.ydata) + self.update_overlay(event.xdata, event.ydata) + + def on_mouse_release(self, event): + if event.button != 1: + return + self.callback_on_release(self.geometry) + + def on_move(self, event): + if not self.ax.in_axes(event): + self._cursor.set_visible(False) + self.redraw() # make sure cursor is not visible + return + self._cursor.set_visible(True) + + self.update_cursor(event.xdata, event.ydata) + if event.button != 1: + self.redraw() # update cursor position + return + self.update_overlay(event.xdata, event.ydata) + self.callback_on_move(self.geometry) + + def update_overlay(self, x, y): + overlay = self.overlay + overlay[self.window.at(y, x)] = self.label + # Note that overlay calls `redraw` + self.overlay = overlay + + def update_cursor(self, x, y): + x = x - self.radius - 1 + y = y - self.radius - 1 + self._cursor.set_xy((x, y)) + + @property + def geometry(self): + return self.overlay + + +class CenteredWindow(object): + """Window that create slices numpy arrays over 2D windows. + + Example + ------- + >>> a = np.arange(16).reshape(4, 4) + >>> w = CenteredWindow(1, a.shape) + >>> a[w.at(1, 1)] + array([[ 0, 1, 2], + [ 4, 5, 6], + [ 8, 9, 10]]) + >>> a[w.at(0, 0)] + array([[0, 1], + [4, 5]]) + >>> a[w.at(4, 3)] + array([[14, 15]]) + """ + def __init__(self, radius, array_shape): + self.radius = radius + self.array_shape = array_shape + + def at(self, row, col): + h, w = self.array_shape + r = self.radius + xmin = max(0, col - r) + xmax = min(w, col + r + 1) + ymin = max(0, row - r) + ymax = min(h, row + r + 1) + return [slice(ymin, ymax), slice(xmin, xmax)] + + +if __name__ == '__main__': + np.testing.rundocs() + import matplotlib.pyplot as plt + from skimage import data + + image = data.camera() + + f, ax = plt.subplots() + ax.imshow(image, interpolation='nearest') + paint_tool = PaintTool(ax, image.shape) + plt.show() diff --git a/skimage/viewer/plugins/labelplugin.py b/skimage/viewer/plugins/labelplugin.py new file mode 100644 index 00000000..8ef02234 --- /dev/null +++ b/skimage/viewer/plugins/labelplugin.py @@ -0,0 +1,62 @@ +import numpy as np + +from .base import Plugin +from ..widgets import ComboBox, Slider +from ..canvastools import PaintTool + + +__all__ = ['LabelPainter'] + + +rad2deg = 180 / np.pi + + +class LabelPainter(Plugin): + name = 'LabelPainter' + + def __init__(self, max_radius=20, **kwargs): + super(LabelPainter, self).__init__(**kwargs) + + # These widgets adjust plugin properties instead of an image filter. + self._radius_widget = Slider('radius', low=1, high=max_radius, + value=5, value_type='int', ptype='plugin') + labels = [str(i) for i in range(6)] + self._label_widget = ComboBox('label', labels, ptype='plugin') + self.add_widget(self._radius_widget) + self.add_widget(self._label_widget) + + print self.help() + + def help(self): + helpstr = ("Label painter", + "Hold left-mouse button and paint on canvas.") + return '\n'.join(helpstr) + + def attach(self, image_viewer): + super(LabelPainter, self).attach(image_viewer) + + image = image_viewer.original_image + self.paint_tool = PaintTool(self.image_viewer.ax, image.shape, + on_enter=self.on_enter) + self.paint_tool.radius = self.radius + self.paint_tool.label = self._label_widget.index = 1 + self.artists.append(self.paint_tool) + + def on_enter(self, overlay): + pass + + @property + def radius(self): + return self._radius_widget.val + + @radius.setter + def radius(self, val): + self.paint_tool.radius = val + + @property + def label(self): + return self._label_widget.val + + @label.setter + def label(self, val): + self.paint_tool.label = val diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index bdbfb71e..14259f1e 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -63,7 +63,6 @@ class Text(BaseWidget): self.layout.addWidget(name_label) self.layout.addWidget(self._label) - @property def text(self): return self._label.text() @@ -251,3 +250,11 @@ class ComboBox(BaseWidget): @property def val(self): return self._combo_box.value() + + @property + def index(self): + return self._combo_box.currentIndex() + + @index.setter + def index(self, i): + self._combo_box.setCurrentIndex(i)