biptv3 / code /superpoint_ops /mitsuba_visualize_superpoints.py
YYYYYYUUU's picture
Add core reproduction code (binarization layers, PTv3, superpoint ops, min-repro pack)
7b95dc2 verified
Raw
History Blame Contribute Delete
10.3 kB
#!/usr/bin/env python3
"""
S3DIS 超点 / 语义 GT 可视化(Mitsuba 3)。
模式 superpoint_per_class:对 13 个语义类分别只画该类的点,按超点 id 上色,各输出一张 PNG。
用法见 --help。
"""
from __future__ import annotations
import argparse
import math
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
def _pick_variant():
"""scalar_rgb 与 mi_objects 中 Transform 序列化兼容。"""
import mitsuba as mi
for v in ("scalar_rgb", "llvm_ad_rgb", "cuda_ad_rgb"):
try:
mi.set_variant(v)
return v
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 _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("未安装 Pillow,跳过裁剪", 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("裁剪失败:无法读取刚写入的 PNG", 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(
mi,
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,
):
"""points: (N,3) 已归一化前原始坐标;label_for_color 与 points 等长。"""
import numpy as np
from lib_render import center_normalize
from mi_objects import MiFloor, MiScene, MiSensor, MiSoftlight, MiSphere
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()
import numpy as np
import mitsuba as mi
default_root = os.path.normpath(
os.path.join(_HERE, "..", "_work_biptv3", "pointcept_framework", "data", "s3dis_official")
)
ap = argparse.ArgumentParser()
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="单张输出路径(superpoint / segment 模式必填)",
)
ap.add_argument(
"--out_dir",
type=str,
default=None,
help="superpoint_per_class 时输出目录,默认 outputs/superpoint_vis/<room>_per_class_sp/",
)
ap.add_argument(
"--mode",
choices=("superpoint", "segment", "superpoint_per_class"),
default="superpoint",
)
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)
superpoint = np.load(os.path.join(room, "superpoint.npy")).reshape(-1).astype(np.int64)
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(
mi,
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":
labels = np.load(os.path.join(room, "superpoint.npy")).reshape(-1).astype(np.int64)
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(
mi,
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()