ストリートデータ(グラフ)での近隣(クリーク)の検索


10

都市の近隣をグラフ上のポリゴンとして自動的に定義する方法を探しています。

私の近所の定義には2つの部分があります。

  1. ブロック:通り(エッジ)と交差点(ノード)の数が最低3(三角形)である、いくつかの通りの間で囲まれた領域。
  2. 近隣:特定のブロックについて、そのブロックに直接隣接するすべてのブロックとブロック自体。

例については、この図を参照してください。

ここに画像の説明を入力してください

たとえば、B4は7つのノードとそれらを接続する6つのエッジによって定義されるブロックです。ここでのほとんどの例と同様に、他のブロックは4つのノードとそれらを接続する4つのエッジによって定義されます。また、B1近傍にはB2が含まれ(逆も同様)、B2にB3が含まれます。ます。

OSMからストリートデータを取得するためにosmnxを使用しています。

  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)

関連するかもしれない理論:

無向グラフのすべてのサイクルを列挙する


興味深い問題。それにアルゴリズムタグを追加することができます。あなたがブロックを理解した後、近所がより簡単な問題であると思われます。近所として、あなたが探しているのは共有されたエッジですよね?そして、各ブロックにはエッジのリストがあります...ブロックの場合、ノードで各ストリートオプションの基本的な方向を取得し、回路が完成するか、行き止まりまたは自分自身にループバックし、再帰的にバックトラックします。ただし、興味深いコーナーケースがいくつかあるようです。
ジェフH

私が思うに、この質問はあなたの問題がないと非常によく似ています。1.リンクからわかるように、私は少し問題に取り組みましたが、それは危険な問題です(結局はNPハードであることがわかります)。ただし、私の回答のヒューリスティックでは、十分な結果が得られる可能性があります。
Paul Brodersen

許容できると考えるソリューションはおそらくヒューリスティックなものとなるため、各アプローチを検証するためのテストデータセットを定義することをお勧めします。つまり、グラフの例では、画像内のいくつかの例だけでなく、すべてのブロックの注釈を機械で読み取り可能な形式にするとよいでしょう。
Paul Brodersen

回答:


3

グラフを使用して街区を見つけることは驚くほど簡単ではありません。基本的に、これはNP完全問題である最小リングの最小セット(SSSR)を見つけることになります。この問題(および関連する問題)のレビューはこちらにあります。SOでは、ここでそれを解決するアルゴリズムの1つの説明があります。私の知る限り、対応する実装はありませんnetworkx(または、Pythonでは)。私はこのアプローチを手短に試してから、それを放棄しました。今日、その種の仕事のために私の脳はスクラッチに耐えられません。そうは言っても、後日このページにアクセスして、PythonでSSSRを検出するアルゴリズムのテスト済み実装を投稿する可能性がある人には、賞金を授与します。

代わりに、グラフが平面であることが保証されているという事実を利用して、別のアプローチを追求しました。簡単に言うと、これをグラフの問題として扱うのではなく、画像のセグメンテーションの問題として扱います。まず、画像内のすべての接続された領域を見つけます。次に、各領域の輪郭を決定し、画像座標の輪郭を経度と緯度に戻します。

次のインポートと関数定義があるとします。

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

赤のブロックに属するノードを持つネットワークのプロット

2つのブロックが隣接しているかどうかを判断するのは非常に簡単です。彼らがノードを共有しているかどうかを確認してください:

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クラスを使用することができるはずです。@tmoだけで渡す
テオRubenach

1

コードはありませんが、歩道に着いたら、四隅を右折し続けると、ブロックの端を循環するようになると思います。私はライブラリを知らないので、ここでアルゴについて話します。

  • あなたのポイントから、通りに達するまで北に行きます
  • できるだけ右に曲がって通りを歩く
  • 次のコーナーで、すべてのスティートを見つけ、右から数えたときにストリートとの角度が最小になるスティートを選びます。
  • その通りを歩いてください。
  • 右折など

それは実際には迷路を抜け出すために使用するアルゴリズムです。壁に右手を置いて歩きます。迷路にループがある場合は機能しません。ループするだけです。しかし、それはあなたの問題の解決策を提供します。


これは、私が思っていたよりもはるかに優れたアイデアです。あなたの直感の実装で答えを追加します。
Paul Brodersen

0

これはHashemi Emadのアイデアの実装です。開始位置がきつい円で反時計回りに進む方法が存在するように選択されている限り、うまく機能します。一部のエッジ、特にマップの外側では、これは不可能です。良い開始位置を選択する方法、またはソリューションをフィルタリングする方法はわかりませんが、おそらく他の誰かが持っています。

作業例(エッジ(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')
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.