3D 视觉与点云入门:从深度图到 PointNet——自动驾驶的眼睛

· 约 6 分钟 ·
本文目录(10 节)

从图片转到点云,最先需要调整的是对数据结构的理解。图片有固定的行列,点云通常只有一组坐标:点数不固定,输入顺序也不应该改变预测结果。下面从这些差异出发,解释如何定义邻域、提取特征,再讨论机器人抓取和测距任务需要哪些额外信息。

这篇讲清 3D 数据的表示选择、点云深度学习的关键架构(PointNet 系列)、以及用 Open3D 上手的完整路径。

前置阅读:建议先读 CNN 详解、图像分割 U-Net。

3D 数据的四种表示:没有银弹

表示数据结构优点缺点适用
点云N×(x,y,z,…) 无序点集传感器原始格式、信息无损无序、不规则、密度不均激光雷达、深度相机
体素3D 网格(3D 像素)可用 3D CNN显存爆炸(O(n³))医学影像、小场景
多视图多角度 2D 投影复用成熟 2D 模型视角选择是玄学、遮挡物体分类、检索
网格顶点+边+面紧凑、带拓扑深度学习工具少图形学、建模

工程现实:自动驾驶以点云为纲(激光雷达直接吐点云),机器人抓取点云+体素混合,AR/建模偏网格。本文主攻点云。

点云的三大难题

  1. 无序性:同一组点任意打乱顺序是同一个物体——模型必须对输入排列不变(permutation invariant),2D CNN 的滑动窗口假设直接作废。
  2. 密度不均:激光雷达近处密远处稀,同一物体 10 米外点数剩 1/10——尺度鲁棒性是硬指标。
  3. 旋转不变:物体转个角度坐标全变,语义不该变。

PointNet:点云深度学习的破晓

2017 年的 PointNet 用一个优雅的设计解决了无序性:逐点 MLP 提特征 + 对称函数(max pooling)聚合。

每个点 (x,y,z) → 共享 MLP → 1024 维特征 → MaxPool over points → 全局特征

MaxPool 是关键:无论输入顺序如何、点数多少,逐维取最大值的结果不变——天然排列不变 + 变长输入。分类头接全局特征;分割头把全局特征拼回每个点的局部特征再逐点分类。

import torch
import torch.nn as nn

class PointNetCls(nn.Module):
    def __init__(self, n_classes=40):
        super().__init__()
        self.mlp = nn.Sequential(
            nn.Conv1d(3, 64, 1), nn.BatchNorm1d(64), nn.ReLU(),
            nn.Conv1d(64, 128, 1), nn.BatchNorm1d(128), nn.ReLU(),
            nn.Conv1d(128, 1024, 1), nn.BatchNorm1d(1024), nn.ReLU())
        self.head = nn.Sequential(
            nn.Linear(1024, 512), nn.ReLU(),
            nn.Linear(512, n_classes))

    def forward(self, x):                    # (B, 3, N)
        feat = self.mlp(x)                   # (B, 1024, N)
        global_feat = feat.max(dim=2).values # (B, 1024) ← 对称函数
        return self.head(global_feat)

注意 Conv1d 卷积核为 1——就是逐点共享的全连接,与点数无关。

PointNet 的致命伤:max pooling 只留全局,丢失局部结构——「轮子挨着车身」这种邻域关系学不到。PointNet++ 的修复:分层——局部区域采样(farthest point sampling)+ 分组(ball query)+ 小组内跑迷你 PointNet,逐级扩大感受野,像极了 CNN 的层级化。这个「层次化局部聚合」思想至今是 3D 网络的主流。

Open3D 实战:点云处理的标准工具箱

import open3d as o3d
import numpy as np

# 加载与可视化
pcd = o3d.io.read_point_cloud("scan.ply")
print(f"{len(pcd.points)} 个点")
o3d.visualization.draw_geometries([pcd])

# 预处理三连:降采样 → 去噪 → 法线估计
pcd = pcd.voxel_down_sample(voxel_size=0.05)        # 体素网格降采样
pcd, _ = pcd.remove_statistical_outlier(             # 统计离群点去除
    nb_neighbors=20, std_ratio=2.0)
pcd.estimate_normals(
    o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30))

# 平面分割(RANSAC):自动驾驶里去地面
plane, inliers = pcd.segment_plane(
    distance_threshold=0.1, ransac_n=3, num_iterations=1000)
ground = pcd.select_by_index(inliers)
objects = pcd.select_by_index(inliers, invert=True)

# 聚类:把剩余点分成独立物体
labels = np.array(objects.cluster_dbscan(eps=0.3, min_points=20))

这套「降采样→去地面→聚类」的传统管道在简单场景(结构化仓库)至今好用,不是一切都要深度学习——先想清楚场景复杂度再选武器。

3D 检测:自动驾驶的主战场

3D 目标检测 = 在点云里框出带朝向的 3D 框(x,y,z,l,w,h,θ)。主流路线:

  • PointPillars:把点云按柱子(pillar,俯视网格)组织,每个柱子内 PointNet 提特征,压成伪图像后走 2D CNN 检测头。速度王,Waymo/车载部署的常见选择。
  • VoxelNet/SECOND:体素化 + 稀疏 3D 卷积(只算非空体素,解决显存爆炸)。
  • PV-RCNN:体素提语义 + 点保留细节,双管齐下提精度。

传感器融合是另一个维度:摄像头便宜有颜色纹理,激光雷达精确有深度,前融合(BEV 空间对齐后融合,如 BEVFusion) 是当前自动驾驶的主流——把多传感器特征统一投到鸟瞰图平面再检测。Tesla 坚持的纯视觉路线(Occupancy Network)则是用多目视频重建 3D 占据栅格,赌的是「摄像头信息上限足够 + 成本碾压」。

数据与标注:3D 的特殊成本

  • 公开数据集:ShapeNet(物体)、ModelNet40(分类入门)、KITTI(自动驾驶经典)、Waymo Open / nuScenes(大规模,带多传感器)。
  • 标注成本:一个 3D 框标注 ≈ 10 个 2D 框的时间。nuScenes 级别的数据集标注费千万级——3D 数据稀缺比 2D 严重得多,所以预训练、仿真合成(CARLA)、半监督在 3D 领域尤其活跃。
  • 坐标系陷阱:激光坐标系、车体坐标系、世界坐标系的变换矩阵搞错是最常见的低级事故,KITTI 的 calib 文件不是装饰。

踩坑排查

现象原因解法
模型对点数敏感(换个采样就错)输入未归一化到固定点数FPS/随机采样定长 + 数据增强时重采样
远处物体检测全丢点云密度随距离衰减密度增强;或检测头分距离段调阈值
训练显存爆炸体素分辨率设太细稀疏卷积;或换 PointPillars 柱子化
地面被当成障碍物没去地面或 RANSAC 阈值不当调 distance_threshold;坡道场景分块处理
物体旋转后识别失败缺旋转数据增强训练时绕 z 轴随机旋转增强
可视化窗口打不开(远程服务器)Open3D GUI 需要显示无头环境用 o3d.visualization 离屏渲染存图

练习

  1. 用 Open3D 加载 ModelNet40 的一个样本,完成降采样和法线估计,可视化对比处理前后。
  2. 实现文中的 PointNetCls,在 ModelNet40 的 10 类子集上训练,报告准确率;然后加入旋转增强对比鲁棒性。
  3. 对一段 KITTI 点云做「去地面 + DBSCAN 聚类」,统计聚出了几个物体簇,和真值 3D 框数量对比。
  4. 思考实验:把同一物体的点云分别打乱顺序、随机丢 50% 点、整体旋转 90° 后喂给训练好的 PointNet,观察预测置信度变化——亲手验证三种不变性。

面试常问

Q:PointNet 为什么用 max pooling?有什么局限? 对称函数保证排列不变(顺序无关)且支持变长输入。max 是逐维取最强响应,相当于「这个点云里是否出现过某种模式」。局限:只保留全局最强特征,局部几何(点与点的邻域关系)被聚合掉,对分割、检测等需要空间细节的任务不够——PointNet++ 用层次化局部聚合修复。

Q:体素化方法为什么需要稀疏卷积? 3D 空间绝大多数体素是空的(室内扫描空体素 >90%)。稠密 3D 卷积在空体素上浪费算力且显存 O(n³) 爆炸。稀疏卷积(SparseConv)只在哈希表记录的非空体素上计算,输出也只落到非空位置——把复杂度从「空间体积」降到「表面面积」。

Q:激光雷达和摄像头的优劣? 激光:精确深度(厘米级)、不受光照影响、成本高、点云稀疏无纹理、雨雪衰减。视觉:信息密度高(颜色纹理语义)、便宜、缺直接深度、怕逆光夜晚。融合方案取其长;纯视觉路线赌算法能补齐深度——行业路线之争本质是成本与安全的权衡。

Q:FPS(最远点采样)为什么优于随机采样? 随机采样在稠密区扎堆、稀疏区漏掉;FPS 迭代选「离已选点集最远」的点,保证空间覆盖均匀——相同点数下几何信息保留更好,是 PointNet++ 的标准采样。代价:FPS 本身是 O(N²) 级别,大点云要先体素降采样。

Q:BEV 融合为什么成为自动驾驶主流? 多摄像头 + 激光的特征在各自坐标系无法直接融合;统一到鸟瞰平面后:空间对齐自然(都是俯视)、检测友好(车辆不重叠遮挡少)、时序融合简单(帧间 BEV 对齐 = 自车运动补偿)、下游规划直接在 BEV 上做。LSS(Lift-Splat-Shoot)的「提升每个像素到 3D 再拍扁」是相机转 BEV 的经典操作。

相关阅读

3D 视觉的门槛一半在算法,一半在工程(坐标系、标定、传感器同步)。好消息:预训练模型和 Open3D 把入门成本打下来了——先跑通 KITTI 上的 PointPillars demo,你就超过了 80% 只读过论文的人。

💬 评论

这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。

正在连接评论服务,首次访问可能需要几秒…