Skip to content
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

Merged
merged 29 commits into from
Apr 19, 2017
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
16a7bc8
Manual segmentation tool with matplotlib
pskeshu Mar 24, 2017
677deb0
fixed PEP8 issues
pskeshu Mar 24, 2017
75761d1
added support for RGB images; implemented undo function
pskeshu Mar 24, 2017
49a5ae3
draw a polygon around ROI with some transparency
pskeshu Mar 24, 2017
b7376cd
Optional parameter to speed up mask
pskeshu Mar 25, 2017
3c0dffd
style changes; mask generation with skimage.draw.polygon
pskeshu Mar 26, 2017
082f9a4
Doctest skip; changed the undo button; cleaned up code
pskeshu Mar 27, 2017
453d5f1
resized the button
pskeshu Mar 27, 2017
056383c
doctest skip fix; helper function to generate mask from verts; option…
pskeshu Mar 27, 2017
d8148c7
updated documentation
pskeshu Mar 27, 2017
1baa506
updated docstring
pskeshu Mar 28, 2017
601e1f5
optional draw_lines parameter to draw polygons by clicking on vertice…
pskeshu Apr 3, 2017
33579a6
fixes the wrong button press behaviour when toolbar is active;
pskeshu Apr 3, 2017
6f442cc
interactively change between lasso and polygonal mode
pskeshu Apr 12, 2017
432b766
Move manual segmentation to the future package
jni Apr 12, 2017
38f25e3
Various typo and style fixes
jni Apr 12, 2017
f901d96
Remove mouse configuration
jni Apr 12, 2017
31092ee
Rename mask to labels
jni Apr 12, 2017
1ec15e9
Separate into two separate drawing functions
jni Apr 12, 2017
9325b4c
Use constants for left and right click
jni Apr 12, 2017
2f45cb7
Add API cleanup to TODO
jni Apr 12, 2017
c2cd565
Merge pull request #1 from jni/manual-seg-tweaks
pskeshu Apr 12, 2017
b4ce7cd
temp_list as array before copy
pskeshu Apr 12, 2017
6cdacb1
fixed incorrect function names
pskeshu Apr 12, 2017
2bfe8e8
return an empty image if there are no selections made
pskeshu Apr 13, 2017
d98ddae
fixed temp_list copy
pskeshu Apr 13, 2017
fc76ae7
removed unused variable; fixed typo in doc; force redraw when undo;
pskeshu Apr 13, 2017
c3b2dab
some matplotlib style changes; thanks to @tacaswell
pskeshu Apr 14, 2017
98a41dd
new feature - manual segmentation with matplotlib #2584
pskeshu Apr 14, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions TODO.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Remember to list any API changes below in `doc/source/api_changes.txt`.

Version 0.14
------------
* Finalize ``skimage.future.graph`` API.
* Remove deprecated ``ntiles_*` kwargs in ``equalize_adapthist``.
* Remove deprecated ``skimage.restoration.nl_means_denoising``.
* Remove deprecated ``skimage.filters.gaussian_filter``.
Expand All @@ -24,6 +25,7 @@ Version 0.14

Version 0.15
------------
* Finalize ``skimage.future.manual_segmentation`` API.
* In ``skimage.util.dtype_limits``, set default behavior of `clip_negative` to `False`.
* In ``skimage.transform.radon``, set default behavior of `circle` to `True`.
* In ``skimage.transform.iradon``, set default behavior of `circle` to `True`.
Expand Down
4 changes: 3 additions & 1 deletion skimage/future/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
"""

from . import graph
from .manual_segmentation import manual_polygon_segmentation, manual_lasso_segmentation

__all__ = ['graph']

__all__ = ['graph', 'manual_lasso_segmentation', 'manual_polygon_segmentation']
216 changes: 216 additions & 0 deletions skimage/future/manual_segmentation.py
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):
Copy link
Member

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`.? :D

Copy link
Member

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. =)

"""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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better to optionally take in either fig or ax and only do this is the user does not supply one. This makes your function immediately usable by people embedding Matplotlib in larger GUI applications.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a Figure method, that is a better option.

ax.imshow(image, cmap="gray")
ax.set_axis_off()

def _undo(*args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The 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)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fig.canvas.draw_idle() would be better here than pyplot.


undo_pos = plt.axes([0.85, 0.05, 0.075, 0.075])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to above, better to use the Figure method version of this.

Copy link
Member

Choose a reason for hiding this comment

The 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]))
Copy link
Member

@soupault soupault Apr 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manual_lasso_segmentation(..., return_all=False) manual_polygon_segmentation(..., return_all=False)
image image

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?

Copy link
Member

Choose a reason for hiding this comment

The 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.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is also useful if you want to segment within an object. It is particularly useful for me when I want to segment a cell, and then segment its nucleus, so I can look at the whole cell, if I want to, but I can also look at just the cytoplasm or the nucleus.
figure_1
figure_1-1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jni Ah, I see, thanks!