SoftGroup训练FORinstance森林点云数据集——从零到AP=0.506完整复现


title: SoftGroup训练FORinstance森林点云数据集------从零到AP=0.506完整复现

date: 2025-01-01

tags:

  • 深度学习
  • 点云
  • 实例分割
  • 3D
  • SoftGroup
  • FORinstance
    categories:
  • 深度学习
  • 点云处理
    description: 本文详细记录了使用SoftGroup在FORinstance森林LiDAR数据集上进行3D点云实例分割的完整流程,包括数据预处理、代码修改、三阶段训练、调参过程,最终达到tree AP=0.506。

SoftGroup训练FORinstance森林点云数据集------从零到AP=0.506完整复现

摘要: 本文基于 SoftGroup(CVPR 2022)在 FORinstance 森林 LiDAR 数据集上训练3D点云实例分割模型。通过数据预处理、5处关键代码修改和两阶段渐进训练,最终在11个测试场景上达到 tree AP=0.506, AP_50=0.634, AP_25=0.703

环境: Python 3.7 + PyTorch + spconv2.x + 单卡32GB GPU

目录


一、环境准备

bash 复制代码
# conda 环境 softgroup (Python 3.7, PyTorch, spconv2.x)
conda activate softgroup
pip install laspy munch pyyaml
cd /root/autodl-tmp/2Code/SoftGroup
pip install -e .  # 安装 softgroup 包(含 CUDA 算子编译)

二、解压原始数据

bash 复制代码
cd /root/autodl-tmp/2Code/SoftGroup/data
unzip FORinstance_dataset.zip

解压后结构:

复制代码
data/
├── CULS/    # 捷克
├── NIBIO/   # 挪威
├── RMIT/    # 澳大利亚
├── SCION/   # 新西兰
├── TUWIEN/  # 奥地利
└── data_split_metadata.csv

三、数据预处理(LAS → .pth)

预处理脚本: dataset/forinstance/prepare_data_forinstance.py

功能: 读取LAS → 语义/实例标签映射 → 0.05m体素下采样 → 200K点上限 → 分块保存.pth

bash 复制代码
cd /root/autodl-tmp/2Code/SoftGroup/dataset/forinstance
python prepare_data_forinstance.py
# 产出: train/ (24个.pth) + val/ (11个.pth) + coordShift.json

3.1 语义映射规则

FORinstance原始类别 映射后语义标签 说明
Terrain(2) + LowVeg(1) 0 (ground) stuff类,不做实例分割
Stem(4) + LiveBranches(5) 1 (tree) thing类,按treeID分实例
Unclassified(0) + Out-points(3) -100 (ignore) 训练与评估时忽略

实例映射规则: 同一 treeID 的 stem + branches 合为一个实例,ground 为 stuff(inst=-100)。


四、新增FORinstance数据集类

创建 softgroup/data/forinstance.py

python 复制代码
from .custom import CustomDataset

class FORinstanceDataset(CustomDataset):
    CLASSES = ('tree', 'ground')

    def getInstanceInfo(self, xyz, instance_label, semantic_label):
        ret = super().getInstanceInfo(xyz, instance_label, semantic_label)
        instance_num, instance_pointnum, instance_cls, pt_offset_label = ret
        # tree (sem=1) -> instance class 0
        # ground (sem=0) -> -100 (ignored, stuff class)
        instance_cls = [x - 1 if x == 1 else -100 for x in instance_cls]
        return instance_num, instance_pointnum, instance_cls, pt_offset_label

softgroup/data/__init__.py 中注册:

python 复制代码
from .forinstance import FORinstanceDataset

# 在 build_dataset() 中添加:
elif data_type == 'forinstance':
    return FORinstanceDataset(**_data_cfg)

五、代码修改(5处关键修改)

5.1 修改1:Offset Loss ------ XY加权

文件: softgroup/model/softgroup.py ~L169

原因: 森林树木Z方向偏移大(高树),XY方向偏移小(才能区分同层树冠),因此大幅加权XY、压低Z。

python 复制代码
# ===== 原代码 =====
offset_loss = torch.sum(torch.abs(pt_offsets[pos_inds] - pt_offset_labels[pos_inds])) / pos_inds.sum()

# ===== 修改为 =====
offset_diff = torch.abs(pt_offsets[pos_inds] - pt_offset_labels[pos_inds])
offset_weight = torch.tensor([5.0, 5.0, 0.1], device=offset_diff.device)
offset_loss = (offset_diff * offset_weight).sum() / pos_inds.sum()

5.2 修改2:BFS分组 ------ XY-only聚类

文件: softgroup/model/softgroup.py ~L457

原因: BFS聚类时仅用offset位移后的XY坐标,Z置零。避免高树被分段,同时靠XY分开密集树冠。

python 复制代码
# 在 ball_query 之前,替换 coords_for_query 的计算:
use_xy_grouping = getattr(self.grouping_cfg, 'xy_only', False)
if use_xy_grouping:
    coords_for_query = (coords_ + pt_offsets_).clone()
    coords_for_query[:, 2] = 0.0  # shifted XY + zero Z
else:
    coords_for_query = coords_ + pt_offsets_

5.3 修改3:评估防空数组崩溃

文件: softgroup/evaluation/instance_eval.py ~L161

python 复制代码
# 在 num_true_examples = y_true_sorted_cumsum[-1] 之前添加:
if len(y_true_sorted_cumsum) == 0:
    continue

5.4 修改4:单GPU分布式绕过

文件: softgroup/util/dist.py ~L46, L79

python 复制代码
# 在 collect_results_gpu 和 collect_results_cpu 函数开头添加:
if world_size == 1:
    return result_part[:size]  # cpu版本
    # 或
    return result_part          # gpu版本

5.5 修改5:Scale数据增强范围

文件: softgroup/data/custom.py L109

python 复制代码
# 原代码
scale_factor = np.random.uniform(0.95, 1.05)
# 改为
scale_factor = np.random.uniform(0.8, 1.25)

六、两阶段训练

6.1 阶段一:Backbone预训练(语义分割,30 epoch)

配置文件: configs/softgroup/softgroup_forinstance_backbone_v2.yaml

关键参数:

参数 说明
channels 32 32通道UNet,~30M参数
with_coords true 输入RGB+XYZ 6通道
semantic_only true 只训语义分割头
use_elastic true 弹性形变增强
voxel_cfg.scale 3 0.33m分辨率
epochs 30 ---
batch_size 4 ---
lr 0.001 Adam优化器
bash 复制代码
cd /root/autodl-tmp/2Code/SoftGroup
OMP_NUM_THREADS=1 python tools/train.py configs/softgroup/softgroup_forinstance_backbone_v2.yaml

结果: mIoU=97.9, Acc=99.1
产出: work_dirs/softgroup_forinstance_backbone_v2/latest.pth

6.2 阶段二:实例分割(渐进式训练)

Step 1 ------ v15:低分辨率(200 epoch)

配置文件: configs/softgroup/softgroup_forinstance_v15.yaml

关键参数:

参数 说明
pretrain backbone_v2/latest.pth 从Backbone继续
semantic_only false 开启实例分割头
grouping_cfg.radius 0.15 ---
grouping_cfg.xy_only true XY-only BFS
instance_voxel_cfg.spatial_shape 50 ---
pos_iou_thr 0.1 降低正样本阈值
voxel_cfg.scale 3 0.33m分辨率
epochs 200 ---
bash 复制代码
OMP_NUM_THREADS=1 python tools/train.py configs/softgroup/softgroup_forinstance_v15.yaml

结果: 最佳 AP=0.316 (epoch 38)

Step 2 ------ v16:高分辨率(300 epoch,最终版)

配置文件: configs/softgroup/softgroup_forinstance_v16.yaml

从v15的epoch_36恢复,提高体素分辨率 + 缩小聚类半径

关键参数变化(相对v15):

参数 v15 v16 说明
pretrain backbone_v2 v15/epoch_36 渐进式预训练
voxel_cfg.scale 3 5 0.2m(关键改进!)
voxel_cfg.spatial_shape [128,512] [200,800] 匹配高分辨率
instance_voxel.spatial_shape 50 100 改善mask质量
grouping_cfg.radius 0.15 0.10 更小半径分密集树冠
test_cfg.min_npoint 50 30 ---
epochs 200 300 ---
step_epoch 20 100 ---
bash 复制代码
OMP_NUM_THREADS=1 nohup python tools/train.py configs/softgroup/softgroup_forinstance_v16.yaml > train_v16.log 2>&1 &

最终结果:AP=0.506, AP_50=0.634, AP_25=0.703


七、关键调参历程

以下总结了从AP=0到AP=0.506的关键改进及其贡献。

改动 原因 AP影响
channels: 16→32 原模型仅7.7M参数,容量不足以学习精确offset +0.06
with_coords: true 添加XYZ坐标作为输入特征,网络才知道点的绝对位置 +0.03
offset_weight: [5,5,0.1] 森林场景XY偏移准确才能分树,Z偏移无关紧要 必要
xy_only BFS 只用XY做聚类,避免高树被纵向切段 必要
radius: 0.25→0.10 密集林需要更小搜索半径 +0.08
pos_iou_thr: 0.5→0.1 降低正样本阈值,让更多proposal参与mask学习 必要
voxel scale: 3→5 0.2m分辨率(vs 0.33m),密集树冠需要更精细分辨 +0.10
instance_voxel spatial: 50→100 更大的mask体素空间,提升高树mask质量 +0.02
scale augment: 0.8-1.25 更大的尺度变化增强泛化性 +0.02
use_elastic: true 弹性形变增强 +0.01

八、Per-Scene评估结果

复制代码
场景                | AP     AP50    AP25    备注
--------------------|------  ------  ------  ----------------
CULS_plot_2         | 0.942  1.000   1.000   稀疏种植园, 树距2.97m+
SCION_plot_61       | 0.674  0.830   0.830
SCION_plot_31       | 0.403  0.545   0.680
NIBIO_plot_18       | 0.388  0.702   0.702
NIBIO_plot_5        | 0.369  0.464   0.521
NIBIO_plot_22       | 0.333  0.426   0.646
NIBIO_plot_17       | 0.318  0.454   0.600
RMIT_test           | 0.277  0.418   0.559
NIBIO_plot_1        | 0.247  0.402   0.511
NIBIO_plot_23       | 0.148  0.305   0.450
TUWIEN_test         | 0.020  0.073   0.356   极密集欧洲林, 树距0.55m
--------------------|------  ------  ------
OVERALL             | 0.506  0.634   0.703

📌 稀疏林(CULS, 树距>2m)AP极高;极密集林(TUWIEN, 树距0.55m)AP最低,是主要瓶颈。


九、测试与可视化

9.1 测试命令

bash 复制代码
python tools/test.py configs/softgroup/softgroup_forinstance_v16.yaml \
  work_dirs/softgroup_forinstance_v16/epoch_288.pth

预期输出:

复制代码
tree     AP=0.506  AP_50=0.634  AP_25=0.703
ground   AP=0.959  AP_50=1.000  AP_25=1.000
average  AP=0.732  AP_50=0.817  AP_25=0.852
mIoU=98.3  Acc=99.3

9.2 可视化(导出PLY → CloudCompare)

bash 复制代码
# 导出全部11个测试场景
python tools/export_ply.py configs/softgroup/softgroup_forinstance_v16.yaml \
  work_dirs/softgroup_forinstance_v16/epoch_288.pth \
  --out vis_results/

# 只导出指定场景
python tools/export_ply.py configs/softgroup/softgroup_forinstance_v16.yaml \
  work_dirs/softgroup_forinstance_v16/epoch_288.pth \
  --scenes CULS_plot_2_annotated_0 NIBIO_plot_1_annotated_0 \
  --out vis_results/

产出文件在 vis_results/ 目录,每个场景生成 *_pred.ply(预测)和 *_gt.ply(GT),用 CloudCompare 打开即可查看彩色实例分割效果。


十、文件结构总览

复制代码
SoftGroup/
├── configs/softgroup/
│   ├── softgroup_forinstance_backbone_v2.yaml   # 阶段一:Backbone
│   ├── softgroup_forinstance_v15.yaml           # 阶段二 Step 1
│   └── softgroup_forinstance_v16.yaml           # 阶段二 Step 2(最终版)
├── dataset/forinstance/
│   ├── prepare_data_forinstance.py              # 数据预处理脚本
│   ├── train/  (24个.pth)                       # 训练数据
│   └── val/    (11个.pth)                       # 验证数据
├── softgroup/
│   ├── data/
│   │   ├── __init__.py        (添加forinstance注册)
│   │   ├── custom.py          (修改scale增强范围)
│   │   └── forinstance.py     (新增数据集类)
│   ├── model/
│   │   └── softgroup.py       (修改offset loss + BFS分组)
│   ├── evaluation/
│   │   └── instance_eval.py   (修复空数组)
│   └── util/
│       └── dist.py            (单GPU绕过)
├── tools/
│   ├── train.py               # 训练脚本
│   ├── test.py                # 测试脚本(含ground评估)
│   └── export_ply.py          # PLY导出脚本
├── work_dirs/
│   ├── softgroup_forinstance_backbone_v2/       # Backbone权重
│   ├── softgroup_forinstance_v15/               # v15权重
│   └── softgroup_forinstance_v16/               # v16最终权重 ★
└── vis_results/                                 # 可视化PLY文件

十一、快速复现命令汇总

bash 复制代码
# 0. 环境
conda activate softgroup && cd /root/autodl-tmp/2Code/SoftGroup

# 1. 解压数据
cd data && unzip FORinstance_dataset.zip && cd ..

# 2. 预处理
cd dataset/forinstance && python prepare_data_forinstance.py && cd ../..

# 3. 训练 Backbone (约15分钟)
OMP_NUM_THREADS=1 python tools/train.py configs/softgroup/softgroup_forinstance_backbone_v2.yaml

# 4. 训练 v15 (约2小时)
OMP_NUM_THREADS=1 python tools/train.py configs/softgroup/softgroup_forinstance_v15.yaml

# 5. 训练 v16 (约3.5小时)
OMP_NUM_THREADS=1 python tools/train.py configs/softgroup/softgroup_forinstance_v16.yaml

# 6. 测试
python tools/test.py configs/softgroup/softgroup_forinstance_v16.yaml \
  work_dirs/softgroup_forinstance_v16/epoch_288.pth

# 7. 可视化
python tools/export_ply.py configs/softgroup/softgroup_forinstance_v16.yaml \
  work_dirs/softgroup_forinstance_v16/epoch_288.pth --out vis_results/

附录A:完整配置文件

📄 点击展开 softgroup_forinstance_backbone_v2.yaml

yaml 复制代码
model:
  channels: 32
  num_blocks: 7
  semantic_classes: 2
  instance_classes: 1
  sem2ins_classes: []
  semantic_only: true
  semantic_weight:
  - 3.5
  - 1.0
  ignore_label: -100
  with_coords: true
  grouping_cfg:
    score_thr: 0.4
    radius: 0.15
    mean_active: 500
    class_numpoint_mean:
    - -1.0
    - 2352.0
    npoint_thr: 0.005
    ignore_classes:
    - 0
  instance_voxel_cfg:
    scale: 5
    spatial_shape: 50
  train_cfg:
    max_proposal_num: 300
    pos_iou_thr: 0.1
  test_cfg:
    x4_split: false
    cls_score_thr: 0.001
    mask_score_thr: -0.5
    min_npoint: 50
    eval_tasks:
    - semantic
  fixed_modules: []
data:
  train:
    type: forinstance
    data_root: dataset/forinstance
    prefix: train
    suffix: _inst_nostuff.pth
    training: true
    repeat: 4
    voxel_cfg:
      scale: 3
      spatial_shape:
      - 128
      - 512
      max_npoint: 250000
      min_npoint: 5000
      use_elastic: true
  test:
    type: forinstance
    data_root: dataset/forinstance
    prefix: val
    suffix: _inst_nostuff.pth
    training: false
    voxel_cfg:
      scale: 3
      spatial_shape:
      - 128
      - 512
      max_npoint: 250000
      min_npoint: 5000
dataloader:
  train:
    batch_size: 4
    num_workers: 2
  test:
    batch_size: 1
    num_workers: 1
optimizer:
  type: Adam
  lr: 0.001
eval_min_npoint: 10
fp16: false
epochs: 30
step_epoch: 20
save_freq: 2
pretrain: ''
work_dir: work_dirs/softgroup_forinstance_backbone_v2

📄 点击展开 softgroup_forinstance_v15.yaml

yaml 复制代码
model:
  channels: 32
  num_blocks: 7
  semantic_classes: 2
  instance_classes: 1
  sem2ins_classes: []
  semantic_only: false
  semantic_weight:
  - 3.5
  - 1.0
  ignore_label: -100
  with_coords: true
  grouping_cfg:
    score_thr: 0.4
    radius: 0.15
    mean_active: 500
    class_numpoint_mean:
    - -1.0
    - 2352.0
    npoint_thr: 0.005
    ignore_classes:
    - 0
    xy_only: true
  instance_voxel_cfg:
    scale: 5
    spatial_shape: 50
  train_cfg:
    max_proposal_num: 300
    pos_iou_thr: 0.1
    match_low_quality: true
    min_pos_thr: 0.0
  test_cfg:
    x4_split: false
    cls_score_thr: 0.001
    mask_score_thr: -0.5
    min_npoint: 50
    eval_tasks:
    - semantic
    - instance
  fixed_modules: []
data:
  train:
    type: forinstance
    data_root: dataset/forinstance
    prefix: train
    suffix: _inst_nostuff.pth
    training: true
    repeat: 4
    voxel_cfg:
      scale: 3
      spatial_shape:
      - 128
      - 512
      max_npoint: 250000
      min_npoint: 5000
      use_elastic: true
  test:
    type: forinstance
    data_root: dataset/forinstance
    prefix: val
    suffix: _inst_nostuff.pth
    training: false
    voxel_cfg:
      scale: 3
      spatial_shape:
      - 128
      - 512
      max_npoint: 250000
      min_npoint: 5000
dataloader:
  train:
    batch_size: 2
    num_workers: 2
  test:
    batch_size: 1
    num_workers: 1
optimizer:
  type: Adam
  lr: 0.001
eval_min_npoint: 10
fp16: false
epochs: 200
step_epoch: 20
save_freq: 4
pretrain: work_dirs/softgroup_forinstance_backbone_v2/latest.pth
work_dir: work_dirs/softgroup_forinstance_v15

📄 点击展开 softgroup_forinstance_v16.yaml

yaml 复制代码
model:
  channels: 32
  num_blocks: 7
  semantic_classes: 2
  instance_classes: 1
  sem2ins_classes: []
  semantic_only: false
  semantic_weight:
  - 3.5
  - 1.0
  ignore_label: -100
  with_coords: true
  grouping_cfg:
    score_thr: 0.4
    radius: 0.10
    mean_active: 500
    class_numpoint_mean:
    - -1.0
    - 2352.0
    npoint_thr: 0.005
    ignore_classes:
    - 0
    xy_only: true
  instance_voxel_cfg:
    scale: 5
    spatial_shape: 100
  train_cfg:
    max_proposal_num: 300
    pos_iou_thr: 0.1
    match_low_quality: true
    min_pos_thr: 0.0
  test_cfg:
    x4_split: false
    cls_score_thr: 0.001
    mask_score_thr: -0.5
    min_npoint: 30
    eval_tasks:
    - semantic
    - instance
  fixed_modules: []
data:
  train:
    type: forinstance
    data_root: dataset/forinstance
    prefix: train
    suffix: _inst_nostuff.pth
    training: true
    repeat: 4
    voxel_cfg:
      scale: 5
      spatial_shape:
      - 200
      - 800
      max_npoint: 250000
      min_npoint: 5000
      use_elastic: true
  test:
    type: forinstance
    data_root: dataset/forinstance
    prefix: val
    suffix: _inst_nostuff.pth
    training: false
    voxel_cfg:
      scale: 5
      spatial_shape:
      - 200
      - 800
      max_npoint: 250000
      min_npoint: 5000
dataloader:
  train:
    batch_size: 2
    num_workers: 2
  test:
    batch_size: 1
    num_workers: 1
optimizer:
  type: Adam
  lr: 0.001
eval_min_npoint: 10
fp16: false
epochs: 300
step_epoch: 100
save_freq: 4
pretrain: work_dirs/softgroup_forinstance_v15/epoch_36.pth
work_dir: work_dirs/softgroup_forinstance_v16

附录B:完整预处理脚本

📄 点击展开 prepare_data_forinstance.py

python 复制代码
"""
FORinstance LAS -> SoftGroup .pth 预处理
- 0.05m 体素下采样
- 200K 点上限
- 语义: ground(0), tree(1), ignore(-100)
- 实例: 每棵树一个实例ID, ground=-100
- Train: 50m crop, Val: 250m crop
"""
import os
import json
import numpy as np
import laspy
import torch
from pathlib import Path

# =============================================================================
# 修改这里的路径
# =============================================================================
DATA_ROOT = '/root/autodl-tmp/2Code/SoftGroup/data'
OUTPUT_DIR = '/root/autodl-tmp/2Code/SoftGroup/dataset/forinstance'

VOXEL_SIZE = 0.05       # 5cm 下采样
MAX_POINTS = 200000     # 每块最大点数
TRAIN_CROP = 50.0       # 训练集裁切尺寸(m)
VAL_CROP = 250.0        # 验证集裁切尺寸(m)

# FORinstance 语义映射
SEMANTIC_MAP = {
    0: -100,  # unclassified -> ignore
    1: 0,     # low_vegetation -> ground
    2: 0,     # terrain -> ground
    3: -100,  # out_points -> ignore
    4: 1,     # stem -> tree
    5: 1,     # live_branches -> tree
}

# 数据集划分 (从 data_split_metadata.csv)
TRAIN_PLOTS = {
    'CULS': ['plot_1'],
    'NIBIO': ['plot_2', 'plot_3', 'plot_4', 'plot_6', 'plot_7', 'plot_8',
              'plot_9', 'plot_10', 'plot_11', 'plot_12', 'plot_13', 'plot_14',
              'plot_15', 'plot_16', 'plot_19', 'plot_20', 'plot_21', 'plot_24'],
    'RMIT': ['train'],
    'SCION': ['plot_21', 'plot_51'],
    'TUWIEN': ['train']
}

VAL_PLOTS = {
    'CULS': ['plot_2'],
    'NIBIO': ['plot_1', 'plot_5', 'plot_17', 'plot_18', 'plot_22', 'plot_23'],
    'RMIT': ['test'],
    'SCION': ['plot_31', 'plot_61'],
    'TUWIEN': ['test']
}


def voxel_downsample(points, colors, sem_labels, inst_labels, voxel_size):
    """体素下采样 - 每个体素随机选一个点"""
    voxel_indices = np.floor(points / voxel_size).astype(np.int64)
    _, unique_indices = np.unique(
        voxel_indices[:, 0] * 1000000 + voxel_indices[:, 1] * 1000 + voxel_indices[:, 2],
        return_index=True
    )
    return points[unique_indices], colors[unique_indices], sem_labels[unique_indices], inst_labels[unique_indices]


def process_block(points, colors, sem_labels, inst_labels, max_points):
    """下采样 + 限制点数"""
    points, colors, sem_labels, inst_labels = voxel_downsample(
        points, colors, sem_labels, inst_labels, VOXEL_SIZE
    )
    if len(points) > max_points:
        idx = np.random.choice(len(points), max_points, replace=False)
        points, colors, sem_labels, inst_labels = points[idx], colors[idx], sem_labels[idx], inst_labels[idx]
    return points, colors, sem_labels, inst_labels


def read_las(filepath):
    """读取LAS文件"""
    las = laspy.read(filepath)
    points = np.vstack([las.x, las.y, las.z]).T.astype(np.float32)

    # 颜色
    if hasattr(las, 'red'):
        colors = np.vstack([las.red, las.green, las.blue]).T.astype(np.float32)
        if colors.max() > 255:
            colors = colors / 256.0
        colors = colors / 127.5 - 1.0  # 归一化到 [-1, 1]
    else:
        colors = np.zeros((len(points), 3), dtype=np.float32)

    # 语义标签
    classification = np.array(las.classification, dtype=np.int32)
    sem_labels = np.array([SEMANTIC_MAP.get(c, -100) for c in classification], dtype=np.int32)

    # 实例标签
    tree_id = np.array(las.treeID, dtype=np.int32)
    inst_labels = np.full(len(points), -100, dtype=np.int32)
    tree_mask = sem_labels == 1
    if tree_mask.any():
        unique_ids = np.unique(tree_id[tree_mask])
        unique_ids = unique_ids[unique_ids > 0]
        for new_id, old_id in enumerate(unique_ids):
            inst_labels[(tree_id == old_id) & tree_mask] = new_id

    return points, colors, sem_labels, inst_labels


def crop_and_save(points, colors, sem_labels, inst_labels, crop_size, output_path, name, is_train=True):
    """按crop_size裁切并保存"""
    min_xy = points[:, :2].min(axis=0)
    max_xy = points[:, :2].max(axis=0)
    range_xy = max_xy - min_xy

    z_range = points[:, 2].max() - points[:, 2].min()
    if z_range < 6:
        print(f"  Skipping {name}: z_range={z_range:.1f}m < 6m")
        return []

    saved = []
    block_id = 0

    if range_xy[0] <= crop_size and range_xy[1] <= crop_size:
        pts, col, sem, inst = process_block(points, colors, sem_labels, inst_labels, MAX_POINTS)
        if is_train:
            valid_inst = np.unique(inst[inst >= 0])
            if len(valid_inst) < 2:
                print(f"  Skipping {name}: only {len(valid_inst)} tree instance(s)")
                return []
        save_path = os.path.join(output_path, f"{name}_block{block_id}_inst_nostuff.pth")
        torch.save((pts, col, sem, inst), save_path)
        saved.append(save_path)
    else:
        stride = crop_size * 0.5
        x_starts = np.arange(min_xy[0], max_xy[0], stride)
        y_starts = np.arange(min_xy[1], max_xy[1], stride)
        for xs in x_starts:
            for ys in y_starts:
                mask = (
                    (points[:, 0] >= xs) & (points[:, 0] < xs + crop_size) &
                    (points[:, 1] >= ys) & (points[:, 1] < ys + crop_size)
                )
                if mask.sum() < 1000:
                    continue
                crop_pts = points[mask]
                crop_col = colors[mask]
                crop_sem = sem_labels[mask]
                crop_inst = inst_labels[mask]

                valid_inst = np.unique(crop_inst[crop_inst >= 0])
                if is_train and len(valid_inst) < 2:
                    continue
                for new_id, old_id in enumerate(valid_inst):
                    crop_inst[crop_inst == old_id] = new_id

                crop_pts, crop_col, crop_sem, crop_inst = process_block(
                    crop_pts, crop_col, crop_sem, crop_inst, MAX_POINTS
                )
                save_path = os.path.join(output_path, f"{name}_block{block_id}_inst_nostuff.pth")
                torch.save((crop_pts, crop_col, crop_sem, crop_inst), save_path)
                saved.append(save_path)
                block_id += 1

    return saved


def main():
    os.makedirs(os.path.join(OUTPUT_DIR, 'train'), exist_ok=True)
    os.makedirs(os.path.join(OUTPUT_DIR, 'val'), exist_ok=True)

    for split, plots_dict, crop_size in [
        ('train', TRAIN_PLOTS, TRAIN_CROP),
        ('val', VAL_PLOTS, VAL_CROP)
    ]:
        print(f"\n=== Processing {split} set ===")
        all_saved = []
        coord_shifts = {}
        for site, plots in plots_dict.items():
            for plot in plots:
                las_path = os.path.join(DATA_ROOT, site, f"{plot}.las")
                if not os.path.exists(las_path):
                    print(f"  WARNING: {las_path} not found, skipping")
                    continue
                print(f"  Processing {site}/{plot}...")
                points, colors, sem_labels, inst_labels = read_las(las_path)

                coord_shift = points.min(axis=0)
                points -= coord_shift
                coord_shifts[f"{site}_{plot}"] = coord_shift.tolist()

                name = f"{site}_{plot}"
                is_train = (split == 'train')
                saved = crop_and_save(points, colors, sem_labels, inst_labels,
                                     crop_size, os.path.join(OUTPUT_DIR, split),
                                     name, is_train=is_train)
                all_saved.extend(saved)
                print(f"    -> {len(saved)} blocks saved")

        with open(os.path.join(OUTPUT_DIR, split, 'coordShift.json'), 'w') as f:
            json.dump(coord_shifts, f, indent=2)

        print(f"\n{split}: {len(all_saved)} blocks total")


if __name__ == '__main__':
    main()

附录C:完整代码修改详情

📄 点击展开全部代码修改(带上下文)

C1. softgroup/model/softgroup.py --- Offset Loss 修改

找到 offset_loss 的计算位置(约第165-175行),替换为:

python 复制代码
        # offset loss
        pos_inds = (instance_labels != self.ignore_label)
        if pos_inds.sum() == 0:
            offset_loss = 0 * pt_offsets.sum()
        else:
            # 修改: XY方向权重5.0, Z方向权重0.1
            offset_diff = torch.abs(pt_offsets[pos_inds] - pt_offset_labels[pos_inds])
            offset_weight = torch.tensor([5.0, 5.0, 0.1], device=offset_diff.device)
            offset_loss = (offset_diff * offset_weight).sum() / pos_inds.sum()

C2. softgroup/model/softgroup.py --- BFS XY-only 分组

找到 BFS ball_query 调用前的坐标准备位置(约第450-470行):

python 复制代码
            pt_offsets_ = pt_offsets[batch_mask]
            coords_ = coords[batch_mask]

            # --- XY-only grouping 修改 ---
            use_xy_grouping = getattr(self.grouping_cfg, 'xy_only', False)
            if use_xy_grouping:
                coords_for_query = (coords_ + pt_offsets_).clone()
                coords_for_query[:, 2] = 0.0   # XY偏移后坐标 + Z置零
            else:
                coords_for_query = coords_ + pt_offsets_

            idx, start_len = ball_query(
                coords_for_query, batch_idxs_.int(),

C3. softgroup/data/forinstance.py --- 新增文件

python 复制代码
from .custom import CustomDataset

class FORinstanceDataset(CustomDataset):
    CLASSES = ('tree', 'ground')

    def getInstanceInfo(self, xyz, instance_label, semantic_label):
        ret = super().getInstanceInfo(xyz, instance_label, semantic_label)
        instance_num, instance_pointnum, instance_cls, pt_offset_label = ret
        instance_cls = [x - 1 if x == 1 else -100 for x in instance_cls]
        return instance_num, instance_pointnum, instance_cls, pt_offset_label

C4. softgroup/data/init.py --- 注册数据集

python 复制代码
from .forinstance import FORinstanceDataset

# 在 build_dataset 函数中添加分支
elif data_type == 'forinstance':
    return FORinstanceDataset(**data_cfg)

C5. softgroup/data/custom.py --- Scale增强 + Elastic开关

python 复制代码
# L109: scale增强范围改为 0.8~1.25
scale_factor = np.random.uniform(0.8, 1.25)

# L141: elastic增强由配置开关控制
use_elastic = getattr(self.voxel_cfg, 'use_elastic', True)
if use_elastic:
    xyz = self.elastic(xyz, 6, 40.)
    xyz = self.elastic(xyz, 20, 160.)

C6. softgroup/evaluation/instance_eval.py --- 空数组防崩

python 复制代码
        y_true_sorted_cumsum = np.cumsum(y_true_sorted)
        if len(y_true_sorted_cumsum) == 0:   # <-- 新增
            continue                          # <-- 新增
        num_true_examples = y_true_sorted_cumsum[-1]

C7. softgroup/util/dist.py --- 单GPU分布式绕过

python 复制代码
def collect_results_gpu(result_part, size, tmpdir=None):
    world_size = get_dist_info()[1]
    if world_size == 1:
        return result_part
    ...

def collect_results_cpu(result_part, size, tmpdir=None):
    world_size = get_dist_info()[1]
    if world_size == 1:
        return result_part[:size]
    ...

附录D:训练日志关键指标

阶段 Epoch 指标
Backbone v2 30 mIoU=97.9, Acc=99.1
v15 36~38 AP=0.316, AP_50=0.473, AP_25=0.602
v16 288 AP=0.506, AP_50=0.634, AP_25=0.703

v16训练过程中 Offset MAE 从 2.18m 逐步降低到 0.82m。


附录E:常见问题(FAQ)

Q:AP一直为0怎么办?

检查以下几点:

  1. 确认 offset_weight 已改为 [5, 5, 0.1]
  2. 确认 xy_only BFS 已启用
  3. 确认 pos_iou_thr 设为 0.1(不是默认0.5)
  4. 确认 radius 足够小(0.10~0.15)

Q:显存不够怎么办?

batch_size 从2降为1,或降低 max_npoint

Q:评估时报错 "index out of range"?

确认 instance_eval.py 的空数组防护已添加。

Q:训练中loss突然变NaN?

检查 fp16 设为 false,Adam lr=0.001 在此场景稳定。


最终结果:tree AP=0.506, AP_50=0.634, AP_25=0.703

如有问题欢迎评论区交流!

相关推荐
InternLM1 小时前
LMDeploy重磅更新:从支撑模型到被模型反哺,推理引擎迈入协同进化时代!
人工智能·大模型·多模态大模型·大模型推理·书生大模型
AI周红伟2 小时前
周红伟老师《企业级 RAG+Agent+Skills+OpenClaw 智能体实战内训》的完整课程大纲(5 天 / 40 小时)
人工智能
火红色祥云2 小时前
深度学习入门:基于Python的理论与实现笔记
笔记·python·深度学习
FoldWinCard2 小时前
Python 第五次作业
linux·windows·python
hit56实验室2 小时前
【易经系列】《蒙卦》六五:童蒙,吉。
人工智能
AI浩2 小时前
VISION KAN:基于Kan的无注意力视觉骨干网络
人工智能·目标检测
China_Yanhy2 小时前
转型AI运维工程师·Day 10:拥抱“不确定性” —— 断点续训与 Spot 实例抢占
运维·人工智能·python
木昆子2 小时前
实战A2UI:从JSON到像素——深入Lit渲染引擎
前端·人工智能
TGITCIC2 小时前
AI Agent中的 ReAct 和 Ralph Loop对比说明
人工智能·ai大模型·ai agent·ai智能体·agent开发·大模型ai·agent设计模式