views:

519

answers:

3

Hi,

I want to calculate a convex hull around a shape in a binary NxM matrix. The convex hull algorithm expects a list of coordinates, so I take numpy.argwhere(im) to have all shape point coordinates. However, most of those points are not contributing to the convex hull (they lie on the inside of the shape). Because convex hull computation time is at least proportional to the number of points that it gets as input, I devised an idea to filter the plethora of useless points on beforehand and only pass those that span the outline. The idea is quite simple, that for each row in the binary NxM matrix I take only the minimal and maximal indices. So for example:

im = np.array([[1,1,1,0],
              [1,0,1,1],
              [1,1,0,1],
              [0,0,0,0],
              [0,1,1,1]], dtype=np.bool)
outline = somefunc(im)

Then outline should read (in tuples or as a 5x2 numpy array, I don't mind):

[(0,0),(0,2),(1,0),(1,3),(2,0),(2,3),(4,1),(4,3)]

Any convex hull tight around this shape (im), must a subset of these points (outline). In other words, if "somefunc()" is efficient in filtering the inside points then it saves time for the convex hull computation.

I have code that does the above trick, but I am hoping someone has a more clever (read faster) approach since I need to run it many many times. The code I have is:

# I have a 2D binary field. random for the purpose of demonstration.
import numpy as np
im = np.random.random((320,360)) > 0.9

# This is my algorithm so far. Notice that coords is sorted.
coords = np.argwhere(im)
left = np.roll(coords[:,0], 1, axis=0) != coords[:,0]
outline = np.vstack([coords[left], coords[left[1:]], coords[-1]])

Another idea I had was to use Python's reduce() so I'd need to run over the list of coords only once. But I have difficulty finding a good reducing function.

Any help would greatly be appreciated!

edit

In the meanwhile I have found a faster way to go from im directly to outline. At least with large images this is significantly faster. In the apparent absence of an external solution I am positing it as the solution to this question.

Still, if somebody knows an even faster method, please speak up :)

A: 

This assignment seems to accomplish the same thing as your last two steps:

outline = np.array(dict(reversed(coords)).items() + dict(coords).items())

Don't know if it's any faster, however.

Skyhawker
That is an interesting approach. Trying it now...
Paul
Too bad, it's 10 times(!) slower.
Paul
A: 

For more general solution, you could use somekind of edge detection method to find only the edge points. I believe (Google..) that NumPy has built-in sobel filter, which will do that.

Harriv
yes, I know the sobel filter and it is marvelous. In fact, that is how I got to these binary images. the filter does not give indices however.
Paul
The filter will give you the bitmap/matrix where you can find all the indices like you did in your code.
Harriv
Oh wait, you got the example image with sobel and it has too many points?
Harriv
You're right. I want to fit a polygon around the (thresholded) sobel. And therefore first, I'd like to reduce the number of points. This last part is where I am trying to find fast code for.
Paul
In machine vision application I've been using there's contour output in blob tool, but unfortunately I don't see such feature in OpenCV. That would've been elegant solution..
Harriv
A: 

In the absence of an acceptable answer I post my best working code as the solution.

def outline(im):
    ''' Input binary 2D (NxM) image. Ouput array (2xK) of K (y,x) coordinates
        where 0 <= K <= 2*M.
    '''
    topbottom = np.empty((1,2*im.shape[1]), dtype=np.uint16)
    topbottom[0,0:im.shape[1]] = np.argmax(im, axis=0)
    topbottom[0,im.shape[1]:] = (im.shape[0]-1)-np.argmax(np.flipud(im), axis=0)
    mask      = np.tile(np.any(im, axis=0), (2,))
    xvalues   = np.tile(np.arange(im.shape[1]), (1,2))
    return np.vstack([topbottom,xvalues])[:,mask].T
Paul