【小白深度教程 1.26】手把手教你使用 Open3D(9)对点云进行语义分割(完整代码在最后)
在这篇文章中,我们将学习如何使用 Open3D 对自动驾驶目的的点云进行实时语义分割。
我们必须处理几个阶段,包括:
- 预处理,
- 自定义TensorFlow 算子集成
- 后处理
- 可视化
此外,我们会展示如何实现最大的精度和运行时性能,以及 Open3D 如何帮助简化这一过程。
1. 点云分割
我们使用广为人知的 PointNet++ 架构:
然后我们的实现使用 Open3D 重新构建,并在需要时偏离了参考设计,以提升性能,具体改进将在下文中描述。
我们使用 KITTI 的数据来进行实验:
在使用 KITTI 数据集进行推理时,我们将感兴趣区域设置为汽车前后各 30m,左右各 10m 以适应大小。
我们使用的语义分割模型是在 Semantic3D 数据集上训练的,我们重点介绍了使 KITTI 数据集上的实时推理成为可能的技术。
2. 使用 Open3D 加速的 TensorFlow 操作优化 PointNet++
在 PointNet++ 的集合抽象层中,原始点云会被下采样,并且下采样点的特征必须通过插值传播到所有原始点(参见 PointNet++ 的第 3.4 节)。这通过 3-近邻搜索来实现,作者提供了一个通过自定义 TensorFlow 操作
ThreeNN
实现的简单
C++ 实现
。然而,这成为了 PointNet++ 预测模型的瓶颈。
以下基准测试结果是在有颜色的 Semantic3D 数据集上,对一批 64 个样本进行推理时运行的基准测试脚本获得的。正如我们所见,
ThreeNN
操作占据了图执行时间的 87%。
// Batch time
Batch size: 64, batch_time: 1.8208365440368652
// Per-op time
node name | total execution time | accelerator execution time | cpu execution time |
ThreeNN 1.73sec (100.00%, 87.61%), 0us (100.00%, 0.00%), 1.73sec (100.00%, 95.87%)
ThreeInterpolate 60.68ms (12.39%, 3.07%), 0us (100.00%, 0.00%), 60.68ms (4.13%, 3.36%)
GroupPoint 27.31ms (9.32%, 1.38%), 27.03ms (100.00%, 15.85%), 275us (0.77%, 0.02%)
Conv2D 26.91ms (7.94%, 1.36%), 23.99ms (84.15%, 14.07%), 2.91ms (0.76%, 0.16%)
Open3D 使用 FLANN 构建 KD 树以快速检索最近邻,这可以用于加速
ThreeNN
操作。此自定义 TensorFlow 操作的实现必须与 Open3D 和 TensorFlow 库链接。为了方便地链接各种依赖项,我们提供了一个 CMake 文件,它会自动下载、构建并链接 Open3D。当 Open3D 正确安装(在本例中是自动的)后,可以简单地使用 Open3D 的 CMake 查找器来包含头文件并链接 Open3D,如下所示:
target_include_directories(tf_interpolate PUBLIC ${Open3D_INCLUDE_DIRS})
target_link_libraries(tf_interpolate tensorflow_framework ${Open3D_LIBRARIES})
有关如何将 C++ 项目链接到 Open3D 的更多详细信息,请参阅相关文档。
接下来,我们重构了 ThreeNN 以使用 Open3D。简而言之,首先使用参考点创建一个 KD 树:
open3d::KDTreeFlann reference_kd_tree(reference_pcd);
然后,对于每个目标点,在 KD 树中搜索 3 个最近邻:
// for each j:
reference_kd_tree.SearchKNN(target_pcd.points_[j], 3, three_indices, three_dists);
在使用 Open3D 重构 ThreeNN 之后,我们看到 ThreeNN 操作和整个模型在批量大小为 64 时的运行时间都加速了约 2 倍。
// Batch time
Batch size: 64, batch_time: 0.7777869701385498
// Per-op time
node name | total execution time | accelerator execution time | cpu execution time |
ThreeNN 694.14ms (100.00%, 73.72%), 0us (100.00%, 0.00%), 694.14ms (100.00%, 90.20%)
ThreeInterpolate 62.94ms (26.28%, 6.68%), 0us (100.00%, 0.00%), 62.94ms (9.80%, 8.18%)
GroupPoint 27.18ms (19.60%, 2.89%), 26.90ms (100.00%, 15.63%), 287us (1.62%, 0.04%)
Conv2D 26.39ms (16.71%, 2.80%), 23.83ms (84.37%, 13.85%), 2.56ms (1.58%, 0.33%)
3. 后处理:加速标签插值
由于我们在向PointNet++提供点之前对原始数据集进行了抽样,因此网络输出只对应于原始点云的一个稀疏子集。
推理结果:
插值结果:
稀疏标签需要插值来生成所有输入点的标签。这种插值可以通过使用open3d的最近邻搜索来实现。KDTreeFlann和多数投票,类似于我们上面在ThreeNN op中所做的。
def interpolate_dense_labels(sparse_points, sparse_labels, dense_points, k=3):
sparse_pcd = open3d.PointCloud()
sparse_pcd.points = open3d.Vector3dVector(sparse_points)
sparse_pcd_tree = open3d.KDTreeFlann(sparse_pcd)
dense_labels = []
for dense_point in dense_points:
_, sparse_indexes, _ = sparse_pcd_tree.search_knn_vector_3d(
dense_point, k
)
knn_sparse_labels = sparse_labels[sparse_indexes]
dense_label = np.bincount(knn_sparse_labels).argmax()
dense_labels.append(dense_label)
return dense_labels
然而,在Python中这样做可能会对性能造成重大影响。我们在KITTI数据集上运行完整的kitti_predict.py推断以进行基准测试。插补步骤大约占用总运行时间的90%,并将整个管道减慢到大约1 FPS。
$ python kitti_predict.py --ckpt path/to/checkpoint.ckpt
...
[ 1.05 FPS] load_data: 0.0028, predict: 0.0375, interpolate: 0.9076, visualize: 0.0031, total: 0.9545
[ 1.06 FPS] load_data: 0.0028, predict: 0.0355, interpolate: 0.8952, visualize: 0.0025, total: 0.9396
[ 1.04 FPS] load_data: 0.0028, predict: 0.0348, interpolate: 0.9214, visualize: 0.0024, total: 0.9653
...
为了解决性能问题,我们添加了另一个自定义的 TensorFlow C++ 操作
InterpolateLabel
。该操作接受稀疏点
sparse_points
、稀疏标签
sparse_labels
、密集点
dense_points
并输出密集标签
dense_labels
。使用 OpenMP 并行化 KNN 树搜索。同时,操作中还添加了
dense_colors
输出,以直接输出标签着色的密集点。有关详细信息,请参考源代码。
使用这种方法的另一个好处是,现在整个预测和插值的管道都可以用一个 TensorFlow 操作图实现。也就是说,TensorFlow 会话接受原始的密集点,并直接返回密集标签和标签着色的密集点。这种方法比在 TensorFlow 图外部进行插值更模块化且高效。经过优化后,端到端的管道在 KITTI 数据集上实现了平均 10+ FPS 的速度,这比 KITTI 的捕获速率 10 FPS 更快。
4. 代码地址:
https://github.com/isl-org/Open3D-PointNet2-Semantic3D