My current project involves sampling a set of points on which I’ll learn a
control barrier function. I need to automate the detection of boundary
points in the sampled set, which is not guaranteed to be convex. I’m using
this post to document my experimentation with a number of algorithms that
could solve this problem.
The concept of an ‘alpha shape’ is a nonconvex generalization of the convex hull. Most theorems and proofs regarding the alpha shape of a set of points necessitate placing the points in ‘general position’.
Considering points that are in general position is a way to avoiding edge cases when constructing proofs and programs. In \(n\)-dimensional space, general position is the
I’ve decided to use the ‘Gift Wrapping’ algorithm to compute the convex hull since it generalizes nicely to computing the alpha shape as I’ll explain. Loosely, the procedure is as follows:
Gift Wrapping
1. Find the maximum point along a dimension (wlog, highest point in Y), this is the first hull point. 2. Repeat 3-5 until the 'new hull point' is the first one: 3. Find the point whose slope with the previous hull point makes the minimum counterclockwise angle (we'll call it theta) starting from pi. 4. This is the next hull point. Add it to the hull. 5. Center the dataset on the new hull point, and then rotate clockwise by theta so that the new hull point is horizontally aligned with the previous one.
Since, for each hull point, we must iterate through each of the other \(n\) points, this procedure has a time-complexity of \(O(hn)\), where \(h\) is the number of hull points.
As in step 1
, we first find the point that
is maximal in the \(y\)-dimension. Given a datasent of shape \((n,2)\), We’ll
create a copy of the data to process, and iterate through it to obtain its
maximum in \(y\).
# sort and remove point with maximum y
= pts[0,:]
pt_max = [pt_max]
pts_copy for idx, x in enumerate(pts[1:]):
if x[1] > pt_max[1]:
= x
pt_max pts_copy.append(x)
This section covers step 3
and step 4
and is primarily a do-while-loop that checks whether each new focus is the
original (in which case, we’ve completed the hull). Recall that the new focus
is the point which makes the minimum nonnegative slope with the previous
focus.
A useful trick here, for initializing the minimum slope, is to set it
to np.infty
, which is evaluates as larger than any numeric data
type. This is an easy way to ensure that the first prospective point will
always be chosen as the initial candidate.
Note that multiple points could generate the same slope with respect to the focus. In this case we want to pick the point that is furthest from the focus
In order to emulate a ‘do-while’, we use while True:
to ensure
that the loop always runs at least once, and then we evaluate the condition
focus != pt_max
at the end of the loop and break if it is false.
Note: ### THE REST ###
denotes what processing must be
done between iterations to ensure the slope conditions are continually valid,
and is what we’ll discuss next.
# for each focus f, find min slope >= 0,
# then rotate clockwise
= pt_max
f = []
hull = 0 # this will come in handy later
total_theta while True:
= np.infty
m_min for idx, pt in enumerate(pts_copy):
# compute slope, update focus candidate
# 'fc' if slope is nonnegative and lesser
# than previous
= (f[1] - pt[1]) / (f[0] - pt[0])
m if m >= 0 and m < min_m:
= idx
fc = m
min_m # if same slope as previous candidate, take
# whichever is further from current focus
elif m == m_min:
if np.linalg.norm(f - pt)
> np.linalg.norm(f - pts_copy[fc]):
= idx
fc # new focus is pt w/ minimum positive slope
# wrt prev focus, no need to consider in
# next iteration
= pts_copy[fc]
f
hull.append(f)
pts_copy.remove(fc)
############
# THE REST #
############
# a do-while in python:
if f != pt_max:
continue
else:
break
We next regard step 5
and consider how the dataset must be
rotated between iterations to ensure that the minimum nonnegative slope
condition is still valid. The code here will be inserted at the point
marked ### THE REST ###
in the above snippet