确定一组线段的非凸壳体

32
我有一个计算几何问题,我认为应该有一个相对简单的解决方案,但我无法弄清楚。
我需要确定由多条线段定义的区域的非凸轮廓。
我知道各种非凸包算法(例如alpha形状),但是在大多数情况下,由于线段定义了唯一的解决方案,我不需要完全通用的算法。
正如@Jean-FrançoisCorbett所指出的那样,有些情况存在多个解决方案。我显然需要更多地思考我的定义。
然而,我的目的是反向工程并使用专有文件格式,以便我可以对自己和他人收集的数据进行基本分析。文件格式足够简单,但确定他们用来定义边界的算法要困难得多。
加入许多会导致非唯一解的特殊情况会导致相关软件在没有警告的情况下崩溃或未能静默读取文件。
因此,在存在多个解时,生成一个可接受的解之一或能够确定存在多个解都是可以接受的。
多边形的轮廓不应跨越任何线段,并且应由连接所有线段端点的直线组成。 所有线段必须完全位于或沿着多边形的边界上。 大多数软件库需要多边形成为封闭的,因此不考虑通过在末尾添加第一个点来“关闭”多边形。
在满足此标准的情况下存在多个解决方案时,任何一个这些解决方案都是可以接受的。(能够确定解决方案是否不唯一会很好,但这并非严格必要。)
例如,如果我有以下情况:Segments Defining the Area,我希望描绘出以下区域:Desired Outline
它也适用于不相交的线段。 例如: enter image description here enter image description here 根据前面概述的标准,在任一情况下都有一个唯一的解决方案吗?(编辑:通常不存在唯一解决方案,如@Jean-FrançoisCorbett所指出。 然而,我仍然对能够生成可接受解之一的算法感兴趣。)
以下是生成上述图形的代码。 我在此使用python,但问题与编程语言无关。
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()

有什么想法吗?

我开始怀疑所尝试复现结果的软件在某种“内部”坐标系中使用了径向扫描算法(例如,一个坐标系具有通过点的分布定义的主轴上缩放和旋转的x-primey-prime。这使问题更加“圆形”),但是这会产生解决方案,在许多情况下与线段相交。虽然很容易检测到并从那里进行蛮力处理,但肯定有更好的方法,对吧?


当你说“条形图唯一地定义了一个解”时,你的意思是所有的条形图都必须位于最终多边形内吗? - andrew cooke
是的!我应该把这个信息加上。谢谢! - Joe Kington
5
根据之前提出的条件,我认为在任一情况下都存在唯一解。但实际上不一定如此。请尝试将第二个示例中的蓝色部分旋转90度。您问题定义中没有排除这样做,但现在有两个解决方案。 - Jean-François Corbett
当直线不相交时,预期的结果是什么? - ElKamina
@JoeKington:当使用所有点时没有解决方案会发生什么(例如Jean-Francois Corbett给出的第二个示例)? - Reinstate Monica
显示剩余5条评论
2个回答

17
  1. 选择一个安全的起点。可以是x值最大的端点。
  2. 沿着线段前进。
  3. 遇到任何交叉点时,始终向左转并沿着这条新线段前进。
  4. 遇到端点时,记录它。返回步骤2。
  5. 当你回到起点时停止。你记录的端点列表现在组成了凸包的有序顶点列表。

注意:如果存在没有与其他线段相交的“自由浮动”离群线段,则此方法将失败。但是,您指定“条形图唯一定义解决方案”,这排除了此故障条件。(离群分段可实现两种不同的解决方案。)

编辑…或者换句话说,离群分段可能会使两种不同的解决方案成为可能-具体取决于布局。证明:下面是一个例子,其中我添加的黄色分段使两个解决方案成为可能(蓝色和灰色可怕的手绘线)。如果黄色分段与现在绘制方式垂直排列,则仅可能得出一种解决方案。听起来你的问题定义得很糟糕。

enter image description here

编辑实际上,即使在“非常凹”的情况下,这个解决方案也可能失败,例如,如果有端点隐藏在您的线段堆积的偏僻角落中。在下图中,我添加了一条黑色线段。我的算法将非法地将其端点连接到另一个端点(虚线灰线)。我会保留我的答案,以便其他人可以在此基础上进行开发。

经过更多思考后编辑:即使在“非常凹”的情况下,该解决方案肯定会向您提供正确顺序的所有凸包点,但是它们可能与额外的不适当点混杂在一起,例如黑色点。因此可能存在太多点。

答案当然是要进行一些修剪。如果您可以有多个连续的“隐士点”,比如黑色点,那么修剪会变得相当复杂,因此我没有想到一个聪明的算法。但即使是盲目的暴力方法也是可行的。每个点可以被接受或拒绝(布尔值),因此如果您在凸包中有N个正确排序的候选点,则只需要检查2^N种可能性。这比您最初的排列问题使用暴力破解的可能性要少得多,该问题将具有对于k=1:(n-1),(n!/(n-k)!)的和种可能性(请原谅我的符号)。因此,这种算法显着缩小了您的问题范围。
我认为这是正确的方法。 enter image description here

3
不是很简单…你如何决定是拒绝“新遇到的”还是“之前遇到的”端点?如果我拒绝新的(灰色的那个),那么算法在这个示例中会失败。如果我拒绝“之前”的(我添加的黑色的那个),那么算法将在这个示例的镜像中失败。 - Jean-François Corbett
@JoeKington:如果存在异常值段,那么并不总是存在唯一解。我会在我的回答中添加一个例子。听起来你需要重新定义问题...并编辑你的问题... - Jean-François Corbett
@Jean-FrançoisCorbett - 我同意有关离群段与其他相交段的问题,但是当没有段相交时仍应该有唯一解(尽管我可能完全错误,在这种情况下,我可能需要重新思考我的问题定义)。我已更新我的问题。第二个例子实际上是一个相当常见的情况。无论如何,感谢您的帮助!您的答案让我考虑到了许多以前没有考虑过的事情。 - Joe Kington
1
@Jean:你说得对。我认为使用类似的回溯扫描算法可能会起作用,尽管(从0到2pi扫描所有角度,以当前杆作为0弧度开始,并连接您可以“看到”的第一个端点。如果您看不到任何端点,请返回到上一个端点)。这也适用于他的情况,其中一些杆不与其他杆相交,尽管它可能无法提供最优(最小面积)的解决方案。 - BlueRaja - Danny Pflughoeft
1
@JoeKington:我想我现在明白了。我的算法并不那么糟糕,它只需要一个额外的步骤,可以是蛮力法。请参见编辑。但仍然只适用于定义明确的问题...即没有未连接的片段。 - Jean-François Corbett
显示剩余4条评论

2

这不是一个完全成熟的想法,但是无论如何:假设您从一个中心点开始使用圆周扫描算法计算凸包(其中您按照点相对于该中心点的角度进行排序和处理)。 如果所有点都在凸包内,那么就完成了。如果没有,那么您必须“收紧”凸包以包括这些点。每个这样的点最初都是凸包的候选点,但因为它们破坏了凸性而被移除。有时(例如第一个示例中的上部紫色点),我们可以简单地将它们留在原位。当我们无法这样做时,因为新的凸包段穿过一条线段(例如在第一个示例中从底部绿色到底部紫色,假设底部水色点在绿色点之前被处理),修复过程就会更加复杂(这是问题最新编辑所涉及但我还没有深入探讨的部分)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接