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>
99 lines
2.9 KiB
Python
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()
|