Files
gta6/tools/extract/run.py
T
knsoandClaude Sonnet 5 2201be37bd feat(extract): Pipeline baut Straßen aus dem Graphen
run.py baut jetzt aus dem Straßengraphen (Tasks 3-11) statt aus den nachgezeichneten
Straßenrändern: Fahrbahn, Bordstein, Gehweg und Restflächen aus build_street(),
amerikanische Markierungen aus markings(). fit_to_carriageway() bringt Bodenplatte und
Beläge auf dasselbe Loch wie die Fahrbahn und hält Palmen dabei von ihr fern - nach
derselben Regel, mit der apply_street() sie schon von der Jefferson Avenue fernhält.

split_by_chunk_all() bekommt dafür eine parametrisierbare Splittergrenze: Ein Strich der
gestrichelten Linien ist 3 m x 10 cm = 0,3 m² und wäre an der bisherigen 0,5-m²-Grenze für
Flächen verschwunden. Markierungen laufen deshalb über den neuen Helfer chunk_markings()
mit eigener, kleinerer Grenze; alle anderen Aufrufer bleiben bei 0,5 m².

Am echten Netz gemessen: 1.537 Knoten, 1.790 Kanten, 830 Bordsteinstücke, 171
Restflächen, 22.394 Markierungen, 56 Palmen an der Fahrbahn entfernt. Fahrbahn-, Gehweg-
und Restflächen sowie die Bordsteinlänge treffen die vorab am echten Netz gemessenen
Erwartungswerte auf 0,03-0,3 % genau.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 19:43:16 +02:00

200 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Erzeugt aus den Kacheln die Chunk-Dateien. Aufruf: uv run python run.py"""
import json
from collections import Counter
from dataclasses import replace
from pathlib import Path
from scipy import ndimage as ndi
from shapely.geometry import LineString, Point
from shapely.ops import unary_union
from leonida.annotations import load_annotations, mark_generated, remove_annotations
from leonida.buildings import building_polygons
from leonida.classify import CLASSES, class_shares, classify, drop_map_labels
from leonida.emit import (chunk_box_of, chunk_payload, in_chunk, split_by_chunk_all,
write_chunks, write_debug_png)
from leonida.heights import PALM_KERB, assign_heights, palm_points
from leonida.marking import markings
from leonida.park import World, apply_flamingo_park
from leonida.roads import road_graph
from leonida.section import SECTION
from leonida.street import build_street, punch
from leonida.tiles import COMMIT, download_tiles, load_mosaic
from leonida.vectors import land_polygons, mask_polygons, road_mask, road_polygons
from overview import render_map
SEED = 20260912
OUT = Path(__file__).resolve().parents[2] / "public" / "data" / "vice-a"
PARK = Path("data/flamingo-park.toml")
TOLERANCE = {"wasser": (0.30, 0.36), "strasse": (0.10, 0.14), "gebaeude": (0.09, 0.13)}
MARKING_MIN_AREA = 0.001 # m² — ein Strich ist 3 m × 10 cm = 0,3 m², die Flächengrenze 0,5 m² verwürfe ihn
def _pieces(geometry) -> list:
return [g for g in getattr(geometry, "geoms", [geometry]) if not g.is_empty]
def _clip_lines(line, cell) -> list:
"""Der Anteil einer Linie in diesem Chunk, in Stücken."""
part = line.intersection(cell)
return [g for g in _pieces(part) if g.geom_type == "LineString" and g.length > 0.5]
def fit_to_carriageway(world: World, carriage) -> World:
"""Die Welt an die gebaute Fahrbahn anpassen (Entwurf, Ablauf Schritte 3 und 7).
Bodenplatte und Beläge bekommen dasselbe Loch — fehlt es in einer Schicht, liegt dort ein Belag
über der Mulde oder die Platte schließt sie. Palmen näher als PALM_KERB an der Fahrbahn fallen
weg, nach derselben Regel, mit der apply_street() sie von der Jefferson Avenue fernhält:
palm_points() hält nur Abstand zum gemessenen Korridor, und die Fahrbahn ragt an Kurven und
Kreuzungen darüber hinaus.
"""
return replace(world,
land=punch(world.land, carriage),
surfaces=[(part, kind) for poly, kind in world.surfaces
for part in punch([poly], carriage)],
palms=[p for p in world.palms if carriage.distance(Point(p)) >= PALM_KERB])
def chunk_markings(marks, i: int, j: int) -> list:
"""Die Markierungen in Chunk (i, j) — an der Chunkgrenze geteilt, nicht verworfen."""
return [(part, art) for poly, art in marks
for part in split_by_chunk_all([poly], i, j, min_area=MARKING_MIN_AREA)]
def _classified(street, graph) -> list:
"""Jede Fahrbahnfläche mit der Klasse der breitesten Kante, deren Mittelpunkt in ihr liegt.
Vorsicht beim Lesen des Felds: carriageway_area() verschmilzt das Netz zu wenigen
zusammenhängenden Teilen, und ein Teil trägt dann die breiteste Klasse aller Straßen darin. Am
echten Netz sind das 5 von 213 Teilen, aber 91 % der Fahrbahnfläche tragen "boulevard". Über
die einzelne Straße sagt das Feld also wenig — die Klasse je Straße steht in edges. Heute liest
es kein Verbraucher; Browser und Minimap färben die Fahrbahn einheitlich.
"""
mitten = [(e.klass, LineString(e.points).interpolate(0.5, normalized=True)) for e in graph.edges]
out = []
for poly in _pieces(street.carriageway):
treffer = [klass for klass, mitte in mitten if poly.contains(mitte)]
klass = max(treffer, key=lambda k: SECTION[k].carriageway) if treffer else "wohnstrasse"
out.append((poly, klass))
return out
def main() -> None:
cache = Path("cache")
download_tiles(cache)
mosaic = load_mosaic(cache)
labels, unknown_share = classify(mosaic, report_unknown=True)
labels = drop_map_labels(labels)
removal = remove_annotations(mosaic, labels, load_annotations(Path("data/annotations.toml")))
labels = removal.labels
labels = drop_map_labels(labels) # Inselreste, die erst das Entfernen im Wasser hinterlässt
print(removal.report())
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)
# Der Graph ersetzt die nachgezeichneten Straßenränder (Entwurf Teil 2). Die
# Distanztransformation misst an jedem Straßenpixel den Abstand zum nächsten Nicht-Straßen-
# pixel; doppelt genommen ist das die Korridorbreite an dieser Stelle.
distance = ndi.distance_transform_edt(road_mask(labels))
graph = road_graph(labels, distance)
print(f"Straßennetz: {len(graph.nodes)} Knoten, {len(graph.edges)} Kanten")
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)
marked = mark_generated([p for p, _ in buildings], heights, removal.generated)
print(f"Gebäude: {len(buildings)}, davon {marked} unter entfernten Beschriftungen ergänzt")
edge_lines = [LineString(e.points) for e in graph.edges]
palms = palm_points(sand, parks, edge_lines, [p for p, _ in buildings], road_shapes, land, SEED)
world = World(land=land, surfaces=surfaces, roads=roads, graph=graph,
buildings=[(p, h, s) for (p, s), h in zip(buildings, heights)], palms=palms)
world, messages = apply_flamingo_park(world, PARK, SEED)
for message in messages:
print(message)
corridor = unary_union([p for p, bridge in world.roads if not bridge])
bridge_polys = [p for p, bridge in world.roads if bridge]
street = build_street(world.graph, corridor, bridge_polys)
marks = markings(world.graph)
palmen = len(world.palms)
world = fit_to_carriageway(world, street.carriageway)
arten = dict(sorted(Counter(art for _, art in marks).items()))
print(f"Straßen: {len(street.kerb)} Bordsteinstücke, {len(street.lots)} Restflächen, "
f"{len(marks)} Markierungen {arten}, {palmen - len(world.palms)} Palmen an der Fahrbahn entfernt")
classified = _classified(street, world.graph) # einmal, nicht je Chunk: 5,8 s am echten Netz
payloads = []
for j in range(12):
for i in range(12):
payloads.append(chunk_payload(
i, j,
land=list(split_by_chunk_all(world.land, i, j)),
surfaces=[(part, kind) for (p, kind) in world.surfaces
for part in split_by_chunk_all([p], i, j)],
carriageway=[(part, klass) for poly, klass in classified
for part in split_by_chunk_all([poly], i, j)],
curb=[part for line in street.kerb
for part in _clip_lines(line, chunk_box_of(i, j))],
sidewalk=list(split_by_chunk_all(_pieces(street.sidewalk), i, j)),
markings=chunk_markings(marks, i, j),
lots=list(split_by_chunk_all(street.lots, i, j)),
bridges=list(split_by_chunk_all(bridge_polys, i, j)),
edges=[(part, e.klass, e.width, SECTION[e.klass].carriageway)
for e in world.graph.edges
for part in _clip_lines(LineString(e.points), chunk_box_of(i, j))],
buildings=[(p, h, s) for (p, h, s) in world.buildings
if p.intersects(chunk_box_of(i, j))],
palms=[pt for pt in world.palms if in_chunk(pt, i, j)],
paths=[part for p in world.paths for part in split_by_chunk_all([p], 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). "
"Flamingo Park: © OpenStreetMap-Mitwirkende (ODbL). "
"Bordstein und Gehweg: Just8 (Sketchfab, CC BY 4.0).",
})
print(render_map(OUT, OUT), "->", OUT)
if __name__ == "__main__":
main()