feat(extract): Chunk-Ausgabe, Manifest und Ablaufsteuerung
Task 6: leonida/emit.py (chunk_box, split_by_chunk, chunk_payload und die
Chunk-lokalen three.js-Koordinaten, write_chunks, write_debug_png) sowie
run.py als Ablaufsteuerung, die alles aus den Tasks 3-5 zu 144 Chunk-Dateien
und einem Manifest unter public/data/vice-a/ zusammenfuehrt.
Zwei Abweichungen vom Referenzcode der Aufgabe, beide durch Gegenrechnen
gegen die dort genannten Realwerte gefunden:
- test_emit.py::test_coordinates_are_chunk_local_three_js gab im Original
das ungeschnittene BIG (spannt vier Chunks) direkt an chunk_payload();
chunk_payload() clippt selbst nicht (das tut in run.py split_by_chunk_all
vorher), darum lag der Rand bei x=300 statt <=250 und der Test schlug mit
dem gegebenen Code fehl. Fix: BIG vor dem Aufruf mit split_by_chunk auf
Chunk (0,0) zuschneiden, wie es run.py fuer "land" ohnehin tut.
- run.pys Filter "Gebaeude, die zu >50% in einem Strassenpolygon liegen,
gelten als Strasse" pruefte im Original ALLE rohen Gebaeude gegen
road_polygons(labels) direkt. road_polygons() zaehlt "spekulativ" selbst
zu den Strassenklassen (ROAD_CLASSES) und schliesst zusaetzlich mit
disk(3) gegen Strassennamen; ein isoliertes "spekulativ"-Gebaeude
ueberlappt sich darueber praktisch mit sich selbst. Ergebnis am realen
Mosaik: von 267 rohen spekulativen Gebaeuden ueberlebten nur 3 (statt der
in der Aufgabe genannten Groessenordnung von 258), und die Schliessung
riss zusaetzlich 211 gewoehnliche graue Gebaeude mit heraus, die die
Notiz aus Task 5 ausdruecklich nicht meinte ("die ROTEN Flaechen ...
werden abgezogen"). Fix: eigenes, unzweideutiges Strassenmass nur aus
"strasse"/"weg" (ohne "spekulativ", ohne Namens-Schliessung), Pruefung
nur auf spekulative Kandidaten begrenzt. Ergebnis: 2.348 Gebaeude, davon
263 spekulativ (Aufgabe: 2.352 bzw. 258) - Wahrzeichenhoehen (68 Stueck,
31-248 m) und Palmenzahl (2.133) treffen die genannten Referenzwerte exakt.
Pipeline-Lauf am gecachten Mosaik: 144 Chunk-Dateien + Manifest, 2,5 MB
gesamt (Budget 10 MB), groesste Datei chunk_9_1.json/chunk_10_1.json mit
je 40 KB. Alle drei Toleranzgrenzen (wasser/strasse/gebaeude) eingehalten.
Debug-PNGs stichprobenartig gesichtet: Kuestenlinie, Strassennetz
(inklusive Kreuzungen und einer Auffahrt-Rampe) und ein Strand mit
Palmenreihe liegen deckungsgleich auf dem Mosaik; der wasserreine Chunk
(4,10) zeigt korrekt keine Vektoren.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2c355aa3d8
commit
ce90ea1d5f
292 files changed
+953
No files matched your search
@@ -0,0 +1,90 @@
|
||||
"""Erzeugt aus den Kacheln die Chunk-Dateien. Aufruf: uv run python run.py"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from leonida.buildings import building_polygons
|
||||
from leonida.classify import CLASSES, class_shares, classify
|
||||
from leonida.emit import (chunk_box_of, chunk_payload, in_chunk, split_by_chunk_all,
|
||||
write_chunks, write_debug_png)
|
||||
from leonida.heights import assign_heights, palm_points
|
||||
from leonida.tiles import COMMIT, download_tiles, load_mosaic
|
||||
from leonida.vectors import centerlines, land_polygons, mask_polygons, road_polygons
|
||||
|
||||
SEED = 20260912
|
||||
OUT = Path(__file__).resolve().parents[2] / "public" / "data" / "vice-a"
|
||||
TOLERANCE = {"wasser": (0.30, 0.36), "strasse": (0.10, 0.14), "gebaeude": (0.09, 0.13)}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cache = Path("cache")
|
||||
download_tiles(cache)
|
||||
mosaic = load_mosaic(cache)
|
||||
labels, unknown_share = classify(mosaic, report_unknown=True)
|
||||
if unknown_share > 0.05:
|
||||
raise SystemExit(f"{unknown_share:.1%} der Pixel unbekannt. Palette kalibrieren, nicht die Grenze anheben.")
|
||||
shares = class_shares(labels)
|
||||
for name, (lo, hi) in TOLERANCE.items():
|
||||
if not lo <= shares[name] <= hi:
|
||||
raise SystemExit(f"Klassenanteil {name}={shares[name]:.3f} außerhalb {lo}..{hi}. Palette prüfen.")
|
||||
|
||||
land = land_polygons(labels)
|
||||
roads = road_polygons(labels)
|
||||
lines = centerlines(labels)
|
||||
sand = mask_polygons(labels == CLASSES["sand"], 50, 1.0)
|
||||
parks = mask_polygons(labels == CLASSES["gruen"], 50, 1.0)
|
||||
urban = mask_polygons(labels == CLASSES["stadt"], 80, 1.5)
|
||||
surfaces = [(p, "sand") for p in sand] + [(p, "park") for p in parks] + [(p, "urban") for p in urban]
|
||||
|
||||
road_shapes = [p for p, _ in roads]
|
||||
raw = building_polygons(labels)
|
||||
# Nur "spekulativ" (rot) ist mit road_polygons() ueberhaupt doppelt vergeben: die
|
||||
# ROAD_CLASSES in vectors.py zaehlen "spekulativ" selbst als Strassenklasse, und
|
||||
# road_mask() schliesst zusaetzlich mit disk(3), um Strassennamen zu ueberbruecken.
|
||||
# Wuerde man road_shapes (= road_polygons(labels), inklusive dieser Schliessung und
|
||||
# inklusive "spekulativ") direkt gegen ALLE rohen Gebaeude pruefen, vergleicht ein
|
||||
# isoliertes "spekulativ"-Gebaeude sich praktisch mit sich selbst (dieselben Pixel
|
||||
# bilden dort auch das Strassenpolygon) und faellt so gut wie immer heraus: am realen
|
||||
# Mosaik blieben nur 3 von 267 "spekulativ"-Gebaeuden uebrig statt der erwarteten
|
||||
# Groessenordnung (Task-6-Referenzwerte: 258 von 267), und die Schliessung riss
|
||||
# zusaetzlich 211 gewoehnliche (graue) Gebaeude mit, die die Notiz in Task 5 gar nicht
|
||||
# meinte ("die ROTEN Flaechen ... werden abgezogen"). Deshalb hier ein eigenes,
|
||||
# unzweideutiges Strassenmass nur aus "strasse"/"weg" (ohne "spekulativ", ohne die
|
||||
# Namens-Schliessung) und die Pruefung nur auf spekulative Kandidaten begrenzt; graue
|
||||
# Gebaeude durchlaufen ungeprueft. Ergebnis am realen Mosaik: 2.348 Gebaeude, davon 263
|
||||
# spekulativ (Referenz: 2.352 bzw. 258) — Wahrzeichenhoehen (68, 31-248 m) und
|
||||
# Palmenzahl (2.133) treffen die Referenz exakt.
|
||||
unambiguous_mask = (labels == CLASSES["strasse"]) | (labels == CLASSES["weg"])
|
||||
unambiguous_roads = mask_polygons(unambiguous_mask, min_area=20, simplify=0.8)
|
||||
buildings = [(p, s) for p, s in raw
|
||||
if not s or not any(p.intersection(r).area > 0.5 * p.area for r in unambiguous_roads)]
|
||||
|
||||
landmarks = json.loads(Path("data/skyline.json").read_text())["landmarks"]
|
||||
districts = json.loads(Path("data/districts.json").read_text())
|
||||
heights = assign_heights([p for p, _ in buildings], landmarks, districts, SEED)
|
||||
palms = palm_points(sand, parks, lines, [p for p, _ in buildings], road_shapes,
|
||||
land, SEED)
|
||||
|
||||
payloads = []
|
||||
for j in range(12):
|
||||
for i in range(12):
|
||||
payloads.append(chunk_payload(
|
||||
i, j,
|
||||
land=list(split_by_chunk_all(land, i, j)),
|
||||
surfaces=[(part, kind) for (p, kind) in surfaces for part in split_by_chunk_all([p], i, j)],
|
||||
roads=[(part, b) for (p, b) in roads for part in split_by_chunk_all([p], i, j)],
|
||||
centerlines=[ln for ln in lines if ln.intersects(chunk_box_of(i, j))],
|
||||
buildings=[(p, h, s) for (p, s), h in zip(buildings, heights)
|
||||
if p.intersects(chunk_box_of(i, j))],
|
||||
palms=[pt for pt in palms if in_chunk(pt, i, j)],
|
||||
))
|
||||
write_debug_png(Path("debug"), mosaic, payloads)
|
||||
write_chunks(OUT, payloads, {
|
||||
"slice": "vice-a", "source_commit": COMMIT, "seed": SEED,
|
||||
"chunk_size": 250, "chunks": [p["chunk"] for p in payloads],
|
||||
"class_shares": shares,
|
||||
"attribution": "Kartenkacheln: GTA VI Map (CC BY 4.0) — https://map.gtadb.org, YANIS v15. Wahrzeichen: rolux/gtamaplib-vc (Unlicense).",
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user