diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 307379f8..bff86ccc 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -84,3 +84,5 @@ - Nelle Varoquaux Renaming of the package to ``skimage``. +- W. Randolph Franklin + Point in polygon test. diff --git a/doc/examples/plot_convex_hull.py b/doc/examples/plot_convex_hull.py new file mode 100644 index 00000000..8308bd90 --- /dev/null +++ b/doc/examples/plot_convex_hull.py @@ -0,0 +1,35 @@ +""" +=========== +Convex Hull +=========== + +The convex hull of a binary image is the set of pixels included in the +smallest convex polygon that surround all white pixels in the input. + +In this example, we show how the input pixels (white) get filled in by the +convex hull (white and grey). + +A good overview of the algorithm is given on `Steve Eddin's blog +`__. + +""" + +import numpy as np +import matplotlib.pyplot as plt + +from skimage.morphology import convex_hull + +image = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=float) + +chull = convex_hull(image) +image[chull] += 1.7 +image -= -1.7 + +plt.imshow(image, cmap=plt.cm.gray, interpolation='nearest') +plt.show() diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index efb92507..5a7d67d7 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -3,3 +3,4 @@ from selem import * from .ccomp import label from watershed import watershed, is_local_maximum from skeletonize import skeletonize, medial_axis +from .convex_hull import convex_hull diff --git a/skimage/morphology/_pnpoly.h b/skimage/morphology/_pnpoly.h new file mode 100644 index 00000000..a477a70f --- /dev/null +++ b/skimage/morphology/_pnpoly.h @@ -0,0 +1,71 @@ +/* `pnpoly` is from + + http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html + + Copyright (c) 1970-2003, Wm. Randolph Franklin + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, + modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimers. + 2. Redistributions in binary form must reproduce the above + copyright notice in the documentation and/or other materials + provided with the distribution. + 3. The name of W. Randolph Franklin may not be used to endorse or + promote products derived from this Software without specific + prior written permission. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. */ + +#ifdef __cplusplus +extern "C" { +#endif + +int pnpoly(int nr_verts, double *xp, double *yp, double x, double y) +{ + int i,j, c=0; + for (i = 0, j = nr_verts-1; i < nr_verts; j = i++) { + if ((((yp[i]<=y) && (yvx.data, vy.data, + x.shape[0], x.data, y.data, + out.data) + + return out.astype(bool) + diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py new file mode 100644 index 00000000..36c142aa --- /dev/null +++ b/skimage/morphology/convex_hull.py @@ -0,0 +1,56 @@ +__all__ = ['convex_hull'] + +import numpy as np +from scipy.spatial import Delaunay +from ._pnpoly import points_inside_poly + +def convex_hull(image): + """Compute the convex hull of a binary image. + + The convex hull is the set of pixels included in the smallest convex + polygon that surround all white pixels in the input image. + + Parameters + ---------- + image : ndarray + Binary input image. This array is cast to bool before processing. + + Returns + ------- + hull : ndarray of uint8 + Binary image with pixels in convex hull set to 255. + + References + ---------- + .. [1] http://blogs.mathworks.com/steve/2011/10/04/binary-image-convex-hull-algorithm-notes/ + + """ + image = image.astype(bool) + r, c = np.nonzero(image) + coords = np.vstack((r, c)).T + + # Add a vertex for the middle of each pixel edge + coords_corners = np.empty((0, 2)) + for x_offset, y_offset in zip((0, 0, -0.5, 0.5), + (-0.5, 0.5, 0, 0)): + coords_corners = np.vstack((coords_corners, + coords + [x_offset, y_offset])) + + coords = coords_corners + + # Find the convex hull + chull = Delaunay(coords).convex_hull + v = coords[np.unique(chull)] + + # Sort vertices clock-wise + v_centred = v - v.mean(axis=0) + angles = np.arctan2(v_centred[:, 0], v_centred[:, 1]) + v = v[np.argsort(angles)] + + # For each pixel coordinate, check whether that pixel + # lies inside the convex hull + xy = np.dstack(np.mgrid[:image.shape[0], :image.shape[1]]).reshape(-1, 2) + mask = points_inside_poly(xy, v) + mask = mask.reshape(image.shape[:2]) + + return mask diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index 46010989..2b3f605c 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -15,6 +15,7 @@ def configuration(parent_package='', top_path=None): cython(['cmorph.pyx'], working_path=base_path) cython(['_watershed.pyx'], working_path=base_path) cython(['_skeletonize.pyx'], working_path=base_path) + cython(['_pnpoly.pyx'], working_path=base_path) config.add_extension('ccomp', sources=['ccomp.c'], include_dirs=[get_numpy_include_dirs()]) @@ -24,6 +25,8 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('_skeletonize', sources=['_skeletonize.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_pnpoly', sources=['_pnpoly.c'], + include_dirs=[get_numpy_include_dirs()]) return config diff --git a/skimage/morphology/tests/test_convex_hull.py b/skimage/morphology/tests/test_convex_hull.py new file mode 100644 index 00000000..f6abe58f --- /dev/null +++ b/skimage/morphology/tests/test_convex_hull.py @@ -0,0 +1,25 @@ +import numpy as np +from numpy.testing import assert_array_equal +from skimage.morphology import convex_hull + +def test_basic(): + image = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=bool) + + expected = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=bool) + + assert_array_equal(convex_hull(image), expected) + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/morphology/tests/test_pnpoly.py b/skimage/morphology/tests/test_pnpoly.py new file mode 100644 index 00000000..eb8dbf87 --- /dev/null +++ b/skimage/morphology/tests/test_pnpoly.py @@ -0,0 +1,26 @@ +import numpy as np + +from skimage.morphology._pnpoly import points_inside_poly + +class test_poly(): + def test_square(self): + v = np.array([[0, 0], + [0, 1], + [1, 1], + [1, 0]]) + assert(points_inside_poly([[0.5, 0.5]], v)[0]) + assert(not points_inside_poly([[-0.1, 0.1]], v)[0]) + + def test_triangle(self): + v = np.array([[0, 0], + [1, 0], + [0.5, 0.75]]) + assert(points_inside_poly([[0.5, 0.7]], v)[0]) + assert(not points_inside_poly([[0.5, 0.76]], v)[0]) + assert(not points_inside_poly([[0.7, 0.5]], v)[0]) + + def test_type(self): + assert(points_inside_poly([[0, 0]], [[0, 0]]).dtype == np.bool) + +if __name__ == "__main__": + np.testing.run_module_suite()