Поиск окрестностей (клик) по данным улиц (график)

10

Я ищу способ автоматического определения районов в городах как полигонов на графике.

Мое определение окрестности состоит из двух частей:

  1. Блок : область, заключенная между количеством улиц, где количество улиц (ребер) и перекрестков (узлов) составляет минимум три (треугольник).
  2. Окрестности : для любого данного блока, все блоки, непосредственно прилегающие к этому блоку и сам блок.

Смотрите эту иллюстрацию для примера:

введите описание изображения здесь

Например, B4 - это блок, определяемый 7 узлами и 6 ребрами, соединяющими их. Как большинство примеров здесь, другие блоки определяются 4 узлами и 4 ребрами, соединяющими их. Кроме того , соседство из B1 включает B2 (и наоборот) , а B2 также включает в себя B3 .

Я использую osmnx для получения уличных данных из OSM.

  1. Используя osmnx и networkx, как я могу пройти по графику, чтобы найти узлы и ребра, которые определяют каждый блок?
  2. Как найти соседние блоки для каждого блока?

Я работаю над частью кода, который принимает график и пару координат (широта, долгота) в качестве входных данных, идентифицирует соответствующий блок и возвращает многоугольник для этого блока и окрестности, как определено выше.

Вот код, используемый для создания карты:

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)

и моя попытка найти клики с разным количеством узлов и степеней.

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)

Теория, которая может быть актуальной:

Перечисление всех циклов в неориентированном графе

денежный перевод по телеграфу
источник
Интересная проблема. Возможно, вы захотите добавить тег алгоритма к нему. Кажется, что окрестности были бы более легкой проблемой после того, как вы выяснили блоки. Как районы, все, что вы ищете, это общая граница, верно? И у каждого блока будет список ребер ... Я думаю, что для блоков будет полезно определить кардинальное направление каждого варианта улицы в узле и "продолжать поворачивать направо" (или налево), пока вы не завершите кругооборот или не достигнете тупик или зацикливайся на себе и возвращайся рекурсивно. Похоже, что было бы несколько интересных угловых случаев, хотя.
Джефф Х
Я думаю, что этот вопрос очень похож на вашу проблему нет. 1. Как вы видите по ссылке, я немного поработал над этой проблемой, и она довольно трудная (оказывается NP-трудной). Эвристика в моем ответе, однако, все еще может дать вам достаточно хорошие результаты.
Пол Бродерсен
Поскольку любое решение, которое вы сочтете приемлемым, вероятно, также будет эвристическим, было бы неплохо определить набор тестовых данных для проверки каждого подхода. Это означает, что для вашего примера графика было бы хорошо иметь аннотацию всех блоков в машиночитаемой форме, а не только несколько примеров на изображении.
Пол Бродерсен

Ответы:

3

Поиск городских кварталов с помощью графика на удивление нетривиален. По сути, это сводится к поиску наименьшего набора наименьших колец (SSSR), что является NP-полной проблемой. Обзор этой проблемы (и связанных с ней проблем) можно найти здесь . На SO, есть одно описание алгоритма, чтобы решить это здесь . Насколько я могу сказать, нет соответствующей реализации в networkx(или в Python в этом отношении). Я кратко попробовал этот подход, а затем отказался от него - у меня сегодня не до мозга костей для такой работы сегодня. Тем не менее, я назначу награду любому, кто может посетить эту страницу позже, и опубликую протестированную реализацию алгоритма, который находит SSSR в python.

Вместо этого я придерживался другого подхода, используя тот факт, что граф гарантированно является плоским. Вкратце, вместо того, чтобы рассматривать это как проблему с графом, мы рассматриваем это как проблему сегментации изображения. Сначала мы находим все связанные области на изображении. Затем мы определяем контур вокруг каждой области, преобразуем контуры в координатах изображения обратно в долготы и широты.

Имеются следующие определения импорта и функции:

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

Загрузите данные. Кэшируйте импорт, если проверяете это повторно, иначе ваш аккаунт может быть заблокирован. Говоря из опыта здесь.

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

Удалите узлы и ребра, которые не могут быть частью цикла. Этот шаг не является строго необходимым, но приводит к более приятным контурам.

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

обрезанный граф

Преобразуйте график в изображение и найдите связанные регионы:

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)

сюжет региональных меток

Для каждой маркированной области найдите контур и преобразуйте координаты пикселя контура обратно в координаты данных.

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

участок контура наложен на обрезанный график

Определите все точки исходного графика, которые попадают внутрь (или на) контура.

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)

участок сети с узлами, принадлежащими блоку в красном

Выяснить, являются ли два блока соседями, довольно легко. Просто проверьте, имеют ли они общий узел:

if set(nodes_inside_block_1) & set(nodes_inside_block_2): # empty set evaluates to False
    print("Blocks are neighbors.")
Пол Бродерсен
источник
2

Я не совсем уверен, что cycle_basisэто даст вам окрестности, которые вы ищете, но если это так, то получить граф окрестностей очень просто:

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)
соль-матрица
источник
Привет соль умереть, добро пожаловать на SO и спасибо за сколы. При выполнении nx.Graph(G)я теряю много информации (Направленность и типа мультиграфа) , так что я имею трудное время проверки вашего ответа, поскольку я не могу показаться , чтобы связать новый граф Iс мой оригинальный график G.
ТМО
Будет немного работы по сохранению геометрической информации из исходного графика. Я попытаюсь это скоро.
Соляная смерть
@tmo просто проходя мимо: вы должны быть в состоянии использовать класс MultiDiGraph (который расширяет Graph) в этом случае
Тео Рубенах
1

У меня нет кода, но я предполагаю, что как только я окажусь на тротуаре, если я продолжу поворачивать вправо в каждом углу, я буду циклически проходить по краям моего блока. Я не знаю библиотек, поэтому я просто поговорю здесь.

  • с вашей точки, идите на север, пока не дойдете до улицы
  • поверните направо столько, сколько сможете и идите по улице
  • в следующем углу найдите всех steets, выберите тот, который делает наименьший угол с вашей улицей, считая справа.
  • ходить по этой улице.
  • повернуть направо и т. д.

На самом деле это алгоритм, который можно использовать для выхода из лабиринта: держите правую руку на стене и ходите. Это не работает в случае петель в лабиринте, вы просто зацикливаетесь. Но это дает решение вашей проблемы.

Хашеми Эмад
источник
Это гораздо лучшая идея, чем у меня была. Я добавлю ответ с реализацией вашей интуиции.
Пол Бродерсен
0

Это реализация идеи Хашеми Эмада . Это работает хорошо, если исходная позиция выбрана таким образом, что существует способ сделать шаг против часовой стрелки в крутом круге. Для некоторых ребер, в частности вокруг карты, это невозможно. У меня нет идеи, как выбирать хорошие стартовые позиции или как фильтровать решения - но, может быть, у кого-то еще есть такая.

Рабочий пример (начиная с ребра (1204573687, 4555480822)):

введите описание изображения здесь

Пример, где этот подход не работает (начиная с края (1286684278, 5818325197)):

введите описание изображения здесь

Код

#!/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')
Пол Бродерсен
источник