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>
86 lines
3.1 KiB
Python
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()
|