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>
This commit is contained in:
1 parent
892558fc50
commit
2201be37bd
3 files changed
+181
-15
No files matched your search
@@ -130,16 +130,20 @@ def chunk_box_of(i: int, j: int) -> Polygon:
|
|||||||
return chunk_box(i, j)
|
return chunk_box(i, j)
|
||||||
|
|
||||||
|
|
||||||
def split_by_chunk_all(polys, i: int, j: int):
|
def split_by_chunk_all(polys, i: int, j: int, min_area: float = 0.5):
|
||||||
"""Alle Anteile der übergebenen Polygone, die in Chunk (i, j) liegen."""
|
"""Alle Anteile der übergebenen Polygone, die in Chunk (i, j) liegen.
|
||||||
|
|
||||||
|
min_area verwirft die Splitter, die beim Zuschneiden an der Chunkgrenze entstehen. 0,5 m² passt
|
||||||
|
für Flächen; Markierungen sind kleiner und brauchen eine eigene Grenze (run.py, chunk_markings).
|
||||||
|
"""
|
||||||
cell = chunk_box(i, j)
|
cell = chunk_box(i, j)
|
||||||
for poly in polys:
|
for poly in polys:
|
||||||
part = poly.intersection(cell)
|
part = poly.intersection(cell)
|
||||||
if not part.is_empty and part.area > 0.5:
|
if not part.is_empty and part.area > min_area:
|
||||||
if part.geom_type == "Polygon":
|
if part.geom_type == "Polygon":
|
||||||
yield part
|
yield part
|
||||||
else:
|
else:
|
||||||
yield from (g for g in part.geoms if g.geom_type == "Polygon" and g.area > 0.5)
|
yield from (g for g in part.geoms if g.geom_type == "Polygon" and g.area > min_area)
|
||||||
|
|
||||||
|
|
||||||
def in_chunk(point, i: int, j: int) -> bool:
|
def in_chunk(point, i: int, j: int) -> bool:
|
||||||
|
|||||||
+100
-11
@@ -1,22 +1,83 @@
|
|||||||
"""Erzeugt aus den Kacheln die Chunk-Dateien. Aufruf: uv run python run.py"""
|
"""Erzeugt aus den Kacheln die Chunk-Dateien. Aufruf: uv run python run.py"""
|
||||||
import json
|
import json
|
||||||
|
from collections import Counter
|
||||||
|
from dataclasses import replace
|
||||||
from pathlib import Path
|
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.annotations import load_annotations, mark_generated, remove_annotations
|
||||||
from leonida.buildings import building_polygons
|
from leonida.buildings import building_polygons
|
||||||
from leonida.classify import CLASSES, class_shares, classify, drop_map_labels
|
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,
|
from leonida.emit import (chunk_box_of, chunk_payload, in_chunk, split_by_chunk_all,
|
||||||
write_chunks, write_debug_png)
|
write_chunks, write_debug_png)
|
||||||
from leonida.heights import assign_heights, palm_points
|
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.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.tiles import COMMIT, download_tiles, load_mosaic
|
||||||
from leonida.vectors import centerlines, land_polygons, mask_polygons, road_polygons
|
from leonida.vectors import land_polygons, mask_polygons, road_mask, road_polygons
|
||||||
from overview import render_map
|
from overview import render_map
|
||||||
|
|
||||||
SEED = 20260912
|
SEED = 20260912
|
||||||
OUT = Path(__file__).resolve().parents[2] / "public" / "data" / "vice-a"
|
OUT = Path(__file__).resolve().parents[2] / "public" / "data" / "vice-a"
|
||||||
PARK = Path("data/flamingo-park.toml")
|
PARK = Path("data/flamingo-park.toml")
|
||||||
TOLERANCE = {"wasser": (0.30, 0.36), "strasse": (0.10, 0.14), "gebaeude": (0.09, 0.13)}
|
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:
|
def main() -> None:
|
||||||
@@ -38,7 +99,12 @@ def main() -> None:
|
|||||||
|
|
||||||
land = land_polygons(labels)
|
land = land_polygons(labels)
|
||||||
roads = road_polygons(labels)
|
roads = road_polygons(labels)
|
||||||
lines = centerlines(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)
|
sand = mask_polygons(labels == CLASSES["sand"], 50, 1.0)
|
||||||
parks = mask_polygons(labels == CLASSES["gruen"], 50, 1.0)
|
parks = mask_polygons(labels == CLASSES["gruen"], 50, 1.0)
|
||||||
urban = mask_polygons(labels == CLASSES["stadt"], 80, 1.5)
|
urban = mask_polygons(labels == CLASSES["stadt"], 80, 1.5)
|
||||||
@@ -72,25 +138,47 @@ def main() -> None:
|
|||||||
heights = assign_heights([p for p, _ in buildings], landmarks, districts, SEED)
|
heights = assign_heights([p for p, _ in buildings], landmarks, districts, SEED)
|
||||||
marked = mark_generated([p for p, _ in buildings], heights, removal.generated)
|
marked = mark_generated([p for p, _ in buildings], heights, removal.generated)
|
||||||
print(f"Gebäude: {len(buildings)}, davon {marked} unter entfernten Beschriftungen ergänzt")
|
print(f"Gebäude: {len(buildings)}, davon {marked} unter entfernten Beschriftungen ergänzt")
|
||||||
palms = palm_points(sand, parks, lines, [p for p, _ in buildings], road_shapes,
|
edge_lines = [LineString(e.points) for e in graph.edges]
|
||||||
land, SEED)
|
palms = palm_points(sand, parks, edge_lines, [p for p, _ in buildings], road_shapes, land, SEED)
|
||||||
|
|
||||||
world = World(land=land, surfaces=surfaces, roads=roads, centerlines=lines,
|
world = World(land=land, surfaces=surfaces, roads=roads, graph=graph,
|
||||||
buildings=[(p, h, s) for (p, s), h in zip(buildings, heights)], palms=palms)
|
buildings=[(p, h, s) for (p, s), h in zip(buildings, heights)], palms=palms)
|
||||||
world, messages = apply_flamingo_park(world, PARK, SEED)
|
world, messages = apply_flamingo_park(world, PARK, SEED)
|
||||||
for message in messages:
|
for message in messages:
|
||||||
print(message)
|
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 = []
|
payloads = []
|
||||||
for j in range(12):
|
for j in range(12):
|
||||||
for i in range(12):
|
for i in range(12):
|
||||||
payloads.append(chunk_payload(
|
payloads.append(chunk_payload(
|
||||||
i, j,
|
i, j,
|
||||||
land=list(split_by_chunk_all(world.land, 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)],
|
surfaces=[(part, kind) for (p, kind) in world.surfaces
|
||||||
roads=[(part, b) for (p, b) in world.roads for part in split_by_chunk_all([p], i, j)],
|
for part in split_by_chunk_all([p], i, j)],
|
||||||
centerlines=[ln for ln in world.centerlines if ln.intersects(chunk_box_of(i, j))],
|
carriageway=[(part, klass) for poly, klass in classified
|
||||||
buildings=[(p, h, s) for (p, h, s) in world.buildings if p.intersects(chunk_box_of(i, j))],
|
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)],
|
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)],
|
paths=[part for p in world.paths for part in split_by_chunk_all([p], i, j)],
|
||||||
))
|
))
|
||||||
@@ -101,7 +189,8 @@ def main() -> None:
|
|||||||
"class_shares": shares,
|
"class_shares": shares,
|
||||||
"attribution": "Kartenkacheln: GTA VI Map (CC BY 4.0) — https://map.gtadb.org, YANIS v15. "
|
"attribution": "Kartenkacheln: GTA VI Map (CC BY 4.0) — https://map.gtadb.org, YANIS v15. "
|
||||||
"Wahrzeichen: rolux/gtamaplib-vc (Unlicense). "
|
"Wahrzeichen: rolux/gtamaplib-vc (Unlicense). "
|
||||||
"Flamingo Park: © OpenStreetMap-Mitwirkende (ODbL).",
|
"Flamingo Park: © OpenStreetMap-Mitwirkende (ODbL). "
|
||||||
|
"Bordstein und Gehweg: Just8 (Sketchfab, CC BY 4.0).",
|
||||||
})
|
})
|
||||||
print(render_map(OUT, OUT), "->", OUT)
|
print(render_map(OUT, OUT), "->", OUT)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"""Die Reihenfolge, in der die Straßen entstehen — ohne den vollen Lauf."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from shapely.geometry import box
|
||||||
|
from shapely.ops import unary_union
|
||||||
|
|
||||||
|
from leonida.emit import chunk_box_of
|
||||||
|
from leonida.park import World
|
||||||
|
from leonida.roads import Edge, Graph, Node
|
||||||
|
from leonida.street import build_street, punch
|
||||||
|
from run import chunk_markings, fit_to_carriageway
|
||||||
|
|
||||||
|
|
||||||
|
def _gerade_wohnstrasse():
|
||||||
|
"""100 m Wohnstraße entlang y = 0: Fahrbahn 7 m breit, also von y = -3,5 bis 3,5."""
|
||||||
|
graph = Graph(nodes=[Node((0.0, 0.0)), Node((100.0, 0.0))],
|
||||||
|
edges=[Edge(0, 1, [(0.0, 0.0), (100.0, 0.0)], 12.0, "wohnstrasse")])
|
||||||
|
return graph, build_street(graph, box(0, -6, 100, 6), bridges=[])
|
||||||
|
|
||||||
|
|
||||||
|
def _welt(graph, *, surfaces=(), palms=()):
|
||||||
|
return World(land=[box(-20, -50, 120, 50)], surfaces=list(surfaces), roads=[], graph=graph,
|
||||||
|
buildings=[], palms=list(palms))
|
||||||
|
|
||||||
|
|
||||||
|
def test_boden_und_belag_bekommen_dasselbe_loch():
|
||||||
|
"""Vergisst run.py eine der beiden Schichten, liegt dort ein Belag über der Mulde oder die
|
||||||
|
Bodenplatte schließt sie."""
|
||||||
|
graph, street = _gerade_wohnstrasse()
|
||||||
|
platte = box(-20, -50, 120, 50)
|
||||||
|
welt = _welt(graph, surfaces=[(box(-20, -50, 50, 50), "urban"), (box(50, -50, 120, 50), "park")])
|
||||||
|
out = fit_to_carriageway(welt, street.carriageway)
|
||||||
|
boden = unary_union(out.land)
|
||||||
|
assert boden.intersection(street.carriageway).area < 0.01, "die Fahrbahn liegt im Loch"
|
||||||
|
loch = street.carriageway.intersection(platte).area
|
||||||
|
assert abs(platte.area - boden.area - loch) < 0.01, "das Loch ist nicht größer als die Fahrbahn"
|
||||||
|
for kind in ("urban", "park"):
|
||||||
|
belag = unary_union([poly for poly, k in out.surfaces if k == kind])
|
||||||
|
assert belag.intersection(street.carriageway).area < 0.01, f"kein Belag {kind} über der Mulde"
|
||||||
|
assert abs(unary_union([poly for poly, _ in out.surfaces]).area - boden.area) < 0.01, "dasselbe Loch"
|
||||||
|
|
||||||
|
|
||||||
|
def test_gehweg_liegt_nicht_im_loch():
|
||||||
|
graph, street = _gerade_wohnstrasse()
|
||||||
|
land = unary_union(punch([box(-20, -50, 120, 50)], street.carriageway))
|
||||||
|
assert street.sidewalk.difference(land).area < 0.5, "der Gehweg liegt auf der Bodenplatte"
|
||||||
|
|
||||||
|
|
||||||
|
def test_keine_palme_auf_oder_dicht_an_der_fahrbahn():
|
||||||
|
"""Entwurf, Ablauf Schritt 7: Palmen werden nach dem Straßenbau neu gesetzt. Am echten Netz
|
||||||
|
standen ohne diesen Schritt 24 Palmen auf der Fahrbahn und 32 weitere näher als PALM_KERB."""
|
||||||
|
graph, street = _gerade_wohnstrasse()
|
||||||
|
welt = _welt(graph, palms=[(50.0, 0.0), (50.0, 4.5), (50.0, 5.5)])
|
||||||
|
out = fit_to_carriageway(welt, street.carriageway)
|
||||||
|
assert out.palms == [(50.0, 5.5)], "auf der Fahrbahn und 1 m daneben weg, 2 m daneben bleibt"
|
||||||
|
|
||||||
|
|
||||||
|
def test_striche_ueberleben_den_zuschnitt():
|
||||||
|
"""Ein Strich der gestrichelten Mitte ist 3 m × 10 cm = 0,3 m². Mit der Splittergrenze der
|
||||||
|
Flächen (0,5 m²) fiel am echten Netz jeder einzelne der 4.363 Striche weg."""
|
||||||
|
zelle = chunk_box_of(0, 0)
|
||||||
|
x, y = zelle.centroid.x, zelle.centroid.y
|
||||||
|
stuecke = chunk_markings([(box(x, y, x + 3.0, y + 0.1), "gelb_gestrichelt")], 0, 0)
|
||||||
|
assert [art for _, art in stuecke] == ["gelb_gestrichelt"]
|
||||||
|
assert abs(stuecke[0][0].area - 0.3) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_strich_ueber_der_chunkgrenze_wird_geteilt_nicht_verworfen():
|
||||||
|
zelle = chunk_box_of(0, 0)
|
||||||
|
grenze, y = zelle.bounds[2], zelle.centroid.y # rechte Kante von Chunk (0, 0)
|
||||||
|
strich = [(box(grenze - 1.0, y, grenze + 2.0, y + 0.1), "weiss_gestrichelt")]
|
||||||
|
assert [round(poly.area, 6) for poly, _ in chunk_markings(strich, 0, 0)] == [0.1]
|
||||||
|
assert [round(poly.area, 6) for poly, _ in chunk_markings(strich, 1, 0)] == [0.2]
|
||||||
Reference in new issue
Block a user