biptv3 / code /superpoint_ops /superpoint_visualize_s3dis.py
YYYYYYUUU's picture
Add core reproduction code (binarization layers, PTv3, superpoint ops, min-repro pack)
7b95dc2 verified
Raw
History Blame Contribute Delete
15.3 kB
#!/usr/bin/env python3
"""
模式
----
- superpoint:整场景点云,按超点 id 上色
- segment:整场景点云,按语义 GT 上色
- superpoint_per_class:每个语义类单独一张图,仅该类点,按超点 id 上色
----
cd /path/to/PAMI2026
python superpoint_visualize_s3dis.py --room Area_1/office_1 --mode superpoint \\
--out_png outputs/superpoint_vis/office_1_mitsuba_superpoint.png
python superpoint_visualize_s3dis.py --room Area_1/office_1 --mode superpoint_per_class \\
--out_dir outputs/superpoint_vis/Area_1_office_1_per_class_sp_hq --film_size 2560 --spp 256
"""
from __future__ import annotations
import argparse
import math
import os
import sys
import mitsuba as mi
import numpy as np
_HERE = os.path.dirname(os.path.abspath(__file__))
def _pick_variant() -> None:
for v in ("scalar_rgb", "llvm_ad_rgb", "cuda_ad_rgb"):
try:
mi.set_variant(v)
return
except Exception:
continue
raise RuntimeError("无法设置任何 Mitsuba variant")
_GOLDEN = 0.618033988749895
def _hsv_to_rgb(h: float, s: float, v: float):
import colorsys
r, g, b = colorsys.hsv_to_rgb(h % 1.0, s, v)
return r, g, b
def _hash_color(uid: int):
h = (int(uid) * _GOLDEN) % 1.0
return _hsv_to_rgb(h, 0.82, 0.96)
S3DIS_SEGMENT_RGB = [
[0.90, 0.90, 0.92],
[0.72, 0.52, 0.30],
[0.42, 0.72, 0.48],
[0.35, 0.55, 0.85],
[0.95, 0.75, 0.25],
[0.45, 0.85, 0.95],
[0.92, 0.35, 0.35],
[0.95, 0.55, 0.20],
[0.75, 0.45, 0.92],
[0.55, 0.40, 0.88],
[0.50, 0.82, 0.45],
[0.88, 0.50, 0.65],
[0.55, 0.55, 0.58],
]
S3DIS_CLASS_NAMES = [
"ceiling",
"floor",
"wall",
"beam",
"column",
"window",
"door",
"table",
"chair",
"sofa",
"bookcase",
"board",
"clutter",
]
def _class_color(cid: int, n_cls: int = 13):
i = int(cid) % max(n_cls, 1)
return tuple(S3DIS_SEGMENT_RGB[i])
def center_normalize(pcl: np.ndarray) -> np.ndarray:
centroid = np.mean(pcl, axis=0)
pcl_centered = pcl - centroid
max_dist = np.max(pcl_centered)
pcl_normalized = pcl_centered / (2 * max_dist)
pcl_normalized[:, 2] += -0.5 - np.min(pcl_normalized[:, 2])
return pcl_normalized.astype(np.float32)
class MiScene(object):
def __init__(self):
self.scene = {
"type": "scene",
"integrator": {"type": "path", "max_depth": -1},
}
def dict(self):
return self.scene
def add(self, key, object):
self.scene[key] = object.dict()
class MiSensor(object):
def __init__(
self,
origin: list,
target: list,
fov: int = 25,
sample_count: int = 256,
film_width: int = 540,
film_height: int = 540,
):
self.origin = origin
self.target = target
self.fov = fov
self.sample_count = sample_count
self.film_width = film_width
self.film_height = film_height
def dict(self):
return {
"type": "perspective",
"fov": self.fov,
"near_clip": 0.1,
"far_clip": 100.0,
"to_world": mi.Transform4f().look_at(
origin=self.origin, target=self.target, up=[0, 0, 1]
),
"sampler": {"type": "ldsampler", "sample_count": self.sample_count},
"film": {
"type": "hdrfilm",
"width": self.film_width,
"height": self.film_height,
"rfilter": {"type": "gaussian"},
},
}
class MiFloor(object):
def __init__(self, width: int = 10, height: int = 10, color: list | None = None):
self.width = width
self.height = height
self.color = color if color is not None else [1, 1, 1]
def dict(self):
return {
"type": "rectangle",
"to_world": mi.Transform4f()
.scale([self.width, self.height, 1])
.translate([0, 0, -0.5]),
"bsdf": {
"type": "roughplastic",
"distribution": "ggx",
"alpha": 0.05,
"int_ior": 1.46,
"diffuse_reflectance": {"type": "rgb", "value": self.color},
},
}
class MiSoftlight(object):
def __init__(
self,
origin: list,
target: list,
intensity: float = 12.0,
radiance: list | None = None,
width: int = 10,
height: int = 10,
):
self.origin = origin
self.target = target
self.width = width
self.height = height
if radiance is not None:
self.radiance = [float(x) for x in radiance]
else:
v = float(intensity)
self.radiance = [v, v, v]
def dict(self):
return {
"type": "rectangle",
"to_world": mi.Transform4f()
.look_at(origin=self.origin, target=self.target, up=[0, 0, 1])
.scale([self.width, self.height, 1]),
"emitter": {
"type": "area",
"radiance": {"type": "rgb", "value": self.radiance},
},
}
class MiSphere(object):
def __init__(self, center: list, radius: float, color: list, plastic: bool = False):
self.center = center
self.radius = radius
self.color = color
self.plastic = plastic
def dict(self):
if self.plastic:
bsdf = {
"type": "plastic",
"diffuse_reflectance": {"type": "rgb", "value": self.color},
"specular_reflectance": {"type": "rgb", "value": [0.25, 0.25, 0.25]},
"int_ior": 1.5,
}
else:
bsdf = {
"type": "diffuse",
"reflectance": {"type": "rgb", "value": self.color},
}
return {
"type": "sphere",
"center": self.center,
"radius": self.radius,
"bsdf": bsdf,
}
def _maybe_crop_png(
path: str, crop_wh: tuple[int, int] | None, center: bool = False
) -> str | None:
if not crop_wh:
return None
try:
import time
from PIL import Image
except ImportError:
print("跳过裁剪", file=sys.stderr)
return None
w, h = crop_wh
im = None
for _ in range(40):
try:
im = Image.open(path)
im.load()
break
except (OSError, Exception):
time.sleep(0.05)
if im is None:
print("裁剪失败", file=sys.stderr)
return None
im = im.convert("RGB")
cw, ch = min(w, im.size[0]), min(h, im.size[1])
if center:
left = max(0, (im.size[0] - cw) // 2)
top = max(0, (im.size[1] - ch) // 2)
im = im.crop((left, top, left + cw, top + ch))
else:
im = im.crop((0, 0, cw, ch))
base, ext = os.path.splitext(path)
out = base + "_crop" + ext
im.save(out)
return out
def _render_one(
points: np.ndarray,
label_for_color: np.ndarray,
color_fn,
out_png: str,
sphere_radius: float,
film_size: int,
spp: int,
fov: float,
use_plastic: bool,
light_intensity: float,
fill_irradiance: float,
) -> bool:
n = points.shape[0]
if n == 0:
return False
pts = center_normalize(points.astype(np.float64))
pts[:, 2] += sphere_radius / 2.0
lds_spp = max(16, 2 ** int(round(math.log(max(int(spp), 16), 2))))
lds_spp = min(lds_spp, 1024)
scene = MiScene()
scene.add(
"sensor",
MiSensor(
origin=[2, 2, 2],
target=[0, 0, 0],
fov=int(round(fov)),
sample_count=lds_spp,
film_width=int(film_size),
film_height=int(film_size),
),
)
scene.add("floor", MiFloor(width=10, height=10, color=[0.96, 0.96, 0.98]))
scene.add(
"soft_light",
MiSoftlight(
origin=[-4, 4, 20],
target=[0, 0, 0],
intensity=float(light_intensity),
),
)
for i in range(n):
c = color_fn(int(label_for_color[i]))
clr = [float(c[0]), float(c[1]), float(c[2])]
scene.add(
f"sphere{i}",
MiSphere(pts[i].tolist(), float(sphere_radius), clr, plastic=use_plastic),
)
scene_dict = scene.dict()
fi = float(fill_irradiance)
scene_dict["fill_sun"] = {
"type": "directional",
"direction": [0.35, -0.55, 0.75],
"irradiance": {"type": "rgb", "value": [fi, fi * 0.98, fi * 0.95]},
}
scene_mi = mi.load_dict(scene_dict)
img = mi.render(scene_mi, spp=int(spp))
out = os.path.abspath(out_png)
os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
mi.util.write_bitmap(out, img)
print("MITSUBA_PNG", out)
return True
def main():
_pick_variant()
default_root = os.path.normpath(
os.path.join(_HERE, "..", "_work_biptv3", "pointcept_framework", "data", "s3dis_official")
)
ap = argparse.ArgumentParser(description="S3DIS 超点superpoint/语义segment可视化")
ap.add_argument("--data_root", type=str, default=default_root)
ap.add_argument("--room", type=str, required=True)
ap.add_argument(
"--out_png",
type=str,
default=None,
help="输出路径",
)
ap.add_argument(
"--out_dir",
type=str,
default=None,
help="输出目录默认 outputs/superpoint_vis/<room>_per_class_sp/",
)
ap.add_argument(
"--mode",
choices=("superpoint", "segment", "superpoint_per_class"),
default="superpoint",
)
ap.add_argument(
"--label_npy",
type=str,
default=None,
help="optional override for room/superpoint.npy in superpoint modes",
)
ap.add_argument("--max_points", type=int, default=40000)
ap.add_argument("--sphere_radius", type=float, default=0.008)
ap.add_argument("--film_size", type=int, default=2560)
ap.add_argument("--spp", type=int, default=256)
ap.add_argument("--fov", type=float, default=25.0)
ap.add_argument("--crop", type=str, default=None)
ap.add_argument("--crop_center", action="store_true")
ap.add_argument("--no_plastic_spheres", action="store_true")
ap.add_argument("--light_intensity", type=float, default=14.0)
ap.add_argument("--fill_irradiance", type=float, default=3.5)
ap.add_argument(
"--min_class_points",
type=int,
default=50,
help="superpoint_per_class:点数少于此的类需要过绿",
)
args = ap.parse_args()
room = os.path.join(args.data_root, args.room)
coord = np.load(os.path.join(room, "coord.npy"))
n_all = coord.shape[0]
use_plastic = not args.no_plastic_spheres
room_tag = args.room.replace("/", "_")
if args.mode == "superpoint_per_class":
segment = np.load(os.path.join(room, "segment.npy")).reshape(-1).astype(np.int64)
label_path = args.label_npy or os.path.join(room, "superpoint.npy")
superpoint = np.load(label_path).reshape(-1).astype(np.int64)
print("LABEL_NPY", os.path.abspath(label_path))
if len(segment) != n_all or len(superpoint) != n_all:
raise ValueError("segment/superpoint 与 coord 长度不一致需要检查")
out_dir = args.out_dir
if not out_dir:
out_dir = os.path.join(_HERE, "outputs", "superpoint_vis", f"{room_tag}_per_class_sp")
out_dir = os.path.abspath(out_dir)
os.makedirs(out_dir, exist_ok=True)
written = []
for cls in range(13):
mask = segment == cls
cnt = int(mask.sum())
if cnt < args.min_class_points:
print(
f"skip cls{cls:02d} {S3DIS_CLASS_NAMES[cls]}: n={cnt} (<{args.min_class_points})"
)
continue
coord_c = coord[mask]
sp_c = superpoint[mask]
if cnt > args.max_points:
rng = np.random.default_rng(cls)
idx = rng.choice(cnt, size=args.max_points, replace=False)
coord_c = coord_c[idx]
sp_c = sp_c[idx]
name = S3DIS_CLASS_NAMES[cls]
out_png = os.path.join(out_dir, f"{room_tag}_cls{cls:02d}_{name}_superpoint.png")
ok = _render_one(
coord_c,
sp_c,
_hash_color,
out_png,
args.sphere_radius,
args.film_size,
args.spp,
args.fov,
use_plastic,
args.light_intensity,
args.fill_irradiance,
)
if ok:
written.append(out_png)
if args.crop:
wh = args.crop.lower().replace(" ", "").split("x")
if len(wh) == 2:
cw, ch = int(wh[0]), int(wh[1])
cpath = _maybe_crop_png(out_png, (cw, ch), center=args.crop_center)
if cpath:
print("MITSUBA_PNG_CROP", cpath)
print("PER_CLASS_DONE", len(written), "files in", out_dir)
return
if not args.out_png:
sys.exit("非 superpoint_per_class --out_png")
if args.mode == "superpoint":
label_path = args.label_npy or os.path.join(room, "superpoint.npy")
labels = np.load(label_path).reshape(-1).astype(np.int64)
print("LABEL_NPY", os.path.abspath(label_path))
color_fn = _hash_color
else:
labels = np.load(os.path.join(room, "segment.npy")).reshape(-1).astype(np.int64)
color_fn = _class_color
if len(labels) != n_all:
raise ValueError("标签与 coord 长度不一致需要检查")
coord_u = coord
labels_u = labels
if n_all > args.max_points:
rng = np.random.default_rng(0)
idx = rng.choice(n_all, size=args.max_points, replace=False)
coord_u = coord[idx]
labels_u = labels[idx]
_render_one(
coord_u,
labels_u,
color_fn,
args.out_png,
args.sphere_radius,
args.film_size,
args.spp,
args.fov,
use_plastic,
args.light_intensity,
args.fill_irradiance,
)
if args.crop:
wh = args.crop.lower().replace(" ", "").split("x")
if len(wh) == 2:
cw, ch = int(wh[0]), int(wh[1])
cropped = _maybe_crop_png(args.out_png, (cw, ch), center=args.crop_center)
if cropped:
print("MITSUBA_PNG_CROP", cropped)
if __name__ == "__main__":
main()