Files
nbrightandClaude Opus 5 f39b093106 Record results so far and fix the memory blowup in the palette decode
Adds STATUS.md as the handoff document: benchmark numbers, the bare-earth
metrics that actually matter for this project, the Korean-data domain gap that
retraining will not fix, and what to do on the 24 GB machine.

The memory problem was in how a prediction's colours were turned back into
class indices. Every consumer built an (N, 13, 3) float64 temporary:

    d = ((rgb[:, None, :] - COLOR_MAP[None, :, :]) ** 2).sum(axis=2)

That is ~250 MB of intermediates per 800k-point tile, several live at once, and
a full 4.7M-point block pushes it into gigabytes. main.py writes exact palette
entries, so an exact hash lookup resolves nearly every point with no large
temporary; only leftovers fall back to a chunked distance search. Peak RSS on a
470k-point tile drops to 61 MB. Extracted to sumparts_palette.py and shared by
coarse_eval.py and split_by_class.py.

Also from this round:

- patch_cm_mutation.sh: ConfusionMatrix.update() rewrote the caller's pred
  tensor in place, folding every ignore_index point into class num_classes-1.
  test() saves its visualization from that same tensor afterwards, so an
  unlabelled tile came out 100% wall and the model looked degenerate when it
  was not.
- patch_class_mask.sh: SUMPARTS_MASK_CLASSES drops known-absent classes from
  the argmax. Measured on Seosan and it does not help - the runner-up for
  "water" is "wall", not "terrain" - but the experiment is worth keeping.
- split_by_class.py now writes .ply alongside .obj. A vertex-only OBJ has zero
  faces and most viewers render nothing, which is why the first export looked
  broken.
- verify_outputs.sh reads exported files back with a parser, so "here are your
  files" can be checked rather than asserted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:07:31 +09:00

86 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Dump what a prediction PLY actually contains.
check_pred.py recovers the class by matching RGB against the palette. If that
reports one class for every point, the question is whether the model really
collapsed or whether the colour decode is wrong — so read the raw fields
instead of interpreting them.
Usage:
python dump_pred.py pred.ply
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
from plyfile import PlyData
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])
v = PlyData.read(str(path))["vertex"]
props = [p.name for p in v.properties]
print(f"file : {path.name}")
print(f"points: {len(v):,}")
print(f"props : {props}")
print()
if "label" in props:
lab = np.asarray(v["label"])
u, c = np.unique(lab, return_counts=True)
print(f"label field: dtype={lab.dtype}")
for k, n in sorted(zip(u.tolist(), c.tolist()), key=lambda t: -t[1]):
name = CLASSES[k] if 0 <= k < len(CLASSES) else f"?{k}"
print(f" {k:>3} {name:<20} {n:>10,} {100*n/len(lab):>6.2f}%")
print()
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:
rgb = np.stack([np.asarray(v[c]) for c in rgb_set], axis=1)
uniq, cnt = np.unique(rgb.reshape(-1, 3), axis=0, return_counts=True)
print(f"colour field {rgb_set}: dtype={rgb.dtype}, {len(uniq)} distinct")
order = np.argsort(-cnt)
for i in order[:15]:
col = uniq[i]
d = ((COLOR_MAP - col.astype(np.float64)) ** 2).sum(axis=1)
k = int(d.argmin())
exact = "exact" if d[k] < 1 else f"nearest (dist {np.sqrt(d[k]):.0f})"
name = CLASSES[k] if k < len(CLASSES) else f"?{k}"
print(f" {tuple(int(x) for x in col)!s:<20} {cnt[i]:>10,} "
f"{100*cnt[i]/len(rgb):>6.2f}% -> {name} ({exact})")
print()
if "label" in props and rgb_set:
lab = np.asarray(v["label"])
rgb = np.stack([np.asarray(v[c]) for c in rgb_set], axis=1)
d = ((rgb[:, None, :].astype(np.float64) - COLOR_MAP[None, :, :]) ** 2).sum(axis=2)
from_colour = d.argmin(axis=1)
agree = int((from_colour == lab).sum())
print(f"label vs colour agreement: {agree:,}/{len(lab):,} "
f"({100*agree/len(lab):.2f}%)")
if agree < len(lab):
print(" -> the two disagree; the label field is authoritative")
if __name__ == "__main__":
main()