Does anyone have experience generating a Concave Hull (Alpha Shape) from a list of points with IronPython? Would greatly appreciate any feedback. Thank you in advance!
A quick search brought up this…
A handy couple of nodes being used (which you can dig around in) and some heavyweight package authors unfortunately very few people are doing what you need…
Hope that helps,
There’s a concave hull algorithm here (in python) which looks easy to implement.
And there’s some links by @Michael_Kirschner2 here which might offer some alternatives:
I’ve found a couple different concave hull algorithms in python; however, all of them use packages (numpy, scipy, matplotlib, shapely) which are not easily or if at all compatible with Iron Python.
So, I was hoping maybe someone had created a concave hull algorithm with vanilla Python 2.7 modules which could be used in Dynamo.
Thank you for the reference. I have reviewed this thread and tried convex hulls for my application but did not find any solution.
Maybe I could reach out to the authors as you suggested. I’m hoping maybe someone has created a concave hull algorithm with vanilla Python 2.7 modules which could be used in Dynamo, but from my research so far there is very few resources regarding this subject.
@ericabbott, have you already go through with Lunchbox ML https://provingground.io/2017/10/03/lunchboxml-for-dynamo/
by @Nathan_Miller . Right now, that’s the furthest you can get.
The link here that MK gave you only uses Math?
There is a C# from string node, but I’m not skilled enough to use it. Or it’s possible you could persuade a package manager to add it…
I had a quick go at translating it to Python, perhaps someone with a maths degree can have a look at it?! But I know @Daniel_Woodcock1 is very busy…
# Enable Python support and load DesignScript library import clr clr.AddReference('ProtoGeometry') from Autodesk.DesignScript.Geometry import * import math points = UnwrapElement(IN) #0. error checking, init if (points == None or points.Count < 2) : raise Exception('AlphaShape needs at least 2 points') BorderEdges =  alpha_2 = alpha * alpha #1. run through all pairs of points i = 0 while i < points.Count: j = i + 1 while j < points.Count: if (points[i] == points[j]): raise Exception('AlphaShape needs pairwise distinct points') dist = (points[i], points[j]) if (dist > 2 * alpha) : continue #circle fits between points ==> p_i, p_j can't be alpha-exposed x1 = points[i].X x2 = points[j].X y1 = points[i].Y y2 = points[j].Y mid = PointByCoordinates((x1 + x2) / 2, (y1 + y2) / 2) #find two circles that contain p_i and p_j; note that center1 == center2 if dist == 2*alpha center1 = PointByCoordinates((mid.X + math.Sqrt(alpha_2 - (dist / 2) * (dist / 2)) * (y1 - y2) / dist), (mid.Y + math.Sqrt(alpha_2 - (dist / 2) * (dist / 2)) * (x2 - x1) / dist)) center2 = PointByCoordinates((mid.X - math.Sqrt(alpha_2 - (dist / 2) * (dist / 2)) * (y1 - y2) / dist), (mid.Y - math.Sqrt(alpha_2 - (dist / 2) * (dist / 2)) * (x2 - x1) / dist)) #check if one of the circles is alpha-exposed, i.e. no other point lies in it c1_empty = true c2_empty = true k = 0 while k < points.Count & (c1_empty or c2_empty): if (points[k] == points[i] or points[k] == points[j]): continue if ((center1.X - points[k].X) * (center1.X - points[k].X) + (center1.Y - points[k].Y) * (center1.Y - points[k].Y) < alpha_2): c1_empty = false if ((center2.X - points[k].X) * (center2.X - points[k].X) + (center2.Y - points[k].Y) * (center2.Y - points[k].Y) < alpha_2): c2_empty = false if (c1_empty or c2_empty): #yup! BorderEdges.append(Line.ByStartEndPoint(points[i], points[j])) k += 1 j += 1 i += 1 OUT = Border.Edges
Mark, thank you for the info and translation attempt! I’ll take a closer look at that when I get a chance and update here with any progress.
I cleaned up the code a bit:
import clr clr.AddReference('ProtoGeometry') from Autodesk.DesignScript.Geometry import Line from math import sqrt, hypot points, alpha = IN OUT =  pLen = len(points) alpha2 = alpha * alpha if pLen < 2: raise Exception('AlphaShape needs at least 2 points') for i, p in enumerate(points): for j in xrange(i+1, pLen): if p.IsAlmostEqualTo(points[j]): raise Exception('AlphaShape needs pairwise distinct points') dist = hypot(p.X - points[j].X, p.Y - points[j].Y) if (dist > 2 * alpha) : continue #circle fits between points ==> p_i, p_j can't be alpha-exposed x1, y1, x2, y2 = p.X, p.Y, points[j].X, points[j].Y midX, midY = (x1 + x2) / 2, (y1 + y2) / 2 #find two circles that contain p_i and p_j; note that center1 == center2 if dist == 2*alpha alphaDist = sqrt(alpha2 - (dist / 2) ** 2) deltaX, deltaY = (x2 - x1) / dist, (y1 - y2) / dist c1x, c1y = midX + alphaDist * deltaY, midY + alphaDist * deltaX c2x, c2y = midX - alphaDist * deltaY, midY - alphaDist * deltaX #check if one of the circles is alpha-exposed, i.e. no other point lies in it c1_empty = True c2_empty = True for k in xrange(pLen): if i == k or j == k: continue if ((c1x - points[k].X) * (c1x - points[k].X) + (c1y - points[k].Y) * (c1y - points[k].Y) < alpha2): c1_empty = False if ((c2x - points[k].X) * (c2x - points[k].X) + (c2y - points[k].Y) * (c2y - points[k].Y) < alpha2): c2_empty = False if not c1_empty and not c2_empty: break if c1_empty or c2_empty: OUT.append(Line.ByStartPointEndPoint(p, points[j]))
Another interesting question is, how do you determine the alpha (scoop) size? It’s correlated to the distance to the nearest neighbors but can’t be deducted exactly from it:
First of all thank you for cleaning Mark’s script and sharing yours! This is exactly what I was looking for.
Yes, determining the alpha size is still an issue. Especially in applications with a large amount of points where testing different alpha values takes a long time to generate.
The reason I’m working on this script is an attempt to save time from having to manually draw lines in CAD; however, if the script for creating a concave hull requires fine tuning of the alpha value it could take more time to do so with the script than just manually doing it.
Do you think it is possible to find a solution that automatically finds the value (or maybe a range to pick from) of the alpha to get the desired result? I see you attempted that, but unfortunately it did not work in my application.
Also, do you think it is possible that another method of this algorithm may be more accurate? In my research I saw there was different methods (e.g. closest neighbor approach) to achieve a concave hull.
Would love to hear your thoughts and perhaps find a working solution! Thanks again.
Ha, @Mark.Ackerley, uni + work does leave for very little time, but I was intrigued.
I know this has been answered, but here is another method that uses the Graham Scan method for “Gift Wrapping”. The best method is Chans algorithm from what I’ve read though as it is a combination of the Jarvis March and Graham Scan methods.
Here is the Python Code ported over from Thomas Switzers GitHub (I take zero credit for this)…
import clr clr.AddReference('ProtoGeometry') from Autodesk.DesignScript.Geometry import * def ToVals(pt): return [pt.X,pt.Y,pt.Z] def ToPts(ptVals): return Point.ByCoordinates(ptVals,ptVals,ptVals) def CreateOutline(pts): crvs =  i = 0 while i < len(pts): if i == len(pts)-1: crvs.append(Line.ByStartPointEndPoint(pts[i],pts)) else: crvs.append(Line.ByStartPointEndPoint(pts[i],pts[i+1])) i = i+1 return PolyCurve.ByJoinedCurves(crvs) ''' Taken from Thomas Switzers awesome GitHub repo and modified to suit Dynamo Context https://gist.github.com/arthur-e/5cf52962341310f438e96c1f3c3398b8 I take zero credit for this work other than porting it across. ''' def convex_hull_graham(points): ''' Returns points on convex hull in CCW order according to Graham's scan algorithm. By Tom Switzer <firstname.lastname@example.org>. ''' TURN_LEFT, TURN_RIGHT, TURN_NONE = (1, -1, 0) def cmp(a, b): return (a > b) - (a < b) def turn(p, q, r): return cmp((q - p)*(r - p) - (r - p)*(q - p), 0) def _keep_left(hull, r): while len(hull) > 1 and turn(hull[-2], hull[-1], r) != TURN_LEFT: hull.pop() if not len(hull) or hull[-1] != r: hull.append(r) return hull points = [ToVals(p) for p in points] points = sorted(points) l = reduce(_keep_left, points, ) u = reduce(_keep_left, reversed(points), ) points = l.extend(u[i] for i in range(1, len(u) - 1)) or l points = [ToPts(p) for p in points] return CreateOutline(points) OUT = convex_hull_graham(IN)
Thanks Dan great work
Nah, scratch that… I thought you were after convex Hull… This does convex not concave… My bad
Still cool to see, thanks for posting
Sorry, I don’t have anything better. The only alternative is to use a while loop / goal seek type of approach, where you try to minimize the alpha size, while still producing a single continuous external loop.
No problem. Thank you for all the help so far, I really appreciate it!
Looking forward to seeing you around the forums.
There is an issue with the above Python code (the one by Dimitar and Mark) if you use it for vertical point sets
the hypothemus (dist) will return zero if two points are vertically above eachother.
And then a division error comes.
Great topic though. Interesting stuff
Comes in handy when working with pointclouds