如何使用 Trimesh Python 库检索构成网格边界的顶点索引?
例如,对于平面网格,我只期望位于外边缘的顶点。如果平面网格有一个孔,我还期望标记孔边缘的顶点。对于开放的圆柱形网格,我只期望排列两个开口的顶点。
我的所有网格都是开放的,就像没有顶部和底部的管道或盒子。它们不防水。
对于给定的网格实例,
edges
(返回的条目比我拥有的顶点多!)和edges_unique
属性都没有返回我所期望的结果。 facets_boundary
适用于平面网格,但不适用于圆柱形网格。在阅读项目 API 文档时,我发现很难理解我应该从这些属性中得到什么。
我必须自己找到边缘吗?使用
vertex_counts = np.bincount(faces.flatten())
?
对于我的网格,这会产生如下结果:
Planar mesh (4x5)
vertex_counts: [2 3 3 3 3 1 3 6 6 6 6 3 3 6 6 6 6 3 3 6 6 6 6 3 1 3 3 3 3 2]
Planar mesh with hole (8x8 with a 3x3 hole in the middle)
vertex_counts: [2 3 3 3 3 3 3 3 1 3 6 6 6 6 6 6 6 3 3 6 4 3 3 3 5 6 3 3 6 3 0 0 0 3 6 3 3
6 3 0 0 0 3 6 3 3 6 3 0 0 0 3 6 3 3 6 5 3 3 3 4 6 3 3 6 6 6 6 6 6 6 3 1 3
3 3 3 3 3 3 2]
Cylindrical mesh (8 circular divisions, 6 longitudinal divisions)
vertex_counts: [3 3 3 3 3 3 3 3 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
6 6 6 6 6 6 6 6 6 6 6 3 3 3 3 3 3 3 3]
对于给定的网格实例,
(返回的条目比我拥有的顶点多!)和edges
属性都没有返回我所期望的结果。edges_unique
让我们从头开始。 (三角形)网格是 3D 坐标(顶点)的列表加上三元组顶点(三角形)的列表。让我们考虑一个超级简单的网格,代表一个正方形:
0 --- 1
| \ |
| \ |
2 --- 3
顶点
0
、1
、2
和 3
通过两个三角形连接:[0, 2, 3]
和 [0, 3, 1]
。在这种情况下,网格的 edges
是所有三角形中的各个线段,即 (0, 2)
、(2, 3)
、(3,0)
、(0, 3)
、(3, 1)
和 (1, 0)
对的列表。正如您所看到的,对角线边缘重复出现,因为它出现在两个三角形中。 edges_unique
属性确保不包含此重复。一般来说,对于具有 $V$ 顶点的三角形网格,您可以预期大约有 $2V$ 面和 $3V$ 边(请参见此处)。
适用于平面网格,但对于圆柱形网格则严重失败。在阅读项目 API 文档时,我发现很难理解我应该从这些属性中得到什么。facets_boundary
来自文档:“返回共面相邻面的面索引列表”。我同意它不是很清楚,但它的作用是将相邻且共面的面(三角形)分组。以圆柱体为例。顶部和底部的圆圈均由一组彼此接触且位于同一平面上的三角形组成。两组三角形被分组为面。类似地,一个盒子有六个面(尽管每个面可以分为许多三角形!)。另一种思考方式:面是一组可以用平面多边形替换的三角形。
现在回到你最初的问题:如何找到边界?答案是保留只属于一个三角形的所有边。如果无序列表的边对您来说足够了,您可以简单地使用:
unique_edges = mesh.edges[trimesh.grouping.group_rows(mesh.edges_sorted, require_count=1)]
(请参阅此 GitHub 问题)。如果您想要 3D 坐标,请考虑使用
Trimesh.outline()
代替。
如果您需要一个有序的顶点列表,那么就有点复杂了。首先,只有当您的网格是流形时,这才有意义(没有边可以属于两个以上的三角形)。然后,正如这个答案中所解释的,在获得边界边列表后,您需要按顺序遍历它们。这是一个可以做到这一点的函数:
def boundary(mesh, close_paths=True):
# Set of all edges and of boundary edges (those that appear only once).
edge_set = set()
boundary_edges = set()
# Iterate over all edges, as tuples in the form (i, j) (sorted with i < j to remove ambiguities).
# For each edge, three cases are possible:
# 1. The edge has never been visited before. In this case, we can add it to the edge set and as a boundary
# candidate as well.
# 2. The edge had already been visited once. We want to keep it into the set of all edges but remove it from the
# boundary set.
# 3. The edge had already been visited at least twice. This is generally an indication that there is an issue with
# the mesh. More precisely, it is not a manifold, and boundaries are not closed-loops.
for e in map(tuple, mesh.edges_sorted):
if e not in edge_set:
edge_set.add(e)
boundary_edges.add(e)
elif e in boundary_edges:
boundary_edges.remove(e)
else:
raise RuntimeError(f"The mesh is not a manifold: edge {e} appears more than twice.")
# Given all boundary vertices, we create a simple dictionary that tells who are their neighbours.
neighbours = defaultdict(lambda: [])
for v1, v2 in boundary_edges:
neighbours[v1].append(v2)
neighbours[v2].append(v1)
# We now look for all boundary paths by "extracting" one loop at a time. After obtaining a path, we remove its edges
# from the "boundary_edges" set. The algorithm terminates when all edges have been used.
boundary_paths = []
while len(boundary_edges) > 0:
# Given the set of remaining boundary edges, get one of them and use it to start the current boundary path.
# In the sequel, v_previous and v_current represent the edge that we are currently processing.
v_previous, v_current = next(iter(boundary_edges))
boundary_vertices = [v_previous]
# Keep iterating until we close the current boundary curve (the "next" vertex is the same as the first one).
while v_current != boundary_vertices[0]:
# We grow the path by adding the vertex "v_current".
boundary_vertices.append(v_current)
# We now check which is the next vertex to visit.
v1, v2 = neighbours[v_current]
if v1 != v_previous:
v_current, v_previous = v1, v_current
elif v2 != v_previous:
v_current, v_previous = v2, v_current
else:
# This line should be un-reachable. I am keeping it only to detect bugs in case I made a mistake when
# designing the algorithm.
raise RuntimeError(f"Next vertices to visit ({v1=}, {v2=}) are both equal to {v_previous=}.")
# Close the path (by repeating the first vertex) if needed.
if close_paths:
boundary_vertices.append(boundary_vertices[0])
# "Convert" the vertices from indices to actual Cartesian coordinates.
boundary_paths.append(mesh.vertices[boundary_vertices])
# Remove all boundary edges that were added to the last path.
boundary_edges = set(e for e in boundary_edges if e[0] not in boundary_vertices)
# Return the list of boundary paths.
return boundary_paths
请注意,它返回 3D 路径列表。如果您想要顶点列表,只需将行
boundary_paths.append(mesh.vertices[boundary_vertices])
更改为 boundary_paths.append(boundary_vertices)
即可。另请注意,如果参数 close_paths
为 True
,则路径的第一个元素也会在末尾重复,以“使其成为”闭合路径。
最后,可能有一些方法可以比我刚刚写的更有效地执行相同的操作!