将三角形和矩形面列表转换为实心多边形?

Convert list of triangles and rectangle facets into a solid polygon?

提问人:Jagerber48 提问时间:6/29/2022 更新时间:6/30/2022 访问量:322

问:

我有一个 2D 几何图形,它由多个小平面指定,其中每个小平面是由 3 个顶点坐标指定的三角形,例如 .facet = [[0, 0], [1, 0], [1, 1]]

我可以通过将每个方面转换为补丁并绘制这些补丁来在 matplotlib 中绘制这些补丁。但是,我想要一种算法,该算法采用我的分面列表并转换为所有外部顶点的单个闭合多边形路径。Polygon

例如,假设我有

facet_list = [[[0, 0], [1, 0], [1, 1]],
              [[0, 0], [1, 1], [0, 1]],
              [[1, 0], [2, 0], [2, 1]],
              [[1, 0], [2, 1], [1, 1]],
              [[1, 1], [2, 1], [2, 2]],
              [[1, 1], [2, 2], [1, 2]]]

solid_vertex_list = [[0, 0],
                     [1, 0],
                     [2, 0],
                     [2, 1],
                     [2, 2],
                     [1, 2],
                     [1, 1],
                     [0, 1]]

第一个数据集是分面列表,而第二个数据集是外部顶点的目标列表。有关这两个数据集的可视化效果,请参见下文:enter image description here

我寻求一种将第一个数据集转换为第二个数据集的算法。此特定示例未捕获一些特征,但这些特征是算法所必需的。

(1) 在此示例中,所有顶点都是外部顶点,但通常可能存在完全位于生成的“大”多边形内部的面。

(2)一般来说,“大”多边形上可能有孔。我不确定如何最好地处理这个问题,但根据这个 matplotlib 文档,如果您以相反的顺序给出孔的顶点,看起来 matplotlib PathPatch 对象可以在多边形中绘制孔。因此,就此算法而言,如果将任何“孔”的顶点路径简单地报告为相反顺序的单独多边形就足够了。

(3) 小平面可能形成不相连的多边形。在这种情况下,应返回单独的顶点列表,指示单独的多边形。如果两个多边形仅由一个或更少的顶点连接,则它们被视为断开连接。

我认为以上 3 点是 facets -> polygon(s)(带孔)算法的要求。总的来说,我认为该算法将返回一个顶点列表列表,其中顶点列表如果对应于断开连接的外部多边形,则顺时针列出,如果它们对应于孔,则逆时针列出。也许需要有一些东西来指示哪个孔对应于哪个外部多边形。一个棘手的边缘情况可能是:如何处理具有一个或多个外部顶点的孔。即当一个孔在一个或多个孤立点接触外部时。

出于此算法的目的,我们可以假设 facet 与其他 facet 共享节点,以便 (1) facet 不重叠,即任何 facet 的节点都不在另一个 facet 内,并且 (2) facet 仅共享完整的边,即一个 facet 的节点没有节点在另一个 facet 的边缘上。另一个问题的主题是如何获取不满足 (1) 和 (2) 的数据,并通过添加更多方面来分解交叉点和中间边缘节点来清理它。

Python 算法 matplotlib 几何

评论


答:

1赞 btilly 6/30/2022 #1

我知道你称它们为分面,但这个词对我来说太过分了。我称它们为三角形。

第一步,确保所有三角形的方向相同。这里有一些简单的代码。

def dot (vector1, vector2):
    return vector1[0] * vector2[0] + vector1[1] * vector2[1]

def vector_from_points (point1, point2):
    return [point2[0] - point1[0], point2[1] - point1[1]]

def rotate_vector_90 (vector):
    return [-vector[1], vector[0]]

def oriented_triangle (triangle):
    vector1 = vector_from_points(triangle[0], triangle[1])
    vector2 = vector_from_points(triangle[1], triangle[2])
    normal = rotate_vector_90(vector1)
    if 0 < dot(normal, vector2):
        return triangle
    else:
        return [triangle[0], triangle[2], triangle[1]]

现在,如果两个三角形共享一条边,则保证一个三角形是形式,另一个是形式。[a, b][b, a]

因此,我们可以按如下方式加载字典:

triangle_position = {}
for triangle in triangles:
    for i in range(3):
        triangle_position[tuple(triangle[i])] = (triangle, i)

现在我们可以遍历所有的边缘。如果它是内部边缘,我们会忽略它。如果它是外边,我们可以在它所来自的三角形中找到下一条边。虽然这不是外边,但我们可以找到具有反边的三角形并移动到 ITS 的下一条边。最终,我们到达了下一个外部边缘。这让我们可以在外部走动。

我可以保证形状的外侧是逆时针方向,形状内的孔是顺时针方向。不过,我不能保证孔与形状的顺序会是什么 - 你可能需要自己弄清楚。我建议使用光线追踪算法。

我不会解决奖金问题。

评论

0赞 Jagerber48 6/30/2022
啊太好了,谢谢!因此,我们知道在两个三角形中发现的任何边都是内边。任何不是内边的边,我们知道是多边形的外边或孔的“外边”。最后,我们可以使用小平面的方向来帮助我们轻松地沿着任何外边缘行走。我认为与外部共享一个点的孔的(字面)角情况仍然需要一些特别的注意,因为将有两个候选的“外部”边缘可供选择。但我现在其实并不太担心这个案子。
0赞 btilly 6/30/2022
@Jagerber48 实际上,它将是反向边缘。因此,如果边缘是单向的,则必须寻找。此外,字典技巧仅适用于不可变数据,因此元组的元组,而不是数组的数组。(a, b)(b, a)
0赞 btilly 6/30/2022
@Jagerber48 同样在字面上的角落情况下,您会发现边缘将从外部切换到孔,然后再切换回来。您可以通过查找节点在结果路径中出现两次的时间来检测此类情况。
2赞 trincot 6/30/2022 #2

假设所有三角形的点都以相同的循环方向(全部顺时针或全部逆时针)给出,您可以执行以下操作:

  1. 收集所有边,只保留那些没有被反边抵消的边缘(所以 [a, b] 被 [b, a] 删除)。这样,您最终会得到应该属于多边形的边。剩余的边描述了一个或多个与三角形具有相同循环方向的多边形,以及零个或多个具有相反方向的孔。

  2. 为步骤 1 的边缘创建邻接列表。

  3. 只要存在未访问的边,请选择一条未访问的边,将其从邻接列表中移除,使用该边的起始顶点开始一个新多边形,然后重复以下步骤以使用更多顶点填充多边形:

    • 将 Edge 标记为已访问
    • 如果结束顶点在邻接列表中没有传出边,则退出此环路
    • 将结束顶点追加到多边形
    • 从邻接列表中提取相邻顶点(并将其从中删除)
    • 从拳头项目符号点开始,用这个相邻的边缘重复。
  4. 将此多边形添加到多边形列表中

  5. 从步骤 3 开始重复,直到所有边都标记为已访问。

下面是执行此操作的代码:

def triangles_to_polygons(triangles):
    # Collect those edges that are not negated by an opposite edge
    edges = set()
    for triangle in triangles:
        a, b, c = map(tuple, triangle)
        for start, end in (a, b), (b, c), (c, a):
            if (end, start) in edges:
                edges.remove((end, start))
            else:
                edges.add((start, end))
    # Create an adjacency list
    adj = {}
    for a, b in edges:
        adj.setdefault(a, []).append(b)
    
    # Populate polygons
    polygons = []
    visited = set()
    for edge in edges:
        if edge not in visited:
            a, b = edge
            adj[a].remove(b)
            polygon = [a]
            while True:
                visited.add((a, b))
                if not adj[b]:
                    break
                polygon.append(b)
                a, b = b, adj[b].pop()
            polygons.append(polygon)
    return polygons

运行示例:

triangles = [[[0, 0], [1, 0], [1, 1]],
             [[0, 0], [1, 1], [0, 1]],
             [[1, 0], [2, 0], [2, 1]],
             [[1, 0], [2, 1], [1, 1]],
             [[1, 1], [2, 1], [2, 2]],
             [[1, 1], [2, 2], [1, 2]]]
polygons = triangles_to_polygons(triangles)
print(polygons)

评论

0赞 Jagerber48 7/1/2022
问题:在非退化情况下,所有边都完全共享(并且没有只共享单个节点的三角形),则邻接字典元素的形式始终为 。在这种情况下,该列表似乎并非完全必要。但是,如果两个三角形仅在一个点相遇,则一个节点可能具有多个相邻节点: 。你是这样写代码来涵盖这种情况的吗?如果存在这种极端情况,此代码会优雅地处理它吗?node_a: [node_b,]node_a: [node_b, node_c]
1赞 trincot 7/1/2022
是的,三角形仅在一点上相遇的情况是我花在这件事上 90% 的时间推动我思考的案例。但即使在没有发生这种情况的情况下,字典也表示一个链表,这仍然很有用。在没有该字典的情况下找到正确的边顺序将是一个效率较低的过程。