-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Manual segmentation tool with matplotlib #2584
Changes from 27 commits
16a7bc8
677deb0
75761d1
49a5ae3
b7376cd
3c0dffd
082f9a4
453d5f1
056383c
d8148c7
1baa506
601e1f5
33579a6
6f442cc
432b766
38f25e3
f901d96
31092ee
1ec15e9
9325b4c
2f45cb7
c2cd565
b4ce7cd
6cdacb1
2bfe8e8
d98ddae
fc76ae7
c3b2dab
98a41dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,216 @@ | ||||||
from functools import reduce | ||||||
import numpy as np | ||||||
import matplotlib | ||||||
import matplotlib.pyplot as plt | ||||||
from matplotlib.patches import Polygon | ||||||
from matplotlib.collections import PatchCollection | ||||||
from ..draw import polygon | ||||||
|
||||||
|
||||||
LEFT_CLICK = 1 | ||||||
RIGHT_CLICK = 3 | ||||||
|
||||||
|
||||||
def _mask_from_vertices(vertices, shape, label): | ||||||
mask = np.zeros(shape, dtype=int) | ||||||
pr = [y for x, y in vertices] | ||||||
pc = [x for x, y in vertices] | ||||||
rr, cc = polygon(pr, pc, shape) | ||||||
mask[rr, cc] = label | ||||||
return mask | ||||||
|
||||||
|
||||||
def _draw_polygon(ax, vertices, alpha=0.4): | ||||||
polygon = Polygon(vertices, closed=True) | ||||||
p = PatchCollection([polygon], match_original=True, alpha=alpha) | ||||||
polygon_object = ax.add_collection(p) | ||||||
plt.draw() | ||||||
return polygon_object | ||||||
|
||||||
|
||||||
def manual_polygon_segmentation(image, alpha=0.4, return_all=False): | ||||||
"""Return a label image based on polygon selections made with the mouse. | ||||||
|
||||||
Parameters | ||||||
---------- | ||||||
image : (M, N[, 3]) array | ||||||
Grayscale or RGB image. | ||||||
|
||||||
alpha : float, optional | ||||||
Transparency value for polygons drawn over the image. | ||||||
|
||||||
return_all : bool, optional | ||||||
If True, an array containing each separate polygon drawn is returned. | ||||||
(The polygons may overlap.) If False (default), latter polygons | ||||||
"overwrite" earlier ones where they overlap. | ||||||
|
||||||
Returns | ||||||
------- | ||||||
labels : array of int, shape ([Q, ]M, N) | ||||||
The segmented regions. If mode is `'separate'`, the leading dimension | ||||||
of the array corresponds to the number of regions that the user drew. | ||||||
|
||||||
Notes | ||||||
----- | ||||||
Use left click to select the vertices of the polygon | ||||||
and right click to confirm the selection once all vertices are selected. | ||||||
|
||||||
Examples | ||||||
-------- | ||||||
>>> from skimage import data, future, io | ||||||
>>> camera = data.camera() | ||||||
>>> mask = future.manual_polygon_segmentation(camera) # doctest: +SKIP | ||||||
>>> io.imshow(mask) # doctest: +SKIP | ||||||
>>> io.show() # doctest: +SKIP | ||||||
""" | ||||||
list_of_vertex_lists = [] | ||||||
polygons_drawn = [] | ||||||
|
||||||
temp_list = [] | ||||||
preview_polygon_drawn = [] | ||||||
|
||||||
if image.ndim not in (2, 3): | ||||||
raise ValueError('Only 2D grayscale or RGB images are supported.') | ||||||
|
||||||
fig, ax = plt.subplots() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is better to optionally take in either There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tacaswell we use both figure and axes methods... Would you suggest we take in a (fig, ax) tuple, perhaps? The undo button placement could also be generalised for cases where the axes are only a subplot... =\ What do you think? Personally, I'm happy to leave this for a near-future PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are asking questions about a layout manager, which we have an open issue for someone to write ;) |
||||||
plt.subplots_adjust(bottom=0.2) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is also a |
||||||
ax.imshow(image, cmap="gray") | ||||||
ax.set_axis_off() | ||||||
|
||||||
def _undo(*args, **kwargs): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've noticed that the undo button removes the last polygon/contour only when the mouse is back to image axes. Is it possible to fix this (for example, forcing a redraw operation at the end of this function)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good catch. Fixed it. :) |
||||||
if list_of_vertex_lists: | ||||||
list_of_vertex_lists.pop() | ||||||
# Remove last polygon from list of polygons... | ||||||
last_poly = polygons_drawn.pop() | ||||||
# ... then from the plot | ||||||
last_poly.remove() | ||||||
plt.draw() | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
|
||||||
undo_pos = plt.axes([0.85, 0.05, 0.075, 0.075]) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similar to above, better to use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tacaswell I looked for a Figure method but couldn't find it for some reason! It was probably morning. =P It's worth pointing out that all the relevant mpl gallery examples use plt.axes. |
||||||
undo_button = matplotlib.widgets.Button(undo_pos, u'\u27F2') | ||||||
undo_button.on_clicked(_undo) | ||||||
|
||||||
def _extend_polygon(event): | ||||||
# Do not record click events outside axis or in undo button | ||||||
if event.inaxes is None or event.inaxes is undo_pos: | ||||||
return | ||||||
# Do not record click events when toolbar is active | ||||||
if fig.canvas.manager.toolbar._active is not None: | ||||||
return | ||||||
|
||||||
if event.button == LEFT_CLICK: # Select vertex | ||||||
temp_list.append([event.xdata, event.ydata]) | ||||||
# Remove previously drawn preview polygon if any. | ||||||
if preview_polygon_drawn: | ||||||
poly = preview_polygon_drawn.pop() | ||||||
poly.remove() | ||||||
|
||||||
# Preview polygon with selected vertices. | ||||||
polygon = _draw_polygon(ax, temp_list, alpha=(alpha / 1.4)) | ||||||
preview_polygon_drawn.append(polygon) | ||||||
|
||||||
elif event.button == RIGHT_CLICK: # Confirm the selection | ||||||
if not temp_list: | ||||||
return | ||||||
|
||||||
# Store the vertices of the polygon as shown in preview. | ||||||
# Redraw polygon and store it in polygons_drawn so that | ||||||
# `_undo` works correctly. | ||||||
list_of_vertex_lists.append(temp_list[:]) | ||||||
polygon_object = _draw_polygon(ax, temp_list, alpha=alpha) | ||||||
polygons_drawn.append(polygon_object) | ||||||
|
||||||
# Empty the temporary variables. | ||||||
preview_poly = preview_polygon_drawn.pop() | ||||||
preview_poly.remove() | ||||||
del temp_list[:] | ||||||
|
||||||
plt.draw() | ||||||
|
||||||
fig.canvas.mpl_connect('button_press_event', _extend_polygon) | ||||||
|
||||||
plt.show(block=True) | ||||||
|
||||||
labels = (_mask_from_vertices(vertices, image.shape[:2], i) | ||||||
for i, vertices in enumerate(list_of_vertex_lists, start=1)) | ||||||
if return_all: | ||||||
return np.stack(labels) | ||||||
else: | ||||||
return reduce(np.maximum, labels, np.broadcast_to(0, image.shape[:2])) | ||||||
|
||||||
|
||||||
def manual_lasso_segmentation(image, alpha=0.4, return_all=False): | ||||||
"""Return a label image based on freeform selections made with the mouse. | ||||||
|
||||||
Parameters | ||||||
---------- | ||||||
image : (M, N[, 3]) array | ||||||
Grayscale or RGB image. | ||||||
|
||||||
alpha : float, optional | ||||||
Transparency value for polygons drawn over the image. | ||||||
|
||||||
return_all : bool, optional | ||||||
If True, an array containing each separate polygon drawn is returned. | ||||||
(The polygons may overlap.) If False (default), latter polygons | ||||||
"overwrite" earlier ones where they overlap. | ||||||
|
||||||
Returns | ||||||
------- | ||||||
labels : array of int, shape ([Q, ]M, N) | ||||||
The segmented regions. If mode is `'separate'`, the leading dimension | ||||||
of the array corresponds to the number of regions that the user drew. | ||||||
|
||||||
Notes | ||||||
----- | ||||||
Press and hold the left mouse button to draw around each object. | ||||||
|
||||||
Examples | ||||||
-------- | ||||||
>>> from skimage import data, future, io | ||||||
>>> camera = data.camera() | ||||||
>>> mask = future.manual_lasso_segmentation(camera) # doctest: +SKIP | ||||||
>>> io.imshow(mask) # doctest: +SKIP | ||||||
>>> io.show() # doctest: +SKIP | ||||||
""" | ||||||
list_of_vertex_lists = [] | ||||||
polygons_drawn = [] | ||||||
|
||||||
if image.ndim not in (2, 3): | ||||||
raise ValueError('Only 2D grayscale or RGB images are supported.') | ||||||
|
||||||
fig, ax = plt.subplots() | ||||||
ax.imshow(image, cmap="gray") | ||||||
ax.set_axis_off() | ||||||
|
||||||
def _undo(*args, **kwargs): | ||||||
if list_of_vertex_lists: | ||||||
list_of_vertex_lists.pop() | ||||||
# Remove last polygon from list of polygons... | ||||||
last_poly = polygons_drawn.pop() | ||||||
# ... then from the plot | ||||||
last_poly.remove() | ||||||
plt.draw() | ||||||
|
||||||
undo_pos = plt.axes([0.85, 0.05, 0.075, 0.075]) | ||||||
undo_button = matplotlib.widgets.Button(undo_pos, u'\u27F2') | ||||||
undo_button.on_clicked(_undo) | ||||||
|
||||||
def _on_lasso_selection(vertices): | ||||||
if len(vertices) < 3: | ||||||
return | ||||||
list_of_vertex_lists.append(vertices) | ||||||
polygon_object = _draw_polygon(ax, vertices, alpha=alpha) | ||||||
polygons_drawn.append(polygon_object) | ||||||
plt.draw() | ||||||
|
||||||
lasso = matplotlib.widgets.LassoSelector(ax, _on_lasso_selection) | ||||||
|
||||||
plt.show(block=True) | ||||||
|
||||||
labels = (_mask_from_vertices(vertices, image.shape[:2], i) | ||||||
for i, vertices in enumerate(list_of_vertex_lists, start=1)) | ||||||
if return_all: | ||||||
return np.stack(labels) | ||||||
else: | ||||||
return reduce(np.maximum, labels, np.broadcast_to(0, image.shape[:2])) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hmm... Could you imagine the case where this kind of output is useful? In my opinion, it should rather be a intersection (binary) of partial masks. I mean it is really cool, and users can just threshold the masks, but why have an extra step? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @soupault the point is that the multi-labels are useful, not that the overlap is useful. And they are very useful: if you want to segment 10 objects in an image, for example. It's much more annoying adding the extra step of collapsing the masks, or labelling them (in which case you have to be really really careful that they don't touch), than it is to add a >0 if you need it. Which you probably don't because functions that deal with segmentations deal with integer labels. (And I think it's worth having that be consistent throughout the library.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jni Ah, I see, thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jni How do I
[ ] Check that new features are mentioned in `doc/release/release_dev.rst`.
? :DThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@soupault you ask @pskeshu to add it to that file, or you merge and then you immediately submit a PR. =)