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()