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

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