Nachbarschaften (Cliquen) in Straßendaten finden (Grafik)

10

Ich suche nach einer Möglichkeit, Stadtteile in Städten automatisch als Polygone in einem Diagramm zu definieren.

Meine Definition einer Nachbarschaft besteht aus zwei Teilen:

  1. Ein Block : Ein Bereich zwischen mehreren Straßen, in dem die Anzahl der Straßen (Kanten) und Kreuzungen (Knoten) mindestens drei beträgt (ein Dreieck).
  2. Eine Nachbarschaft : Für jeden Block alle Blöcke direkt neben diesem Block und den Block selbst.

In dieser Abbildung finden Sie ein Beispiel:

Geben Sie hier die Bildbeschreibung ein

ZB ist B4 ein Block, der durch 7 Knoten und 6 Kanten definiert ist, die sie verbinden. Wie die meisten Beispiele hier sind die anderen Blöcke durch 4 Knoten und 4 Kanten definiert, die sie verbinden. Außerdem umfasst die Nachbarschaft von B1 B2 (und umgekehrt), während B2 auch B3 enthält .

Ich verwende osmnx , um Straßendaten von OSM abzurufen .

  1. Wie kann ich mit osmnx und networkx ein Diagramm durchlaufen, um die Knoten und Kanten zu finden, die jeden Block definieren?
  2. Wie finde ich für jeden Block die benachbarten Blöcke?

Ich arbeite mich an einem Code, der einen Graphen und ein Koordinatenpaar (Breite, Länge) als Eingabe verwendet, den relevanten Block identifiziert und das Polygon für diesen Block und die Nachbarschaft wie oben definiert zurückgibt.

Hier ist der Code, mit dem die Karte erstellt wurde:

import osmnx as ox
import networkx as nx
import matplotlib.pyplot as plt

G = ox.graph_from_address('Nørrebrogade 20, Copenhagen Municipality',
                          network_type='all', 
                          distance=500)

und mein Versuch, Cliquen mit unterschiedlicher Anzahl von Knoten und Graden zu finden.

def plot_cliques(graph, number_of_nodes, degree):
    ug = ox.save_load.get_undirected(graph)
    cliques = nx.find_cliques(ug)
    cliques_nodes = [clq for clq in cliques if len(clq) >= number_of_nodes]
    print("{} cliques with more than {} nodes.".format(len(cliques_nodes), number_of_nodes))
    nodes = set(n for clq in cliques_nodes for n in clq)
    h = ug.subgraph(nodes)
    deg = nx.degree(h)
    nodes_degree = [n for n in nodes if deg[n] >= degree]
    k = h.subgraph(nodes_degree)
    nx.draw(k, node_size=5)

Theorie, die relevant sein könnte:

Auflisten aller Zyklen in einem ungerichteten Diagramm

tmo
quelle
Interessantes Problem. Möglicherweise möchten Sie das Algorithmus-Tag hinzufügen. Scheint, dass Nachbarschaften das einfachere Problem wären, nachdem Sie die Blöcke herausgefunden haben. Als Nachbarschaften suchen Sie nur einen gemeinsamen Vorteil, richtig? Und jeder Block wird eine Liste von Kanten haben ... Für Blöcke ist es meiner Meinung nach hilfreich, die Himmelsrichtung jeder Straßenoption an einem Knoten zu ermitteln und "weiter nach rechts abzubiegen" (oder nach links), bis Sie einen Rundgang abgeschlossen haben oder erreichen eine Sackgasse oder eine Schleife zurück auf sich selbst und rekursiv zurückverfolgen. Es scheint jedoch einige interessante Eckfälle zu geben.
Jeff H
Ich denke, diese Frage ist Ihrem Problem Nr. Sehr ähnlich. 1. Wie Sie im Link sehen können, habe ich ein wenig an dem Problem gearbeitet, und es ist ein knorriges Problem (stellt sich als NP-hart heraus). Die Heuristik in meiner Antwort könnte jedoch immer noch ausreichend gute Ergebnisse liefern.
Paul Brodersen
Da jede Lösung, die Sie für akzeptabel halten, wahrscheinlich auch eine Heuristik ist, ist es möglicherweise eine gute Idee, einen Testdatensatz zu definieren, um jeden Ansatz zu validieren. Für Ihr Beispieldiagramm wäre es also gut, eine Annotation aller Blöcke in maschinenlesbarer Form zu haben - nicht nur einige Beispiele in einem Bild.
Paul Brodersen

Antworten:

3

Das Auffinden von Stadtblöcken mithilfe des Diagramms ist überraschenderweise nicht trivial. Grundsätzlich bedeutet dies, den kleinsten Satz kleinster Ringe (SSSR) zu finden, was ein NP-vollständiges Problem darstellt. Eine Übersicht über dieses Problem (und verwandte Probleme) finden Sie hier . Auf SO gibt es eine Beschreibung eines Algorithmus, um ihn hier zu lösen . Soweit ich das beurteilen kann, gibt es in networkx(oder in Python) keine entsprechende Implementierung . Ich habe diesen Ansatz kurz ausprobiert und ihn dann aufgegeben - mein Gehirn ist für diese Art von Arbeit heute nicht mehr auf dem neuesten Stand. That being said, werde ich eine Prämie niemandem vergeben , die diese Seite zu einem späteren Zeitpunkt besuchen könnte und eine getestete Implementierung eines Algorithmus schreiben, der die SSSR in Python findet.

Ich habe stattdessen einen anderen Ansatz verfolgt und die Tatsache genutzt, dass der Graph garantiert planar ist. Kurz gesagt, anstatt dies als Diagrammproblem zu behandeln, behandeln wir dies als Bildsegmentierungsproblem. Zunächst finden wir alle verbundenen Regionen im Bild. Wir bestimmen dann die Kontur um jede Region und transformieren die Konturen in Bildkoordinaten zurück in Längen- und Breitengrade.

Angesichts der folgenden Importe und Funktionsdefinitionen:

#!/usr/bin/env python
# coding: utf-8

"""
Find house blocks in osmnx graphs.
"""

import numpy as np
import osmnx as ox
import networkx as nx
import matplotlib.pyplot as plt

from matplotlib.path import Path
from matplotlib.patches import PathPatch
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from skimage.measure import label, find_contours, points_in_poly
from skimage.color import label2rgb

ox.config(log_console=True, use_cache=True)


def k_core(G, k):
    H = nx.Graph(G, as_view=True)
    H.remove_edges_from(nx.selfloop_edges(H))
    core_nodes = nx.k_core(H, k)
    H = H.subgraph(core_nodes)
    return G.subgraph(core_nodes)


def plot2img(fig):
    # remove margins
    fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)

    # convert to image
    # https://stackoverflow.com/a/35362787/2912349
    # https://stackoverflow.com/a/54334430/2912349
    canvas = FigureCanvas(fig)
    canvas.draw()
    img_as_string, (width, height) = canvas.print_to_buffer()
    as_rgba = np.fromstring(img_as_string, dtype='uint8').reshape((height, width, 4))
    return as_rgba[:,:,:3]

Laden Sie die Daten. Zwischenspeichern Sie die Importe, wenn Sie dies wiederholt testen. Andernfalls kann Ihr Konto gesperrt werden. Ich spreche aus Erfahrung hier.

G = ox.graph_from_address('Nørrebrogade 20, Copenhagen Municipality',
                          network_type='all', distance=500)
G_projected = ox.project_graph(G)
ox.save_graphml(G_projected, filename='network.graphml')

# G = ox.load_graphml('network.graphml')

Beschneiden Sie Knoten und Kanten, die nicht Teil eines Zyklus sein können. Dieser Schritt ist nicht unbedingt erforderlich, führt aber zu schöneren Konturen.

H = k_core(G, 2)
fig1, ax1 = ox.plot_graph(H, node_size=0, edge_color='k', edge_linewidth=1)

beschnittenes Diagramm

Konvertieren Sie das Diagramm in ein Bild und suchen Sie nach verbundenen Regionen:

img = plot2img(fig1)
label_image = label(img > 128)
image_label_overlay = label2rgb(label_image[:,:,0], image=img[:,:,0])
fig, ax = plt.subplots(1,1)
ax.imshow(image_label_overlay)

Diagramm der Regionsbezeichnungen

Suchen Sie für jeden beschrifteten Bereich die Kontur und konvertieren Sie die Konturpixelkoordinaten zurück in Datenkoordinaten.

# using a large region here as an example;
# however we could also loop over all unique labels, i.e.
# for ii in np.unique(labels.ravel()):
ii = np.argsort(np.bincount(label_image.ravel()))[-5]

mask = (label_image[:,:,0] == ii)
contours = find_contours(mask.astype(np.float), 0.5)

# Select the largest contiguous contour
contour = sorted(contours, key=lambda x: len(x))[-1]

# display the image and plot the contour;
# this allows us to transform the contour coordinates back to the original data cordinates
fig2, ax2 = plt.subplots()
ax2.imshow(mask, interpolation='nearest', cmap='gray')
ax2.autoscale(enable=False)
ax2.step(contour.T[1], contour.T[0], linewidth=2, c='r')
plt.close(fig2)

# first column indexes rows in images, second column indexes columns;
# therefor we need to swap contour array to get xy values
contour = np.fliplr(contour)

pixel_to_data = ax2.transData + ax2.transAxes.inverted() + ax1.transAxes + ax1.transData.inverted()
transformed_contour = pixel_to_data.transform(contour)
transformed_contour_path = Path(transformed_contour, closed=True)
patch = PathPatch(transformed_contour_path, facecolor='red')
ax1.add_patch(patch)

Konturdiagramm über dem beschnittenen Diagramm

Bestimmen Sie alle Punkte im Originaldiagramm, die innerhalb (oder auf) der Kontur liegen.

x = G.nodes.data('x')
y = G.nodes.data('y')
xy = np.array([(x[node], y[node]) for node in G.nodes])
eps = (xy.max(axis=0) - xy.min(axis=0)).mean() / 100
is_inside = transformed_contour_path.contains_points(xy, radius=-eps)
nodes_inside_block = [node for node, flag in zip(G.nodes, is_inside) if flag]

node_size = [50 if node in nodes_inside_block else 0 for node in G.nodes]
node_color = ['r' if node in nodes_inside_block else 'k' for node in G.nodes]
fig3, ax3 = ox.plot_graph(G, node_color=node_color, node_size=node_size)

Diagramm des Netzwerks mit Knoten, die zum Block gehören, in rot

Es ist ziemlich einfach herauszufinden, ob zwei Blöcke Nachbarn sind. Überprüfen Sie einfach, ob sie einen Knoten gemeinsam nutzen:

if set(nodes_inside_block_1) & set(nodes_inside_block_2): # empty set evaluates to False
    print("Blocks are neighbors.")
Paul Brodersen
quelle
2

Ich bin mir nicht ganz sicher, ob cycle_basisSie die gewünschten Nachbarschaften erhalten, aber wenn dies der Fall ist, ist es ganz einfach, das Nachbarschaftsdiagramm daraus zu erhalten:

import osmnx as ox
import networkx as nx
import matplotlib.pyplot as plt

G = ox.graph_from_address('Nørrebrogade 20, Copenhagen Municipality',
                          network_type='all',
                          distance=500)

H = nx.Graph(G) # make a simple undirected graph from G

cycles = nx.cycles.cycle_basis(H) # I think a cycle basis should get all the neighborhoods, except
                                  # we'll need to filter the cycles that are too small.
cycles = [set(cycle) for cycle in cycles if len(cycle) > 2] # Turn the lists into sets for next loop.

# We can create a new graph where the nodes are neighborhoods and two neighborhoods are connected if
# they are adjacent:

I = nx.Graph()
for i, n in enumerate(cycles):
    for j, m in enumerate(cycles[i + 1:], start=i + 1):
        if not n.isdisjoint(m):
            I.add_edge(i, j)
Salzwürfel
quelle
Hallo salz sterben, willkommen zu SO und Dank für das Hacken in. Wenn tun nx.Graph(G)ich eine Menge Informationen (Gerichtetheit und Multigraph - Typ) verliere so dass ich eine schwierige Zeit zu überprüfen Sie Ihre Antwort habe, als ich die neue Grafik beziehen kann nicht scheinen Izu meine ursprüngliche Grafik G.
Bis zum
Es ist ein wenig Arbeit, die geometrischen Informationen aus dem Originaldiagramm beizubehalten. Ich werde es bald versuchen.
Salt-Die
@tmo geht gerade vorbei: In diesem Fall sollten Sie in der Lage sein, die MultiDiGraph-Klasse (die Graph erweitert) zu verwenden
Théo Rubenach
1

Ich habe keinen Code, aber ich denke, wenn ich auf dem Bürgersteig bin und an jeder Ecke nach rechts abbiege, fahre ich durch die Ränder meines Blocks. Ich kenne die Bibliotheken nicht, also rede ich hier nur algo.

  • Gehen Sie von Ihrem Punkt aus nach Norden, bis Sie eine Straße erreichen
  • Biegen Sie so weit wie möglich rechts ab und gehen Sie auf der Straße
  • Suchen Sie in der nächsten Ecke alle Straßen und wählen Sie die Straße aus, die den kleinsten Winkel bildet, wobei Ihre Straße von rechts zählt.
  • Gehen Sie auf dieser Straße.
  • biegen Sie rechts ab usw.

Es ist eigentlich ein Algorithmus, um ein Labyrinth zu verlassen: Halten Sie Ihre rechte Hand an der Wand und gehen Sie. Es funktioniert nicht bei Schleifen im Labyrinth, Sie drehen sich nur um. Aber es gibt eine Lösung für Ihr Problem.

Hashemi Emad
quelle
Das ist eine viel bessere Idee als ich. Ich werde eine Antwort mit einer Umsetzung Ihrer Intuition hinzufügen.
Paul Brodersen
0

Dies ist eine Implementierung von Idee Hashemi Emad . Es funktioniert gut, solange die Startposition so gewählt ist, dass es eine Möglichkeit gibt, in einem engen Kreis gegen den Uhrzeigersinn zu treten. Bei einigen Kanten, insbesondere außerhalb der Karte, ist dies nicht möglich. Ich habe keine Ahnung, wie man gute Startpositionen auswählt oder wie man Lösungen filtert - aber vielleicht hat jemand anderes eine.

Arbeitsbeispiel (beginnend mit Kante (1204573687, 4555480822)):

Geben Sie hier die Bildbeschreibung ein

Beispiel, bei dem dieser Ansatz nicht funktioniert (beginnend mit edge (1286684278, 5818325197)):

Geben Sie hier die Bildbeschreibung ein

Code

#!/usr/bin/env python
# coding: utf-8

"""
Find house blocks in osmnx graphs.
"""

import numpy as np
import networkx as nx
import osmnx as ox

import matplotlib.pyplot as plt; plt.ion()

from matplotlib.path import Path
from matplotlib.patches import PathPatch


ox.config(log_console=True, use_cache=True)


def k_core(G, k):
    H = nx.Graph(G, as_view=True)
    H.remove_edges_from(nx.selfloop_edges(H))
    core_nodes = nx.k_core(H, k)
    H = H.subgraph(core_nodes)
    return G.subgraph(core_nodes)


def get_vector(G, n1, n2):
    dx = np.diff([G.nodes.data()[n]['x'] for n in (n1, n2)])
    dy = np.diff([G.nodes.data()[n]['y'] for n in (n1, n2)])
    return np.array([dx, dy])


def angle_between(v1, v2):
    # https://stackoverflow.com/a/31735642/2912349
    ang1 = np.arctan2(*v1[::-1])
    ang2 = np.arctan2(*v2[::-1])
    return (ang1 - ang2) % (2 * np.pi)


def step_counterclockwise(G, edge, path):
    start, stop = edge
    v1 = get_vector(G, stop, start)
    neighbors = set(G.neighbors(stop))
    candidates = list(set(neighbors) - set([start]))
    if not candidates:
        raise Exception("Ran into a dead end!")
    else:
        angles = np.zeros_like(candidates, dtype=float)
        for ii, neighbor in enumerate(candidates):
            v2 = get_vector(G, stop, neighbor)
            angles[ii] = angle_between(v1, v2)
        next_node = candidates[np.argmin(angles)]
        if next_node in path:
            # next_node might not be the same as the first node in path;
            # therefor, we backtrack until we end back at next_node
            closed_path = [next_node]
            for node in path[::-1]:
                closed_path.append(node)
                if node == next_node:
                    break
            return closed_path[::-1] # reverse to have counterclockwise path
        else:
            path.append(next_node)
            return step_counterclockwise(G, (stop, next_node), path)


def get_city_block_patch(G, boundary_nodes, *args, **kwargs):
    xy = []
    for node in boundary_nodes:
        x = G.nodes.data()[node]['x']
        y = G.nodes.data()[node]['y']
        xy.append((x, y))
    path = Path(xy, closed=True)
    return PathPatch(path, *args, **kwargs)


if __name__ == '__main__':

    # --------------------------------------------------------------------------------
    # load data

    # # DO CACHE RESULTS -- otherwise you can get banned for repeatedly querying the same address
    # G = ox.graph_from_address('Nørrebrogade 20, Copenhagen Municipality',
    #                           network_type='all', distance=500)
    # G_projected = ox.project_graph(G)
    # ox.save_graphml(G_projected, filename='network.graphml')

    G = ox.load_graphml('network.graphml')

    # --------------------------------------------------------------------------------
    # prune nodes and edges that should/can not be part of a cycle;
    # this also reduces the chance of running into a dead end when stepping counterclockwise

    H = k_core(G, 2)

    # --------------------------------------------------------------------------------
    # pick an edge and step counterclockwise until you complete a circle

    # random edge
    total_edges = len(H.edges)
    idx = np.random.choice(total_edges)
    start, stop, _ = list(H.edges)[idx]

    # good edge
    # start, stop = 1204573687, 4555480822

    # bad edge
    # start, stop = 1286684278, 5818325197

    steps = step_counterclockwise(H, (start, stop), [start, stop])

    # --------------------------------------------------------------------------------
    # plot

    patch = get_city_block_patch(G, steps, facecolor='red', edgecolor='red', zorder=-1)

    node_size = [100 if node in steps else 20 for node in G.nodes]
    node_color = ['crimson' if node in steps else 'black' for node in G.nodes]
    fig1, ax1 = ox.plot_graph(G, node_size=node_size, node_color=node_color, edge_color='k', edge_linewidth=1)
    ax1.add_patch(patch)
    fig1.savefig('city_block.png')
Paul Brodersen
quelle