Open3d 二 点云转mesh并实现空洞修复、mesh简化、mesh细化

点云是由大量的点组成的三维数据集,而mesh是由三角形组成的三维模型。点云转换为mesh的过程称为曲面重建。曲面重建的目的是从点云数据中提取出表面的形状信息。曲面重建的方法有很多种,其中一种常用的方法是贪心投影三角化法。该方法的大致流程如下:将点云通过法线投影到某一二维坐标平面内,对投影得到的点云做平面内的三角化,从而得到各点的拓扑连接关系。最后根据平面内投影点的拓扑连接关系确定各原始三维点间的拓扑连接,所得三角网格即为重建得到的曲面模型(mesh)。

1、基本转换(点云直接转mesh)

关键词:点云法线、点的法向量、法线的作用

点云法线计算的原理是通过计算点云中每个点的法向量来得到整个点云的法向量。法向量是垂直于曲面的向量,它可以用于描述曲面的方向和形状(区分凹凸)。在点云中,每个点的法向量可以通过计算其周围的点的几何特征来得到。

计算点的法向量有两种方法:基于协方差矩阵的方法基于曲率的方法基于协方差矩阵的方法是通过计算每个点周围的点与该点的协方差矩阵来得到该点的法向量基于曲率的方法是通过计算点s附近的多个点拟合出一个平面,然后求拟合平面的法线,该法线即为点s的法线

法线可用于点云的渲染区分正反面重建法线贴图。在点云渲染中主要是设置点云或模型的光照效果,用于区分物体正侧面、得到物体的凹凸感(如下图左兔子纯色渲染没有立体感,而下图右兔子则有明暗区别,体现出较强的立体感)。
在这里插入图片描述

我们可以基于点云的渲染效果来初步判定法线的正确性,光源在模型的正前方,下图右模型的阴影区域与模型立体结构明显不符,故可得出该点云区域法线方向计算错误。
在这里插入图片描述

区分正反面,通过对点s附近的点的法线方向进行统计,使其保持在一个大方向,使相邻点法线方向变化最小,即可矫正方向错误的法线。
在这里插入图片描述
在这里插入图片描述

以上内容与图片参考自:http://geometryhub.net/notes/pointcloudnormal
https://www.shili8.cn/article/detail_20000668276.html
http://geometryhub.net/notes/pointcloudnormal

1.1 完整代码案例

import open3d as o3d
import numpy as np
import trimesh
import copy 

pcd = o3d.io.read_point_cloud("points cloud document/cat.pcd")

#转换的mash存在孔洞
# 法线估计
radius1 = 0.1   # 搜索半径
max_nn = 100     # 邻域内用于估算法线的最大点数
pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(radius1, max_nn))     # 执行法线估计
# 可视化
o3d.visualization.draw_geometries([pcd], 
                                  window_name = "可视化参数设置",
                                  width = 600,
                                  height = 450,
                                  left = 30,
                                  top = 30,
                                  point_show_normal = True)
 
# 滚球半径的估计
distances = pcd.compute_nearest_neighbor_distance()
avg_dist = np.mean(distances)
radius = 1.5 * avg_dist   
mesh = o3d.geometry.TriangleMesh.create_from_point_cloud_ball_pivoting(
           pcd,
           o3d.utility.DoubleVector([radius, radius * 2]))
print(mesh.get_surface_area())
o3d.visualization.draw_geometries([mesh], window_name='Open3D downSample', width=800, height=600, left=50,
                                  top=50, point_show_normal=True, mesh_show_wireframe=True, mesh_show_back_face=True,)
# 从open3d创建具有顶点和面的三角形网格
tri_mesh = trimesh.Trimesh(np.asarray(mesh.vertices), np.asarray(mesh.triangles),
                          vertex_normals=np.asarray(mesh.vertex_normals))
trimesh.convex.is_convex(tri_mesh)
#mesh保存
tri_mesh.export("cat_hole.ply")
#o3d.io.write_triangle_mesh("hole.obj", tri_mesh)

滚球半径越小模型越精细(不能比平均距离小,否则大部分较远的点都无法卡住滚球,导致被忽略),滚球半径越大模型越粗糙(较远处的高点会顶着滚球,导致两个凸起肢体内的点被忽略,如下图滚球卡在了猫爪上,导致猫爪内部的mesh无法生成)
在这里插入图片描述

2、点云下采样

进行点云下采样可以减少点云数据集中点云的数量,提高计算效率。

2.1体素下采样

体素滤波器可以达到向下采样同时不破坏点云本身几何结构的功能,但是会移动点的位置。 此外体素滤波器可以去除一定程度的噪音点及离群点。主要功能是用来进行下采样。

扫描二维码关注公众号,回复: 17230532 查看本文章

体素下采样通过输入的点云数据创建一个三维体素栅格,然后在每个非空体素内,用体素中所有点的质心来近似显示体素中其他点,这样该体素内所有点就用一个质心点最终表示,从而实现点云的下采样。

该算法分为两个步骤:
1、将点规整地划分到三维体素栅格中。
2、对每个三维体素栅格内的所有点用其质心点来表示。

2.2 均匀下采样

均匀下采样有多种不同的采样方式,其中最远点采样是较为简单的一种,首先需要选取一个种子点,并设置一个内点集合,每次从点云中不属于内点的集合找出一点距离内点最远的点,具体方法如下:

1、输入点云记为C,采样点集记为S,S初始化为空集。

2、随机采样一个种子点Seed,放入S。如下图所示。

3、每次采样一个点,放入S。采样的方法是,在集合C-S里,找一点距离集合S距离最远的点。其中点到集合的距离为,这点到集合里所有点距最小的距离。如下图所示,采样点S的数量分别为2,4,10,20,100.
在这里插入图片描述
这种方式的下采样点云分布均匀,但是算法复杂度较高效率低。

2.3 完整代码案例

import open3d as o3d
import numpy as np
 
print("->正在加载点云... ")
#pcd = o3d.io.read_point_cloud("points cloud document/cat.pcd")
pcd = o3d.io.read_point_cloud("2023_09_27_14_10_50/PointCloudxyzrgb/00000003.txt", format='xyzrgb')
print(pcd)

print("->正在可视化原始点云")
o3d.visualization.draw_geometries([pcd])
print("->正在体素下采样...")
voxel_size = 100
#downpcd = pcd.voxel_down_sample(voxel_size)#体素下采样
downpcd = o3d.geometry.PointCloud.uniform_down_sample(pcd, 100)#均匀下采样
print(downpcd)
print("->正在可视化下采样点云")
o3d.visualization.draw_geometries([downpcd])

原始点云图:
在这里插入图片描述
体素下采样效果图:
在这里插入图片描述

均匀下采样效果图:
在这里插入图片描述

以上内容参考自:https://blog.csdn.net/yanfeng1022/article/details/109322887
https://blog.csdn.net/u014072827/article/details/111944421

3、mesh孔洞填补

点云转mesh通常会由于各种原因存在孔洞,我们需要找出孔洞区域并进行填补,这里使用pymeshfix实现mesh孔洞的填补。 pymeshfix采样多边形网格(mesh)作为输入, PyMeshFix旨在纠正mesh中存在的典型缺陷,因此它可能会产生粗略或误修复的结果。其官网地址为:https://pymeshfix.pyvista.org/index.html ,安装命令为:pip install pymeshfix

3.1 修复案例1

修复案例1参考自https://pymeshfix.pyvista.org/examples/bunny.html 相比于案例2要简介很多,同时修复成功率要高很多

import pymeshfix as mf

# sphinx_gallery_thumbnail_number = 2
import pyvista as pv

bunny = pv.read("_tmp.ply")

# Define a camera position that shows the holes in the mesh

# Show mesh
bunny.plot()

meshfix = mf.MeshFix(bunny)
holes = meshfix.extract_holes()

p = pv.Plotter()
p.add_mesh(bunny, color=True)
p.add_mesh(holes, color="r", line_width=8)
p.enable_eye_dome_lighting()  # helps depth perception
p.show()

meshfix.repair(verbose=True)
meshfix.mesh.plot()

在这里插入图片描述
在这里插入图片描述

3.2 修复案例2

使用代码参考自https://pymeshfix.pyvista.org/examples/repair_planar.html,具体用法如下:

# sphinx_gallery_thumbnail_number = 1
import numpy as np
from pymeshfix import MeshFix
from pymeshfix._meshfix import PyTMesh
from pymeshfix.examples import planar_mesh
import pyvista as pv

##########################  加载mesh并展示其空洞 #######################################
#读取mesh
print(planar_mesh)
planar_mesh="hole.ply"
orig_mesh = pv.read(planar_mesh)
#orig_mesh = pv.read("hole.obj")
# orig_mesh.plot_boundaries()

#计算mesh的孔洞
meshfix = MeshFix(orig_mesh)
holes = meshfix.extract_holes()

#将mesh与孔洞进行叠加展示
plotter = pv.Plotter()
plotter.add_mesh(orig_mesh, color=True)
plotter.add_mesh(holes, color="r", line_width=5)
plotter.enable_eye_dome_lighting()  # helps depth perception
_ = plotter.show()


############################  mesh空洞修复   ####################################
#构建PyTMesh修复对象mfix,并加载要修复的mesh
mfix = PyTMesh(False)  
mfix.load_file(planar_mesh)
#mfix = MeshFix(orig_mesh)
#填充最多具有“nbe”边界边缘的所有孔,如果“refine”为true,添加内部顶点以重现采样周围环境的密度。返回修补的孔数。
#“nbe”为0(默认值),则修复所有孔,值越大则表明
mfix.fill_small_boundaries(nbe=0, refine=True)

############################  将PyTMesh对象转换为pyvista mesh #########################################
#
vert, faces = mfix.return_arrays()
triangles = np.empty((faces.shape[0], 4), dtype=faces.dtype)
triangles[:, -3:] = faces
triangles[:, 0] = 3
mesh = pv.PolyData(vert, triangles)

############################ 进行可视化,并保存mesh #########################################

plotter = pv.Plotter()
plotter.add_mesh(mesh, color=True)
plotter.add_mesh(holes, color="r", line_width=5)
plotter.enable_eye_dome_lighting()  # helps depth perception
_ = plotter.show()

mesh.save(planar_mesh.replace(".ply","_r.ply"))

运行效果如下
在这里插入图片描述
在这里插入图片描述

4、网格简化

Open3D提供了两种方法来实现网格简化:simplify_vertex_clustering和simplify_quadric_decimation。

4.1 顶点聚类

simplify_vertex_clustering是一种基于顶点聚类的方法,它将高分辨率的网格以更少的顶点和面表示出来。 这个方法会根据指定的聚类参数将相似的顶点合并在一起,从而减少网格的复杂性。
具体用法如下:

import numpy as np
import open3d as o3d

mesh=o3d.io.read_triangle_mesh("hole_r.ply")

voxel_size = max(mesh.get_max_bound() - mesh.get_min_bound()) / 32
print(f'voxel_size = {
      
      voxel_size:e}')
mesh2 = mesh.simplify_vertex_clustering(
    voxel_size=voxel_size,
    contraction=o3d.geometry.SimplificationContraction.Average)
#mesh2 = mesh.simplify_vertex_clustering(100)
mesh2.translate([2000,0,0])

o3d.visualization.draw_geometries([mesh,mesh2], window_name='Open3D downSample', width=800, height=600, left=50,
                                  top=50, point_show_normal=True, mesh_show_wireframe=True, mesh_show_back_face=True,)
o3d.io.write_triangle_mesh("hole_r2.obj", mesh2)

运行效果如下:
在这里插入图片描述

4.2 网格抽取

simplify_quadric_decimation是一种基于网格抽取的方法,我们选择一个使误差度量最小化的三角形并将其删除。重复此过程直到满足指定的三角形数量时停止。函数的参数包括输入三角网格和目标三角形数量。默认情况下,函数会将输入网格简化为目标三角形数量。函数的返回值是一个新的三角网格对象。

具体用法如下:

import numpy as np
import open3d as o3d

mesh=o3d.io.read_triangle_mesh("hole_r.ply")
mesh_smp1 = mesh.simplify_quadric_decimation( target_number_of_triangles=6500)#目标三角形的数量为6500
print(f'Simplified mesh has {
      
      len(mesh_smp1.vertices)} vertices and {
      
      len(mesh_smp1.triangles)} triangles')
#o3d.visualization.draw_geometries([mesh_smp])
 
mesh_smp2 = mesh.simplify_quadric_decimation(target_number_of_triangles=1700)
print(f'Simplified mesh has {
      
      len(mesh_smp2.vertices)} vertices and {
      
      len(mesh_smp2.triangles)} triangles')
mesh_smp1.translate([3000,0,0])
mesh_smp2.translate([6000,0,0])
o3d.visualization.draw_geometries([mesh,mesh_smp1,mesh_smp2],window_name='Open3D downSample', width=1000, height=600, left=50,
                                  top=50, point_show_normal=True, mesh_show_wireframe=True, mesh_show_back_face=True,)

代码运行效果如下所示:

在这里插入图片描述

5.网格细分

网格细分就是把每个三角形划分为更小的三角形,可以通过subdivide_midpoint函数实现。该函数采用中点算法,将每个三角形划分为四个覆盖相同表面的三角形。函数的参数包括输入三角网格和迭代次数(number_of_iterations参数定义了重复细分多少次)。细分后3D曲面和面积保持不变但是顶点和三角形的数量增加了。默认情况下,每次迭代都会将每个三角形划分为四个三角形。函数的返回值是一个新的三角网格对象。
网格有 8 个顶点和 12 个三角形在这里插入图片描述
细分后,它有 26 个顶点和 48 个三角形
在这里插入图片描述

具体使用代码如下:

import open3d as o3d
mesh=o3d.io.read_triangle_mesh("hole_smp.ply")
mesh1 = mesh.subdivide_midpoint( number_of_iterations=1) #number_of_iterations=1,重复细分1次。
mesh1.translate([3000,0,0])
print(f'After subdivision it has {
      
      len(mesh.vertices)} vertices and {
      
      len(mesh.triangles)} triangles')
o3d.visualization.draw_geometries([mesh,mesh1], window_name='Open3D downSample', width=1000, height=600, left=50,
                                  top=50, point_show_normal=True, mesh_show_wireframe=True, mesh_show_back_face=True,)


代码运行效果如下:

在这里插入图片描述
以上内容参考自:https://blog.csdn.net/u014072827/article/details/112399050

猜你喜欢

转载自blog.csdn.net/m0_74259636/article/details/134451498