第二章:Open3D 几何数据处理 第二章:Open3D 几何数据处理详解 2.1 Open3D 中的几何数据类型 Open3D 主要处理以下几种核心几何数据类型,它们构成了 3D 数据处理的基础: 点云 (Point Cloud):点云是 3D 空间中点的集合,每个点通常包含位置信息 (X, Y, Z),还可以包含颜色、法线、强度等属性信息。点云是 3D 数据最基本、最灵活的表示形式,广泛应用于LiDAR、RGB-D 相机等传感器数据。在 Open3D 中,点云由 类表示。 网格 (Triangle Mesh):网格是由三角形面片连接而成的表面模型,用于表示物体的形状和表面结构。网格是一种结构化的几何表示,常用于 CAD 模型、3D 扫描重建等领域。在 Open3D 中,网格由 类表示。
Open3D 主要处理以下几种核心几何数据类型,它们构成了 3D 数据处理的基础:
点云 (Point Cloud):点云是 3D 空间中点的集合,每个点通常包含位置信息 (X, Y, Z),还可以包含颜色、法线、强度等属性信息。点云是 3D 数据最基本、最灵活的表示形式,广泛应用于LiDAR、RGB-D 相机等传感器数据。在 Open3D 中,点云由 PointCloud 类表示。
网格 (Triangle Mesh):网格是由三角形面片连接而成的表面模型,用于表示物体的形状和表面结构。网格是一种结构化的几何表示,常用于 CAD 模型、3D 扫描重建等领域。在 Open3D 中,网格由 TriangleMesh 类表示。
RGB-D 图像 (RGB-D Image):RGB-D 图像是由彩色图像 (RGB) 和深度图像 (Depth) 组成的图像对。深度图像记录了每个像素到相机的距离信息,可以用于生成点云或进行 3D 重建。在 Open3D 中,RGB-D 图像由 RGBDImage 类表示。
体素网格 (Voxel Grid):体素网格是将 3D 空间划分为规则的立方体网格,每个立方体(体素)可以存储 occupancy 或其他属性信息。体素网格常用于空间占用表示、碰撞检测等领域。在 Open3D 中,体素网格由 VoxelGrid 类表示。
本章将重点关注点云和网格两种最常用的几何数据类型,并深入探讨它们在 Open3D 中的处理方法。
点云是 Open3D 中最核心的几何数据类型之一。Open3D 提供了丰富的函数和方法来处理点云数据,包括数据输入输出、可视化、滤波、特征提取、配准等。
在 Open3D 中,我们可以通过多种方式创建点云对象,最常见的方式是从文件中读取点云数据,Open3D 支持多种点云文件格式,如 .xyz, .xyzn, .xyzrgb, .pts, .ply, .pcd 等。
代码实践 2.2.1:读取和写入点云
import open3d as o3d import numpy as np # 1. 从文件读取点云 pcd = o3d.io.read_point_cloud("path/to/your/point_cloud.ply") # 替换为你的点云文件路径 print(pcd) # 打印点云信息,例如点的数量和是否有法线、颜色等 # 2. 创建一个简单的点云 (例如一个立方体) points = [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]] pcd_manual = o3d.geometry.PointCloud() pcd_manual.points = o3d.utility.Vector3dVector(points) print(pcd_manual) # 3. 将点云写入文件 o3d.io.write_point_cloud("output_point_cloud.pcd", pcd) print("点云已保存到 output_point_cloud.pcd")
代码详解 2.2.1:
o3d.io.read_point_cloud("path/to/your/point_cloud.ply"): 使用 read_point_cloud 函数从指定路径的 .ply 文件读取点云数据,并返回一个 PointCloud 对象。你需要将 "path/to/your/point_cloud.ply" 替换为你实际的点云文件路径。
print(pcd): 打印 PointCloud 对象的信息,包括点的数量 (points),以及是否包含法线 (normals) 和颜色 (colors) 等属性。
创建手动点云部分:
points = [...]: 定义一个包含 8 个点的列表,每个点是三维坐标。
pcd_manual = o3d.geometry.PointCloud(): 创建一个空的 PointCloud 对象。
pcd_manual.points = o3d.utility.Vector3dVector(points): 将 Python 列表 points 转换为 Open3D 可以识别的 Vector3dVector 类型,并赋值给 pcd_manual 的 points 属性。
o3d.io.write_point_cloud("output_point_cloud.pcd", pcd): 使用 write_point_cloud 函数将点云 pcd 保存到名为 "output_point_cloud.pcd" 的文件中,文件格式由后缀名 .pcd 决定。
Graph TD 图 2.2.1:点云 I/O 流程
可视化是理解和调试 3D 数据的重要手段。Open3D 提供了简单易用的可视化功能,可以方便地显示点云。
代码实践 2.2.2:点云可视化
import open3d as o3d pcd = o3d.io.read_point_cloud("path/to/your/point_cloud.ply") # 替换为你的点云文件路径 # 1. 基本可视化 o3d.visualization.draw_geometries([pcd]) # 2. 自定义可视化参数 (例如点的大小) vis = o3d.visualization.Visualizer() vis.create_window() vis.add_geometry(pcd) opt = vis.get_render_option() opt.point_size = 3.0 # 设置点的大小 vis.run() vis.destroy_window()
代码详解 2.2.2:
o3d.visualization.draw_geometries([pcd]): 这是最简单的可视化方法,draw_geometries 函数接受一个几何对象列表作为输入,并创建一个窗口显示这些几何对象。这里我们只传入一个点云 pcd。
自定义可视化参数部分:
vis = o3d.visualization.Visualizer(): 创建一个 Visualizer 对象,用于更精细的可视化控制。
vis.create_window(): 创建可视化窗口。
vis.add_geometry(pcd): 将点云 pcd 添加到可视化器中。
opt = vis.get_render_option(): 获取渲染选项对象。
opt.point_size = 3.0: 设置渲染选项中的点大小为 3.0,使点在窗口中显示得更大更清晰。
vis.run(): 启动可视化窗口的事件循环,窗口保持打开状态,直到用户关闭。
vis.destroy_window(): 销毁可视化窗口,释放资源。
交互操作: 在可视化窗口中,你可以使用鼠标进行交互操作:
旋转: 按住鼠标左键并拖动。
平移: 按住鼠标右键并拖动。
缩放: 滚动鼠标滚轮。
原始点云数据通常包含噪声、离群点或冗余数据,滤波是点云预处理的重要步骤。Open3D 提供了多种点云滤波方法。
2.2.3.1 统计离群点移除 (Statistical Outlier Removal)
统计离群点移除方法基于统计学原理,移除与其他点距离较远的点,即离群点。
代码实践 2.2.3.1:统计离群点移除
import open3d as o3d pcd = o3d.io.read_point_cloud("path/to/your/noisy_point_cloud.ply") # 替换为你的噪声点云文件路径 # 统计离群点移除 cl, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0) pcd_filtered = pcd.select_by_index(ind) print("原始点云点数:", len(pcd.points)) print("滤波后点云点数:", len(pcd_filtered.points)) # 可视化滤波前后对比 o3d.visualization.draw_geometries([pcd, pcd_filtered], window_name="滤波前后对比", zoom=0.6, front=[-0.5, -0.5, -0.5], lookat=[0, 0, 0], up=[0, 1, 0])
代码详解 2.2.3.1:
pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0): 调用 remove_statistical_outlier 函数进行统计离群点移除。
nb_neighbors=20: 指定在计算平均距离时考虑的邻居点数量。
std_ratio=2.0: 标准差比例系数,用于设定距离阈值。距离超过平均距离 std_ratio 倍标准差的点被认为是离群点。
函数返回两个值:
cl: 一个 PointCloud 对象,但在这个上下文中,我们主要关注第二个返回值 ind。
ind: 一个索引列表,指示哪些点被认为是非离群点。
pcd_filtered = pcd.select_by_index(ind): 使用 select_by_index 函数,根据索引列表 ind 从原始点云 pcd 中选择非离群点,生成滤波后的点云 pcd_filtered。
可视化部分:使用 draw_geometries 函数同时显示原始点云 pcd 和滤波后的点云 pcd_filtered,方便对比滤波效果。 window_name, zoom, front, lookat, up 参数用于调整可视化窗口的视角和缩放比例。
2.2.3.2 半径滤波 (Radius Outlier Removal)
半径滤波方法基于邻域半径内的点数量,移除邻域内点太少的点,这些点通常是孤立的离群点。
代码实践 2.2.3.2:半径滤波
import open3d as o3d pcd = o3d.io.read_point_cloud("path/to/your/noisy_point_cloud.ply") # 替换为你的噪声点云文件路径 # 半径滤波 cl, ind = pcd.remove_radius_outlier(nb_points=16, radius=0.05) pcd_filtered_radius = pcd.select_by_index(ind) print("原始点云点数:", len(pcd.points)) print("半径滤波后点云点数:", len(pcd_filtered_radius.points)) # 可视化滤波前后对比 o3d.visualization.draw_geometries([pcd, pcd_filtered_radius], window_name="半径滤波前后对比", zoom=0.6, front=[-0.5, -0.5, -0.5], lookat=[0, 0, 0], up=[0, 1, 0])
代码详解 2.2.3.2:
pcd.remove_radius_outlier(nb_points=16, radius=0.05): 调用 remove_radius_outlier 函数进行半径滤波。
nb_points=16: 指定在半径 radius 内最少需要的邻居点数量。
radius=0.05: 指定邻域半径。
函数返回和统计离群点移除相同的结果:cl (在这个上下文中不重要) 和 ind (非离群点索引列表)。
pcd_filtered_radius = pcd.select_by_index(ind): 根据索引列表 ind 选择非离群点,生成半径滤波后的点云 pcd_filtered_radius。
可视化部分与统计离群点移除类似,只是窗口名称改为 "半径滤波前后对比"。
Graph TD 图 2.2.3:点云滤波流程
点云数据通常非常庞大,为了提高处理效率,常常需要对点云进行下采样,减少点云的点数。Open3D 提供了多种下采样方法,其中最常用的是体素下采样。
2.2.4.1 体素下采样 (Voxel Downsampling)
体素下采样通过创建一个三维体素网格,并在每个体素内只保留一个代表点(通常是体素中心点或体素内点的重心)来实现点云的均匀下采样。
代码实践 2.2.4.1:体素下采样
import open3d as o3d pcd = o3d.io.read_point_cloud("path/to/your/dense_point_cloud.ply") # 替换为你的高密度点云文件路径 # 体素下采样 pcd_downsampled = pcd.voxel_down_sample(voxel_size=0.02) # 体素大小设置为 0.02 print("原始点云点数:", len(pcd.points)) print("体素下采样后点云点数:", len(pcd_downsampled.points)) # 可视化下采样前后对比 o3d.visualization.draw_geometries([pcd, pcd_downsampled], window_name="体素下采样前后对比", zoom=0.6, front=[-0.5, -0.5, -0.5], lookat=[0, 0, 0], up=[0, 1, 0])
代码详解 2.2.4.1:
pcd.voxel_down_sample(voxel_size=0.02): 调用 voxel_down_sample 函数进行体素下采样。
voxel_size=0.02: 指定体素的大小,单位与点云坐标单位一致。较小的体素大小会保留更多的点,下采样程度较低;较大的体素大小会减少更多的点,下采样程度较高。
函数返回体素下采样后的点云 pcd_downsampled。
可视化部分与滤波示例类似,窗口名称改为 "体素下采样前后对比"。
Graph TD 图 2.2.4:体素下采样流程
法线是点云的重要特征,描述了点云表面的局部方向。法线估计是许多点云处理算法的基础,例如曲面重建、特征匹配等。
代码实践 2.2.5:点云法线估计
import open3d as o3d pcd = o3d.io.read_point_cloud("path/to/your/point_cloud.ply") # 替换为你的点云文件路径 # 法线估计 pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30)) pcd.orient_normals_towards_camera_location(camera_location=np.array([0., 0., 0.])) # 可选: 法线方向对齐相机 print("点云是否包含法线:", pcd.has_normals()) # 可视化点云和法线 o3d.visualization.draw_geometries([pcd], point_show_normal=True, zoom=0.6, front=[-0.5, -0.5, -0.5], lookat=[0, 0, 0], up=[0, 1, 0])
代码详解 2.2.5:
pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30)): 调用 estimate_normals 函数进行法线估计。
search_param: 指定邻域搜索参数,用于确定每个点的邻域。这里使用 KDTreeSearchParamHybrid,它结合了半径搜索和 K 近邻搜索。
radius=0.1: 指定半径搜索的半径。
max_nn=30: 指定 K 近邻搜索的最大邻居点数量。
法线估计基于邻域内的点拟合平面,平面的法向量即为该点的法线。
pcd.orient_normals_towards_camera_location(camera_location=np.array([0., 0., 0.])): (可选) 调用 orient_normals_towards_camera_location 函数将法线方向对齐到相机位置。这可以使法线方向更加一致,方便后续处理。
print("点云是否包含法线:", pcd.has_normals()): 检查点云是否成功计算了法线。
可视化部分:draw_geometries 函数的 point_show_normal=True 参数使点云在可视化时同时显示法线,法线以线段的形式从每个点出发。
Graph TD 图 2.2.5:点云法线估计流程
网格是另一种重要的几何数据类型,Open3D 也提供了丰富的网格处理功能。
与点云类似,Open3D 可以从文件中读取网格数据,并支持多种网格文件格式,如 .ply, .obj, .off, .stl 等。
代码实践 2.3.1:读取和写入网格
import open3d as o3d import numpy as np # 1. 从文件读取网格 mesh = o3d.io.read_triangle_mesh("path/to/your/mesh.ply") # 替换为你的网格文件路径 print(mesh) # 打印网格信息,例如顶点和三角形面片的数量 # 2. 创建一个简单的网格 (例如一个三角形) vertices = [[0, 0, 0], [1, 0, 0], [0, 1, 0]] triangles = [[0, 1, 2]] mesh_manual = o3d.geometry.TriangleMesh() mesh_manual.vertices = o3d.utility.Vector3dVector(vertices) mesh_manual.triangles = o3d.utility.Vector3iVector(triangles) print(mesh_manual) # 3. 将网格写入文件 o3d.io.write_triangle_mesh("output_mesh.obj", mesh) print("网格已保存到 output_mesh.obj")
代码详解 2.3.1:
o3d.io.read_triangle_mesh("path/to/your/mesh.ply"): 使用 read_triangle_mesh 函数从指定路径的 .ply 文件读取网格数据,并返回一个 TriangleMesh 对象。
print(mesh): 打印 TriangleMesh 对象的信息,包括顶点数量 (vertices) 和三角形面片数量 (triangles),以及是否包含法线 (vertex_normals) 和颜色 (vertex_colors) 等属性。
创建手动网格部分:
vertices = [...]: 定义一个包含 3 个顶点的列表。
triangles = [...]: 定义一个包含 1 个三角形面片的列表,每个三角形由三个顶点索引组成,索引对应于 vertices 列表中的顶点。
mesh_manual = o3d.geometry.TriangleMesh(): 创建一个空的 TriangleMesh 对象。
mesh_manual.vertices = o3d.utility.Vector3dVector(vertices): 将顶点列表转换为 Vector3dVector 并赋值给 vertices 属性。
mesh_manual.triangles = o3d.utility.Vector3iVector(triangles): 将三角形面片列表转换为 Vector3iVector 并赋值给 triangles 属性。
o3d.io.write_triangle_mesh("output_mesh.obj", mesh): 使用 write_triangle_mesh 函数将网格 mesh 保存到名为 "output_mesh.obj" 的文件中,文件格式由后缀名 .obj 决定。
Graph TD 图 2.3.1:网格 I/O 流程
网格的可视化与点云类似,可以使用 draw_geometries 函数。
代码实践 2.3.2:网格可视化
import open3d as o3d mesh = o3d.io.read_triangle_mesh("path/to/your/mesh.ply") # 替换为你的网格文件路径 # 基本可视化 o3d.visualization.draw_geometries([mesh]) # 自定义可视化参数 (例如线框模式) vis = o3d.visualization.Visualizer() vis.create_window() vis.add_geometry(mesh) opt = vis.get_render_option() opt.mesh_style = o3d.visualization.RenderOption.MeshStyle.WIREFRAME # 设置为线框模式 vis.run() vis.destroy_window()
代码详解 2.3.2:
基本可视化部分与点云可视化相同。
自定义可视化参数部分:
opt.mesh_style = o3d.visualization.RenderOption.MeshStyle.WIREFRAME: 设置渲染选项中的网格显示风格为线框模式 (WIREFRAME),网格将以线条轮廓的形式显示,而不是实体表面。还可以设置为 RenderOption.MeshStyle.SURFACE (默认,表面模式) 或 RenderOption.MeshStyle.POINTS (点模式)。网格的顶点法线可以通过计算每个顶点周围三角形面片的法线的加权平均值来估计。
代码实践 2.3.3:网格法线计算
import open3d as o3d mesh = o3d.io.read_triangle_mesh("path/to/your/mesh.ply") # 替换为你的网格文件路径 # 计算顶点法线 mesh.compute_vertex_normals() print("网格是否包含顶点法线:", mesh.has_vertex_normals()) # 可视化网格和法线 o3d.visualization.draw_geometries([mesh], mesh_show_back_face=True, mesh_show_wireframe=True, mesh_show_normal=True, zoom=0.6, front=[-0.5, -0.5, -0.5], lookat=[0, 0, 0], up=[0, 1, 0])
代码详解 2.3.3:
mesh.compute_vertex_normals(): 调用 compute_vertex_normals 函数计算网格的顶点法线。
print("网格是否包含顶点法线:", mesh.has_vertex_normals()): 检查网格是否成功计算了顶点法线。
可视化部分:draw_geometries 函数的 mesh_show_normal=True 参数使网格在可视化时同时显示顶点法线。 mesh_show_back_face=True 和 mesh_show_wireframe=True 参数分别控制是否显示背面和线框。
Graph TD 图 2.3.3:网格法线计算流程
网格简化旨在减少网格的三角形面片数量,从而降低网格的复杂度,提高渲染和处理效率。Open3D 提供了多种网格简化算法,例如二次误差度量简化 (Quadratic Error Metrics simplification)。
代码实践 2.3.4:网格简化
import open3d as o3d mesh = o3d.io.read_triangle_mesh("path/to/your/high_poly_mesh.ply") # 替换为你的高多边形网格文件路径 # 网格简化 (二次误差度量简化) target_triangle_number = 2000 # 目标三角形面片数量 mesh_simplified = mesh.simplify_quadric_decimation(target_triangle_number=target_triangle_number) print("原始网格三角形面片数:", len(mesh.triangles)) print("简化后网格三角形面片数:", len(mesh_simplified.triangles)) # 可视化简化前后对比 o3d.visualization.draw_geometries([mesh, mesh_simplified], window_name="网格简化前后对比", zoom=0.6, front=[-0.5, -0.5, -0.5], lookat=[0, 0, 0], up=[0, 1, 0])
代码详解 2.3.4:
mesh.simplify_quadric_decimation(target_triangle_number=target_triangle_number): 调用 simplify_quadric_decimation 函数进行网格简化。
target_triangle_number=target_triangle_number: 指定目标三角形面片数量。简化算法会尽可能将网格简化到这个面片数量。
函数返回简化后的网格 mesh_simplified。
可视化部分与之前的示例类似,窗口名称改为 "网格简化前后对比"。
Graph TD 图 2.3.4:网格简化流程 (二次误差度量简化)
Open3D 提供了方便的几何变换功能,可以对点云和网格进行平移、旋转、缩放等变换。