1. 从零开始:理解AVP感知任务与点云处理的挑战
大家好,我是老张,在自动驾驶的感知算法领域摸爬滚打了十来年,尤其对AVP(自动代客泊车)这个场景情有独钟。今天想和大家深入聊聊,在一个真实的AVP项目里,我们是如何把一坨原始、无序的激光雷达点云,一步步变成系统能理解的“这里有几个人,分别在哪里”的感知结果的。这个过程,说白了就是“语义分割”加“实例化”,听着挺学术,但拆解开来,每一步都有很多实战中的门道和“坑”。
想象一下这个场景:你的车开进一个昏暗的地下停车场,激光雷达“唰唰”地扫,每秒传回来几十万个三维空间点。这些点混在一起,有柱子、有墙、有车、当然也可能有走动的行人。AVP系统的核心任务之一,就是得从这团“毛线”里,准确地把人(特别是动态的行人)给揪出来,并且还得区分开这是张三还是李四(实例化),这样才能预测他们的轨迹,规划出安全的泊车路径。这可比在图像里找人要难多了,因为点云数据天生就是稀疏的、不均匀的,而且没有像像素那样的规则网格结构。
所以,我们整个技术栈的核心思路就很清晰了:先用一个强大的神经网络(比如PointNet++)给每一个点打上标签,区分它是“人”还是“其他背景”;然后,再利用基于密度的聚类算法(比如DBSCAN),把属于“人”的这些点,按照空间上的聚集程度,分成一个个独立的个体。听起来是不是有点像“先认出来是一群羊,再数清楚有几只”?下面,我就结合我趟过的路,把这个流程掰开揉碎了讲给你听。
2. 基石构建:PointNet++语义分割实战全解
2.1 为什么是PointNet++?从原理到选型心路
在早期,处理点云要么把它体素化成小方块(像乐高积木),要么投影到二维图像上,再用传统的CNN去处理。但这样会损失原始的三维几何信息,或者引入不必要的量化误差。直到PointNet的出现,它开创了直接处理原始点云的先河。它最巧妙的设计是利用了一个对称函数(比如最大池化max-pooling)来聚合所有点的信息,保证了无论点云顺序怎么打乱,输出的结果都一样(置换不变性)。你可以把它想象成:每个点先自己学一套“个人简历”(通过多层感知机MLP),然后我们不看简历内容,只把所有简历里最亮眼的几项挑出来(max-pooling),用这些来代表整个点云。
但PointNet有个明显的短板:它缺乏对局部结构的感知。它平等地对待每一个点,然后直接汇总全局特征,忽略了点与点之间邻近关系所蕴含的丰富信息。这在AVP场景下问题很大——人的胳膊和躯干上的点,它们之间的空间关系,对于识别“人”这个整体至关重要。
于是,PointNet++来了,它就像是PointNet的“升级豪华版”。它的核心思想是“分层抽象”和“局部感知”。具体怎么做的呢?我打个比方:我们要了解一个城市,PointNet是给每个市民发问卷然后总结;而PointNet++是先划分街区(采样和分组),了解每个街区的特点(提取局部特征),然后再汇总各个街区的情况去了解整个城市。
在代码层面,PointNet++会先通过最远点采样,从海量点中选出一些有代表性的“锚点”。然后,以每个锚点为中心,划一个半径为R的“球”,把球内的所有点作为一个局部区域。对这个区域内的所有点,再用一个小型的PointNet网络(或者MLP)来提取这个局部区域的综合特征。这个过程会重复好几层,每一层都在更大的感受野上抽象更高级的特征。这种结构让它能很好地捕捉从局部到全局的几何信息,非常适合做精细的点级别语义分割。
2.2 动手训练:数据、代码与调参的魔鬼细节
理论懂了,接下来就是撸起袖子干。我们选用的是PyTorch版本的PointNet++开源实现,这个代码库在社区里非常活跃。但记住,开源代码往往是为标准数据集(如ShapeNet)设计的,直接拿来喂我们自己的AVP数据,八成会跑不通。这里我分享几个关键的适配点,都是踩过坑的。
第一,修改模型定义。 原代码是针对“部件分割”(比如把飞机分成机翼、机身)设计的,输出通道数是num_part。而我们现在做的是“语义分割”(人 vs 背景),类别数很少。但为了保持代码灵活性,我建议在模型初始化时同时传入num_part(对我们来说就是2:人和背景)和num_classes(场景类别,可以设为1)。关键是要修改网络最后的卷积层,确保其输出通道数等于我们的num_part。同时,前向传播中那个用于条件注入的cls_label(类别标签)的one-hot编码维度,也要从固定的16改为我们传入的num_classes。具体修改就像下面这样,我在关键处加了注释:
class get_model(nn.Module):
# 修改初始化函数,显式区分分割类别数和场景类别数
def __init__(self, num_part, num_classes=16, normal_channel=False):
super(get_model, self).__init__()
self.num_part = num_part
self.num_classes = num_classes
# ... 中间的网络层定义不变 ...
# 修改特征传播层的输入通道数,其中一部分来自num_classes
self.fp1 = PointNetFe

6679

被折叠的 条评论
为什么被折叠?



