Open3D 有一个用于 3D 三角形网格的数据结构,称为TriangleMesh。下面的代码显示了如何从ply文件中读取三角形网格并打印其顶点和三角形。
import open3d as o3d
import numpy as np
print("Testing mesh in Open3D...")
armadillo_path = r'..\PointCloud\ArmadilloMesh.ply'
mesh = o3d.io.read_triangle_mesh(armadillo_path)
knot_mesh_path = r'KnotMesh.ply'
mesh = o3d.io.read_triangle_mesh(knot_mesh_path)
print(mesh)
print('Vertices:')
print(np.asarray(mesh.vertices))
print('Triangles:')
print(np.asarray(mesh.triangles))
TriangleMesh 有一些字段如顶点(vertices)和三角(triangles)。可以用过numpy直接访问这些字段。
print("Try to render a mesh with normals (exist: " +
str(mesh.has_vertex_normals()) + ") and colors (exist: " +
str(mesh.has_vertex_colors()) + ")")
o3d.visualization.draw_geometries([mesh])
print("A mesh with no normals and no colors does not look good.")
当前的’KnotMesh.ply’看起来并不3D,因为当前mesh没有法线。
我们计算法线后再绘制网格
print("Computing normal and rendering it.")
mesh.compute_vertex_normals()
print(np.asarray(mesh.triangle_normals))
o3d.visualization.draw_geometries([mesh])
使用的是mesh的成员函数 compute_vertex_normals、 paint_uniform_color。
我们借助numpy,操作triangle和triangle_normals数据可以直接移除一半的表面。
print("We make a partial mesh of only the first half triangles.")
mesh1 = copy.deepcopy(mesh)#import copy
mesh1.triangles = o3d.utility.Vector3iVector(
np.asarray(mesh1.triangles)[:len(mesh1.triangles) // 2, :])
mesh1.triangle_normals = o3d.utility.Vector3dVector(
np.asarray(mesh1.triangle_normals)[:len(mesh1.triangle_normals) // 2, :])
print(mesh1.triangles)
o3d.visualization.draw_geometries([mesh1])
使用 numpy.asarray() 访问数据。
paint_uniform_color 用均匀的颜色绘制网格. 颜色在RGB空间内,[0,1]范围内。
print("Painting the mesh")
mesh1.paint_uniform_color([1, 0.706, 0])
o3d.visualization.draw_geometries([mesh1])
三角形网格具有多个属性,可以使用 Open3D 进行测试。一个重要的属性是流形(manifold)属性,我们可以测试三角形网格是否边流形is_edge_manifold,和顶点流形is_vertex_manifold。如果每条边都与一个或两个三角形接壤,三角形网格是边流形,。函数is_edge_manifold具有bool类型参数allow_boundary_edges,定义是否允许边界边。此外,如果顶点的星形是边流形和边连接的,则三角形网格是顶点流形,例如,两个或多个面仅由顶点连接,而不是由边连接。
另一个属性是自交的测试。如果网格中存在与另一个网格相交的三角形,则函数is_self_intersecting返回True。水密网格可以定义为边流形、顶点流形而不自相交的网格。函数is_watertight实现检查水密网格。
我们还可以测试三角形网格,如果它是可定向的(orientable),即三角形可以以所有法线都指向外部的方式定向。Open3D 中的相应函数称为 is_orientable。
下面的代码针对这些属性测试了许多三角形网格,并将结果可视化。非流形边显示为红色,边界边显示为绿色,非流形顶点显示为绿色点,自相交三角形显示为粉红色。
import open3d as o3d
import numpy as np
#https://github.com/isl-org/Open3D/blob/master/examples/python/open3d_example.py
import open3d_example as o3dtut
def check_properties(name, mesh):
mesh.compute_vertex_normals()
edge_manifold = mesh.is_edge_manifold(allow_boundary_edges=True)
edge_manifold_boundary = mesh.is_edge_manifold(allow_boundary_edges=False)
vertex_manifold = mesh.is_vertex_manifold()
self_intersecting = mesh.is_self_intersecting()
watertight = mesh.is_watertight()
orientable = mesh.is_orientable()
print(name)
print(f" edge_manifold: {edge_manifold}")
print(f" edge_manifold_boundary: {edge_manifold_boundary}")
print(f" vertex_manifold: {vertex_manifold}")
print(f" self_intersecting: {self_intersecting}")
print(f" watertight: {watertight}")
print(f" orientable: {orientable}")
geoms = [mesh]
if not edge_manifold:
edges = mesh.get_non_manifold_edges(allow_boundary_edges=True)
geoms.append(o3dtut.edges_to_lineset(mesh, edges, (1, 0, 0)))
if not edge_manifold_boundary:
edges = mesh.get_non_manifold_edges(allow_boundary_edges=False)
geoms.append(o3dtut.edges_to_lineset(mesh, edges, (0, 1, 0)))
if not vertex_manifold:
verts = np.asarray(mesh.get_non_manifold_vertices())
pcl = o3d.geometry.PointCloud(
points=o3d.utility.Vector3dVector(np.asarray(mesh.vertices)[verts]))
pcl.paint_uniform_color((0, 0, 1))
geoms.append(pcl)
if self_intersecting:
intersecting_triangles = np.asarray(
mesh.get_self_intersecting_triangles())
intersecting_triangles = intersecting_triangles[0:1]
intersecting_triangles = np.unique(intersecting_triangles)
print(" # visualize self-intersecting triangles")
triangles = np.asarray(mesh.triangles)[intersecting_triangles]
edges = [
np.vstack((triangles[:, i], triangles[:, j]))
for i, j in [(0, 1), (1, 2), (2, 0)]
]
edges = np.hstack(edges).T
edges = o3d.utility.Vector2iVector(edges)
geoms.append(o3dtut.edges_to_lineset(mesh, edges, (1, 0, 1)))
o3d.visualization.draw_geometries(geoms, mesh_show_back_face=True)
if __name__ == '__main__':
knot_mesh_path = r'KnotMesh.ply'
knot_mesh = o3d.io.read_triangle_mesh(knot_mesh_path)
check_properties('KnotMesh', knot_mesh)
check_properties('Mobius', o3d.geometry.TriangleMesh.create_mobius(twists=1))
check_properties("non-manifold edge", o3dtut.get_non_manifold_edge_mesh())
check_properties("non-manifold vertex", o3dtut.get_non_manifold_vertex_mesh())
check_properties("open box", o3dtut.get_open_box_mesh())
check_properties("intersecting_boxes", o3dtut.get_intersecting_boxes_mesh())
Open3D 包含许多过滤网格的方法。在下文中,我们将展示用于平滑噪声三角形网格的滤波器。
最简单的过滤器是平均过滤器。一个顶点可以通过和领接点平均进行平均过滤。
此过滤器可以用来去噪。
函数filter_smooth_simple的参数number_of_iterations指定平滑过滤的次数。
print('create noisy mesh')
knot_mesh = o3d.data.KnotMesh()
mesh_in = o3d.io.read_triangle_mesh(knot_mesh.path)
vertices = np.asarray(mesh_in.vertices)
noise = 5
vertices += np.random.uniform(0, noise, size=vertices.shape)
mesh_in.vertices = o3d.utility.Vector3dVector(vertices)
mesh_in.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_in])
print('filter with average with 1 iteration')
mesh_out = mesh_in.filter_smooth_simple(number_of_iterations=1)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])
print('filter with average with 5 iterations')
mesh_out = mesh_in.filter_smooth_simple(number_of_iterations=5)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])
另一个重要的网格滤波器是拉普拉斯滤波器,定义为
v i = v i ⋅ λ ∑ n ∈ N w n v n − v i vi=vi⋅λ\sum_{n∈N}w_nv_n−v_i vi=vi⋅λn∈N∑wnvn−vi
其中λ是滤波器的强度,并且 w n w_n wn是与相邻顶点的距离相关的归一化权重。筛选器在filter_smooth_laplacian中实现,并具有参数number_of_iterations和lambda.
print('filter with Laplacian with 10 iterations')
mesh_out = mesh_in.filter_smooth_laplacian(number_of_iterations=10)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])
print('filter with Laplacian with 50 iterations')
mesh_out = mesh_in.filter_smooth_laplacian(number_of_iterations=50)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])
平均滤波器和拉普拉斯滤波器的问题在于它们会导致三角形网格的收缩。[Taubin1995] 表明,应用两个λ参数不同的拉普拉斯滤波器可以防止网格收缩。筛选器在filter_smooth_taubin中实现。
print('filter with Taubin with 10 iterations')
mesh_out = mesh_in.filter_smooth_taubin(number_of_iterations=10)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])
print('filter with Taubin with 100 iterations')
mesh_out = mesh_in.filter_smooth_taubin(number_of_iterations=100)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])
Open3D 包含从三角形网格对点云进行采样的函数。最简单的方法sample_points_uniformly根据三角形面积均匀地从 3D 曲面对点进行采样。参数number_of_points定义从三角形曲面采样的点数。
mesh = o3d.geometry.TriangleMesh.create_sphere()
mesh.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh])
pcd = mesh.sample_points_uniformly(number_of_points=500)
o3d.visualization.draw_geometries([pcd])
bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
mesh.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh])
pcd = mesh.sample_points_uniformly(number_of_points=500)
o3d.visualization.draw_geometries([pcd])
均匀采样可以在表面上产生点簇,而一种称为泊松圆盘采样的方法可以均匀地分布表面上的点。sample_points_poisson_disk方法实现样本消除。它从采样的点云开始,并删除点以满足采样标准。该方法支持两个选项来提供初始点云:
1.默认通过参数init_factor:该方法首先用init_factor X number_of_points均匀地从网格中抽取点云,并使用它进行消除。
2.可以提供点云并将其传递给方法sample_points_poisson_disk。然后,此点云用于消除。
mesh = o3d.geometry.TriangleMesh.create_sphere()
pcd = mesh.sample_points_poisson_disk(number_of_points=500, init_factor=5)
o3d.visualization.draw_geometries([pcd])
pcd = mesh.sample_points_uniformly(number_of_points=2500)
pcd = mesh.sample_points_poisson_disk(number_of_points=500, pcl=pcd)
o3d.visualization.draw_geometries([pcd])
bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
mesh.compute_vertex_normals()
pcd = mesh.sample_points_poisson_disk(number_of_points=500, init_factor=5)
o3d.visualization.draw_geometries([pcd])
pcd = mesh.sample_points_uniformly(number_of_points=2500)
pcd = mesh.sample_points_poisson_disk(number_of_points=500, pcl=pcd)
o3d.visualization.draw_geometries([pcd])
在网格细分中,我们将每个三角形划分为许多较小的三角形。在最简单的情况下,我们计算每个三角形每条边的中点,并将三角形分成四个较小的三角形。函数subdivide_midpoint中实现。3D 表面和面积保持不变,但顶点和三角形的数量会增加。参数number_of_iterations定义此过程应重复多少次。
import open3d as o3d
mesh = o3d.geometry.TriangleMesh.create_box()
mesh.compute_vertex_normals()
print(
f'The mesh has {len(mesh.vertices)} vertices and {len(mesh.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh], mesh_show_wireframe=True)
mesh = mesh.subdivide_midpoint(number_of_iterations=1)
print(
f'After subdivision it has {len(mesh.vertices)} vertices and {len(mesh.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh], mesh_show_wireframe=True)
有时我们希望用较少三角形和顶点的表示高分辨率网格,但低分辨率网格仍应接近高分辨率网格。为此,Open3D实现了许多网格简化方法。
顶点聚类方法将落入给定大小的体素中的所有顶点汇集到单个顶点。该方法在simplify_vertex_clustering中实现,参数voxel_size定义体素网格大小和contraction定义顶点池化方式。 o3d.geometry.SimplificationContraction.Average计算平均值(汇聚方式)。
bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
mesh.compute_vertex_normals()
print(
f'Input mesh has {len(mesh_in.vertices)} vertices and {len(mesh_in.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh_in])
voxel_size = max(mesh_in.get_max_bound() - mesh_in.get_min_bound()) / 32
print(f'voxel_size = {voxel_size:e}')
mesh_smp = mesh_in.simplify_vertex_clustering(
voxel_size=voxel_size,
contraction=o3d.geometry.SimplificationContraction.Average)
print(
f'Simplified mesh has {len(mesh_smp.vertices)} vertices and {len(mesh_smp.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh_smp])
voxel_size = max(mesh_in.get_max_bound() - mesh_in.get_min_bound()) / 16
print(f'voxel_size = {voxel_size:e}')
mesh_smp = mesh_in.simplify_vertex_clustering(
voxel_size=voxel_size,
contraction=o3d.geometry.SimplificationContraction.Average)
print(
f'Simplified mesh has {len(mesh_smp.vertices)} vertices and {len(mesh_smp.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh_smp])
网格简化方法的另一类是网格抽取,它以增量步骤运行。我们选择一个三角形,以最小化误差指标并将其删除。重复此操作,直到达到所需数量的三角形。Open3D 实现了simplify_quadric_decimation最小化误差二次(到相邻平面的距离) 的实现。参数target_number_of_triangles定义抽取算法的停止规则。
mesh_smp = mesh_in.simplify_quadric_decimation(target_number_of_triangles=6500)
print(
f'Simplified mesh has {len(mesh_smp.vertices)} vertices and {len(mesh_smp.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh_smp])
mesh_smp = mesh_in.simplify_quadric_decimation(target_number_of_triangles=1700)
print(
f'Simplified mesh has {len(mesh_smp.vertices)} vertices and {len(mesh_smp.triangles)} triangles'
)
o3d.visualization.draw_geometries([mesh_smp])
各种重建方法的结果。Open3D 实现了连接组件算法,算法cluster_connected_triangles将每个三角形分配给一组连接的三角形。它为每个三角形返回 triangle_clusters中的簇的索引,并为每个簇返回中的三角形数cluster_n_triangles 和簇的表面积cluster_area。
这在例如RGBD集成中很有用,RGBD集成并不总是单个三角形网格,而是许多网格。一些较小的部件是由于噪音引起的,我们很可能想要移除它们。
下面的代码显示了cluster_connected_triangles的应用以及如何使用它来消除虚假三角形。
print("Generate data")
bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
mesh.compute_vertex_normals()
mesh = mesh.subdivide_midpoint(number_of_iterations=2)
vert = np.asarray(mesh.vertices)
min_vert, max_vert = vert.min(axis=0), vert.max(axis=0)
for _ in range(30):
cube = o3d.geometry.TriangleMesh.create_box()
cube.scale(0.005, center=cube.get_center())
cube.translate(
(
np.random.uniform(min_vert[0], max_vert[0]),
np.random.uniform(min_vert[1], max_vert[1]),
np.random.uniform(min_vert[2], max_vert[2]),
),
relative=False,
)
mesh += cube
mesh.compute_vertex_normals()
print("Show input mesh")
o3d.visualization.draw_geometries([mesh])
##
print("Cluster connected triangles")
with o3d.utility.VerbosityContextManager(
o3d.utility.VerbosityLevel.Debug) as cm:
triangle_clusters, cluster_n_triangles, cluster_area = (
mesh.cluster_connected_triangles())
triangle_clusters = np.asarray(triangle_clusters)
cluster_n_triangles = np.asarray(cluster_n_triangles)
cluster_area = np.asarray(cluster_area)
###
print("Show mesh with small clusters removed")
mesh_0 = copy.deepcopy(mesh)
triangles_to_remove = cluster_n_triangles[triangle_clusters] < 100
mesh_0.remove_triangles_by_mask(triangles_to_remove)
o3d.visualization.draw_geometries([mesh_0])