#!/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()