Trovare quartieri (cricche) nei dati stradali (un grafico)


10

Sto cercando un modo per definire automaticamente i quartieri nelle città come poligoni su un grafico.

La mia definizione di quartiere ha due parti:

  1. Un blocco : un'area compresa tra un numero di strade, in cui il numero di strade (bordi) e incroci (nodi) è un minimo di tre (un triangolo).
  2. Un quartiere : per ogni blocco dato, tutti i blocchi direttamente adiacenti a quel blocco e il blocco stesso.

Vedi questa illustrazione per un esempio:

inserisci qui la descrizione dell'immagine

Ad esempio B4 è un blocco definito da 7 nodi e 6 bordi che li collegano. Come la maggior parte degli esempi qui, gli altri blocchi sono definiti da 4 nodi e 4 bordi che li collegano. Inoltre, il quartiere di B1 include B2 (e viceversa) mentre B2 include anche B3 .

Sto usando osmnx per ottenere dati di strada da OSM.

  1. Usando osmnx e networkx, come posso attraversare un grafico per trovare i nodi e gli spigoli che definiscono ciascun blocco?
  2. Per ogni blocco, come posso trovare i blocchi adiacenti?

Sto lavorando a un pezzo di codice che accetta come input un grafico e una coppia di coordinate (latitudine, longitudine), identifica il blocco pertinente e restituisce il poligono per quel blocco e il vicinato come definito sopra.

Ecco il codice utilizzato per creare la mappa:

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)

e il mio tentativo di trovare cricche con diverso numero di nodi e gradi.

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)

Teoria che potrebbe essere pertinente:

Enumerazione di tutti i cicli in un grafico non indirizzato


Problema interessante. Potresti voler aggiungere il tag dell'algoritmo. Sembra che i quartieri sarebbero il problema più semplice dopo aver capito i blocchi. Come quartieri, tutto ciò che stai cercando è un vantaggio condiviso, giusto? E ogni blocco avrà un elenco di spigoli ... Per i blocchi, penso che sarà utile ottenere la direzione cardinale di ogni opzione di strada in un nodo e "continuare a girare a destra" (o a sinistra) fino a quando non si completa un circuito o si raggiunge un vicolo cieco o tornare indietro su te stesso e tornare indietro ricorsivamente. Sembra che ci sarebbero alcuni casi d'angolo interessanti, però.
Jeff H,

Penso che questa domanda sia molto simile al tuo problema no. 1. Come puoi vedere nel link, ho lavorato sul problema per un po ', ed è nodoso (risulta essere NP-difficile). L'euristica nella mia risposta, tuttavia, potrebbe comunque darti risultati abbastanza buoni.
Paul Brodersen,

Poiché qualsiasi soluzione che ritieni accettabile sarà probabilmente anche euristica, potrebbe essere una buona idea definire un set di dati di test per convalidare ogni approccio. Significato, per il tuo esempio grafico, sarebbe bene avere un'annotazione di tutti i blocchi in forma leggibile dalla macchina - non solo alcuni esempi in un'immagine.
Paul Brodersen,

Risposte:


3

Trovare blocchi di città usando il grafico è sorprendentemente non banale. Fondamentalmente, questo equivale a trovare il set più piccolo di anelli più piccoli (SSSR), che è un problema NP-completo. Una recensione di questo problema (e dei relativi problemi) può essere trovata qui . Su SO, c'è una descrizione di un algoritmo per risolverlo qui . Per quanto ne so, non esiste un'implementazione corrispondente in networkx(o in Python per quella materia). Ho provato brevemente questo approccio e poi l'ho abbandonato - il mio cervello non è all'altezza per quel tipo di lavoro oggi. Detto questo, assegnerò una taglia a chiunque possa visitare questa pagina in un secondo momento e pubblicherò un'implementazione testata di un algoritmo che trova l'SSSR in Python.

Ho invece seguito un approccio diverso, sfruttando il fatto che il grafico è garantito per essere planare. In breve, invece di considerare questo come un problema grafico, lo trattiamo come un problema di segmentazione dell'immagine. Innanzitutto, troviamo tutte le regioni connesse nell'immagine. Determiniamo quindi il contorno attorno a ciascuna regione, trasformiamo i contorni in coordinate dell'immagine in lunghezze e latitudini.

Date le seguenti definizioni di importazione e funzione:

#!/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]

Carica i dati. Memorizza nella cache le importazioni, se la verifica ripetutamente, altrimenti il ​​tuo account potrebbe essere bannato. Parlando per esperienza qui.

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')

Elimina nodi e bordi che non possono far parte di un ciclo. Questo passaggio non è strettamente necessario ma comporta contorni più piacevoli.

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

grafico potato

Converti trama in immagine e trova regioni collegate:

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)

trama delle etichette delle regioni

Per ogni regione etichettata, trova il contorno e converti le coordinate dei pixel del contorno in coordinate dei dati.

# 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)

trama del contorno sovrapposta al grafico potato

Determina tutti i punti nel grafico originale che rientrano (o accedono) al contorno.

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)

trama della rete con nodi appartenenti al blocco in rosso

Capire se due blocchi sono vicini è abbastanza facile. Controlla se condividono un nodo:

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

2

Non sono del tutto sicuro che cycle_basisti darà i quartieri che cerchi, ma se lo fa, è semplice ottenere il grafico del quartiere da esso:

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)

Ciao salt-die, benvenuti in SO e grazie per averci contattato. Quando lo faccio, nx.Graph(G)sto perdendo molte informazioni (orientamento e tipo multigrafo), quindi ho difficoltà a verificare la vostra risposta, poiché non riesco a mettere in relazione il nuovo grafico Icon il mio grafico originale G.
TMO

Sarà un po 'di lavoro per preservare le informazioni geometriche dal grafico originale. Ci proverò presto.
Salt-die

@tmo sta solo passando: dovresti essere in grado di usare la classe MultiDiGraph (che estende Graph) in quel caso
Théo Rubenach

1

Non ho un codice, ma immagino che una volta che sono sul marciapiede, se continuo a girare a destra in ogni angolo, scorrerò i bordi del mio blocco. Non conosco le biblioteche, quindi parlerò semplicemente qui.

  • dal tuo punto, vai a nord fino a raggiungere una strada
  • gira a destra il più possibile e cammina per strada
  • all'angolo successivo, trova tutti gli steets, scegli quello che fa l'angolo più piccolo con la tua strada contando da destra.
  • camminare su quella strada.
  • gira a destra, ecc.

In realtà è un algoritmo da usare per uscire da un labirinto: tieni la mano destra sul muro e cammina. Non funziona in caso di loop nel labirinto, fai semplicemente un giro. Ma offre una soluzione al tuo problema.


Questa è un'idea molto migliore di quella che avevo. Aggiungerò una risposta con un'implementazione del tuo intuito.
Paul Brodersen,

0

Questa è un'implementazione dell'idea di Hashemi Emad . Funziona bene fino a quando viene scelta la posizione iniziale in modo tale che esista un modo per avanzare in senso antiorario in un cerchio stretto. Per alcuni bordi, in particolare attorno alla parte esterna della mappa, ciò non è possibile. Non ho idea di come selezionare buone posizioni di partenza o come filtrare le soluzioni, ma forse qualcun altro ne ha una.

Esempio di lavoro (a partire dal bordo (1204573687, 4555480822)):

inserisci qui la descrizione dell'immagine

Esempio, in cui questo approccio non funziona (a partire da edge (1286684278, 5818325197)):

inserisci qui la descrizione dell'immagine

Codice

#!/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')
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.