Determine non-convex hull of collection of line segments

Question:

I have a computational geometry problem that I feel should have a relatively simple solution, but I can’t quite figure it out.

I need to determine the non-convex outline of a region defined by several line segments.

I’m aware of various non-convex hull algorithms (e.g. alpha shapes), but I don’t need a fully general algorithm, as the line segments define a unique solution in most cases.


As @Jean-FrançoisCorbett has pointed out, there are cases where there are multiple solutions. I clearly need to think more about my definition.

However, what I’m trying to do is reverse-engineer and use a proprietary file format so that I can run basic analyses on data collected by myself and others. The file format is simple enough, but determining the algorithm they use to define the boundary is considerably harder.

Putting in many of the edge cases that would result in a non-unique solution causes the software in question to either crash without warning or silently fail to read the file.

Therefore, when there are multiple solutions, either generating one of the acceptable solutions or being able to determine that there are multiple solutions would be acceptable.


Problem Definition:

The polygon’s outline should never cross any of the segments and should be formed of lines joining all of the segments’ endpoints. All segments must lie entirely inside or along the boundary of the polygon. No endpoint may be used more than once in the outline (Ignoring “closing” the polygon by adding the first point at the end for software libraries that require polygons to close.).

In cases where there are multiple solutions that meet this criteria, any one of those solutions would be acceptable. (It would be nice to be able to determine when the solution is non-unique, but this isn’t strictly necessary.)


Examples:

As an example, I have something along these lines:
Segments Defining the Area

And I’d like to delineate the following area:
Desired Outline

It also should work for non-intersecting segments. E.g.

enter image description here
enter image description here

I think (?) there’s a unique solution in either case, subject to the criteria outline earlier. (Edit: There isn’t a unique solution in general, as @Jean-FrançoisCorbett pointed out. However, I’m still interested in an algorithm that would either generate one of the acceptable solutions.)

Test Cases

For a test case, here’s the code to generate the above figures. I’m using python here, but the question is language-agnostic.

import matplotlib.pyplot as plt

def main():
    test1()
    test2()
    plt.show()

def test1():
    """Intersecting segments."""
    segments = [[(1, 1), (1, 3)],
                [(3.7, 1), (2, 4)],
                [(2, 0), (3.7, 3)],
                [(4, 0), (4, 4)],
                [(4.3, 1), (4.3, 3)],
                [(0, 2), (6, 3)]]

    desired_outline = [segments[0][0], segments[5][0], segments[0][1], 
                       segments[1][1], segments[2][1], segments[3][1],
                       segments[4][1], segments[5][1], segments[4][0],
                       segments[3][0], segments[1][0], segments[2][0],
                       segments[0][0]]

    plot(segments, desired_outline)

def test2():
    """Non-intersecting segments."""
    segments = [[(0, 1), (0, 3)],
                [(1, 0), (1, 4)],
                [(2, 1), (2, 3)],
                [(3, 0), (3, 4)]]

    desired_outline = [segments[0][0], segments[0][1], segments[1][1],
                       segments[2][1], segments[3][1], segments[3][0], 
                       segments[2][0], segments[1][0], segments[0][0]]

    plot(segments, desired_outline)


def plot(segments, desired_outline):
    fig, ax = plt.subplots()
    plot_segments(ax, segments)
    ax.set_title('Segments')

    fig, ax = plt.subplots()
    ax.fill(*zip(*desired_outline), facecolor='gray')
    plot_segments(ax, segments)
    ax.set_title('Desired Outline')

def plot_segments(ax, segments):
    for segment in segments:
        ax.plot(*zip(*segment), marker='o', linestyle='-')
    xmin, xmax, ymin, ymax = ax.axis()
    ax.axis([xmin - 0.5, xmax + 0.5, ymin - 0.5, ymax + 0.5])

if __name__ == '__main__':
    main()

Any ideas?

I’m beginning to suspect that the software whose results I’m trying to reproduce uses a radial-sweep algorithm in some sort of “internal” coordinate system (e.g. A coordinate system with x-prime and y-prime scaled and rotated along the principal axes defined by the spread of points. This makes the problem more “circular”.) However, this produces solutions where the outline intersects line segments in many cases. It’s easy enough to detect this and brute force it from there, but surely there’s a better way?

Asked By: Joe Kington

||

Answers:

  1. Pick a safe starting point. Can be e.g. the endpoint with maximum x.
  2. March along the line segment.
  3. Upon encountering any intersection, always turn left and march along this new segment.
  4. Upon encountering an endpoint, record it. Goto 2.
  5. Stop when you have returned to your starting point. Your list of recorded endpoints now makes up the ordered list of vertices of your concave hull.

NB: This will fail if there is a “free-floating” outlying line segment that does not intersect any other line segment. However, you specify that “the bars uniquely define a solution”, which rules out this fail condition. (Outlying segments make possible two distinct solutions.)

EDIT … or rather, outlying segments can make two distinct solutions possible — depending on the exact layout. Proof: Below is an example where the yellow segment that I added makes two solutions possible (blue and grey horribly hand-drawn lines). Were the yellow segment orientated perpendicular to the way it’s drawn now, only one solution would be possible. Sounds like your problem is poorly defined.

enter image description here

EDIT Actually this can also fail if your segment collection is “very concave”, i.e. if there are endpoints tucked away in recluse corners of your pile of segments. In the figure below I added a black segment. My algorithm would illegally join its endpoint to another endpoint (dashed grey line). I’ll leave my answer be in case others are inclined to build upon it.

EDIT after giving this some more thought: Even in the “very concave” case, this solution will definitely give you all of the points of your concave hull in the proper order, but they may be interspersed with extra, inappropriate points such as the black one. So there may be too many points.

The answer is then, of course, to do some pruning. It would be fairly complicated pruning especially if you can have multiple, consecutive “recluse points” like the black one, so I don’t have a smart algorithm in mind. But even blind, brute force could be feasible. Each point can be either accepted or rejected (boolean), so if you have N properly ordered candidate points in your concave hull, then there are only 2^N possibilities to check. This is way, way fewer possibilities than brute force for your original problem of permutations, which would have SUM of (n!/(n-k)!) for k=1:(n-1) possibilities (pardon my notation). So this algorithm narrows down your problem significantly.

I think this is the way to go.

enter image description here

Not a fully fleshed-out idea, but anyway: Suppose you started w/ the circular-sweep algorithm for a convex-hull (where you sort, and then process, the points by their angle from a center point). If all of the points end up in this hull, you’re done. If not, then you have to “tighten” the hull to include these points. Each of these points were at once candidates for the convex hull, and got removed because they broke the convexness. Sometimes (as with the top purple point in the first example), we can simply leave them in. Where we can’t, because the new segment of the hull crosses a segment (as going from the bottom green to the bottom purple in the first example, assuming the bottom aqua point got processed before the green one), the fix is a little more involved (and the part I haven’t fleshed out, and is the very part alluded to in the question’s latest edit).

Answered By: Scott Hunter