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

98 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Stream an OBJ and report the numbers that decide how to tile it for SUM Parts.
Reads line by line so a multi-GB mesh does not have to fit in RAM. Reports
vertex/face counts, the bounding box, whether UVs and vertex colours are
present, and the material/texture references.
Usage:
python inspect_obj.py path/to/Block.obj [more.obj ...]
"""
from __future__ import annotations
import sys
from pathlib import Path
def inspect(path: Path) -> dict:
n_v = n_vt = n_vn = n_f = 0
has_vertex_color = False
mtllib: list[str] = []
usemtl: set[str] = set()
lo = [float("inf")] * 3
hi = [float("-inf")] * 3
with path.open("r", encoding="utf-8", errors="replace") as f:
for line in f:
if line.startswith("v "):
n_v += 1
parts = line.split()
x, y, z = float(parts[1]), float(parts[2]), float(parts[3])
if x < lo[0]: lo[0] = x
if y < lo[1]: lo[1] = y
if z < lo[2]: lo[2] = z
if x > hi[0]: hi[0] = x
if y > hi[1]: hi[1] = y
if z > hi[2]: hi[2] = z
# "v x y z r g b" is how some exporters carry per-vertex colour
if len(parts) >= 7:
has_vertex_color = True
elif line.startswith("vt "):
n_vt += 1
elif line.startswith("vn "):
n_vn += 1
elif line.startswith("f "):
n_f += 1
elif line.startswith("mtllib"):
mtllib.append(line.split(maxsplit=1)[1].strip())
elif line.startswith("usemtl"):
usemtl.add(line.split(maxsplit=1)[1].strip())
return {
"path": path,
"size_mb": path.stat().st_size / 1024 / 1024,
"n_v": n_v, "n_vt": n_vt, "n_vn": n_vn, "n_f": n_f,
"vertex_color": has_vertex_color,
"mtllib": mtllib, "usemtl": sorted(usemtl),
"lo": lo, "hi": hi,
}
def main() -> None:
if len(sys.argv) < 2:
print(__doc__)
raise SystemExit(2)
for arg in sys.argv[1:]:
p = Path(arg)
if not p.exists():
print(f"{arg}: not found")
continue
r = inspect(p)
ext = [r["hi"][i] - r["lo"][i] for i in range(3)]
area = ext[0] * ext[1]
print(f"=== {p.name} ({r['size_mb']:,.0f} MB) ===")
print(f" vertices : {r['n_v']:>12,}")
print(f" faces : {r['n_f']:>12,}")
print(f" texcoords : {r['n_vt']:>12,} {'(UV present)' if r['n_vt'] else '(NO UV)'}")
print(f" normals : {r['n_vn']:>12,}")
print(f" vtx color : {r['vertex_color']}")
print(f" mtllib : {r['mtllib']}")
print(f" materials : {len(r['usemtl'])} -> {r['usemtl'][:5]}"
f"{' ...' if len(r['usemtl']) > 5 else ''}")
print(f" bbox min : {[round(v, 2) for v in r['lo']]}")
print(f" bbox max : {[round(v, 2) for v in r['hi']]}")
print(f" extent : {[round(v, 2) for v in ext]} (metres, local)")
print(f" footprint : {area / 1e6:.3f} km^2")
if area:
print(f" density : {r['n_f'] / area:,.1f} faces/m^2")
# SUM Parts tiles are ~252 m square; this is how many that footprint is
print(f" ~252m tiles: {max(1, round(ext[0] / 252)) * max(1, round(ext[1] / 252))}")
print()
if __name__ == "__main__":
main()