mirror of
https://github.com/wassname/scikit-image.git
synced 2026-08-15 12:54:54 +08:00
ENH: Add PaintTool and LabelPlugin
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
from linetool import LineTool, ThickLineTool
|
||||
from recttool import RectangleTool
|
||||
from painttool import PaintTool
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user