【Python】OSMnxで車両走行道路の最短経路探索＆可視化をしてみる

最近まで高精度地図データ整備や交通情報生成の業務に携わっていたmoshiです。

オープンソースの地図データ「**OpenStreetMap**」ではどんなことができるのかなと、ふと気になって最近調べてました。その中で、最短経路探索・可視化まで手軽にできる「**OSMnx**」が面白いなと思ったので、紹介したいと思います。

# OSMnxとは

OSMnxは、OpenStreetMapから地理空間データをダウンロードし、実世界の道路網やその他の地理空間形状をモデル化・投影・可視化・分析することができるPythonパッケージ。
たった一行のPythonコードで、歩きやすい、運転しやすい、または自転車に乗りやすい都市ネットワークをダウンロードしてモデル化し、簡単に分析して視覚化することができる。
また他のインフラタイプ・公共施設/ポイント・建物の占有面積・標高データ・道路の方位/方向・速度/移動時間なども同様に簡単にダウンロードして操作することができる。

https://github.com/gboeing/osmnx

# 道路情報取得 〜 最短経路探索・可視化手順

愛知県名古屋市中村区を対象地域例として、車両走行道路における最短経路探索・可視化までの手順を以下に紹介します。

## 0. 環境準備

下記pipコマンドにより必要なライブラリをインストール (可視化用にfoliumも必要)。
`pip install osmnx folium`

**ただし、上記はWindowsでエラーが出ることがあるそうです。** (コンパイル時の問題？)
エラーが出る場合は、以下の方法を試すと良さそうです。

- OSMnxの依存パッケージGDAL・Fionaをwheel形式ファイルにより事前に直接ダウンロード・インストールする。  
  その後、pipコマンドを利用してインストール `pip install osmnx folium`  
  - wheel形式パッケージのGDAL・Fionaダウンロード・インストールは下記記事が参考になります。  
  [GeoPandasのインストールに失敗した場合の対処法](https://qiita.com/doraneko94/items/a9a3f4d7e32f953e7c5c)
- pipではなくAnacondaのcondaコマンドを利用してインストール `conda install osmnx folium`  

## 1. 対象地域の道路情報(グラフネットワーク)取得・可視化

```python
import folium
import osmnx as ox

# 対象地域の道路情報取得 (愛知県名古屋市中村区)
query = "Nakamuraku,Nagoya,Aichi,Japan"
G = ox.graph_from_place(query, network_type="drive")

# 道路グラフネットワーク可視化
fmap = ox.plot_graph_folium(G)
fmap.save(outfile="road_network.html")

opts = {"node_size": 5, "bgcolor": "white", "node_color": "blue", "edge_color": "blue"}
ox.plot_graph(G, show=False, save=True, filepath="road_network.png", **opts)
```

愛知県名古屋市中村区の道路情報は、クエリーを`"Nakamuraku,Nagoya,Aichi,Japan"`として、`ox.graph_from_place()`関数を利用することで取得できる。
※ その他に、ある座標地点から1km範囲内の道路情報取得、等も可能です (道路情報取得用の関数一覧は[こちら](https://osmnx.readthedocs.io/en/stable/osmnx.html#module-osmnx.graph))。

Pythonライブラリの**NetworkX**により、道路情報はグラフネットワークとして格納されます。
⇒ NetworkXについては、[こちら](https://docs.pyq.jp/python/math_opt/graph.html)のサイトが分かりやすく参考になるかと思います。

道路情報の可視化は下記の2パターンで行うことができる。  

- **foliumによるHTML形式での可視化 (road_network.html)**  
   `ox.plot_graph_folium()`関数を利用した出力結果  
   <img width="500" alt="folium_road_network.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/b19b7507-6175-f3f8-601b-93788b59afc9.png">
- **matplotlibによるPNG形式での可視化 (road_network.png)**  
   `ox.plot_graph()`関数による出力結果  
   <img width="500" alt="road_network.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/5903770b-5d23-702e-2e44-71c7e96a622c.png">


## 2. 取得道路情報のCSV出力

```python
# 道路グラフネットワークの各ノード・エッジ取得・CSV出力
nodes, edges = ox.graph_to_gdfs(G)
nodes.to_csv("road_network_nodes.csv")
edges.to_csv("road_network_edges.csv")
```
`ox.graph_to_gdfs()`関数により、グラフネットワーク(G)の各ノード・エッジデータを取得できる。
PandasのDataFrameを地理情報データ用に拡張したGeoDataFrame(gdf)形式でデータは取得される。
`to_csv()`関数によりCSV形式で各ノード・エッジデータのファイル出力・確認ができる。

## 3. 任意の二地点間での最短経路探索・可視化

```python
# 最短経路探索
start_point = (35.18253738854321, 136.85996828365532)
start_node = ox.get_nearest_node(G, start_point)
end_point = (35.16163249834248, 136.8824509819242)
end_node = ox.get_nearest_node(G, end_point)

shortest_path = ox.shortest_path(G, start_node, end_node)

# 最短経路探索結果の可視化
new_fmap = ox.plot_route_folium(G, shortest_path, route_map=fmap, color="red")
folium.Marker(location=start_point, tooltip="start").add_to(new_fmap)
folium.Marker(location=end_point, tooltip="end").add_to(new_fmap)
new_fmap.save(outfile="shortest_path_road_network.html")

outfile = "shortest_path_road_network.png"
opts = {"node_size": 5, "bgcolor": "white", "node_color": "blue", "edge_color": "blue"}
ox.plot_graph_route(G, shortest_path, show=False, save=True, filepath=outfile, **opts)
```

`ox.get_nearest_node()`関数で始終点座標の各最近傍ノードを取得し、
`ox.shortest_path()`関数により二地点間での最短経路探索が実行される。

最短経路探索結果は以下のように可視化される。

- **foliumによるHTML形式での可視化 (shortest_path_road_network.html)**  
   `ox.plot_route_folium()`関数を利用した出力結果  
   下図のマーカーで示す始点・終点間で問題なく最短経路が算出されていそう。  
   <img width="500" alt="shortest_path_road_network.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/3484134c-e61a-d177-1dcb-234dbbdf14e7.png">
- **matplotlibによるPNG形式での可視化 (shortest_path_road_network.png)**  
   `ox.plot_graph_route()`関数による出力結果  
   <img width="500" alt="folium_shortest_path_road_network.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/d1a972c5-47b1-c634-d4de-96facae2c233.png">

# まとめ
OSMnxを利用することで、数行のPythonコードでOSMの道路可視化・分析ができたので、とても便利だなと思いました。
これだけのデータ可視化・分析がオープンソースで手軽に実施できるのは、とてもありがたいことだなと感じます。

最後に最短経路探索＆可視化を利用しやすいようにひとまとめにしたコードを記載します。

```python:osmnx_search_shortest_path.py
import os
from pathlib import Path

import folium
import osmnx as ox

# 対象地域検索クエリ (愛知県名古屋市中村区)
query = "Nakamuraku,Nagoya,Aichi,Japan"

# 各種出力ファイルパス
outdir_path = Path(query.replace(",", "_"))
os.makedirs(outdir_path, exist_ok=True)

# 道路グラフネットワーク取得
graphml_outfile = outdir_path / "road_network.graphml"
if not os.path.isfile(graphml_outfile):
    # 走行可能な道路グラフネットワークを取得
    G = ox.graph_from_place(query, network_type="drive")
    # 取得データを再利用目的でGraphml形式にて保存
    ox.save_graphml(G, filepath=graphml_outfile)
else:
    # 前回取得の道路グラフネットワークを再利用
    G = ox.load_graphml(graphml_outfile)

# 道路グラフネットワーク可視化
fmap = ox.plot_graph_folium(G)
folium_outfile = outdir_path / "road_network.html"
fmap.save(outfile=str(folium_outfile))

png_outfile = outdir_path / "road_network.png"
opts = {"node_size": 5, "bgcolor": "white", "node_color": "blue", "edge_color": "blue"}
ox.plot_graph(G, show=False, save=True, filepath=png_outfile, **opts)

# 道路グラフネットワークの各ノード・エッジ取得・CSV出力
nodes, edges = ox.graph_to_gdfs(G)
nodes_csv_outfile = outdir_path / "road_network_nodes.csv"
nodes.to_csv(nodes_csv_outfile)
edges_csv_outfile = outdir_path / "road_network_edges.csv"
edges.to_csv(edges_csv_outfile)

# 最短経路探索
start_point = (35.18253738854321, 136.85996828365532)
start_node = ox.get_nearest_node(G, start_point)
end_point = (35.16163249834248, 136.8824509819242)
end_node = ox.get_nearest_node(G, end_point)

shortest_path = ox.shortest_path(G, start_node, end_node)

# 最短経路探索結果の可視化
new_fmap = ox.plot_route_folium(G, shortest_path, route_map=fmap, color="red")
folium.Marker(location=start_point, tooltip="start").add_to(new_fmap)
folium.Marker(location=end_point, tooltip="end").add_to(new_fmap)
folium_path_outfile = outdir_path / "shortest_path_road_network.html"
new_fmap.save(outfile=str(folium_path_outfile))

path_png_outfile = outdir_path / "shortest_path_road_network.png"
ox.plot_graph_route(
    G, shortest_path, show=False, save=True, filepath=path_png_outfile, **opts
)
```

# 参考

- [OSMnxを用いて，オープンストリートマップから道路ネットワークデータを取得しよう．](https://qiita.com/keijipoon/items/afa555326b1e5043588f)
- [OSMnx開発者による実行サンプル例 (GitHubリポジトリ)](https://github.com/gboeing/osmnx-examples)