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
目录
- SoftGroup训练FORinstance森林点云数据集------从零到AP=0.506完整复现
-
- 一、环境准备
- 二、解压原始数据
- [三、数据预处理(LAS → .pth)](#三、数据预处理(LAS → .pth))
-
- [3.1 语义映射规则](#3.1 语义映射规则)
- 四、新增FORinstance数据集类
- 五、代码修改(5处关键修改)
-
- [5.1 修改1:Offset Loss ------ XY加权](#5.1 修改1:Offset Loss —— XY加权)
- [5.2 修改2:BFS分组 ------ XY-only聚类](#5.2 修改2:BFS分组 —— XY-only聚类)
- [5.3 修改3:评估防空数组崩溃](#5.3 修改3:评估防空数组崩溃)
- [5.4 修改4:单GPU分布式绕过](#5.4 修改4:单GPU分布式绕过)
- [5.5 修改5:Scale数据增强范围](#5.5 修改5:Scale数据增强范围)
- 六、两阶段训练
-
- [6.1 阶段一:Backbone预训练(语义分割,30 epoch)](#6.1 阶段一:Backbone预训练(语义分割,30 epoch))
- [6.2 阶段二:实例分割(渐进式训练)](#6.2 阶段二:实例分割(渐进式训练))
-
- [Step 1 ------ v15:低分辨率(200 epoch)](#Step 1 —— v15:低分辨率(200 epoch))
- [Step 2 ------ v16:高分辨率(300 epoch,最终版)](#Step 2 —— v16:高分辨率(300 epoch,最终版))
- 七、关键调参历程
- 八、Per-Scene评估结果
- 九、测试与可视化
-
- [9.1 测试命令](#9.1 测试命令)
- [9.2 可视化(导出PLY → CloudCompare)](#9.2 可视化(导出PLY → CloudCompare))
- 十、文件结构总览
- 十一、快速复现命令汇总
- 附录A:完整配置文件
- 附录B:完整预处理脚本
- 附录C:完整代码修改详情
-
- [C1. softgroup/model/softgroup.py --- Offset Loss 修改](#C1. softgroup/model/softgroup.py — Offset Loss 修改)
- [C2. softgroup/model/softgroup.py --- BFS XY-only 分组](#C2. softgroup/model/softgroup.py — BFS XY-only 分组)
- [C3. softgroup/data/forinstance.py --- 新增文件](#C3. softgroup/data/forinstance.py — 新增文件)
- [C4. softgroup/data/init.py --- 注册数据集](#C4. softgroup/data/init.py — 注册数据集)
- [C5. softgroup/data/custom.py --- Scale增强 + Elastic开关](#C5. softgroup/data/custom.py — Scale增强 + Elastic开关)
- [C6. softgroup/evaluation/instance_eval.py --- 空数组防崩](#C6. softgroup/evaluation/instance_eval.py — 空数组防崩)
- [C7. softgroup/util/dist.py --- 单GPU分布式绕过](#C7. softgroup/util/dist.py — 单GPU分布式绕过)
- 附录D:训练日志关键指标
- 附录E:常见问题(FAQ)
一、环境准备
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怎么办?
检查以下几点:
- 确认
offset_weight已改为[5, 5, 0.1]- 确认
xy_onlyBFS 已启用- 确认
pos_iou_thr设为0.1(不是默认0.5)- 确认
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,Adamlr=0.001在此场景稳定。
最终结果:tree AP=0.506, AP_50=0.634, AP_25=0.703
如有问题欢迎评论区交流!