Reproduces the SUM Parts (CVPR 2025) face-labeling benchmark on a single consumer GPU, then applies it to drone-photogrammetry road survey meshes. Verified on RTX 3060 12GB / WSL2 Ubuntu 22.04 / CUDA 11.8 / torch 2.0.1: - CUDA extensions build (pointnet2_batch, pointops, chamfer_dist, emd, subsampling) - PointNet 100 epochs reaches mIoU 17.19, matching the paper's reported 15.1 - OBJ -> PLY conversion round-trips through the model and yields per-point predictions Four upstream source patches, all idempotent, originals preserved: - numpy aliases removed in 1.24 (np.long etc.) and collections ABCs moved in python 3.10 - the blind test split ships label = -1, which crashed ConfusionMatrix - mode=val referenced `epoch` before assignment Documents the traps that cost the most time, including VRAM overflow silently falling back to host RAM on WSL2 (25-100x slowdown, no OOM) and the colour scale mismatch between r/g/b float32 and red/green/blue uint8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
69 lines
2.3 KiB
Python
69 lines
2.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Rewrite a SUM-schema PLY into the spelling desktop viewers expect.
|
|
|
|
The training files carry colour as r/g/b float32 in [0,1] — that is what the
|
|
SUM Parts loader reads and what the network was trained on. Most mesh/point
|
|
viewers (CloudCompare, MeshLab, Mapple) look for red/green/blue uint8 instead,
|
|
and when they don't find it they either render the cloud flat grey or list the
|
|
floats as scalar fields.
|
|
|
|
This converts colour spelling only. Coordinates and labels pass through
|
|
untouched, so the geometry you inspect is exactly the geometry the model saw.
|
|
|
|
Usage:
|
|
python ply_for_viewer.py in.ply out.ply
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
from plyfile import PlyData, PlyElement
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) != 3:
|
|
print(__doc__)
|
|
raise SystemExit(2)
|
|
|
|
src, dst = Path(sys.argv[1]), Path(sys.argv[2])
|
|
if not src.exists():
|
|
raise SystemExit(f"{src}: not found")
|
|
|
|
v = PlyData.read(str(src))["vertex"]
|
|
props = [p.name for p in v.properties]
|
|
print(f"in : {src.name} {len(v):,} points props={props}")
|
|
|
|
rgb_set = next((s for s in (("r", "g", "b"), ("red", "green", "blue"))
|
|
if all(c in props for c in s)), None)
|
|
if rgb_set is None:
|
|
raise SystemExit("no colour channels found")
|
|
|
|
rgb = np.stack([np.asarray(v[c], dtype=np.float64) for c in rgb_set], axis=1)
|
|
if rgb.max() <= 1.0:
|
|
rgb = rgb * 255.0
|
|
print(" colour was float 0-1 -> scaling to 0-255")
|
|
rgb = np.clip(rgb, 0, 255).astype(np.uint8)
|
|
|
|
dtype = [("x", "f4"), ("y", "f4"), ("z", "f4"),
|
|
("red", "u1"), ("green", "u1"), ("blue", "u1")]
|
|
if "label" in props:
|
|
dtype.append(("label", "i4"))
|
|
|
|
arr = np.empty(len(v), dtype=dtype)
|
|
arr["x"], arr["y"], arr["z"] = v["x"], v["y"], v["z"]
|
|
arr["red"], arr["green"], arr["blue"] = rgb[:, 0], rgb[:, 1], rgb[:, 2]
|
|
if "label" in props:
|
|
arr["label"] = np.asarray(v["label"]).astype(np.int32)
|
|
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
PlyData([PlyElement.describe(arr, "vertex")], text=False).write(str(dst))
|
|
|
|
print(f"out : {dst} ({dst.stat().st_size / 1e6:.1f} MB)")
|
|
print(f" mean RGB {rgb.mean(axis=0).round(1).tolist()}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|