如果我们有一种快速的方法将从现实中捕获的点云转换为 3D 网格会怎样?如果这些 3D 网格是基于体素的组件会怎样?这是有道理的吗?这对你的创意或专业目的有何帮助?🤔

在探索 Python 自动化提示和技巧 😎 之前,让我们深入了解乐高积木的故事。

1、乐高积木、体素和点云

好吧,如果你的童年是被乐高(现在的我的世界)占据的,那么说服你应该不会太难😊。你还记得将这些小块组装成 3D 表示形式,我们可以用来创建新故事或模拟我们最喜欢的电影是多么有趣吗?

不,我的目标不是强迫你购买一大堆乐高积木,而是说明这些简单的物理积木有多酷,以及我们可以用它们做什么事情。

但是乐高积木与体素的实际联系是什么?好吧,简单地说,体素是 2D 像素的 3D 模拟(有人说是 3D 像素,但这听起来很奇怪)。这是一种构建最初无序的 3D 数据集(如点云)的简单方法。然后,你将获得可以轻松链接到乐高组件的原始块组件。主要区别——除了物理属性与数字属性 😊——是体素只有一种类型的基本元素(立方体 🧊),而乐高积木让你玩不同尺寸的各种部件。但是,如果我们在理论上采用多尺度愿景和推理,正如 Dassault Systèmes, Spatial Corp 在Source中所说:

体素是复制现实的完美建模技术,可以远远超出我们的想象。

事实上,我们的 3D 世界是由超微小体素可以大量近似的东西组成的。因此,如果你有足够高的密度以及适当的渲染技术:

你可以使用体素来复制无法与真实事物区分开来的真实世界对象——无论是外观还是行为。

与原始点云相比,这是一个显着的优势:你可以获得模拟现实世界物理的行为可能性,这在其他建模方法中是不可能的😆。

现在是时候动手学习如何以自动化方式快速将 3D 点云转换为体素组件。准备好了吗?

2、编码选择

免责声明:在编写代码以解决已识别的问题时,没有一个唯一的方向。我将为你提供的解决方案依赖于一些巧妙的open3d技巧和可用的函数栈。但是如果你想依赖最少数量的库,你也可以按照这篇文章,将体素采样策略调整为体素创建策略。

在本教程中,我们将仅依赖三个函数库:( laspy) pip install laspyopen3d( conda install -c open3d-admin open3d) 和numpy( conda install numpy),python 版本为 3.8。下面代码引入这三个库:

import laspy as lp
import numpy as np
import open3d as o3d

🤓注意当前实验使用 Python 3.8.12,laspy版本:2.0.3,numpy版本 1.21.2 和open3d版本 0.11.2 运行。这样,如果需要调试,你首先要做一些检查:)。

完美,现在剩下的就是识别我们想要体素化的点云。幸运的是,我为你创建了一个非常好的点云数据,你可以直接下载:下载数据集 (.las),或使用 Flyvast 在你的网络浏览器中进行可视化。

设置完成后,我们就可以开始在我们的环境中加载数据了。

3、加载数据

首先,我们创建两个变量来处理输入路径(您应该适应您的情况)和数据名称,如下所示:

input_path=”C:/DATA/”
dataname=”heerlen_table.las”

现在,是时候在程序中加载数据了。我们首先将laspy.lasdata.LasData点云存储为 point_cloud变量。

point_cloud=lp.read(input_path+dataname)

然后,为了使用存储在point_cloud变量中的数据,我们将其转换为open3d点云格式。如果你还记得之前的教程,我们将颜色与空间坐标(X、Y 和 Z)分开。

pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(np.vstack((point_cloud.x, point_cloud.y, point_cloud.z)).transpose())
pcd.colors = o3d.utility.Vector3dVector(np.vstack((point_cloud.red, point_cloud.green, point_cloud.blue)).transpose()/65535)

🤓注意如果你仔细观察,你会看到一个奇怪的65535. 这是因为,在laspy格式中,颜色是以 16 位无符号方式编码的整数,这意味着数字范围从0+65535,我们需要缩放到[0,1]区间。

4、创建体素网格

我们有一个点云,想要拟合一组体素立方体来近似它。为此,我们实际上只在已建立的 3D 网格上有点的部分生成体素。

为了获得体素单元,我们首先需要计算点云的边界框,它界定了我们数据集的空间范围。只有这样我们才能将边界框离散化为一个小 3D 立方体的集合:体素。

在我们的例子中,如果你切换输入点云,我们将通过给出一个链接到初始边界框的相对值来简单地计算体素大小,以“概括”该方法。为此,你可以在下面看到我们提取点云的边界框,我们取最大边缘,我们决定将体素大小设置为其值的 0.5%(这绝对是任意的)。最后,我们将获得的值四舍五入为 4 位,而不会受到此后计算不精确的影响。

v_size=round(max(pcd.get_max_bound()-pcd.get_min_bound())*0.005,4)
voxel_grid=o3d.geometry.VoxelGrid.create_from_point_cloud(pcd,voxel_size=v_size)

🤓注意这可能是我写的最垃圾的代码之一,但它提供了一个很好的例子,说明可以通过快速的经验陈述来实现什么。那里可以改进很多,尤其是在舍入误差和任意阈值方面。😉

现在我们已经定义了体素单元,接下来将切换到与空间信息相关的更有效的“表示”:二进制(FalseTrue01)。为此,使用 的第一个open3d技巧是使用以下命令行生成体素网格:

voxel_grid=o3d.geometry.VoxelGrid.create_from_point_cloud(pcd,voxel_size=v_size)

太棒了,我们现在是点云体素表示的所有者,可以通过以下方式可视化(如果在 jupyter 环境之外):

o3d.visualization.draw_geometries([voxel_grid])

这很棒!但是如果我们就此打住,也就是对点云和体素进行了很小的利用。实际上,我们将仅限于使用该open3d库来处理体素结构,其功能数量有限,根据具体需要,这些功能是否适合应用程序。因此,让我们深入研究从这个数据结构创建 3D 网格的过程😆,我们可以以开放格式(.ply 或 .obj)导出并加载到其他软件中,其中包括 MeshLab、Blender、CloudCompare、MagickaVoxels 、Unity、虚幻引擎等。

5、生成体素网格

现在我们有了voxel_grid,我们将提取填充的体素,以便以后可以将它们用作单独的实体。

voxels=voxel_grid.get_voxels()

如果我们要检查voxels变量是什么样的;我们得到一个列表,其中包含体素信息的open3d.cpu.pybind.geometry.Voxel类型:Voxel with grid_index: (19, 81, 57), color: (0.330083, 0.277348, 0.22266)

好的,我们将其转换为 3D 立方网格(描述六个面的 8 个顶点和 12 个三角形)的集合。看到我们正在努力实现的技巧🙃?所以首先,让我们初始化我们的三角形网格实体,它将包含这个立方体组件:

vox_mesh=o3d.geometry.TriangleMesh()

现在,我们将迭代处理所有体素for v in voxels,并且对于每个体素,我们将生成一个大小为 1 的立方体,存储在变量 cube中。然后我们用手头的体素颜色绘制它,再使用grid_index方法提供的索引将体素定位在网格上。最后,我们将新着色和定位的立方体使用vox_mesh+=cube添加到网格实体中。

for v in voxels:
   cube=o3d.geometry.TriangleMesh.create_box(width=1, height=1,
   depth=1)
   cube.paint_uniform_color(v.color)
   cube.translate(v.grid_index, relative=False)
   vox_mesh+=cube

🤓注意:translation 方法将是否应该相对于给定的第一个参数(位置)进行坐标变换作为参数。在我们的例子中,因为我们将其设置为 False,所以第一个参数v.grid_index充当我们系统中的绝对坐标。

6、导出网格对象

我们现在已经为 I/O 操作准备好了一个 3D 网格对象。事实上,我们还处于一个具有任意整数单位的任意系统中。要将自己置于输入点云所施加的初始参考系中,我们必须应用刚性变换(平移、旋转和缩放)以回到原始位置。为了清楚起见,我将其分解为三个代码行。🤓

首先,我们将 3D 网格(体素组件)相对平移一半体素单元。这可以通过以下事实来解释:当我们创建初始体素网格时,参考是体素的最低左点而不是重心([0.5,0.5,0.5]相对地位于单位立方体中)。

vox_mesh.translate([0.5,0.5,0.5], relative=True)

然后,我们按体素大小缩放模型,将每个立方体单元转换为其实际大小。这利用了带有两个参数的 scale 方法。第一个是缩放因子,第二个是缩放时使用的中心。

vox_mesh.scale(voxel_size, [0,0,0])

最后,我们需要通过使用体素网格原点进行相对平移,将我们的体素组件平移到其真实的原始位置。

vox_mesh.translate(voxel_grid.origin, relative=True)

太好了,现在我们的最终立方体组件已正确定位。一个可选命令是合并闭合顶点。每当我们生成一个立方体时,我们可能处于其中一个角顶点与另一个立方体角顶点重叠的配置中。因此,最好在保留拓扑的同时清理它。

vox_mesh.merge_close_vertices(0.0000001)

🥁 我们管道中的最后一个元素是简单地将我们的.ply(或.obj取决于您喜欢的扩展名)文件导出到操作系统浏览器中选择的文件夹中。

o3d.io.write_triangle_mesh(input_path+”voxel_mesh_h.ply”, vox_mesh)

可以在选择的软件中自由使用输出文件。如果在导入时得到一个旋转文件,还可以在 python 代码中添加以下行并将导出的变量更改为vox_mesh.transform(T)

T=np.array([[1, 0, 0, 0],[0, 0, 1, 0],[0, -1, 0, 0],[0, 0, 0, 1]])
o3d.io.write_triangle_mesh(input_path+”4_vox_mesh_r.ply”, vox_mesh.transform(T))

这样做只是简单地创建一个变换矩阵T,它定义了围绕 Y 轴逆时针旋转,之后在某些软件中通常会显示倒置的 Z 轴和 Y 轴。

可以使用此 Google Colab 笔记本直接在浏览器中访问代码。

7、结束语

还记得我们在文章开头称赞体素的多功能性和简单性吗?好吧,我也想给你一些视觉感受。下面看到利用体素可以实现什么。体素的有趣之处在于你得到了一个可以更好处理的有序结构。


原文链接:How to Automate Voxel Modelling of 3D Point Cloud with Python

BimAnt翻译整理,转载请标明出处