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,98 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from shapely.geometry import Polygon, box
|
||||
|
||||
from .coords import CHUNK_SIZE, SLICE
|
||||
|
||||
N = (SLICE["x1"] - SLICE["x0"]) // CHUNK_SIZE # 12
|
||||
|
||||
|
||||
def chunk_box(i: int, j: int) -> Polygon:
|
||||
x0 = SLICE["x0"] + i * CHUNK_SIZE
|
||||
y1 = SLICE["y1"] - j * CHUNK_SIZE
|
||||
return box(x0, y1 - CHUNK_SIZE, x0 + CHUNK_SIZE, y1)
|
||||
|
||||
|
||||
def split_by_chunk(poly: Polygon) -> dict[tuple[int, int], Polygon]:
|
||||
out = {}
|
||||
for j in range(N):
|
||||
for i in range(N):
|
||||
part = poly.intersection(chunk_box(i, j))
|
||||
if not part.is_empty and part.area > 0.5:
|
||||
out[(i, j)] = part
|
||||
return out
|
||||
|
||||
|
||||
def _ring(poly: Polygon, i: int, j: int) -> list[list[float]]:
|
||||
ox = SLICE["x0"] + i * CHUNK_SIZE
|
||||
oy = SLICE["y1"] - j * CHUNK_SIZE
|
||||
# Welt (X, Y) -> three (x, z) -> relativ zur Chunk-Ecke
|
||||
return [[round(x - ox, 2), round(-y - (-oy), 2)] for x, y in poly.exterior.coords]
|
||||
|
||||
|
||||
def chunk_payload(i, j, *, land, surfaces, roads, centerlines, buildings, palms) -> dict:
|
||||
ox = SLICE["x0"] + i * CHUNK_SIZE
|
||||
oy = SLICE["y1"] - j * CHUNK_SIZE
|
||||
return {
|
||||
"chunk": [i, j],
|
||||
"origin": [ox, -oy],
|
||||
"land": [_ring(p, i, j) for p in land],
|
||||
"surfaces": [{"poly": _ring(p, i, j), "kind": k} for p, k in surfaces],
|
||||
"roads": [{"poly": _ring(p, i, j), "bridge": b} for p, b in roads],
|
||||
"centerlines": [[[round(x - ox, 2), round(-y + oy, 2)] for x, y in ln.coords] for ln in centerlines],
|
||||
"buildings": [{"poly": _ring(p, i, j), "height": h["height"],
|
||||
"source": h["source"], "speculative": s} for p, h, s in buildings],
|
||||
"palms": [[round(x - ox, 2), round(-y + oy, 2)] for x, y in palms],
|
||||
}
|
||||
|
||||
|
||||
def chunk_box_of(i: int, j: int) -> Polygon:
|
||||
return chunk_box(i, j)
|
||||
|
||||
|
||||
def split_by_chunk_all(polys, i: int, j: int):
|
||||
"""Alle Anteile der übergebenen Polygone, die in Chunk (i, j) liegen."""
|
||||
cell = chunk_box(i, j)
|
||||
for poly in polys:
|
||||
part = poly.intersection(cell)
|
||||
if not part.is_empty and part.area > 0.5:
|
||||
if part.geom_type == "Polygon":
|
||||
yield part
|
||||
else:
|
||||
yield from (g for g in part.geoms if g.geom_type == "Polygon" and g.area > 0.5)
|
||||
|
||||
|
||||
def in_chunk(point, i: int, j: int) -> bool:
|
||||
x0 = SLICE["x0"] + i * CHUNK_SIZE
|
||||
y1 = SLICE["y1"] - j * CHUNK_SIZE
|
||||
return x0 <= point[0] < x0 + CHUNK_SIZE and y1 - CHUNK_SIZE <= point[1] < y1
|
||||
|
||||
|
||||
def write_debug_png(out_dir: Path, mosaic, payloads: list[dict]) -> None:
|
||||
"""Ein Bild je Chunk: die extrahierten Vektoren über der Karte, zur Sichtprüfung."""
|
||||
from PIL import Image, ImageDraw
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
for payload in payloads:
|
||||
i, j = payload["chunk"]
|
||||
x0, y0 = i * CHUNK_SIZE, j * CHUNK_SIZE
|
||||
crop = Image.fromarray(mosaic[y0:y0 + CHUNK_SIZE, x0:x0 + CHUNK_SIZE]).convert("RGB")
|
||||
draw = ImageDraw.Draw(crop)
|
||||
for ring in payload["land"]:
|
||||
draw.line([tuple(pt) for pt in ring] + [tuple(ring[0])], fill=(0, 255, 255), width=1)
|
||||
for road in payload["roads"]:
|
||||
draw.line([tuple(pt) for pt in road["poly"]], fill=(255, 0, 255), width=1)
|
||||
for b in payload["buildings"]:
|
||||
if len(b["poly"]) >= 3:
|
||||
draw.polygon([tuple(pt) for pt in b["poly"]], outline=(255, 220, 0))
|
||||
for x, z in payload["palms"]:
|
||||
draw.ellipse([x - 2, z - 2, x + 2, z + 2], outline=(0, 200, 0))
|
||||
crop.save(out_dir / f"chunk_{i}_{j}.png")
|
||||
|
||||
|
||||
def write_chunks(out_dir: Path, payloads: list[dict], meta: dict) -> None:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
for payload in payloads:
|
||||
i, j = payload["chunk"]
|
||||
(out_dir / f"chunk_{i}_{j}.json").write_text(json.dumps(payload, separators=(",", ":")))
|
||||
(out_dir / "manifest.json").write_text(json.dumps(meta, indent=1))
|
||||
Reference in new issue
Block a user