1. 背景本文记录在3D-OutDet上跑通处理后数据集WADS的完整过程。原始项目默认使用.bin .label数据格式而我自己的数据为点云.pcd标签.txt记录点索引与类别因此需要完成数据格式转换PCD → BIN标签格式适配数据预处理流程调整2. 数据格式说明原始数据WADS点云.pcd标签.txttxt中的类型 点索引, 类别ID目标格式3DOutDet点云.binfloat32, N×4标签.labelint32, N×1本文核心就是完成这一转换并打通训练流程3.项目结构精简在阅读源码时先剔除与当前任务无关的部分可以降低理解成本dataset/_spray_dataset.py ← SemanticSpray 专用fake_dist.py ← spray 相关saved_models/bin_desnow_kitti/ ← SnowyKITTI 预训练权重bin_filter_spray/ ← spray 预训练权重binary_desnow_kitti.yaml ← KITTI 配置binary_filter_spray.yaml ← spray 配置eval_kitti.pyeval_spray.pytrain_kitti.pytrain_spray.pypublisher.py ← ROS 实时推理用subscriber.py ← ROS 实时推理用flops.py ← 只是测算力不影响训练preprocessing_time.py ← 只是计时不影响训练sample_data/ ← 示例数据可忽略本文重点关注WADS数据适配 训练流程4.环境准备Docker 依赖问题我使用 Docker 运行避免环境污染docker run -it \ --gpus all \ --name 3doutdet \ -v /home/bbb/dataset/data:/home/bbb/dataset/data \ --shm-size16g \ network \ bash这样可以直接访问主机数据路径。这里遇到问题glibc 版本冲突报错torch-cluster requires glibc 2.32 Ubuntu 20.04 → glibc 2.31解决方案降低 torch 版本我采用或更换 Docker 基础镜像5.路径修改dataset/remove_duplicate.pysrc_root /home/bbb/dataset/data/WADS/sequencestrain_wads.pyparser.add_argument(-d, --data_dir, default/home/bbb/dataset/data/WADS2) model_save_path /home/bbb/dataset/data/saved_models/wads/eval_wads.pyparser.add_argument(-d, --data_dir, default/home/bbb/dataset/data/WADS2) parser.add_argument(-p, --model_save_path, default/home/bbb/dataset/data/saved_models/wads/outdet.pt) parser.add_argument(-o, --test_output_path, default/home/bbb/dataset/data/eval_results)6.数据格式转换核心步骤import numpy as np import os import sys, os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from dataset._point_cloud_dataset import get_files # ── PCD 读取函数 ──────────────────────────────────────────────────────────── def read_pcd(filepath): 读取 PCD 文件返回 (N, 4) 的 numpy 数组 [x, y, z, intensity] 支持 ASCII 和 binary 格式intensity 字段不存在时补 0 with open(filepath, rb) as f: # 解析 header fields [] size [] count [] data_type ascii num_points 0 while True: line f.readline().decode(utf-8, errorsignore).strip() if line.startswith(FIELDS): fields line.split()[1:] elif line.startswith(SIZE): size list(map(int, line.split()[1:])) elif line.startswith(COUNT): count list(map(int, line.split()[1:])) elif line.startswith(DATA): data_type line.split()[1] break elif line.startswith(POINTS): num_points int(line.split()[1]) # 读取数据 if data_type ascii: data np.loadtxt(f) if data.ndim 1: data data.reshape(1, -1) elif data_type binary: # 构建 dtype dtype_map {1: np.uint8, 2: np.uint16, 4: np.float32, 8: np.float64} dt np.dtype([(f, dtype_map.get(s, np.float32)) for f, s in zip(fields, size)]) raw np.frombuffer(f.read(), dtypedt) data np.column_stack([raw[field].astype(np.float32) for field in fields]) elif data_type binary_compressed: # 需要 lzf 解压使用 open3d 作为 fallback try: import open3d as o3d pcd_o3d o3d.t.io.read_point_cloud(filepath) pts pcd_o3d.point.positions.numpy() if intensity in pcd_o3d.point: inten pcd_o3d.point[intensity].numpy().reshape(-1, 1) else: inten np.zeros((len(pts), 1), dtypenp.float32) return np.hstack([pts, inten]).astype(np.float32) except ImportError: raise RuntimeError( binary_compressed PCD 需要 open3d请执行: pip install open3d) else: raise ValueError(f不支持的 PCD data 类型: {data_type}) # 提取 x y z field_lower [f.lower() for f in fields] xi field_lower.index(x) if x in field_lower else 0 yi field_lower.index(y) if y in field_lower else 1 zi field_lower.index(z) if z in field_lower else 2 x data[:, xi].reshape(-1, 1).astype(np.float32) y data[:, yi].reshape(-1, 1).astype(np.float32) z data[:, zi].reshape(-1, 1).astype(np.float32) # 提取 intensity找 intensity 或 i 字段 if intensity in field_lower: ii field_lower.index(intensity) intensity data[:, ii].reshape(-1, 1).astype(np.float32) elif i in field_lower: ii field_lower.index(i) intensity data[:, ii].reshape(-1, 1).astype(np.float32) else: intensity np.zeros_like(x) return np.hstack([x, y, z, intensity]) # (N, 4) def read_label_txt(filepath, num_points): 读取稀疏索引格式的 txt 标签文件返回 (N, 1) 的 int32 数组 文件格式每行点云索引, 类别ID ... 未出现在文件中的点默认标签为 0正常点 出现在文件中的点标签为对应类别 ID110 噪声点 参数: filepath: txt 文件路径 num_points: 对应点云的总点数从 pcd 读取 labels np.zeros(num_points, dtypenp.int32) # 默认全部为 0正常点 with open(filepath, r) as f: for line in f: line line.strip() if not line: continue parts line.split(,) if len(parts) 2: continue idx int(parts[0].strip()) cls int(parts[1].strip()) if idx num_points: labels[idx] cls else: print(f[WARN] 标签索引 {idx} 超出点云范围 {num_points}跳过) return labels.reshape(-1, 1) # ── 主流程 ───────────────────────────────────────────────────────────────── if __name__ __main__: src_root /home/bbb/dataset/data/WADS/sequences dst_root /home/bbb/dataset/data/WADS2/sequences split [15, 18, 36, 12, 17, 22, 26, 28, 34, 11, 16, 13, 23, 14, 20, 24, 30, 35, 37, 76] # 收集所有 pcd 文件路径 im_idx list() for i_folder in split: velodyne_dir /.join([src_root, str(i_folder).zfill(2), velodyne]) im_idx get_files(velodyne_dir, pcd) # ← 改为 pcd print(f共找到 {len(im_idx)} 个 pcd 文件) for im in im_idx: # ── 读取点云pcd────────────────────── raw_data read_pcd(im) # (N, 4) float32 # ── 读取标签txt────────────────────── label_path im.replace(velodyne, labels) # 去掉 .pcd 后缀加 .txt label_path os.path.splitext(label_path)[0] .txt num_points raw_data.shape[0] annotated_data read_label_txt(label_path, num_points) # (N, 1) int32 # ── 去重 ──────────────────────────────── comb np.concatenate((raw_data, annotated_data), axis1) unique np.unique(comb, axis0) u_raw unique[:, 0:4].reshape(-1).astype(np.float32) u_lab unique[:, 4].reshape(-1).astype(np.int32) # ── 输出路径写 .bin .label 到 WADS2── # 把 pcd 文件路径的 WADS → WADS2扩展名 .pcd → .bin u_im_file im.replace(WADS, WADS2) u_im_file os.path.splitext(u_im_file)[0] .bin u_lab_file u_im_file.replace(velodyne, labels) u_lab_file os.path.splitext(u_lab_file)[0] .label os.makedirs(os.path.dirname(u_im_file), exist_okTrue) os.makedirs(os.path.dirname(u_lab_file), exist_okTrue) # ── 写出二进制格式后续训练代码直接读── u_raw.tofile(u_im_file) u_lab.tofile(u_lab_file) print(f[OK] {os.path.basename(im)} {unique.shape[0]} pts → {u_im_file})7.代码兼容修改由于删除了 spray/kitti 相关内容需要修改__init__.py# from ._spray_dataset import SemanticSprayDataset, SemSprayPointCloudDataset_point_cloud_dataset.pytry: from cuml.common.device_selection import set_global_device_type except ImportError: def set_global_device_type(*args, **kwargs): return None8.去重执行python 3DOutDet/dataset/remove_duplicate.py后成功跑通9.预计算 kNN修改generate_knn_dist_wads.py路径并补上参数文件parser.add_argument(-d, --data_dir, default/home/bbb/dataset/data/WADS2) parser.add_argument(--label_config, typestr, defaultbinary_desnow_wads.yaml)python -m dataset.utils.generate_knn_dist_wads成功跑通10.开始训练python train_wads.py