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

Convex hull object implementation #522

Closed
wants to merge 12 commits into from
130 changes: 106 additions & 24 deletions skimage/morphology/convex_hull.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,148 @@
__all__ = ['convex_hull_image']
__all__ = ['convex_hull_image', 'connected_component', 'convex_hull_object']

import numpy as np
from ._pnpoly import grid_points_inside_poly
from ._convex_hull import possible_hull

from .selem import square as sq
Copy link
Member

Choose a reason for hiding this comment

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

Keep it as square--the 4 character savings isnt worth it.

from skimage.morphology import label, dilation

def convex_hull_image(image):

Copy link
Member

Choose a reason for hiding this comment

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

This whitespace noise shouldn't be in the PR. Configure your editor to change all tabs to spaces, to remove all trailing whitespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am outside right now, but I will sit and configure this once and for all. Sorry for this. Any suggestions on the preferable text editor to use on OS X ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sublime is hard to beat.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi Josh,

Aah thanks for the suggestion. I'll try it out.

Chintak

"""Compute the convex hull image 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.

hull : ndarray of bool
Binary image with pixels in convex hull set to True.
References
----------
.. [1] http://blogs.mathworks.com/steve/2011/10/04/binary-image-convex-hull-algorithm-notes/

"""

image = image.astype(bool)

# Here we do an optimisation by choosing only pixels that are
# the starting or ending pixel of a row or column. This vastly
# limits the number of coordinates to examine for the virtual
# hull.
coords = possible_hull(image.astype(np.uint8))
N = len(coords)

# Add a vertex for the middle of each pixel edge
coords_corners = np.empty((N * 4, 2))
for i, (x_offset, y_offset) in enumerate(zip((0, 0, -0.5, 0.5),
(-0.5, 0.5, 0, 0))):

coords_corners[i * N:(i + 1) * N] = coords + [x_offset, y_offset]

coords = coords_corners

try:
from scipy.spatial import Delaunay
except ImportError:
raise ImportError('Could not import scipy.spatial, only available in '
'scipy >= 0.9.')

coords = coords_corners
Copy link
Member

Choose a reason for hiding this comment

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

This change will certainly make things break

try:
from scipy.spatial import Delaunay
except ImportError:
raise ImportError('Could not import scipy.spatial, only available in '
'scipy >= 0.9.')

# 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
mask = grid_points_inside_poly(image.shape[:2], v)

return mask

def connected_component(image, start_pixel_tuple):
Copy link
Member

Choose a reason for hiding this comment

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

What do we get from this function that we don't get from label?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

label is genuinely a very useful module however, it returns the complete image with all the objects marked, but not an option for processing a single object. This function can work alongside label to give you a single object or as a 2D version of flood fill algorithm.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but in this case we can probably use the labeled image to get the components we want as masks. As discussed on the mailing list today, it does look like a good and fast floodfill algorithm will be useful.

Copy link
Member

Choose a reason for hiding this comment

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

It would be even more useful if it was nD or at least 3D from the start...
;)

On Tue, Apr 23, 2013 at 12:15 AM, Stefan van der Walt <
notifications@github.com> wrote:

In skimage/morphology/convex_hull.py:

 return mask

+def connected_component(image, start_pixel_tuple):

Yes, but in this case we can probably use the labeled image to get the
components we want as masks. As discussed on the mailing list today, it
does look like a good and fast floodfill algorithm will be useful.


Reply to this email directly or view it on GitHubhttps://github.com//pull/522/files#r3896100
.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To start with, I knew of the implementation using repetitive dilation. But
now on second thoughts, its rather inefficient. Im looking up for
implementation of flood fill algorithm as was seen in a recent message on
the mailing list. Any material you know of in this regards ? Also is there
a better way of representing or storing the individual convex hull of the
objects in an image ? :-)


"""Compute the connected object to a given starting pixel with
8-connectivity for 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.

start_pixel_tuple : tuple of int
Copy link
Member

Choose a reason for hiding this comment

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

position or origin perhaps?

Of the form (x, y) which represents the index of the starting pixel

Returns
-------
obj : ndarray of uint8
Binary image with pixels in convex hull set to True.

"""

next_im = np.zeros(image.shape, dtype=np.uint8)
next_im[start_pixel_tuple] = 1
start_im = np.zeros(image.shape)
# Structuring element for Dilation: square of side 3 with all elements 1.
while not np.array_equal(start_im, next_im):
start_im = next_im.copy()
dilated_im = dilation(start_im, sq(3))
next_im = dilated_im & image

return next_im

def convex_hull_object(image, output_form=None):

"""Compute the convex hull image of individual objects in 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.

output_form : string
if 'single' then outputs a 3D array with separate convex hull computed
for individual objects, where the 3rd index is used to change the object
Default is None, in which case it outputs the convex hull for all
objects individually as a single 2D array

Returns
-------
hull : ndarray of bool
Copy link
Member

Choose a reason for hiding this comment

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

Convex hulls can overlap, so this is not a suitable way to represent the output.

Copy link
Member

Choose a reason for hiding this comment

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

I see matlab does it this way too. I'm not sure how I feel about that. However, since we don't currently have a better way of representing connected components (this is something we can also consider implementing), I guess the union of the hulls of the images (as you have it here) will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How would you prefer the output to be ? That is why there is also an option to return the hulls computed for individual objects. Probably one could use that to process this to suit requirements ? I'd be happy to incorporate another way of combining the hulls.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, thanks, I only saw that output flag now. I think single mode can very quickly result in extremely high memory use, and that we first need to find an efficient way to store a connected region in an image.

Binary image with pixels in convex hull set to True.

"""

# We add 1 to the output of label() so as to make the
# background 0 rather than -1

(m, n) = image.shape
convex_out = np.zeros((m, n), dtype=bool)
labeled_im = label(image, neighbors=8, background=0) +
segmented_objs = np.zeros((m, n, labeled_im.max()), dtype=bool)
convex_objs = np.zeros((m, n, labeled_im.max()), dtype=bool)

for i in range(1, labeled_im.max()+1):

start_pixel_tuple = tuple(transpose(np.where(labeled_im == i))[0])
segmented_objs[:, :, i-1] = connected_component(image, start_pixel_tuple)
convex_objs[:, :, i-1] = convex_hull_image(segmented_objs[:, :, i-1])
convex_out |= convex_objs[:, :, i-1]

if output_form is 'single':
return convex_objs

if output_form is None:
return convex_out
38 changes: 38 additions & 0 deletions skimage/morphology/tests/test_convex_hull.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,44 @@ def test_basic():

assert_array_equal(convex_hull_image(image), expected)

@skipif(not scipy_spatial)
def test_connected_components():
image = np.array(
[[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 1, 1, 0, 1, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 1, 0, 0]], dtype=bool)

expected = np.array(
[[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 1, 0, 0]], dtype=uint8)

assert_array_equal(convex_hull_image(image), expected)

image = np.array(
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 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, 0],
[0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)

assert_array_equal(convex_hull_image(image), expected)


@skipif(not scipy_spatial)
def test_possible_hull():
Expand Down