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

99 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""Compare PLY point clouds against what the SUM Parts loader needs.
Pass the converted file first and a reference SUM Parts tile second to see the
two side by side.
Usage:
python check_ply.py mine.ply [reference.ply ...]
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
from plyfile import PlyData
# read_ply_with_plyfilelib tries red/green/blue first, then r/g/b
RGB_SETS = (("red", "green", "blue"), ("r", "g", "b"))
def report(path: Path) -> bool:
ply = PlyData.read(str(path))
v = ply["vertex"]
props = [p.name for p in v.properties]
print(f"=== {path.name} ===")
print(f" format : {'binary' if not ply.text else 'ascii'}")
print(f" points : {len(v):,}")
print(f" properties: {props}")
ok = True
missing_xyz = [c for c in ("x", "y", "z") if c not in props]
if missing_xyz:
print(f" MISSING : {missing_xyz}")
ok = False
else:
xyz = np.stack([v["x"], v["y"], v["z"]], axis=1)
lo, hi = xyz.min(axis=0), xyz.max(axis=0)
ext = hi - lo
print(f" extent : {np.round(ext, 2).tolist()} m")
print(f" origin : {np.round(lo, 2).tolist()}")
if ext.max() < 0.5:
print(" WARNING : extent < 0.5 -- degrees, not metres?")
ok = False
area = ext[0] * ext[1]
if area > 0:
print(f" density : {len(v) / area:,.1f} pts/m^2")
rgb_set = next((s for s in RGB_SETS if all(c in props for c in s)), None)
if rgb_set is None:
print(f" MISSING : colour -- need {RGB_SETS[0]} or {RGB_SETS[1]}")
ok = False
else:
rgb = np.stack([v[c] for c in rgb_set], axis=1)
print(f" colour : {rgb_set} dtype={rgb.dtype} "
f"range=[{rgb.min()}, {rgb.max()}] mean={rgb.mean(axis=0).round(1).tolist()}")
grey = int((rgb == 128).all(axis=1).sum())
if grey:
print(f" WARNING : {grey:,} points are exactly grey (texture miss?)")
if "label" not in props:
print(" MISSING : label")
ok = False
else:
lab = np.asarray(v["label"])
u, c = np.unique(lab, return_counts=True)
shown = list(zip(u.tolist(), c.tolist()))[:14]
print(f" label : dtype={lab.dtype} uniq={u.tolist()[:20]}")
print(f" label hist: {shown}")
if u.tolist() == [0]:
print(" note : all unclassified -- inference only, cannot train/score")
print(f" verdict : {'OK' if ok else 'INCOMPATIBLE'}")
print()
return ok
def main() -> None:
if len(sys.argv) < 2:
print(__doc__)
raise SystemExit(2)
all_ok = True
for a in sys.argv[1:]:
p = Path(a)
if not p.exists():
print(f"{a}: not found\n")
all_ok = False
continue
all_ok &= report(p)
raise SystemExit(0 if all_ok else 1)
if __name__ == "__main__":
main()