@@ -551,3 +551,237 @@ MRG通过结合来自不同分辨率的特征来实现效率和适应性的平
551
551
> 来自下一级的特征:首先,将来自下一级(更高分辨率)的特征进行汇总,形成一个特征向量。这一过程通过对每个子区域应用集合抽象层(set abstraction level)完成。
552
552
553
553
> 直接处理的原始点特征:另一部分特征是通过在当前分辨率直接对所有原始点应用单个PointNet得到的。
554
+
555
+ ## 点云语义分割
556
+
557
+ ** PointNet++ 完成点云分割任务的过程是一个典型的“编码-解码”结构** ,结合了层级特征提取和多尺度融合机制。
558
+
559
+ ** 目标:** 给定一个点云,模型需要为每个点预测一个类别标签(如桌子、椅子、墙壁等)。
560
+
561
+ - 输入:` xyz: [B, N, 3] `
562
+
563
+ - 输出:` labels: [B, N, C] ` ,其中 ` C ` 是类别数
564
+
565
+
566
+ PointNet++ 分割的整体结构 :
567
+
568
+ ``` python
569
+ Input Points (xyz): [ B, N, 3 ]
570
+ ↓
571
+ Set Abstraction Layers(编码器)
572
+ ↓
573
+ Feature Vectors at Multiple Scales
574
+ ↓
575
+ Feature Propagation Layers(解码器)
576
+ ↓
577
+ Recovered Features at Original Resolution
578
+ ↓
579
+ MLP + Softmax → Per- point Semantic Labels
580
+ ```
581
+ ---
582
+
583
+ ** 第一步:Set Abstraction(集合抽象)—— 编码器:** 对点云进行下采样,并在每个局部区域提取特征。
584
+
585
+ 核心操作包括:
586
+
587
+ 1 . ** FPS(Farthest Point Sampling)** :从点云中选出有代表性的点作为中心点。
588
+
589
+ 2 . ** Ball Query** :为每个中心点找到其邻域内的点。
590
+
591
+ 3 . ** Grouping** :将邻域点组合成局部点云组。
592
+
593
+ 4 . ** PointNet 操作** :使用 T-Net 对局部点云进行变换,然后通过 MLP 提取特征。
594
+
595
+ 5 . ** Pooling** :对局部点云组做最大池化或平均池化,得到该区域的特征。
596
+
597
+ > 多个 Set Abstraction 层堆叠,逐步减少点的数量,增加特征维度,形成多尺度特征表示。
598
+
599
+ ---
600
+
601
+ ** 第二步:Feature Propagation(特征传播)—— 解码器:** 从最稀疏的点开始,逐层将特征插值回原始点数量。
602
+
603
+ 特征插值方式:
604
+
605
+ - 使用 ** 反距离加权插值(IDW)** ,即根据最近的几个邻近点的距离进行加权平均。
606
+
607
+ - 可选地拼接 skip connection 中的原始特征(来自 Set Abstraction 前的某一层)。
608
+
609
+ 输入输出示例:
610
+
611
+ ``` python
612
+ def forward (xyz1 , xyz2 , points1 , points2 ):
613
+ # xyz1: 原始点坐标(多)
614
+ # xyz2: 下采样点坐标(少)
615
+ # points1: 原始点特征(可为空)
616
+ # points2: 下采样点特征
617
+ return interpolated_points # 插值得到的密集特征,形状与 xyz1 一致
618
+ ```
619
+
620
+ > 多个 Feature Propagation 层堆叠,逐渐恢复点数,最终回到原始点数量。
621
+
622
+ ---
623
+
624
+ ** 第三步:Head 预测头 —— 分类每个点:** 对每个点的特征做一个简单的分类器,输出类别概率。
625
+
626
+ 实现方式:
627
+
628
+ - 将最后一层 Feature Propagation 输出的特征送入一个小型 MLP。
629
+
630
+ - 最后一层使用 ` Softmax ` (对于多分类)或 ` Sigmoid ` (对于多标签)激活函数。
631
+
632
+ 例如:
633
+
634
+ ``` python
635
+ mlp = nn.Sequential(
636
+ nn.Conv1d(128 , 128 , 1 ),
637
+ nn.BatchNorm1d(128 ),
638
+ nn.ReLU(),
639
+ nn.Dropout(0.5 ),
640
+ nn.Conv1d(128 , num_classes, 1 )
641
+ )
642
+ logits = mlp(final_features) # shape: [B, C, N]
643
+ ```
644
+
645
+ ### 代码实现
646
+
647
+ PointNet++ 的整体结构是一个典型的 编码器-解码器(Encoder-Decoder)架构 :
648
+
649
+ - Set Abstraction 层 :不断对点云进行下采样 + 提取局部特征(编码过程)
650
+
651
+ - Feature Propagation 层 :从最稀疏的点开始,逐层恢复到原始点数(解码过程)
652
+
653
+ ```
654
+ [Input Points]
655
+ ↓
656
+ SA Layer 1 → [Points: 1024 → 512]
657
+ ↓
658
+ SA Layer 2 → [Points: 512 → 128]
659
+ ↓
660
+ SA Layer 3 → [Points: 128 → 32]
661
+ ↓
662
+ FP Layer 3 ← [Points: 32 → 128]
663
+ ↓
664
+ FP Layer 2 ← [Points: 128 → 512]
665
+ ↓
666
+ FP Layer 1 ← [Points: 512 → 1024]
667
+ ↓
668
+ [Per-point Classification Head]
669
+ ↓
670
+ [Output: per-point labels]
671
+ ```
672
+
673
+ #### 特征传播层
674
+
675
+ PointNetFeaturePropagation 是 PointNet++ 中用于点云“特征传播”(Feature Propagation)的核心模块,主要作用是:
676
+
677
+ - 将稀疏点集的特征插值回原始点集的位置上。
678
+
679
+ 换句话说:
680
+
681
+ - 输入:少量点的坐标 + 特征(如经过下采样后的点)
682
+
683
+ - 输出:在原始点数量下的每个点都拥有一个合理的特征向量
684
+
685
+ 这一步相当于图像任务中的 上采样(upsample)或转置卷积(transpose convolution) ,但在点云这种非结构化数据中,不能直接使用这些操作。
686
+
687
+ ``` python
688
+ class PointNetFeaturePropagation (nn .Module ):
689
+ def __init__ (self , in_channel , mlp ):
690
+ """
691
+ 初始化函数,构建用于特征传播(上采样)的MLP层
692
+
693
+ 参数:
694
+ in_channel: 输入特征的通道数(维度)
695
+ mlp: 一个列表,表示每一层MLP的输出通道数,例如 [64, 128]
696
+ """
697
+ super (PointNetFeaturePropagation, self ).__init__ ()
698
+
699
+ # 用于保存卷积层和批归一化层
700
+ self .mlp_convs = nn.ModuleList()
701
+ self .mlp_bns = nn.ModuleList()
702
+
703
+ last_channel = in_channel # 当前输入通道数初始化为in_channel
704
+
705
+ # 构建MLP层:每个层是一个Conv1d + BatchNorm1d + ReLU
706
+ for out_channel in mlp:
707
+ self .mlp_convs.append(nn.Conv1d(last_channel, out_channel, 1 ))
708
+ self .mlp_bns.append(nn.BatchNorm1d(out_channel))
709
+ last_channel = out_channel # 更新下一层的输入通道数
710
+
711
+ def forward (self , xyz1 , xyz2 , points1 , points2 ):
712
+ """
713
+ 前向传播函数:将稀疏点集points2插值到密集点集xyz1的位置上
714
+
715
+ 参数:
716
+ xyz1: 原始点坐标数据,形状 [B, C, N] (如 1024 个点)
717
+ xyz2: 下采样后的点坐标数据,形状 [B, C, S] (如 256 个点)
718
+ points1: 原始点对应的特征数据,形状 [B, D, N] (可为 None)
719
+ points2: 下采样点对应的特征数据,形状 [B, D, S]
720
+
721
+ 返回:
722
+ new_points: 插值并融合后的特征,形状 [B, D', N]
723
+ """
724
+ # 将坐标和特征从 [B, C, N] 转换为 [B, N, C] 格式,便于后续计算
725
+ xyz1 = xyz1.permute(0 , 2 , 1 ) # [B, N, C]
726
+ xyz2 = xyz2.permute(0 , 2 , 1 ) # [B, S, C]
727
+ points2 = points2.permute(0 , 2 , 1 ) # [B, S, D]
728
+
729
+ B, N, C = xyz1.shape # 原始点数量 N
730
+ _, S, _ = xyz2.shape # 下采样点数量 S
731
+
732
+ # 如果只有1个下采样点,直接复制其特征到所有原始点
733
+ if S == 1 :
734
+ interpolated_points = points2.repeat(1 , N, 1 ) # [B, N, D]
735
+
736
+ else :
737
+ # 计算原始点与下采样点之间的距离矩阵(欧氏距离平方)
738
+ dists = square_distance(xyz1, xyz2) # [B, N, S]
739
+
740
+ # 对每个原始点,找到最近的3个邻近点
741
+ dists, idx = dists.sort(dim = - 1 )
742
+ dists = dists[:, :, :3 ] # 取最小的三个距离 [B, N, 3]
743
+ idx = idx[:, :, :3 ] # 取对应的索引 [B, N, 3]
744
+
745
+ # 使用反距离加权(IDW)计算权重:
746
+ # 1.将距离转换为“权重”,距离越近,权重越大
747
+ dist_recip = 1.0 / (dists + 1e-8 ) # 避免除以零
748
+ # 2.对每个点的3个权重求和,得到归一化因子
749
+ norm = torch.sum(dist_recip, dim = 2 , keepdim = True ) # 归一化因子
750
+ # 3.归一化权重,使得每个点的权重之和为1
751
+ weight = dist_recip / norm # 加权平均系数 [B, N, 3]
752
+
753
+ # 为每个原始点,找到它最近的 3 个邻近点,根据距离分配权重,然后对它们的特征做加权平均,从而插值得到该点的特征。
754
+ # index_points: [B, S, D] -> [B, N, 3, D]
755
+ # weight.view(B, N, 3, 1): 扩展维度后相乘
756
+ interpolated_points = torch.sum(
757
+ # 1. 从下采样点中取出每个原始点对应的最近邻点的特征。
758
+ # points2: [B, S, D] —— 下采样点的特征(S 个点,每个点有 D 维特征)
759
+ # idx: [B, N, 3] —— 每个原始点对应的 3 个最近邻点索引
760
+ # [B, N, 3, D] —— 每个原始点都有了它最近的 3 个邻近点的特征
761
+ index_points(points2, idx)
762
+ # 将之前计算好的权重扩展维度,以便和特征相乘。
763
+ # weight: [B, N, 3] —— 每个点的三个邻近点的权重
764
+ # [B, N, 3, 1] —— 扩展后便于广播乘法
765
+ * weight.view(B, N, 3 , 1 ),
766
+ dim = 2
767
+ ) # [B, N, D]
768
+
769
+ # 如果原始点有特征,则拼接起来(skip connection)
770
+ if points1 is not None :
771
+ points1 = points1.permute(0 , 2 , 1 ) # [B, N, D]
772
+ new_points = torch.cat([points1, interpolated_points], dim = - 1 ) # [B, N, D1+D2]
773
+ else :
774
+ new_points = interpolated_points # [B, N, D]
775
+
776
+ # 恢复张量格式为 [B, D, N],以适配后面的卷积操作
777
+ new_points = new_points.permute(0 , 2 , 1 ) # [B, D', N]
778
+
779
+ # 经过MLP进一步提取和融合特征
780
+ for i, conv in enumerate (self .mlp_convs):
781
+ bn = self .mlp_bns[i]
782
+ new_points = F.relu(bn(conv(new_points))) # Conv1d + BN + ReLU
783
+
784
+ return new_points # 最终输出特征 [B, D', N]
785
+ ```
786
+
787
+
0 commit comments