| |
| """ |
| Superpoint Generation Core — S3DIS |
| =================================== |
| |
| knn_graph(points, k) -> segment_point(points, normals, edges, kThresh, segMinVerts) |
| |
| 超点是点云中的局部过分割单元,类比图像中的 superpixel。 |
| 先把空间上相邻、法向量相近的点分成紧凑的小块, |
| 再供下游语义网络在超点粒度上做学习与推理。 |
| |
| 算法: |
| 1. 对输入点云构建 k-近邻图 (knn_graph, k=50) |
| 2. 对每条边计算权重:w = 1 - dot(n1, n2);凸区域 w = w^2 |
| 3. 按权重升序排列,执行 Felzenszwalb 图分割: |
| - 初始每个点为独立分量,阈值 = kThresh |
| - 若边权 <= 两端分量阈值,合并,更新阈值 = w + kThresh / size |
| 4. 合并过小分量 (< segMinVerts) |
| |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import sys |
| import time |
| from pathlib import Path |
|
|
| import numpy as np |
|
|
| DEFAULT_SEGMENTATOR_CPP = Path('segmentator.cpp') |
| DEFAULT_BUILD_DIR = Path('torch_build') |
|
|
| _cached_seg_module = None |
|
|
|
|
| def load_segmentator(cpp_path: Path = DEFAULT_SEGMENTATOR_CPP, |
| build_dir: Path = DEFAULT_BUILD_DIR, |
| verbose: bool = False): |
| |
| global _cached_seg_module |
| if _cached_seg_module is not None: |
| return _cached_seg_module |
|
|
| from torch.utils.cpp_extension import load |
|
|
| cpp_path = Path(cpp_path) |
| build_dir = Path(build_dir) |
| if not cpp_path.is_file(): |
| raise FileNotFoundError(f'segmentator.cpp not found: {cpp_path}') |
| build_dir.mkdir(parents=True, exist_ok=True) |
|
|
| _cached_seg_module = load( |
| name='libsegmentator_dyn', |
| sources=[str(cpp_path)], |
| build_directory=str(build_dir), |
| extra_cflags=['-O3'], |
| verbose=verbose, |
| ) |
| return _cached_seg_module |
|
|
|
|
| def generate_superpoints( |
| coords: np.ndarray, |
| normals: np.ndarray, |
| knn_k: int = 50, |
| k_thresh: float = 0.01, |
| seg_min_verts: int = 20, |
| cpp_path: Path = DEFAULT_SEGMENTATOR_CPP, |
| build_dir: Path = DEFAULT_BUILD_DIR, |
| ) -> np.ndarray: |
| """基于 knn_graph + Felzenszwalb 图分割生成超点标签。 |
| |
| Parameters |
| ---------- |
| coords : (N, 3) float32 — 点坐标 |
| normals : (N, 3) float32 — 点法向量 |
| knn_k : int — k-近邻数,控制图的连通密度 |
| k_thresh : float — Felzenszwalb 分割阈值,越小超点越细 |
| seg_min_verts : int — 最小超点点数,低于此值的分量会被合并 |
| |
| Returns |
| ------- |
| labels : (N,) int32 — 每个点的超点标签,从 0 开始连续编号 |
| """ |
| import torch |
| from torch_cluster import knn_graph |
|
|
| coords = np.ascontiguousarray(np.asarray(coords, dtype=np.float32)) |
| normals = np.ascontiguousarray(np.asarray(normals, dtype=np.float32)) |
| if coords.shape != normals.shape: |
| raise ValueError(f'coords/normals shape mismatch: {coords.shape} vs {normals.shape}') |
|
|
| |
| normals = normals / (np.linalg.norm(normals, axis=1, keepdims=True) + 1e-8) |
|
|
| seg = load_segmentator(cpp_path, build_dir) |
|
|
| pts = torch.from_numpy(coords) |
| nrm = torch.from_numpy(normals) |
|
|
| |
| edges = knn_graph(pts, k=knn_k).T.contiguous().to(dtype=torch.int64, device='cpu') |
|
|
| |
| labels = seg.segment_point( |
| pts.contiguous(), nrm.contiguous(), edges, |
| float(k_thresh), int(seg_min_verts), |
| ) |
| labels = labels.cpu().numpy().reshape(-1).astype(np.int64) |
|
|
| |
| _, inverse = np.unique(labels, return_inverse=True) |
| return inverse.astype(np.int32) |
|
|
|
|
| def iter_rooms(data_root: Path): |
| for area in sorted(data_root.glob('Area_*')): |
| if not area.is_dir(): |
| continue |
| for room in sorted(area.iterdir()): |
| if room.is_dir() and (room / 'coord.npy').is_file(): |
| yield room |
|
|
|
|
| def process_room(room_dir: Path, args) -> dict: |
| """对单个房间生成超点标签""" |
| coord = np.load(room_dir / 'coord.npy').astype(np.float32) |
| normal_path = room_dir / 'normal.npy' |
| if not normal_path.is_file(): |
| raise FileNotFoundError(f'normal.npy required: {room_dir}') |
| normal = np.load(normal_path).astype(np.float32) |
|
|
| t0 = time.time() |
| labels = generate_superpoints( |
| coord, normal, |
| knn_k=args.knn_k, |
| k_thresh=args.k_thresh, |
| seg_min_verts=args.seg_min_verts, |
| cpp_path=Path(args.segmentator_cpp), |
| build_dir=Path(args.build_dir), |
| ) |
| elapsed = time.time() - t0 |
|
|
| uniq, cnt = np.unique(labels, return_counts=True) |
| rel_path = room_dir.relative_to(args.data_root) |
|
|
| info = { |
| 'room': str(rel_path), |
| 'points': int(coord.shape[0]), |
| 'num_superpoints': int(uniq.size), |
| 'mean_pts_per_sp': round(float(cnt.mean()), 1), |
| 'max_pts_per_sp': int(cnt.max()), |
| 'min_pts_per_sp': int(cnt.min()), |
| 'time_sec': round(elapsed, 1), |
| 'params': { |
| 'knn_k': args.knn_k, |
| 'k_thresh': args.k_thresh, |
| 'seg_min_verts': args.seg_min_verts, |
| }, |
| } |
|
|
|
|
| disk_sp = room_dir / 'superpoint.npy' |
| if disk_sp.is_file(): |
| old = np.load(disk_sp).reshape(-1) |
| if len(old) == len(labels): |
| old_uniq = np.unique(old) |
| info['existing_num_superpoints'] = int(old_uniq.size) |
|
|
| |
| if args.write: |
| if args.output_root: |
| out_path = Path(args.output_root) / rel_path / 'superpoint.npy' |
| else: |
| out_path = room_dir / 'superpoint.npy' |
| out_path.parent.mkdir(parents=True, exist_ok=True) |
| np.save(str(out_path), labels) |
| info['output'] = str(out_path) |
|
|
| return info |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser(description='S3DIS Superpoint Generation (segmentator / Felzenszwalb)') |
| ap.add_argument('--data_root', type=Path, default=Path('/mnt/data/AODUOLI/_work_biptv3/pointcept_framework/data/s3dis_official')) |
| ap.add_argument('--room', type=str, default=None, help=' Area_1/office_1') |
| ap.add_argument('--all', action='store_true', help='ALL') |
| ap.add_argument('--knn_k', type=int, default=50) |
| ap.add_argument('--k_thresh', type=float, default=0.01) |
| ap.add_argument('--seg_min_verts', type=int, default=20) |
| ap.add_argument('--segmentator_cpp', type=str, default=str(DEFAULT_SEGMENTATOR_CPP)) |
| ap.add_argument('--build_dir', type=str, default=str(DEFAULT_BUILD_DIR)) |
| ap.add_argument('--write', action='store_true', help='写出 superpoint.npy') |
| ap.add_argument('--output_root', type=str, default=None, help='output') |
| args = ap.parse_args() |
|
|
| if not args.data_root.is_dir(): |
| print(f'data_root not found: {args.data_root}', file=sys.stderr) |
| sys.exit(1) |
|
|
| rooms = [] |
| if args.room: |
| rooms = [args.data_root / args.room] |
| elif args.all: |
| rooms = list(iter_rooms(args.data_root)) |
| else: |
| print('Please specify --room <path> or --all', file=sys.stderr) |
| sys.exit(1) |
|
|
| print(f'Rooms: {len(rooms)}, knn_k={args.knn_k}, k_thresh={args.k_thresh}, seg_min_verts={args.seg_min_verts}') |
| if not args.write: |
| print('[DRY-RUN] Add --write to save files.\n') |
|
|
| for room_dir in rooms: |
| if not (room_dir / 'coord.npy').is_file(): |
| print(f'SKIP: {room_dir}') |
| continue |
| info = process_room(room_dir, args) |
| print(json.dumps(info, ensure_ascii=False)) |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|