feat(extract): Straßennetz als Graph aus dem Skelett

centerlines() verwirft alles unter 15 m und zerlegt das Netz damit an jeder
Kreuzung: von 3.038 Linienenden treffen sich nur 81. Der neue Graph lässt
das Skelett ungefiltert, macht aus den 5.970 Pixeln mit Grad >= 3 durch
Verschmelzen im Umkreis von 8 m die 862 Kreuzungen und hängt die Kanten
dort ein.

_skeleton_graph/_to_world in vectors.py sind jetzt öffentliche Schnittstelle
für roads.py (skeleton_graph/to_world); inpaint.py zieht seinen einzigen
Aufruf von skeleton_graph mit, sonst bräche dessen Import.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-17 00:36:40 +02:00
1 parent 505b1969ec
commit 3fe3cd5c45
4 files changed
+182 -7

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
"""Der Graph, aus dem die Straßen entstehen. Gegenstück zum Entwurf, Abschnitt "Das Netz"."""
from __future__ import annotations
import numpy as np
from leonida.coords import SLICE
from leonida.roads import JUNCTION_RADIUS, graph_from_skeleton
def skeleton(pixels: list[tuple[int, int]], shape=(120, 120)) -> np.ndarray:
skel = np.zeros(shape, dtype=bool)
for r, c in pixels:
skel[r, c] = True
return skel
def kreuz(arm: int = 40) -> np.ndarray:
"""Ein Pluszeichen: vier Arme von 40 px an einem Mittelpunkt."""
pixels = [(60, c) for c in range(60 - arm, 60 + arm + 1)]
pixels += [(r, 60) for r in range(60 - arm, 60 + arm + 1)]
return skeleton(pixels)
def test_ein_kreuz_gibt_einen_knoten_und_vier_kanten():
graph = graph_from_skeleton(kreuz())
junctions = [i for i in range(len(graph.nodes)) if graph.degree(i) >= 3]
assert len(junctions) == 1
assert graph.degree(junctions[0]) == 4
assert len(graph.edges) == 4
def test_kanten_beginnen_und_enden_am_knotenpunkt():
graph = graph_from_skeleton(kreuz())
mitte = graph.nodes[next(i for i in range(len(graph.nodes)) if graph.degree(i) >= 3)].point
for edge in graph.edges:
enden = {edge.points[0], edge.points[-1]}
assert mitte in enden, "jede Kante des Kreuzes hängt am Mittelknoten"
def test_zwei_kreuzungen_naeher_als_der_radius_werden_ein_knoten():
# Zwei Abzweige im Abstand von 4 m — unter JUNCTION_RADIUS, also ein Knoten.
pixels = [(60, c) for c in range(20, 100)]
pixels += [(r, 58) for r in range(40, 60)]
pixels += [(r, 62) for r in range(60, 80)]
graph = graph_from_skeleton(skeleton(pixels))
assert 4 < JUNCTION_RADIUS
assert len([i for i in range(len(graph.nodes)) if graph.degree(i) >= 3]) == 1
def test_gerade_strecke_ohne_abzweig_gibt_eine_kante_mit_zwei_enden():
graph = graph_from_skeleton(skeleton([(60, c) for c in range(20, 100)]))
assert len(graph.edges) == 1
assert [graph.degree(i) for i in range(len(graph.nodes))] == [1, 1]
def test_punkte_liegen_in_weltkoordinaten():
graph = graph_from_skeleton(skeleton([(60, c) for c in range(20, 100)]))
xs = [x for x, _ in graph.edges[0].points]
assert min(xs) == SLICE["x0"] + 20
assert max(xs) == SLICE["x0"] + 99