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>
82 lines
2.6 KiB
Python
82 lines
2.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Summarise a prediction PLY written by main.py's visualization step.
|
|
|
|
main.py colours predictions through SUMV2_Triangle_COLOR_MAP rather than
|
|
writing a label field, so recover the class by matching each point's RGB back
|
|
to that palette.
|
|
|
|
Usage:
|
|
python check_pred.py .../seosan_BlockYBA_tile0_pred.ply
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
from plyfile import PlyData
|
|
|
|
# openpoints/dataset/sumv2_triangle/sumv2_triangle.py
|
|
CLASSES = ['unclassified', 'terrain', 'high_vegetation', 'facade_surface',
|
|
'water', 'car', 'boat', 'roof_surface', 'chimney', 'dormer',
|
|
'balcony', 'roof_installation', 'wall']
|
|
|
|
COLOR_MAP = np.array([
|
|
(0., 0., 0.), (170., 85., 0.), (0., 255., 0.), (255., 255., 0.),
|
|
(0., 255., 255.), (255., 0., 255.), (0., 0., 153.), (85., 85., 127.),
|
|
(255., 50., 50.), (85., 0., 127.), (50., 125., 150.), (50., 0., 50.),
|
|
(215., 160., 140.),
|
|
])
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) != 2:
|
|
print(__doc__)
|
|
raise SystemExit(2)
|
|
|
|
path = Path(sys.argv[1])
|
|
if not path.exists():
|
|
print(f"{path}: not found")
|
|
raise SystemExit(1)
|
|
|
|
v = PlyData.read(str(path))["vertex"]
|
|
props = [p.name for p in v.properties]
|
|
print(f"=== {path.name} ===")
|
|
print(f" points : {len(v):,}")
|
|
print(f" properties : {props}")
|
|
|
|
rgb_set = next((s for s in (("red", "green", "blue"), ("r", "g", "b"))
|
|
if all(c in props for c in s)), None)
|
|
if rgb_set is None:
|
|
print(" no colour channels -- cannot recover predicted class")
|
|
raise SystemExit(1)
|
|
|
|
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
|
|
|
|
# nearest palette entry per point
|
|
d = ((rgb[:, None, :] - COLOR_MAP[None, :, :]) ** 2).sum(axis=2)
|
|
cls = d.argmin(axis=1)
|
|
resid = np.sqrt(d.min(axis=1))
|
|
|
|
print(f" palette fit: max residual {resid.max():.1f} "
|
|
f"({'exact' if resid.max() < 1 else 'approximate'})")
|
|
print()
|
|
print(f" {'class':<20} {'points':>10} {'share':>7}")
|
|
u, c = np.unique(cls, return_counts=True)
|
|
order = np.argsort(-c)
|
|
for i in order:
|
|
k, n = int(u[i]), int(c[i])
|
|
name = CLASSES[k] if k < len(CLASSES) else f"?{k}"
|
|
print(f" {name:<20} {n:>10,} {100 * n / len(cls):>6.2f}%")
|
|
|
|
print()
|
|
print(f" distinct classes predicted: {len(u)}")
|
|
if len(u) == 1:
|
|
print(" note: single class everywhere -- expected from a 1-epoch model")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|