File size: 4,669 Bytes
08cde47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/usr/bin/env python3
"""
S3DIS 超点可视化(编辑节点 / poplab)

数据侧 superpoint 来源(与预处理一致):
  - 每个房间目录下的 superpoint.npy:与 coord.npy 等长的 int32,表示每个点所属超点 id。
  - 生成逻辑(无外部分割器时):体素坐标 +(可选)法向粗分箱 -> np.unique 得到逆映射作为 id。
  - 见 pointcept/datasets/preprocessing/s3dis/preprocess_s3dis.py 中 generate_superpoint_labels。

用法示例:
  python scripts/visualize_s3dis_superpoints.py \\
    --room Area_1/office_1 \\
    --out /tmp/superpoints_vis.ply

  python scripts/visualize_s3dis_superpoints.py --room Area_1/office_1 --recompute --voxel 0.12 --normal-bins 8
"""

from __future__ import annotations

import argparse
import os
from pathlib import Path

import numpy as np


def generate_superpoint_labels(coords, normals=None, voxel_size=0.12, normal_bins=8):
    """与 preprocess_s3dis.generate_superpoint_labels 一致的几何 fallback。"""
    coords = np.asarray(coords, dtype=np.float32)
    coord_min = coords.min(axis=0, keepdims=True)
    voxel_coord = np.floor((coords - coord_min) / max(float(voxel_size), 1e-4)).astype(np.int64)

    if normals is not None and len(normals) == len(coords):
        normals = np.asarray(normals, dtype=np.float32)
        normals = normals / (np.linalg.norm(normals, axis=1, keepdims=True) + 1e-8)
        normal_q = np.floor((normals + 1.0) * 0.5 * normal_bins).astype(np.int64)
        normal_q = np.clip(normal_q, 0, normal_bins)
        tokens = np.concatenate([voxel_coord, normal_q], axis=1)
    else:
        tokens = voxel_coord

    _, inverse = np.unique(tokens, axis=0, return_inverse=True)
    return inverse.astype(np.int32)


def sp_ids_to_colors(sp: np.ndarray) -> np.ndarray:
    """稳定伪彩色:对每个 superpoint id 做哈希映射到 [0,1]^3。"""
    sp = sp.astype(np.int64).reshape(-1)
    n = len(sp)
    colors = np.zeros((n, 3), dtype=np.float64)
    for uid in np.unique(sp):
        h = (int(uid) * 1103515245 + 12345) & 0x7FFFFFFF
        r = ((h >> 0) & 255) / 255.0
        g = ((h >> 8) & 255) / 255.0
        b = ((h >> 16) & 255) / 255.0
        colors[sp == uid] = (r, g, b)
    return np.clip(colors, 0.0, 1.0)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument(
        "--data_root",
        type=Path,
        default=Path(__file__).resolve().parents[1] / "data" / "s3dis_official",
        help="S3DIS 处理根目录(含 Area_*/*/)",
    )
    ap.add_argument("--room", type=str, required=True, help="例如 Area_1/office_1")
    ap.add_argument("--out", type=Path, default=Path("superpoints_vis.ply"))
    ap.add_argument(
        "--recompute",
        action="store_true",
        help="不读 superpoint.npy,按 generate_superpoint_labels 现场重算(用于对照)",
    )
    ap.add_argument("--voxel", type=float, default=0.12)
    ap.add_argument("--normal-bins", type=int, default=8)
    args = ap.parse_args()

    room_dir = args.data_root / args.room
    coord_p = room_dir / "coord.npy"
    if not coord_p.is_file():
        raise FileNotFoundError(coord_p)

    coord = np.load(coord_p)
    normal = None
    npy_n = room_dir / "normal.npy"
    if npy_n.is_file():
        normal = np.load(npy_n)

    if args.recompute:
        sp = generate_superpoint_labels(
            coord, normals=normal, voxel_size=args.voxel, normal_bins=args.normal_bins
        )
        tag = "recomputed"
    else:
        sp_p = room_dir / "superpoint.npy"
        if not sp_p.is_file():
            raise FileNotFoundError(
                f"{sp_p} 不存在,可先跑预处理加 --generate_superpoint,或改用 --recompute"
            )
        sp = np.load(sp_p).reshape(-1).astype(np.int32)
        tag = "disk"

    if len(sp) != len(coord):
        raise ValueError(f"superpoint 长度 {len(sp)} != coord 长度 {len(coord)}")

    colors = sp_ids_to_colors(sp)
    try:
        import open3d as o3d

        pcd = o3d.geometry.PointCloud()
        pcd.points = o3d.utility.Vector3dVector(coord.astype(np.float64))
        pcd.colors = o3d.utility.Vector3dVector(colors)
        args.out.parent.mkdir(parents=True, exist_ok=True)
        o3d.io.write_point_cloud(str(args.out), pcd)
        print(f"[{tag}] wrote {args.out}  points={len(coord)}  unique_superpoints={len(np.unique(sp))}")
        print("若在本机有显示器,可取消注释 o3d.visualization.draw_geometries([pcd])")
        # o3d.visualization.draw_geometries([pcd])
    except ImportError:
        print("未安装 open3d,无法写 PLY;请: pip install open3d")


if __name__ == "__main__":
    main()