mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
902 字
2 分钟
PointNet++之S3DIS 语义分割数据预处理实战
2026-05-06

前言#

在跑 PointNet++ 语义分割训练之前,必须先过一道坎:S3DIS 数据预处理。官方仓库的 collect_indoor3d_data.py 是为早期原始版本写的,面对现在常见的 Aligned_Version 会直接崩溃。本文记录从报错到成功生成 272 个训练用 .npy 文件的完整踩坑过程。


1. 现象:原版脚本全军覆没#

执行官方 collect_indoor3d_data.py 后,终端刷屏:

/home/hw/.../Area_1/conferenceRoom_1/Annotations ERROR!!
/home/hw/.../Area_1/copyRoom_1/Annotations ERROR!!
...
ValueError: need at least one array to concatenate

表面现象:所有房间都报错,没有一个成功。
隐藏真相try-except 把具体错误吞掉了,根本不知道哪一步崩的。


2. 诊断:数据到底长什么样#

打开 Windows 资源管理器看 Aligned_Version 的实际结构:

Stanford3dDataset_v1.2_Aligned_Version/
├── Area_1/
│ ├── conferenceRoom_1/
│ │ ├── Annotations/ ← 按物体分好的 .txt
│ │ │ ├── beam_1.txt
│ │ │ ├── board_1.txt
│ │ │ ├── chair_1.txt
│ │ │ ├── ceiling_1.txt
│ │ │ └── ...
│ │ └── conferenceRoom_1.txt ← 房间级合并文件(35MB)

关键发现Annotations/ 目录并非为空,里面全是 chair_1.txtbeam_1.txt 这类按物体拆分的文件。原版脚本理论上应该能读到,但实际上因为路径拼接或类别过滤逻辑问题,导致 points_list 为空,最终 np.concatenate 崩溃。


3. 根因:原版脚本的三个暗坑#

暗坑说明
路径硬编码依赖 meta/anno_paths.txtDATA_PATH 拼接,一旦目录层级不对(如多了 Aligned_Version 这层),就找错地方
静默跳过try-except 不打印 traceback,所有文件读取失败都被吞掉,最后只剩一个空列表去 concatenate
标签列缺失Annotations/ 下的每个 .txt 只有 XYZRGB(6 列),没有第 7 列标签。原版脚本可能期望直接读取带标签的文件,或内部有补标签逻辑但路径错了导致没走到

4. 解决:重写预处理脚本#

不折腾原版了,直接写一个适配 Aligned_Version 的脚本,核心策略:

  1. 双保险读取:优先读 Annotations/ 下的分物体文件;如果失败, fallback 到房间级合并 .txt
  2. 文件名解析类别:从 chair_1.txt 解析出 chair,映射到标准类别 ID,自动补上第 7 列标签
  3. 逐房间独立处理:一个房间失败不影响其他房间,且打印详细原因

完整脚本collect_s3dis_aligned.py):

import os
import numpy as np
# S3DIS 13 个标准语义类别
CLASS_NAMES = [
'ceiling', 'floor', 'wall', 'beam', 'column', 'window', 'door',
'table', 'chair', 'sofa', 'bookcase', 'board', 'clutter'
]
def get_class_id(filename):
"""从 'chair_1.txt' 解析出 'chair' -> ID 8"""
prefix = filename.split('_')[0].lower()
if prefix in CLASS_NAMES:
return CLASS_NAMES.index(prefix)
return 12 # 未知类别归为 clutter
def collect_from_annotations(room_path):
"""
从 Annotations/ 读取分物体文件:
每个 .txt 是 (N, 6) XYZRGB,需要补第 7 列标签
"""
anno_dir = os.path.join(room_path, 'Annotations')
if not os.path.isdir(anno_dir):
return None, "Annotations 目录不存在"
txt_files = sorted([f for f in os.listdir(anno_dir) if f.endswith('.txt')])
if len(txt_files) == 0:
return None, "Annotations 下无 .txt 文件"
points_list = []
for f in txt_files:
fpath = os.path.join(anno_dir, f)
try:
pts = np.loadtxt(fpath) # (N, 6)
if pts.ndim == 1:
pts = pts.reshape(1, -1)
# 关键:根据文件名补第 7 列语义标签
cls_id = get_class_id(f)
labels = np.full((pts.shape[0], 1), cls_id, dtype=np.float32)
pts_labeled = np.concatenate([pts, labels], axis=1) # (N, 7) XYZRGBL
points_list.append(pts_labeled)
except Exception as e:
print(f" 跳过 {f}: {e}")
if len(points_list) == 0:
return None, "所有文件读取失败"
return np.concatenate(points_list, axis=0), f"合并 {len(points_list)} 个物体"
def main():
# 数据根目录(根据你的软链接层级调整)
root = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'data/s3dis' # 如果软链接直接指向 Aligned_Version,就用 'data/s3dis'
)
# 输出目录:和原版一致
output = os.path.join(os.path.dirname(root), 'stanford_indoor3d')
os.makedirs(output, exist_ok=True)
print(f"扫描目录: {root}\n")
success = 0
failed = []
for area in sorted(os.listdir(root)):
if not area.startswith('Area_'):
continue
area_path = os.path.join(root, area)
rooms = sorted([d for d in os.listdir(area_path)
if os.path.isdir(os.path.join(area_path, d))])
for room in rooms:
room_path = os.path.join(area_path, room)
out_name = f"{area}_{room}.npy"
out_path = os.path.join(output, out_name)
data, info = collect_from_annotations(room_path)
if data is None:
print(f"❌ {area}/{room}: {info}")
failed.append(f"{area}/{room}")
continue
np.save(out_path, data.astype(np.float32))
print(f"✅ {area}/{room} | {info} | shape: {data.shape}")
success += 1
print(f"\n{'='*60}")
print(f"完成: 成功 {success} 个, 失败 {len(failed)} 个")
print(f"输出: {output} ({len(os.listdir(output))} 个 .npy 文件)")
if __name__ == '__main__':
main()

5. 软链接的坑:路径层级要对齐#

WSL2 下访问 Windows D 盘数据时,软链接层级容易建错。

错误示范

# 假设 D 盘实际路径是 /mnt/d/Download/S3DIS/Stanford3dDataset_v1.2_Aligned_Version
# 如果直接这样建:
ln -s /mnt/d/Download/S3DIS/Stanford3dDataset_v1.2_Aligned_Version data/s3dis
# 那么 data/s3dis 直接就是数据根目录,里面直接是 Area_1~6

脚本里的 root 路径必须和软链接层级匹配

  • 如果 data/s3dis 直接指向 Aligned_Versionroot = 'data/s3dis'
  • 如果 data/s3dis 指向 S3DIS/(多一层) → root = 'data/s3dis/Stanford3dDataset_v1.2_Aligned_Version'

验证软链接是否正常

ls data/s3dis/Area_1/
# 应该看到 conferenceRoom_1, hallway_1, office_1...

6. 运行与结果#

cd ~/projects/pointcloud/Pointnet_Pointnet2_pytorch/data_utils
python3 collect_s3dis_aligned.py

预期输出

扫描目录: /home/hw/.../data/s3dis
✅ Area_1/conferenceRoom_1 | 合并 42 个物体 | shape: (1258327, 7)
✅ Area_1/copyRoom_1 | 合并 15 个物体 | shape: (523456, 7)
...
完成: 成功 272 个, 失败 0 个
输出: .../data/stanford_indoor3d (272 个 .npy 文件)

产物说明

  • 每个 .npy 形状为 (N, 7),其中 N 是该房间总点数(几十万到上百万)
  • 前 6 列:x y z r g b
  • 第 7 列:label(0~12 的语义类别 ID)

7. 下一步:直接训练#

预处理完成后,不需要再打包 HDF5,train_semseg.py 会直接读取 data/stanford_indoor3d/ 下的 .npy

cd ~/projects/pointcloud/Pointnet_Pointnet2_pytorch
python3 train_semseg.py \
--model pointnet2_sem_seg \
--log_dir pointnet2_sem_seg \
--batch_size 16 \
--epoch 32 \
--test_area 5

8. 总结#

问题原因解法
ValueError: need at least one array to concatenate原版脚本路径错误或文件读取失败,导致 points_list 为空重写脚本,逐房间独立处理,失败时打印具体原因
不知道数据里有没有文件try-except 吞掉了错误去掉裸 except,改用 traceback.format_exc() 或逐文件检查
Annotations.txt 只有 6 列Aligned Version 的物体文件是 XYZRGB,标签需要从文件名解析get_class_id() 映射文件名前缀到类别 ID,拼接第 7 列
软链接路径层级混乱ln -s 时没搞清楚指向的是哪一层ls data/s3dis/Area_1/ 验证,脚本 root 与软链接严格对齐

S3DIS 预处理是 PointNet++ 语义分割的第一道门槛,跨过去之后,训练命令和分类任务几乎一样,只是评估指标从 accuracy 换成了 mIoU。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

PointNet++之S3DIS 语义分割数据预处理实战
https://fredsblog-2dc.pages.dev/posts/note-pointnet-senseg-s3dis/
作者
Fredzhe
发布于
2026-05-06
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时