0%

Open3d学习指南

自动摘要: 安装 pipinstallopen3d 几何学 本教程演示了点云的基本用法。 可视化点云 本教程的第一部分读取点云并将其可视化。 ``` importop ……..

安装

1
pip install open3d

几何学

本教程演示了点云的基本用法。

可视化点云

本教程的第一部分读取点云并将其可视化。

1
2
3
4
5
6
7
8
9
10
import open3d as o3d
import numpy as np

print('加载一个ply点云,打印它,渲染它')
ply_point_cloud = o3d.data.PLYPointCloud()
pcd = o3d.io.read_point_cloud(ply_point_cloud.path)
print(pcd)
print(np.asarray(pcd.points))
o3d.visualization.draw_geometries([pcd],zoom=0.3412,front=[0.4257,-0.2125,-0.8795],
lookat=[2.6172,2.0475,1.532],up=[-0.0694,-0.9768,0.2024])

输出结果:

1
2
3
4
5
6
7
8
9
10
11
加载一个ply点云,打印它,渲染它
[Open3D INFO] Downloading https://github.com/isl-org/open3d_downloads/releases/download/20220201-data/fragment.ply
[Open3D INFO] Downloaded to C:\Users\sy/open3d_data/download/PLYPointCloud/fragment.ply
PointCloud with 196133 points.
[[0.65234375 0.84686458 2.37890625]
[0.65234375 0.83984375 2.38430572]
[0.66737998 0.83984375 2.37890625]
...
[2.00839925 2.39453125 1.88671875]
[2.00390625 2.39488506 1.88671875]
[2.00390625 2.39453125 1.88793314]]


它看起来像一个密集的表面,但它实际上是一个渲染为表面的点云。GUI 支持各种键盘功能。例如,’-‘ 键减小了点(表面)的大小。

open3d.ioread_point_cloud(filename,format=auto,remove_nan_points=False,remove_infinite_points=False,print_progress=False)函数功能:read_point_cloud从文件中读取点云。它尝试根据扩展名解码文件。有关支持的文件类型列表,请参阅文件 IO。参数:

  • filename(str):文件路径;
  • format(str,optional,default=auto):输入文件的格式。如果未指定或设置为auto,则从文件扩展名推断格式;
  • remove_nan_points(bool,optional,default=False):如果为真,所有包含NaN的点都会从PointCloud中移除;
  • remove_infinite_points(bool,optional,default=False):如果为真,则从PointCloud中删除所有包含无限值的点。
  • print_progress(bool,optional,default=False):如果设置为True,则会在控制台中显示进度条。

返回值:open3d.geometry.PointCloud

draw_geometries(geometry_list,window_name=’Open3D’,width=1920,height=1080,left=50,top=50,point_show_normal=False,mesh_show_wireframe=False,mesh_show_back_face=False,lookat,up,front,zoom)函数功能:draw_geometries可视化点云。使用鼠标/触控板从不同的视点查看几何图形。参数:

  • geometry_list(List[open3d.geometry.Geometry]):要可视化的几何图形列表;
  • window_name(str,optional,default=’Open3D’):可视化窗口的显示主题;
  • width(int,optional,default=1920):可视化窗口的宽度;
  • height(int,optional,default=1080):可视化窗口的高度;
  • left(int,optional,defalult=50):可视化窗口的左边距;
  • top(int,optional,default=50):可视化窗口的上边距;
  • point_show_normal(bool,optional,default=False):如果设置为True,则可视化点法线;
  • mesh_show_wireframe(bool,optional,default=False):如果设置为True,则可视化网格线框;
  • mesh_show_back_face(bool,optional,default=False):也可视化网格三角形的背面;
  • lookat(numpy.ndarray[numpy.float64[3,1]]):相机的观察向量;
  • up(numpy.ndarray[numpy.float64[3,1]]):相机的向上向量;
  • front(numpy.ndarray[numpy.float64[3,1]]):相机的前向量。
  • zoom(float):相机的变焦;

体素缩减采样

体素缩减采样使用规则的体素网格从输入点云创建均匀的缩减采样点云。它通常用作许多点云处理任务的预处理步骤。该算法分两步运行:

  • 点被桶入体素中;
  • 每个被占用的体素通过平均内部的所有点来生成一个点,即一个体素只表示一个点。
    1
    2
    3
    4
    5
    6
    7
    8
    import open3d as o3d

    print('以0.05的体素对点云进行下采样')
    ply_point_cloud = o3d.data.PLYPointCloud()
    pcd = o3d.io.read_point_cloud(ply_point_cloud.path)
    downpcd = pcd.voxel_down_sample(voxel_size=0.05)
    o3d.visualization.draw_geometries([downpcd],zoom=0.3412,front=[0.4257,-0.2125,-0.8795],
    lookat=[2.6172,2.0475,1.532],up=[-0.0694,-0.9768,0.2024])
    输出结果:
    1
    以0.05的体素对点云进行下采样

顶点法向估计

点云的另一个基本操作是点正态估计。按下N可查看点法线。该键 “-,+” 可用于控制法向长度。

1
2
3
4
5
6
7
8
9
10
11
12
import open3d as o3d

print('重新计算下采样点云的法线')
ply_point_cloud = o3d.data.PLYPointCloud()
pcd = o3d.io.read_point_cloud(ply_point_cloud.path)
downpcd = pcd.voxel_down_sample(voxel_size=0.05)
downpcd.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.1,max_nn=30)
)
o3d.visualization.draw_geometries([downpcd],zoom=0.3412,front=[0.4257,-0.2125,-0.8795],
lookat=[2.6172,2.0475,1.532],up=[-0.0694,-0.9768,0.2024],
point_show_normal=True)

输出结果:

1
重新计算下采样点云的法线

Open3d.geometry.PointCloud:estimate_normals(self,search_param=KDTreeSearchParamKNN with knn=30,fast_normal_computon=True)函数功能:estimate_normals计算每个点法向的函数,该函数查找相邻点并使用协方差分析计算相邻点的主轴。参数:

  • search_param(open3d.geometry.KDTreeSearchParam,option,default=KDTreeSearchParamKNN with knn=30):用于邻域搜索的KDTree搜索参数。
  • fast_normal_computation(bool,optional,default=True):如果为真,则正态估计使用非迭代方法从协方差矩阵中提取特征向量。这更快,但数值不稳定。

KDTreeSearchParamHybrid该函数采用类的实例作为参数。radius = 0.1和max_nn = 30这两个关键参数指定搜索半径和最大最近邻。它具有10cm的搜索半径,最多只能考虑30个邻居,以节省计算时间。 功能:KDTreeSearchParamHybrid混合KNN和半径搜索的KDTree搜索参数。

注意:协方差分析算法生成两个相反的方向作为正态候选项。在不知道几何图形的全局结构的情况下,两者都可能是正确的。这称为正常方向问题。Open3D 尝试将法线定向为与原始法线对齐(如果存在)。否则,Open3D 会进行随机猜测。进一步的方向函数,例如,如果方向是一个问题,则需要调用orient_normals_to_align_with_direction和orient_normals_towards_camera_location。

估计顶点法向

估计的法向量可以从 downpcd 的法向量中检索出来。

1
2
3
4
5
6
7
import open3d as o3d

print('打印第0个点的法向量')
ply_point_cloud = o3d.data.PLYPointCloud()
pcd = o3d.io.read_point_cloud(ply_point_cloud.path)
downpcd = pcd.voxel_down_sample(voxel_size=0.05)
print(downpcd.normals[0])

输出结果:

1
2
打印第0个点的法向量
[ 0.89445022 0.02628609 -0.15662958]

要查看其他变量,请使用help(downpcd)。可以使用np.asarray将法向量变换为 numpy 数组。

1
2
3
4
5
6
7
8
import open3d as o3d
import numpy as np

print('打印前10点的法向量')
ply_point_cloud = o3d.data.PLYPointCloud()
pcd = o3d.io.read_point_cloud(ply_point_cloud.path)
downpcd = pcd.voxel_down_sample(voxel_size=0.05)
print(np.asarray(downpcd.normals)[:10,:])

输出结果:

1
2
3
4
5
6
7
8
9
10
11
打印前10点的法向量
[[ 8.94450224e-01 2.62860943e-02 -1.56629584e-01]
[-3.13758548e-01 3.55896414e-02 -9.44423441e-01]
[-1.47776814e-01 6.17785521e-02 -9.19603061e-01]
[-5.09037352e-01 -2.12256503e-01 -7.93828250e-01]
[-2.65132504e-01 1.45827847e-02 -9.55608159e-01]
[-2.99585901e-02 -9.84751935e-01 -6.62941613e-03]
[ 8.94761360e-01 -6.20907552e-02 3.21312763e-01]
[ 7.64649808e-01 6.31591329e-02 5.80496031e-01]
[-3.03514555e-03 -9.98822002e-01 -2.93296441e-02]
[-2.83795584e-01 1.87381510e-05 -9.48635019e-01]]

裁剪点云

1
2
3
4
5
6
7
8
9
import open3d as o3d

print('加载一个多边形体积并使用它裁剪原始点云')
demo_crop_data = o3d.data.DemoCropPointCloud()
pcd = o3d.io.read_point_cloud(demo_crop_data.point_cloud_path)
vol = o3d.visualization.read_selection_polygon_volume(demo_crop_data.cropped_json_path)
chair = vol.crop_point_cloud(pcd)
o3d.visualization.draw_geometries([chair],zoom=0.7,front=[0.5439,-0.2333,-0.8060],
lookat=[2.4615,2.1331,1.338],up=[-0.1781,-0.9708,0.1608])

输出结果:

1
加载一个多边形体积并使用它裁剪原始点云

class:open3d.data.DemoCropPointCloud介绍:DemoCropPointCloud的数据类包含点云和cropped.json(保存的选定多边形体积文件)。该数据集在Open3D中用于点云裁剪演示。init(self:open3d.cpu.pybind.data.DemoCropPointCloud,data_root:str=“”)->None属性:

  • cropped_json_path:保存的选定多边形体积文件的路径;
  • data_root:获取数据根目录。数据根在构建时设置或自动确定;
  • download_dir:获取下载目录的绝对路径。即${data_root}/${download_prefix}/${prefix};
  • extract_dir:获取提取目录的绝对路径。即${data_root}/${extract_prefix}/${prefix};
  • point_cloud_path:示例点云的路径;
  • prefix:获取数据集的前缀;
  • read_selection_polygon_volume读取指定多边形选择区域的 json 文件,vol.crop_point_cloud(pcd)裁剪点云的函数, 过滤掉点,只剩下椅子了。

绘制点云

1
2
3
4
5
6
7
8
9
10
import open3d as o3d

print('给椅子上色')
demo_crop_data = o3d.data.DemoCropPointCloud()
pcd = o3d.io.read_point_cloud(demo_crop_data.point_cloud_path)
vol = o3d.visualization.read_selection_polygon_volume(demo_crop_data.cropped_json_path)
chair = vol.crop_point_cloud(pcd)
chair.paint_uniform_color([1,0.706,0])
o3d.visualization.draw_geometries([chair],zoom=0.7,front=[0.5439,-0.2333,-0.8060],
lookat=[2.4615,2.1331,1.338],up=[-0.1781,-0.9708,0.1608])

输出结果:

1
给椅子上色


paint_uniform_color将所有点绘制为统一的颜色,颜色位于 RGB 空间 [0, 1] 范围内。

点云距离

Open3D 提供了compute_point_cloud_distance方法来计算从源点云到目标点云的距离,即它计算源点云中每个点到目标点云中最近点的距离。

open3d.geometry.PointCloudcompute_point_cloud_distance(self,target)函数功能:对于源点云中的每个点,计算到目标点云的距离。参数:

  • target(open3d.geometry.PointCloud):目标点云

返回值:open3d.utility.DoubleVector

open3d.geometry.PointCloudselect_by_index(self,indices,invert=False)函数功能:从输入点云中选择点到输出点云中的功能参数:

  • indices(List[int]):要选择的点的索引;
  • invert(bool,optional,default=False):设置为True以反转索引的选择。

返回值:open3d.geometry.PointCloud

在下面的示例中,我们使用该函数来计算两个点云之间的差异。请注意,此方法也可用于计算两个点云之间的倒角距离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import open3d as o3d
import numpy as np

demo_crop_data = o3d.data.DemoCropPointCloud()
pcd = o3d.io.read_point_cloud(demo_crop_data.point_cloud_path)
vol = o3d.visualization.read_selection_polygon_volume(demo_crop_data.cropped_json_path)
chair = vol.crop_point_cloud(pcd)

dists = pcd.compute_point_cloud_distance(chair)
dists = np.asarray(dists)
ind = np.where(dists > 0.01)[0]
pcd_without_chair = pcd.select_by_index(ind)
o3d.visualization.draw_geometries([pcd_without_chair],zoom=0.7,front=[0.5439,-0.2333,-0.8060],
lookat=[2.4615,2.1331,1.338],up=[-0.1781,-0.9708,0.1608])

输出结果:

包围框

点云几何类型与 Open3D 中的所有其他几何类型一样具有包围框。目前,Open3D 实现了一个PointCloudAxisAlignedBoundingBox和一个OrientedBoundingBox,它也可以用来裁剪几何体。

1
2
3
4
5
6
7
8
9
10
11
12
import open3d as o3d

demo_crop_data = o3d.data.DemoCropPointCloud()
pcd = o3d.io.read_point_cloud(demo_crop_data.point_cloud_path)
vol = o3d.visualization.read_selection_polygon_volume(demo_crop_data.cropped_json_path)
chair = vol.crop_point_cloud(pcd)
aabb = chair.get_axis_aligned_bounding_box()
aabb.color = (1,0,0) #红色线,轴线与世界坐标重合
obb = chair.get_oriented_bounding_box()
obb.color = (0,1,0 #绿色线,有方向的
o3d.visualization.draw_geometries([chair,aabb,obb],zoom=0.7,front=[0.5439,-0.2333,-0.8060],
lookat=[2.4615,2.1331,1.338],up=[-0.1781,-0.9708,0.1608])

输出结果:

open3d.geometry.PointCloudget_axis_aligned_bounding_box(self)函数功能:返回几何的轴对齐边界框。参数:无返回值:open3d.geometry.AxisAlignedBoundingBox

open3d.geometry.PointCloudget_oriented_bounding_box(self:open3d.geometry.Geometry3D,robust:bool=False)->open3d::geometry::OrientedBoundingBox函数功能:返回几何体的定向边界框;基于凸包的PCA计算定向边界框。返回的边界框是最小边界框的近似值。参数:

  • robust(bool):如果设置为True,则使用更健壮的方法,该方法适用于退化情况,但会在点坐标中引入噪声。

返回值:定向边界框。边界框的方向使得轴相对于主成分是有序的 open3d.geometry.OrientedBoundingBox。

凸包

点云的凸壳是包含所有点的最小凸集。Open3D 包含计算点云的凸壳的方法compute_convex_hull。实现基于Qhull。在下面的示例代码中,我们首先从网格中对点云进行采样,并计算以三角形网格形式返回的凸壳。然后,我们将凸起的船体可视化为红色LineSet。

1
2
3
4
5
6
7
8
9
10
11
import open3d as o3d

bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
mesh.compute_vertex_normals()

pcl = mesh.sample_points_poisson_disk(number_of_points=2000) #泊松距离
hull,_ = pcl.compute_convex_hull()
hull_ls = o3d.geometry.LineSet.create_from_triangle_mesh(hull)
hull_ls.paint_uniform_color((1,0,0))
o3d.visualization.draw_geometries([pcl,hull_ls])

输出结果:

open3d.ioread_triangle_mesh(filename,enable_post_processing=False,print_progress=False)函数功能:从文件中读取三角形网格参数:

  • filename(str):文件路径;
  • enable_post_processing(bool,optional,default=False):
  • print_progress(bool,optional,default=False):如果设置为True,则会在控制台中显示进度条。

返回值:open3d.geometry.TriangleMesh

open3d.geometry.TriangleMeshcompute_vertex_normals(self,normalized=True)函数功能:计算顶点法线的函数,通常在渲染之前调用参数:normalized(bool,optional,default=True)返回值:open3d.geometry.TriangleMesh

DBSCAN聚类

给定来自例如深度传感器的点云,我们希望将本地点云集群组合在一起。为此,我们可以使用聚类分析算法。Open3D 实现了 DBSCAN [Ester1996] ,这是一种基于密度的聚类算法。该算法在cluster_dbscan中实现并需要两个参数:eps定义到聚类中邻居的距离,并定义形成聚类所需的最小点数min_points。该函数返回labels,其中标签-1指示噪声。

class open3d.utility.VerbosityContextManager介绍:用于临时更改Open3D详细级别的上下文管理器;初始:init(self:open3d.utility.VerbosityContextManager,level:open3d.utility.VerbosityLevel)->None:创建具有给定VerbosityLevel的VerbosityContextManager;属性:无

class open3d.utility.VerbosityLevel介绍:详细级别的枚举类;方法:Debug=VerbosityLevel.Debug:3 Error=< VerbosityLevel.Error:0> Info=< VerbosityLevel.Info:2> Warning=VerbosityLevel.Warning:1属性:value

open3d.geometry.PointCloudcluster_dbscan(self,eps,min_points,print_progress=False)函数功能:根据算法返回点标签列表,-1表示噪声。参数:

  • eps(float):用于查找相邻点的密度函数;
  • min_points(int):形成聚类的最小点数;
  • print_progress(bool,optional,default=False):如果为True,则进度会在控制台中可视化。

class open3d.utility.Vector3dVector介绍:将形状(n,3)的float64 numpy数组转换为Open3D格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import open3d as o3d
import numpy as np
import matplotlib.pyplot as plt


ply_point_cloud = o3d.data.PLYPointCloud()
pcd = o3d.io.read_point_cloud(ply_point_cloud.path)

with o3d.utility.VerbosityContextManager(o3d.utility.VerbosityLevel.Debug) as cm:
labels = np.array(pcd.cluster_dbscan(eps=0.02,min_points=10,print_progress=True))

max_label = labels.max()
print(f'点云有{max_label + 1}类')
colors = plt.get_cmap('tab20')(labels / (max_label if max_label > 0 else 1))
colors[labels < 0] = 0
pcd.colors = o3d.utility.Vector3dVector(colors[:,:3])
o3d.visualization.draw_geometries([pcd],zoom=0.455,front=[-0.4999,-0.1659,-0.8499],
lookat=[2.1813,2.0619,2.0999],up=[0.1204,-0.9852,0.1215])

输出结果:

1
2
3
4
5
6
[Open3D DEBUG] Precompute neighbors.
[Open3D DEBUG] Done Precompute neighbors.
[Open3D DEBUG] Compute Clusters
Precompute neighbors.[========================================] 100%
[Open3D DEBUG] Done Compute Clusters: 10
点云有10类

注意:此算法预先计算所有点的 epsilon 半径内的所有邻居。如果所选的 epsilon 太大,这可能需要大量内存。

RANSAC平面分割

Open3D 还支持使用 RANSAC 分割来自点云的几何基元。要找到在点云中具有最大支撑的平面,我们可以使用segment_plane。该方法有三个参数:distance_threshold定义了点到估计平面的最大距离,这些距离内的点被认为是内点(inlier),ransac_n定义随机采样以估计平面的点数,以及num_iterations定义随机平面采样和验证的频率。然后,该函数返回(a,b,c,d)平面,以便对于平面上的每个点(x,y,z),我们都有ax+by+cz+d=0 。该函数进一步返回内点的索引列表。

open3d.geometry.PointCloudsegment_plane(self,distance_threshold,ransac_n,num_iterations,seed=None)函数功能:使用RANSAC算法在点云中分割平面参数:

  • distance_threshold(float):一个点与平面模型的最大距离,仍然被认为是一个内点;
  • ransac_n(int):每次迭代中被视为内点的初始点数;
  • num_iterations(int):迭代次数;
  • seed(Optional[int],optional,default=None):随机生成器中使用的种子值,设置为None以在每个函数调用中使用随机种子值。

返回值:Tuple[numpy.ndarray[numpy.float64[4,1]],List[int]]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import open3d as o3d

pcd_point_cloud = o3d.data.PCDPointCloud()
pcd = o3d.io.read_point_cloud(pcd_point_cloud.path)

plane_model,inliers = pcd.segment_plane(distance_threshold=0.01,ransac_n=3,num_iterations=1000)

[a,b,c,d] = plane_model
print(f'平面方程:{a:.2f}x + {b:.2f}y + {c:.2f}z + {d:.2f} = 0')

inlier_cloud = pcd.select_by_index(inliers)
inlier_cloud.paint_uniform_color([1.0,0,0])
outlier_cloud = pcd.select_by_index(inliers,invert=True)
o3d.visualization.draw_geometries([inlier_cloud,outlier_cloud],zoom=0.8,front=[-0.4999,-0.1659,-0.8499],
lookat=[2.1813,2.0619,2.0999],up=[0.1204,-0.9852,0.1215])

输出结果:

隐藏点删除

假设您希望从给定视点渲染点云,但背景中的点会泄漏到前景中,因为它们不会被其他点遮挡。为此,我们可以应用隐藏点删除算法。在Open3D中,[Katz2007]的方法实现了从给定视图近似点云的可见性,而无需表面重建或法线估计。

class open3d.data.ArmadilloMesh介绍:ArmadilloMesh 的数据类包含来自 Stanford 3D Scanning Repository 的 ArmadilloMesh.ply方法:init(self:open3d.data.ArmadilloMesh,data_root:str=“”)->None属性:

  • data_root:获取数据根目录。数据根在构建时设置或自动确定;
  • download_dir:获取下载路径的绝对路径;
  • extract_dir:获取解压目录的绝对路径;
  • path:ArmadilloMesh.ply文件的路径;
  • prefix:获取数据集的前缀。
1
2
3
4
5
6
7
8
9
10
11
12
import open3d as o3d
import numpy as np

print('将网格转换为点云并估计尺寸')
armadillo = o3d.data.ArmadilloMesh()
mesh = o3d.io.read_triangle_mesh(armadillo.path)
mesh.compute_vertex_normals()

pcd = mesh.sample_points_poisson_disk(5000)
diameter = np.linalg.norm(
np.asarray(pcd.get_max_bound()) - np.asarray(pcd.get_min_bound()))
o3d.visualization.draw_geometries([pcd])

输出结果:

1
2
3
4
5
6
7
8
9
10
print('定义参数用于删除隐藏点')
camera = [0, 0, diameter]
radius = diameter * 100

print('获取从给定视点可见的所有点')
_, pt_map = pcd.hidden_point_removal(camera, radius)

print('可视化的结果')
pcd = pcd.select_by_index(pt_map)
o3d.visualization.draw_geometries([pcd])

输出结果:

Mesh

https://blog.csdn.net/u014072827/article/details/112399050Open3D 具有一个称为TriangleMesh的 3D 三角形网格的数据结构。下面的代码显示了如何从ply文件中读取三角形网格并打印其顶点和三角形。

1
2
3
4
5
6
7
8
9
10
11
12
13
import open3d as o3d

print('测试网格在open3D')
armadillo_mesh = o3d.data.ArmadilloMesh()
mesh = o3d.io.read_triangle_mesh(armadillo_mesh.path)

knot_mesh = o3d.data.KnotMesh()
mesh = o3d.io.read_triangle_mesh(knot_mesh.path)
print(mesh)
print('顶点:')
print(np.asarray(mesh.vertices))
print('三角:')
print(np.asarray(mesh.triangles))

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
测试网格在open3D
[Open3D INFO] Downloading https://github.com/isl-org/open3d_downloads/releases/download/20220201-data/KnotMesh.ply
[Open3D INFO] Downloaded to C:\Users\sy/open3d_data/download/KnotMesh/KnotMesh.ply
TriangleMesh with 1440 points and 2880 triangles.
顶点:
[[ 4.51268387 28.68865967 -76.55680847]
[ 7.63622284 35.52046967 -69.78063965]
[ 6.21986008 44.22465134 -64.82303619]
...
[-22.12651634 31.28466606 -87.37570953]
[-13.91188431 25.4865818 -86.25827026]
[ -5.27768707 23.36245346 -81.43279266]]
三角:
[[ 0 12 13]
[ 0 13 1]
[ 1 13 14]
...
[1438 11 1439]
[1439 11 0]
[1439 0 1428]]

进程已结束,退出代码0

TriangleMesh类具有一些数据字段,例如vertices和triangles。Open3D 通过numpy提供直接内存访问。

可视化3D网络

1
2
3
4
5
6
7
8
9
10
11
12
13
import open3d as o3d

armadillo_mesh = o3d.data.ArmadilloMesh()
mesh = o3d.io.read_triangle_mesh(armadillo_mesh.path)

knot_mesh = o3d.data.KnotMesh()
mesh = o3d.io.read_triangle_mesh(knot_mesh.path)

print('尝试使用网格法线(存在:' + str(mesh.has_vertex_normals()) + ') 和颜色(存在:' +
str(mesh.has_vertex_colors()) + ')去渲染网格')

o3d.visualization.draw_geometries([mesh])
print('网格没有法线和颜色不美观')

输出结果:

1
2
尝试使用网格法线(存在:False) 和颜色(存在:False)去渲染网格
网格没有法线和颜色不美观

可以旋转和移动这个网格,但是由于它是纯灰色的所以看起来不是并不像一个3D图形。这是因为当前网格没有顶点或面的法线。因此,使用统一颜色着色而不是更复杂的Phong着色。

表面法线估计

使用曲面法线绘制网格。

1
2
3
4
5
6
7
8
9
10
11
import open3d as o3d

armadillo_mesh = o3d.data.ArmadilloMesh()
mesh = o3d.io.read_triangle_mesh(armadillo_mesh.path)

knot_mesh = o3d.data.KnotMesh()
mesh = o3d.io.read_triangle_mesh(knot_mesh.path)
print('计算法线和渲染')
mesh.compute_vertex_normals()
print(np.asarray(mesh.triangle_normals))
o3d.visualization.draw_geometries([mesh])

输出结果:

1
2
3
4
5
6
7
8
9
10
计算法线和渲染
[[ 0.79164373 -0.53951444 0.28674793]
[ 0.8319824 -0.53303008 0.15389681]
[ 0.83488162 -0.09250101 0.54260136]
...
[ 0.16269924 -0.76215917 -0.6266118 ]
[ 0.52755226 -0.83707495 -0.14489352]
[ 0.56778973 -0.76467734 -0.30476777]]

进程已结束,退出代码0

它使用计算compute_vertex_normals和paint_uniform_color作为mesh的成员函数。

裁剪网格

通过numpy直接操作网格的triangle和triangle_normals数据域去除一半的曲面。

1
2
3
4
5
6
7
8
9
10
11
12
import open3d as o3d
import copy
import numpy as np

print('我们只用前半个三角形做一个局部网格')
knot_mesh = o3d.data.KnotMesh()
mesh = o3d.io.read_triangle_mesh(knot_mesh.path)
mesh1 = copy.deepcopy(mesh)
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])

输出结果:

渲染网格

paint_uniform_color给网格涂上统一的颜色。颜色位于 RGB 空间 [0, 1] 范围内。

1
2
3
4
5
6
7
8
9
10
import open3d as o3d
import copy

knot_mesh = o3d.data.KnotMesh()
mesh = o3d.io.read_triangle_mesh(knot_mesh.path)
mesh1 = copy.deepcopy(mesh)

print('渲染网格')
mesh1.paint_uniform_color([1,0.706,0])
o3d.visualization.draw_geometries([mesh1])

输出结果:

1
渲染网格

网格属性

三角形网格具有多个可以使用 Open3D 进行测试的属性。一个重要的属性是流形属性,可使用is_edge_manifold去测试网格是否为边缘流形(edge manifold),使用is_vertex_manifold去测试所有顶点是否为流形。如果每个边缘包括一个或两个三角形,则这个三角网格是边缘流形。函数 is _ edge _ Manifold 具有 bool 参数 allow _ boundary _ edge,该参数定义是否允许边界边缘。此外,如果顶点的星形边是边缘流形和边缘连接的话,则三角形网格是顶点流形。比如两个或者更多的面可能只有一个顶点连接而不是通过边。

另一个属性是自相交测试。如果在一个网格中存在与另一个网格相交的三角形,is_self_intersecting这个函数就会返回true。一个水密网格能够被定义成一个边缘流形,顶点流形和不自交的网格。在Open3D中通过is_watertight接口实现这种检测。

可以测试一个网格是否为可定向的,即三角形可以以所有法线指向外部的方式定向。这个通过is_orientable实现。

下面的示例代码测试了这些属性并且可视化。非流形边缘用红色表示,边界边缘用绿色标识,非流形顶点用绿色点,自交的三角形用粉色显示。其中需要open3d_example.py文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# ----------------------------------------------------------------------------
# - Open3D: www.open3d.org -
# ----------------------------------------------------------------------------
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 www.open3d.org
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# ----------------------------------------------------------------------------

import open3d as o3d
import numpy as np
import os
import shutil
import sys
import zipfile
from os import listdir, makedirs
from os.path import exists, isfile, join, splitext, dirname, basename
import re
from warnings import warn
import json
import open3d as o3d
import copy

if (sys.version_info > (3, 0)):
pyver = 3
from urllib.request import Request, urlopen
else:
pyver = 2
from urllib2 import Request, urlopen


def edges_to_lineset(mesh, edges, color):
ls = o3d.geometry.LineSet()
ls.points = mesh.vertices
ls.lines = edges
ls.paint_uniform_color(color)
return ls


def get_plane_mesh(height=0.2, width=1):
mesh = o3d.geometry.TriangleMesh(
vertices=o3d.utility.Vector3dVector(
np.array(
[[0, 0, 0], [0, height, 0], [width, height, 0], [width, 0, 0]],
dtype=np.float32,
)),
triangles=o3d.utility.Vector3iVector(np.array([[0, 2, 1], [2, 0, 3]])),
)
mesh.compute_vertex_normals()
return mesh


def get_non_manifold_edge_mesh():
verts = np.array(
[[-1, 0, 0], [0, 1, 0], [1, 0, 0], [0, -1, 0], [0, 0, 1]],
dtype=np.float64,
)
triangles = np.array([[0, 1, 3], [1, 2, 3], [1, 3, 4]])
mesh = o3d.geometry.TriangleMesh()
mesh.vertices = o3d.utility.Vector3dVector(verts)
mesh.triangles = o3d.utility.Vector3iVector(triangles)
mesh.compute_vertex_normals()
mesh.rotate(
mesh.get_rotation_matrix_from_xyz((np.pi / 4, 0, np.pi / 4)),
center=mesh.get_center(),
)
return mesh


def get_non_manifold_vertex_mesh():
verts = np.array(
[
[-1, 0, -1],
[1, 0, -1],
[0, 1, -1],
[0, 0, 0],
[-1, 0, 1],
[1, 0, 1],
[0, 1, 1],
],
dtype=np.float64,
)
triangles = np.array([
[0, 1, 2],
[0, 1, 3],
[1, 2, 3],
[2, 0, 3],
[4, 5, 6],
[4, 5, 3],
[5, 6, 3],
[4, 6, 3],
])
mesh = o3d.geometry.TriangleMesh()
mesh.vertices = o3d.utility.Vector3dVector(verts)
mesh.triangles = o3d.utility.Vector3iVector(triangles)
mesh.compute_vertex_normals()
mesh.rotate(
mesh.get_rotation_matrix_from_xyz((np.pi / 4, 0, np.pi / 4)),
center=mesh.get_center(),
)
return mesh


def get_open_box_mesh():
mesh = o3d.geometry.TriangleMesh.create_box()
mesh.triangles = o3d.utility.Vector3iVector(np.asarray(mesh.triangles)[:-2])
mesh.compute_vertex_normals()
mesh.rotate(
mesh.get_rotation_matrix_from_xyz((0.8 * np.pi, 0, 0.66 * np.pi)),
center=mesh.get_center(),
)
return mesh


def get_intersecting_boxes_mesh():
mesh0 = o3d.geometry.TriangleMesh.create_box()
T = np.eye(4)
T[:, 3] += (0.5, 0.5, 0.5, 0)
mesh1 = o3d.geometry.TriangleMesh.create_box()
mesh1.transform(T)
mesh = mesh0 + mesh1
mesh.compute_vertex_normals()
mesh.rotate(
mesh.get_rotation_matrix_from_xyz((0.7 * np.pi, 0, 0.6 * np.pi)),
center=mesh.get_center(),
)
return mesh


def file_downloader(url, out_dir="."):
file_name = url.split('/')[-1]
u = urlopen(url)
f = open(os.path.join(out_dir, file_name), "wb")
if pyver == 2:
meta = u.info()
file_size = int(meta.getheaders("Content-Length")[0])
elif pyver == 3:
file_size = int(u.getheader("Content-Length"))
print("Downloading: %s " % file_name)

file_size_dl = 0
block_sz = 8192
progress = 0
while True:
buffer = u.read(block_sz)
if not buffer:
break
file_size_dl += len(buffer)
f.write(buffer)
if progress + 10 <= (file_size_dl * 100. / file_size):
progress = progress + 10
print(" %.1f / %.1f MB (%.0f %%)" % \
(file_size_dl/(1024*1024), file_size/(1024*1024), progress))
f.close()


def unzip_data(path_zip, path_extract_to):
print("Unzipping %s" % path_zip)
zip_ref = zipfile.ZipFile(path_zip, 'r')
zip_ref.extractall(path_extract_to)
zip_ref.close()
print("Extracted to %s" % path_extract_to)


def sorted_alphanum(file_list_ordered):
convert = lambda text: int(text) if text.isdigit() else text
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
return sorted(file_list_ordered, key=alphanum_key)


def get_file_list(path, extension=None):
if extension is None:
file_list = [path + f for f in listdir(path) if isfile(join(path, f))]
else:
file_list = [
path + f
for f in listdir(path)
if isfile(join(path, f)) and splitext(f)[1] == extension
]
file_list = sorted_alphanum(file_list)
return file_list


def add_if_exists(path_dataset, folder_names):
for folder_name in folder_names:
if exists(join(path_dataset, folder_name)):
path = join(path_dataset, folder_name)
return path
raise FileNotFoundError(
f"None of the folders {folder_names} found in {path_dataset}")


def read_rgbd_image(color_file, depth_file, convert_rgb_to_intensity, config):
color = o3d.io.read_image(color_file)
depth = o3d.io.read_image(depth_file)
rgbd_image = o3d.geometry.RGBDImage.create_from_color_and_depth(
color,
depth,
depth_scale=config["depth_scale"],
depth_trunc=config["depth_max"],
convert_rgb_to_intensity=convert_rgb_to_intensity)
return rgbd_image


def get_rgbd_folders(path_dataset):
path_color = add_if_exists(path_dataset, ["image/", "rgb/", "color/"])
path_depth = join(path_dataset, "depth/")
return path_color, path_depth


def get_rgbd_file_lists(path_dataset):
path_color, path_depth = get_rgbd_folders(path_dataset)
color_files = get_file_list(path_color, ".jpg") + \
get_file_list(path_color, ".png")
depth_files = get_file_list(path_depth, ".png")
return color_files, depth_files


def make_clean_folder(path_folder):
if not exists(path_folder):
makedirs(path_folder)
else:
shutil.rmtree(path_folder)
makedirs(path_folder)


def check_folder_structure(path_dataset):
if isfile(path_dataset) and path_dataset.endswith(".bag"):
return
path_color, path_depth = get_rgbd_folders(path_dataset)
assert exists(path_depth), \
"Path %s is not exist!" % path_depth
assert exists(path_color), \
"Path %s is not exist!" % path_color


def write_poses_to_log(filename, poses):
with open(filename, 'w') as f:
for i, pose in enumerate(poses):
f.write('{} {} {}\n'.format(i, i, i + 1))
f.write('{0:.8f} {1:.8f} {2:.8f} {3:.8f}\n'.format(
pose[0, 0], pose[0, 1], pose[0, 2], pose[0, 3]))
f.write('{0:.8f} {1:.8f} {2:.8f} {3:.8f}\n'.format(
pose[1, 0], pose[1, 1], pose[1, 2], pose[1, 3]))
f.write('{0:.8f} {1:.8f} {2:.8f} {3:.8f}\n'.format(
pose[2, 0], pose[2, 1], pose[2, 2], pose[2, 3]))
f.write('{0:.8f} {1:.8f} {2:.8f} {3:.8f}\n'.format(
pose[3, 0], pose[3, 1], pose[3, 2], pose[3, 3]))


def read_poses_from_log(traj_log):
import numpy as np

trans_arr = []
with open(traj_log) as f:
content = f.readlines()

# Load .log file.
for i in range(0, len(content), 5):
# format %d (src) %d (tgt) %f (fitness)
data = list(map(float, content[i].strip().split(' ')))
ids = (int(data[0]), int(data[1]))
fitness = data[2]

# format %f x 16
T_gt = np.array(
list(map(float, (''.join(
content[i + 1:i + 5])).strip().split()))).reshape((4, 4))

trans_arr.append(T_gt)

return trans_arr


flip_transform = [[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]


def draw_geometries_flip(pcds):
pcds_transform = []
for pcd in pcds:
pcd_temp = copy.deepcopy(pcd)
pcd_temp.transform(flip_transform)
pcds_transform.append(pcd_temp)
o3d.visualization.draw_geometries(pcds_transform)


def draw_registration_result(source, target, transformation):
source_temp = copy.deepcopy(source)
target_temp = copy.deepcopy(target)
source_temp.paint_uniform_color([1, 0.706, 0])
target_temp.paint_uniform_color([0, 0.651, 0.929])
source_temp.transform(transformation)
source_temp.transform(flip_transform)
target_temp.transform(flip_transform)
o3d.visualization.draw_geometries([source_temp, target_temp])


def draw_registration_result_original_color(source, target, transformation):
source_temp = copy.deepcopy(source)
target_temp = copy.deepcopy(target)
source_temp.transform(transformation)
source_temp.transform(flip_transform)
target_temp.transform(flip_transform)
o3d.visualization.draw_geometries([source_temp, target_temp])


class CameraPose:

def __init__(self, meta, mat):
self.metadata = meta
self.pose = mat

def __str__(self):
return 'Metadata : ' + ' '.join(map(str, self.metadata)) + '\n' + \
"Pose : " + "\n" + np.array_str(self.pose)


def read_trajectory(filename):
traj = []
with open(filename, 'r') as f:
metastr = f.readline()
while metastr:
metadata = list(map(int, metastr.split()))
mat = np.zeros(shape=(4, 4))
for i in range(4):
matstr = f.readline()
mat[i, :] = np.fromstring(matstr, dtype=float, sep=' \t')
traj.append(CameraPose(metadata, mat))
metastr = f.readline()
return traj


def write_trajectory(traj, filename):
with open(filename, 'w') as f:
for x in traj:
p = x.pose.tolist()
f.write(' '.join(map(str, x.metadata)) + '\n')
f.write('\n'.join(
' '.join(map('{0:.12f}'.format, p[i])) for i in range(4)))
f.write('\n')


def initialize_opencv():
opencv_installed = True
try:
import cv2
except ImportError:
pass
print("OpenCV is not detected. Using Identity as an initial")
opencv_installed = False
if opencv_installed:
print("OpenCV is detected. Using ORB + 5pt algorithm")
return opencv_installed

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import open3d as o3d
import open3d_example as o3dtut
import numpy as np

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}')
print(f'是否允许边界边缘:{edge_manifold_boundary}')
print(f'顶点流: {vertex_manifold}')
print(f'自相交: {self_intersecting}')
print(f'水密: {watertight}')
print(f'可定向的: {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())
pc1 = o3d.geometry.PointCloud(
points = o3d.utility.Vector3dVector(np.asarray(mesh.vertices)[verts]))
pc1.paint_uniform_color((0,0,1))
geoms.append(pc1)
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[:,1],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)

knot_mesh_data = o3d.data.KnotMesh()
knot_mesh = o3d.io.read_triangle_mesh(knot_mesh_data.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())

输出结果:

1
2
3
4
5
6
7
KnotMesh
edge_manifold: True
edge_manifold_boundary: True
vertex_manifold: True
self_intersecting: False
watertight: True
orientable: True

1
2
3
4
5
6
7
Mobius
edge_manifold: True
edge_manifold_boundary: False
vertex_manifold: True
self_intersecting: False
watertight: False
orientable: False

1
2
3
4
5
6
7
non-manifold edge
edge_manifold: False
edge_manifold_boundary: False
vertex_manifold: True
self_intersecting: False
watertight: False
orientable: True

1
2
3
4
5
6
7
non-manifold vertex
edge_manifold: True
edge_manifold_boundary: True
vertex_manifold: False
self_intersecting: False
watertight: False
orientable: True

1
2
3
4
5
6
7
open box
edge_manifold: True
edge_manifold_boundary: False
vertex_manifold: True
self_intersecting: False
watertight: False
orientable: True

1
2
3
4
5
6
7
8
intersecting_boxes
edge_manifold: True
edge_manifold_boundary: True
vertex_manifold: True
self_intersecting: True
watertight: False
orientable: True
# visualize self-intersecting triangles

网格过滤

Open3d包含许多过滤网格的算法,下面将展示实现的滤波器来平滑噪声三角形网格。

平均过滤器

最简单的过滤器是均值滤波器。给定的顶点由相邻顶点的平均值N给出的。公式如下:

可以使用此过滤器对网格进行降噪,如下面的代码所。filter_smooth_simple函数的参数number_of_iterations用来定义应用于网格的滤波器的频率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
import open3d as o3d
print('添加噪声的网络')
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('使用均值滤波器迭代1次')
mesh_out = mesh_in.filter_smooth_simple(number_of_iterations = 1)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])

print('使用均值滤波器迭代5次')
mesh_out = mesh_in.filter_smooth_simple(number_of_iterations=5)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])

输出结果:

1
添加噪声的网络

1
使用均值滤波器迭代1次

1
使用均值滤波器迭代5次

拉普拉斯算子(Laplacian)

另一个重要的网格过滤器是拉普拉斯算子,其定义如下:
其中λ是滤波器的强度,w_{n}是与相邻顶点的距离相关的归一化权重. 这个滤波器的接口是filter_smooth_laplacian,有两个参数: number_of_iterations 和 lambda。

1
2
3
4
5
6
7
8
9
10
11
12
13
import open3d as o3d

print('使用拉普拉斯滤波器迭代10次')
knot_mesh = o3d.data.KnotMesh()
mesh_in = o3d.io.read_triangle_mesh(knot_mesh.path)
mesh_out = mesh_in.filter_smooth_laplacian(number_of_iterations=10)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])

print('使用拉普拉斯滤波器迭代50次')
mesh_out = mesh_in.filter_smooth_laplacian(number_of_iterations=50)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])

输出结果:

1
使用拉普拉斯滤波器迭代10次

1
使用拉普拉斯滤波器迭代50次

Taubin滤波器

均值滤波和Laplacian滤波有一个问题是他们会使三角网格收缩。[Taubin1995] 展示了使用两种不同λ参数的Laplacian滤波器来防止网格收缩。这个滤波器的实现接口是:filter_smooth_taubin。

1
2
3
4
5
6
7
8
9
10
11
12
13
import open3d as o3d

knot_mesh = o3d.data.KnotMesh()
mesh_in = o3d.io.read_triangle_mesh(knot_mesh.path)
print('使用Taubin滤波迭代10次')
mesh_out = mesh_in.filter_smooth_taubin(number_of_iterations=10)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])

print('使用Taubin滤波迭代100次')
mesh_out = mesh_in.filter_smooth_taubin(number_of_iterations=100)
mesh_out.compute_vertex_normals()
o3d.visualization.draw_geometries([mesh_out])

输出结果:

1
使用Taubin滤波迭代10次

1
使用Taubin滤波迭代100次

采样

Open3d包含了从三角网格中采样点云的功能。最简单的方法是使用sample_points_uniformly函数从三角网格的三维表面均匀采样。参数number_of_points表示从网格中采样的点云的点数。

1
2
3
4
5
6
7
import open3d as o3d

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])

输出结果:

1
2
3
4
5
6
7
8
import open3d as o3d

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函数,之后会进行点云的消除。
    1
    2
    3
    4
    5
    6
    7
    8
    import open3d as o3d
    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])
    输出结果:
1
2
3
4
5
6
7
8
9
10
import open3d as o3d

bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
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参数定义了重复细分多少次。

1
2
3
4
5
6
7
8
9
import open3d as o3d

mesh = o3d.geometry.TriangleMesh.create_box()
mesh.compute_vertex_normals()
print(f'网格有{len(mesh.vertices)}个顶点和{len(mesh.triangles)}个三角形')
o3d.visualization.draw_geometries([mesh],mesh_show_wireframe=True)
mesh = mesh.subdivide_midpoint(number_of_iterations=1)
print(f'再分之后有{len(mesh.vertices)}个顶点和{len(mesh.triangles)}个三角形')
o3d.visualization.draw_geometries([mesh],mesh_show_wireframe=True)

输出结果:

1
网格有8个顶点和12个三角形

1
再分之后有26个顶点和48个三角形

Open3d实现了基于[Loop1987]的附加细分方法。该方法基于四次样条曲线,该样条曲线除了在特殊顶点处生成连续的极限曲面外,其他地方都生成连续的极限曲面。这样可以得到更加平滑的拐角。

1
2
3
4
5
6
7
8
9
import open3d as o3d

mesh = o3d.geometry.TriangleMesh.create_sphere()
mesh.compute_vertex_normals()
print(f'网格有{len(mesh.vertices)}个顶点和{len(mesh.triangles)}个三角形')
o3d.visualization.draw_geometries([mesh],mesh_show_wireframe=True)
mesh = mesh.subdivide_loop(number_of_iterations=2)
print(f'再分之后有{len(mesh.vertices)}个顶点和{len(mesh.triangles)}个三角形')
o3d.visualization.draw_geometries([mesh],mesh_show_wireframe=True)

输出结果:

1
网格有762个顶点和1520个三角形

1
再分之后有12162个顶点和24320个三角形

1
2
3
4
5
6
7
8
9
10
import open3d as o3d

knot_mesh = o3d.data.KnotMesh()
mesh = o3d.io.read_triangle_mesh(knot_mesh.path)
mesh.compute_vertex_normals()
print(f'网格有{len(mesh.vertices)}个顶点和{len(mesh.triangles)}个三角形')
o3d.visualization.draw_geometries([mesh],mesh_show_wireframe=True)
mesh = mesh.subdivide_loop(number_of_iterations=2)
print(f'再分之后有{len(mesh.vertices)}个顶点和{len(mesh.triangles)}个三角形')
o3d.visualization.draw_geometries([mesh],mesh_show_wireframe=True)

输出结果:

1
网格有1440个顶点和2880个三角形

1
再分之后有23040个顶点和46080个三角形

网格简化

有时,我们希望用较少的三角形和顶点表示一个高分辨率网格,但低分辨率网格仍应接近高分辨率网格。为此,Open3D 实现了许多网格简化方法。

顶点聚类

顶点聚类的方法是将所有落入给定大小的体素的顶点聚集到单个顶点。函数接口为simplify_vertex_clustering,参数voxel_size设置体素网格大小。contraction定义如何聚集顶点。

1
输入网格有35947个顶点和69451个三角形

1
2
体素有0.004865593749999999个
简化网格之后有3222个顶点和6454个三角形

1
2
体素有0.009731187499999999个
简化网格之后有845个顶点和1724个三角形

网格抽取

网格细分的另一种方式是逐步执行的网格抽取。我们选择一个使误差度量最小化的三角形并将其删除。重复此过程直到满足指定的三角形数量时停止。Open3D实现了simplify_quadric_decimation接口去最小化误差平方(到相邻平面的距离),参数target_number_of_triangles定义了抽取算法的停止条件。

1
输入网格有35947个顶点和69451个三角形

1
简化网格之后有1978个顶点和1700个三角形

连通分量

各种重建方法的结果。 Open3D实现了一个连接组件算法cluster_connected_triangles,该算法将每个三角形分配给一组连接的三角形。 它为每个三角形返回cluster_index中的簇索引,每个簇返回cluster_n_triangles中三角形的数目以及cluster_area中簇的表面积。下面的代码展示cluster_connected_triangles的应用和如何使用它来删除假(spurious)三角形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import open3d as o3d
import numpy as np

print('生成数据')
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('展示输入网格')
o3d.visualization.draw_geometries([mesh])

输出结果:

1
2
生成数据
展示输入网格

1
2
3
4
5
6
7
8
9
10
11
import open3d as o3d

print('cluster connected triangles')
bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
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)

输出结果:

1
2
3
4
cluster connected triangles
[Open3D DEBUG] [ClusterConnectedTriangles] Compute triangle adjacency
[Open3D DEBUG] [ClusterConnectedTriangles] Done computing triangle adjacency
[Open3D DEBUG] [ClusterConnectedTriangles] Done clustering, #clusters=1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import open3d as o3d
import copy

bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
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('展示删除小簇的网格')
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])

输出结果:

1
展示删除小簇的网格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import open3d as o3d
import copy

bunny = o3d.data.BunnyMesh()
mesh = o3d.io.read_triangle_mesh(bunny.path)
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('展示最大簇')
mesh_1 = copy.deepcopy(mesh)
largest_cluster_idx = cluster_n_triangles.argmax()
triangles_to_remove = triangle_clusters != largest_cluster_idx
mesh_1.remove_triangles_by_mask(triangles_to_remove)
o3d.visualization.draw_geometries([mesh_1])

输出结果:

1
展示最大簇

欢迎关注我的其它发布渠道