Files
nbrightandClaude Opus 5 609d9a6972 Add SUM Parts reproduction and Seosan Myeongcheon application pipeline
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>
2026-08-21 10:29:25 +09:00

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()