32

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?

VividD
  • 10,456
  • 6
  • 64
  • 111
Joe Kington
  • 275,208
  • 71
  • 604
  • 463
  • when you say "the bars uniquely define a solution" do you mean that the bars must all lie inside the final polygon? – andrew cooke Mar 22 '12 at 20:49
  • Yes! I should have added that to the information. Thanks! – Joe Kington Mar 22 '12 at 20:49
  • See the book "Computational Geometry" by Mark de Berg and the CGAL library I think you'll find an efficient algorithm. – mitch Mar 22 '12 at 21:34
  • 5
    "I think (?) there's a unique solution in either case, subject to the criteria outline earlier." There isn't necessarily. Try rotating the blue segment by 90 degrees in your second example. Nothing in you problem definition precludes doing this, yet two solutions are now possible. – Jean-François Corbett Mar 22 '12 at 21:43
  • What is the expected result when lines are non intersecting? – ElKamina Mar 22 '12 at 22:11
  • @mitch - I've used CGAL in the past, but I'm fairly sure it doesn't have anything quite like this (other than alpha shapes). Am I missing something? I'd prefer to avoid alpha shapes, as I think there's a simpler way, but I could be wrong there. I could guess a radius for the calculation from the average segment spacing, I suppose. – Joe Kington Mar 23 '12 at 16:12
  • From your definition, the convex hull is a solution. – Alexandre C. Mar 23 '12 at 16:16
  • @AlexandreC. - You're right, I misstated the definition. All of the points must be used in the outline. I should have made that clearer. – Joe Kington Mar 23 '12 at 16:20
  • @JoeKington: What happens when there is no solution using all points (like in the second example given by Jean-Francois Corbett)? – Reinstate Monica Mar 23 '12 at 21:44
  • Are you sure that just using the points visible from north or south (for upper/lower boundary, resp.) isn't working? If so, please give an example where the output includes points not visible from north or south. – Reinstate Monica Mar 23 '12 at 21:45

2 Answers2

17
  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

Jean-François Corbett
  • 37,420
  • 30
  • 139
  • 188
  • Nice! I want to think it over for a bit, but I think you've nailed it. – Joe Kington Mar 22 '12 at 21:15
  • *"My algorithm would illegally join its endpoint to another endpoint (dashed grey line)."* - Simple enough, just make sure the resulting line doesn't cross any bars or existing lines. – BlueRaja - Danny Pflughoeft Mar 22 '12 at 21:25
  • On second thought, I do actually need it to work for the case of non-intersecting segments. (In fact, after looking through my data, it's a moderately common case that I hadn't noticed before.) There's still a unique solution (as far as I can tell) subject to the same criteria as before (outline can't cross bars and is made up of their endpoints). Sorry for the back-and-forth on accepting the answer! – Joe Kington Mar 22 '12 at 21:27
  • 3
    Not so simple... How do you decide whether to reject the "newly encountered" or the "previously encountered" endpoint? If I reject the new one (the grey one), then the algorithm fails in this example. If I reject the "previous" one (the black one I added), then the algorithm fails in the mirror image of this example. – Jean-François Corbett Mar 22 '12 at 21:29
  • @JoeKington: No, there isn't always a unique solution if there is an outlier segment. I'll add an example in my answer in a sec. Sounds like you need to work on your problem definition... and edit your question... – Jean-François Corbett Mar 22 '12 at 21:33
  • @Jean-FrançoisCorbett - I agree about the outlier segment with other intersecting segments, but there should still be a unique solution when no segments intersect (Though I could well be entirely wrong there, and I may very well need to re-think my problem definition). I've updated my question. The second example is actually a moderately common case. Either way, thanks for your help! Your answer has me thinking of a number of things I wasn't considering before. – Joe Kington Mar 22 '12 at 21:39
  • 1
    @Jean: You're right. I think a similar sweeping algorithm with backtracking might work, though *(sweep all angles from 0 to 2pi, starting along the current bar as 0 rads, and connect with the first endpoint you can 'see'. If you can't see any endpoints, backtrack to the previous endpoint)*. This should also work for his case where some bars do not intersect the rest, though it might not give the optimal (smallest area) solution. – BlueRaja - Danny Pflughoeft Mar 22 '12 at 21:42
  • @JoeKington: Nope, can still have two solutions... See comment to your question. – Jean-François Corbett Mar 22 '12 at 21:44
  • 1
    @JoeKington: I think I've got it now. My algorithm ain't so bad after all, it just needs an extra step, which could be brute force. See edit. Still only works for the well-defined problem, though... i.e. no unconnected segments. – Jean-François Corbett Mar 27 '12 at 07:11
2

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).

Scott Hunter
  • 48,888
  • 12
  • 60
  • 101