The convex hull of a 2D object
Last year I wrote about computing the the boundary length and various other measures, given an object’s chain code. The chain code is a simple way of encoding the polygon that represents a 2D object. It is very simple to compute the object’s convex hull given this polygon. Why would I want to do that? Well, the convex hull gives several interesting object properties, such as the convexity (object area divided by convex hull area). Certain other properties, such as the Feret diameters, are identical for an object and its convex hull, and the convex hull thus gives an efficient algorithm to compute these properties.
Avraham Melkman (1987) has published a very efficient algorithm to compute the convex hull given a polygon (find the PDF through Google Scholar) . It has been said that this might very well be the most efficient algorithm possible. It visits each polygon vertex only once, and keeps only those vertices that form the convex hull. To accomplish this, it uses a deque (also known as double-ended queue) to represent the convex hull. The deque has 4 basic operations: push and pop add and remove elements from the end of the queue, and insert and remove add and remove elements from the front of the queue. By using such a data structure, Melkman was able to add and remove vertices from his convex polygon at either side. The points in the deque always constitute a convex polygon, and is the convex hull of the input points seen up to that point.
The Melkman algorithm also depends on a function, which I’ve called position here, which returns 1 if
pt3 is to the
right of the directed line formed by
pt2, -1 if it is to the left, or 0 if all three points are collinear
(form a straight line). This function simply returns the sign of the signed area of the parallelogram given by the
pt3-pt1. It we embed these vectors in 3D (adding the value 0 as the z component), and compute
the cross product, the norm of this cross product gives the area of the parallelogram, whereas its direction is given by
the ordering of the three points on the plane (as per the right-hand rule). The cross product will, however, always have
x and y components equal to 0, so we only need to compute the z component. Let’s define an anonymous
position(pt1,pt2,pt3) for this:
position = @(pt1,pt2,pt3) ... sign( (pt2(1)-pt1(1))*(pt3(2)-pt1(2)) - ... (pt2(2)-pt1(2))*(pt3(1)-pt1(1)) );
Before we can apply the algorithm, we’ll need some data. I like the image “cermet”. Let’s pick object #31, and get its chain code:
img = label(readim('cermet')<120); cc = dip_imagechaincode(img,2,31);
The code below, however, takes a polygon represented by a set of points, not a chain code. We can easily convert the
cc into a list of coordinates
coords (this is the same code as in the post on chain codes):
directions = [ 1, 0 1,-1 0,-1 -1,-1 -1, 0 -1, 1 0, 1 1, 1]; coords = cumsum(directions(cc.chain+1,:)); N = size(coords,1); % Draw input polygon: figure('position',[1000,500,450,450]); plot([coords(:,1);coords(1,1)],[coords(:,2);coords(1,2)],'b-') hold on, axis equal, axis off
Melkman’s algorithm starts by taking the first three points in the list and forming them into a start of the convex hull. However, the algorithm assumes these points are not collinear. So let’s examine the first 3 points in the set, and delete the second one if all 3 are collinear:
while N>=3 && position(coords(1,:),coords(2,:),coords(3,:)) == 0 coords(2,:) = ; N = size(coords,1); end
At this point, we’d of course check to make sure that we have more than 3 points left. If not, we’re done! There is also a special case that could happen if our object is a single line. In this case, all points in the object are collinear, and the code above leaves only 2 points. We’d need to detect this case, and make sure that we keep the two extreme points, rather than the first and last points in the list. Otherwise length measurements on our convex hull will be wrong.
We start by creating a deque data structure
D (in MATLAB we can just use a simple array), and put the first three
points in it. We first check the order of these points, and put them in the deque so that the polygon is right-turning.
Furthermore, we always want the first and last point in our deque to be the same point:
if position(coords(1,:),coords(2,:),coords(3,:)) > 0 D = coords([3,1,2,3],:); else D = coords([3,2,1,3],:); end % Add convex hull to figure: h = plot(D(:,1),D(:,2),'ro-');
Next, the algorithm takes one new point
v at the time, and checks where it lies with respect to the first two points
and the last two points in the deque. This tells us if the point is already inside the convex hull or not. If it is
inside, we ignore the point and take the next one. If it is not inside, we’ll be wanting to add this point at the
beginning and at the end of the deque (push and insert). But before we add this new point, we need to remove (pop and
remove) previous points that will potentially end up inside the convex hull. We start with the end of the deque, and see
if the last point is inside the line formed by the second to last point and the new one. If so, we pop that point. This
is repeated until we cannot pop the last point any more, and then push the new point. We repeat the same procedure with
the front of the deque, removing points and then inserting the new one. Now the algorithm loops back to process the next
point. This is the code:
ii = 3; while 1 ii = ii+1; if ii>N, break, end v = coords(ii,:); while position(v,D(1,:),D(2,:)) >= 0 && ... position(D(end-1,:),D(end,:),v) >= 0 ii = ii+1; if ii>N, break, end v = coords(ii,:); end if ii>N,break,end while position(D(end-1,:),D(end,:),v) <= 0 D(end,:) = ; % pop end D = [D;v]; % push v while position(v,D(1,:),D(2,:)) <= 0 D(1,:) = ; % remove end D = [v;D]; % insert v % Update convex hull in figure: set(h,'xdata',D(:,1),'ydata',D(:,2)); pause(0.2) end D(end,:) = ; % start and end points are the same, remove one.
When running the code, you will see the convex hull being updated as new points are examined by the algorithm (note that
the slowness comes from the
pause statement in the code!). At the end of the algorithm you’ll see this on your screen:
In a future post we’ll use this convex hull to compute Feret diameters.