Skip to content

Instantly share code, notes, and snippets.

@tixxit
Created December 9, 2009 03:23
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save tixxit/252229 to your computer and use it in GitHub Desktop.
Save tixxit/252229 to your computer and use it in GitHub Desktop.
# Chan's Convex Hull O(n log h) - Tom Switzer <thomas.switzer@gmail.com>
TURN_LEFT, TURN_RIGHT, TURN_NONE = (1, -1, 0)
def turn(p, q, r):
"""Returns -1, 0, 1 if p,q,r forms a right, straight, or left turn."""
return cmp((q[0] - p[0])*(r[1] - p[1]) - (r[0] - p[0])*(q[1] - p[1]), 0)
def _keep_left(hull, r):
while len(hull) > 1 and turn(hull[-2], hull[-1], r) != TURN_LEFT:
hull.pop()
return (not len(hull) or hull[-1] != r) and hull.append(r) or hull
def _graham_scan(points):
"""Returns points on convex hull of an array of points in CCW order."""
points.sort()
lh = reduce(_keep_left, points, [])
uh = reduce(_keep_left, reversed(points), [])
return lh.extend(uh[i] for i in xrange(1, len(uh) - 1)) or lh
def _rtangent(hull, p):
"""Return the index of the point in hull that the right tangent line from p
to hull touches.
"""
l, r = 0, len(hull)
l_prev = turn(p, hull[0], hull[-1])
l_next = turn(p, hull[0], hull[(l + 1) % r])
while l < r:
c = (l + r) / 2
c_prev = turn(p, hull[c], hull[(c - 1) % len(hull)])
c_next = turn(p, hull[c], hull[(c + 1) % len(hull)])
c_side = turn(p, hull[l], hull[c])
if c_prev != TURN_RIGHT and c_next != TURN_RIGHT:
return c
elif c_side == TURN_LEFT and (l_next == TURN_RIGHT or
l_prev == l_next) or \
c_side == TURN_RIGHT and c_prev == TURN_RIGHT:
r = c # Tangent touches left chain
else:
l = c + 1 # Tangent touches right chain
l_prev = -c_next # Switch sides
l_next = turn(p, hull[l], hull[(l + 1) % len(hull)])
return l
def _min_hull_pt_pair(hulls):
"""Returns the hull, point index pair that is minimal."""
h, p = 0, 0
for i in xrange(len(hulls)):
j = min(xrange(len(hulls[i])), key=lambda j: hulls[i][j])
if hulls[i][j] < hulls[h][p]:
h, p = i, j
return (h, p)
def _next_hull_pt_pair(hulls, pair):
"""
Returns the (hull, point) index pair of the next point in the convex
hull.
"""
p = hulls[pair[0]][pair[1]]
next = (pair[0], (pair[1] + 1) % len(hulls[pair[0]]))
for h in (i for i in xrange(len(hulls)) if i != pair[0]):
s = _rtangent(hulls[h], p)
q, r = hulls[next[0]][next[1]], hulls[h][s]
t = turn(p, q, r)
if t == TURN_RIGHT or t == TURN_NONE and _dist(p, r) > _dist(p, q):
next = (h, s)
return next
def convex_hull(pts):
"""Returns the points on the convex hull of pts in CCW order."""
for m in (1 << (1 << t) for t in xrange(len(pts))):
hulls = [_graham_scan(pts[i:i + m]) for i in xrange(0, len(pts), m)]
hull = [_min_hull_pt_pair(hulls)]
for throw_away in xrange(m):
p = _next_hull_pt_pair(hulls, hull[-1])
if p == hull[0]:
return [hulls[h][i] for h, i in hull]
hull.append(p)
@danielSanchezQ
Copy link

Good one dude, really helpful for understanding chans algorithm. Thanks

@mdunschen
Copy link

Hi Tom

I've found a problem in your code, on line 40: 'l' can grow to the len(hull) here and then subsequent lines will fail with an index error. I am not sure what the behaviour should be if l == len(hull). The problem might be very specific to my set of points I want to find a convex hull for, but if you want, I can email it. (list of 352 points).

Or alternatively use my 'gist' here: https://gist.github.com/mdunschen/2ce8ba071a1c6c768d5d

@tuanchauict
Copy link

@mdunschen Have you found out the solution?

@zactodd
Copy link

zactodd commented Feb 20, 2019

def _rtangent(hull, p):
    """Return the index of the point in hull that the right tangent line from p
    to hull touches.
    """
    l, r = 0, len(hull)
    l_prev = turn(p, hull[0], hull[-1])
    l_next = turn(p, hull[0], hull[(l + 1) % r])
    c = r // 2
    while l < r:
        c_prev = turn(p, hull[c], hull[(c - 1) % len(hull)])
        c_next = turn(p, hull[c], hull[(c + 1) % len(hull)])
        c_side = turn(p, hull[l], hull[c])

        if c_prev != TURN_RIGHT and c_next != TURN_RIGHT:
            return c
        elif c_side == TURN_LEFT and (l_next == TURN_RIGHT or l_prev == l_next) or \
                c_side == TURN_RIGHT and c_prev == TURN_RIGHT:
            r = c               # Tangent touches left chain
            c = (l + r) // 2
        else:
            l = c + 1
            l_prev = -c_next    # Switch sides
            l_next = turn(p, hull[l], hull[(l + 1) % len(hull)])
    return l

image

@constdesch
Copy link

constdesch commented Dec 23, 2020

Hi Tom

I've found a problem in your code, on line 40: 'l' can grow to the len(hull) here and then subsequent lines will fail with an index error. I am not sure what the behaviour should be if l == len(hull). The problem might be very specific to my set of points I want to find a convex hull for, but if you want, I can email it. (list of 352 points).

Or alternatively use my 'gist' here: https://gist.github.com/mdunschen/2ce8ba071a1c6c768d5d

I had the same problem, with this for instance

hull = [] 
hull += [[997.00, 38.00]]
hull += [[469.00, 854.00]]
hull += [[23.00, 902.00]]
hull += [[355.00, 588.00]]
p = [1019.00, 4.00]
_rtangent(hull,p)

and noticed that when it is the case, it seems that the function should always return 1. Also, I used this function in C. Python and C do not agree on -1%n, python will see it as 1 while C will see it as -1 so to be safe the following line
c_prev = turn(p, hull[c], hull[(c - 1) % len(hull)])
can be changed like this
c_prev = turn(p, hull[c], hull[abs((c - 1) % len(hull))])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment