この記事は「FOSS4G Advent Calendar 2025」の17日目の記事です。
1. はじめに
初投稿です。よろしくお願いします。
好きな地域の地図グッズが欲しかったので、OpenStreetMapのデータとOSMnxを使用して自作してみました。
本年7月に開催されたFOSS4G KANSAIでOSMnxを知ったこともあり、FOSS4Gのアドベントカレンダーに参加することとしました。
人生初の個人開発ということもあり、拙い点も多々あるかと思いますが、ご容赦いただけますと幸いです。
2. 作成したもの
1. プログラム
Pythonのライブラリ「OSMnx」を使用したプログラムです。
OpenStreetMapから指定したタグおよび範囲のデータを取得したのち、pngで保存する処理を行います。
なお、データを重ね合わせるために「mapplotlib」を使用しています。
ソースコード
import osmnx as ox
import matplotlib.pyplot as plt
import contextily as ctx
import geopandas as gpd
import matplotlib.patheffects as pe
# 画像設定
fig_height = 3.8 # 82mm=3.22inch
fig_width = 5 # 105mm=4.13inch
dpi = 600
# 色設定
background_color = '#fefcf7' # ミルキーホワイト(より明るく)
water_color = '#5dbbe3' # パステルブルー(鮮やかに)
road_color = '#64c2a6' # 明るいグリーン(白背景に映える)
railways_color = '#2e2e2e' # 鉄道:印刷で潰れないように濃いグレ
buildings_color = '#d8a976' # はっきりしたベージュオレンジ系
national_road_color = '#e6725b' # 国道:明るめレンガ色
# 地図の設定
north = 44.97 #44.97
south = 44.87 #44.87
east = 141.88 # 141.9
west = 142.07 #142.07
# キャッシュの設定
ox.settings.use_cache = True
ox.settings.log_console = True
# 画像の設定
fig, ax = plt.subplots(figsize=(fig_width, fig_height)) # 図のサイズと背景色
plt.subplots_adjust(left=0, right=1, bottom=0, top=1) # 余白を極力狭くする
ax.set_facecolor(background_color) # グラフエリア内の背景色
# データの取得と出力
print("川データ")
rivers = ox.features_from_bbox(bbox=[west, south, east, north], tags={'waterway': 'river'})
rivers.plot(ax=ax, color=water_color, linewidth=0.5)
print("水域データ")
water = ox.features_from_bbox(bbox=[west,south,east,north],tags={'natural': 'water'})
ox.plot_footprints(water, ax=ax, color=water_color, show=False, close=False)
print("車両道路データ")
road = ox.graph_from_bbox(bbox=[west,south,east,north],network_type='drive')
ox.plot_graph(road, ax=ax, node_size=0, edge_color=road_color, edge_linewidth=0.5, show=False, close=False)
print('県道データ')
secondary_road = ox.features_from_bbox(bbox=[west,south,east,north],tags={'highway': 'secondary'})
secondary_road.plot(ax=ax, color=road_color, linewidth=1)
print('国道データ')
trunk = ox.features_from_bbox(bbox=[west,south,east,north],tags={'highway': 'trunk'})
trunk.plot(ax=ax, color=national_road_color, linewidth=1.2, alpha=0.9)
print('鉄道データ')
railways = ox.features_from_bbox(bbox=[west,south,east,north],tags={'railway': 'rail'})
railways.plot(ax=ax, color=railways_color, linewidth=1, alpha=0.9)
print('建物データ')
buildings = ox.features_from_bbox(bbox=[west, south, east, north], tags={'building': True})
ox.plot_footprints(buildings, ax=ax, color=buildings_color, edge_color=buildings_color, edge_linewidth=0.3, show=True, close=True)
# ライセンスを表示
credit_text = "Map data from OpenStreetMap."
ax.text(
0.03, 0.03,
credit_text,
transform=ax.transAxes,
fontsize=6,
color="black",
ha="left", va="bottom",
alpha=0.7,
path_effects=[pe.withStroke(linewidth=0.5, foreground="black")]
)
# pngファイルで出力
fig.savefig("output/sample.png", dpi=dpi)
print("地図画像を出力しました: output/sample.png")
2. 地図グッズ
1で作成した画像データを元に、オリジナルグッズの印刷を請け負う業者に発注し、IDホルダーを作成します。
余談ですが、今回対象とした地域は北海道幌延町に位置する問寒別周辺です。
問寒別は執筆者が年に何回か訪問する、北海道の北の方に位置する人口200人程度の集落です。
3. OSMnx
3-1. OSMnxとは
OSMnxは、OpenStreetMapから道路網やその他の地理空間フィーチャを簡単にダウンロード、モデル化、分析、視覚化できるPythonパッケージです。歩行者、車、自転車のネットワークを1行のコードでダウンロードしてモデル化し、分析と視覚化が可能です。
公式ドキュメントはこちら。
今回は上述のうち、「地理空間地物のダウンロード」と「モデル化」の機能を使用しました
3-2. 使用したモジュール
- features モジュール
- OpenStreetMapの地理空間フィーチャからGeoDataFrameをダウンロードして作成します
- graph モジュール
- OpenStreetMap データをダウンロードしてグラフを作成します
- plot モジュール
- 道路網、ルート、方向、地理空間機能を視覚化します
4. つまづいたところ
4-1. 範囲指定の方法
OSMnxには範囲指定の方法が複数あります。
例えば、features モジュールでは、以下6種類の範囲指定の方法があります。
-
features_from_address()- 住所から一定の距離内にある OSM フィーチャをダウンロードします
-
features_from_bbox()- 緯度経度境界ボックス内の OSM フィーチャをダウンロードします
-
features_from_place()- ある場所の境界内にある OSM フィーチャをダウンロードします
-
features_from_point()- 緯度経度ポイントから一定の距離内にある OSM フィーチャをダウンロードします
-
features_from_polygon()- (マルチ)ポリゴンの境界内の OSM フィーチャをダウンロードします
-
features_from_xml()- OSM XML ファイルのデータから OSM フィーチャの GeoDataFrame を作成します
まずはfeatures_from_point()を使用し、中心となる緯度経度とデータの取得範囲を指定してみました。
結果は以下の通りです。
サンプルコード
# 中心となる緯度経度を指定
latitude = 44.908699
longitude = 141.957398
# データの取得範囲を指定(単位はメートル)
dist = 8000
# 水域データの取得
water = ox.features_from_point(center_point=(latitude,longitude), dist=dist, tags={'natural': 'water'})
# 道路データの取得
road = ox.graph_from_point(center_point=(latitude,longitude), dist=dist,network_type='all')
# FigureとAxesを作成
fig, ax = plt.subplots(figsize=(8, 8))
# 道路を描画
ox.plot_graph(road, ax=ax, show=False, close=False, bgcolor='#ffffff', edge_color='green', node_size=0)
# 水域を描画
ox.plot_footprints(water, ax=ax, show=True, close=True, color='blue', edge_color='none')
# pngファイルで出力
fig.savefig("output/sample.png", dpi=300)
print("地図画像を出力しました: output/sample.png")
なんということでしょう。国内第4位の長さを誇る天塩川が、上流から下流まで、余すことなく出力されてしまったではありませんか。
続いてfeatures_from_bbox()を使用し、データ取得範囲を緯度軽度で指定してみます。
結果は以下の通りです。
サンプルコード
# データ取得範囲の緯度経度を指定
north = 44.97
south = 44.87
east = 141.9
west = 142.07
# 水域データ・道路データの取得
water = ox.features_from_bbox(bbox=[west,south,east,north],tags={'natural': 'water'})
road = ox.graph_from_bbox(bbox=[west,south,east,north],network_type='all')
# 鉄道データの取得(追加)
railways = ox.graph_from_bbox(bbox=[west, south, east, north], network_type='all', simplify=True, retain_all=True, truncate_by_edge=True, custom_filter='["railway"~"rail"]')
# 建物データの取得(追加)
buildings = ox.features_from_bbox(bbox=[west, south, east, north], tags={'building': True})
# FigureとAxesを作成(縦横比を修正)
fig, ax = plt.subplots(figsize=(10, 8))
# 道路・水域を描画
ox.plot_graph(road, ax=ax, show=False, close=False, bgcolor='#ffffff', edge_color='green', node_size=0)
ox.plot_footprints(water, ax=ax, show=True, close=True, color='blue', edge_color='none')
# 鉄道を描画(追加)
ox.plot_graph(railways, ax=ax, show=True, close=True, edge_color='black', edge_linewidth=0.5, node_size=0)
# 建物を描画(追加)
ox.plot_footprints(buildings, ax=ax, show=True, close=True, color='orange', edge_color='none')
# pngファイルで出力(dpiを修正)
fig.savefig("output/sample.png", dpi=600)
print("地図画像を出力しました: output/sample.png")
ひとまず、それらしい画像データが出力できました。
4-2.余白を狭くする
先ほど作成した画像は、上下左右の余白があまりにも大きい状態となっています。これらの余白は画像サイズで変わるため、値を調整しながら決めていきます。
執筆者が検証したところ、画像の高さが3.5インチにすると、左右の余白が小さくなり丁度良い塩梅となりました。一方で、高さを4インチにすると、上下にたくさん余白が出てきてしまいました。
また、subplots_adjust()のパラメーターをすべて0にしたり、0.1など1未満の値にした場合はエラーとなったり、余白が大きくなってしまいました。
サンプルコード(画像サイズ設定)
# 画像のサイズを指定(単位はインチ)
fig_height = 3.8
fig_width = 5
# 画像データの作成
fig, ax = plt.subplots(figsize=(fig_width, fig_height))
# 余白の指定
plt.subplots_adjust(left=0, right=1, bottom=0, top=1)
また、合わせて色の設定とライセンス表示の処理を追加しました。
執筆者は色のセンスに自信がないため、色コードはChatGPTに出力してもらいました。
結果は以下の通りです。
サンプルコード(色設定)
# 色設定
background_color = '#fefcf7' # ミルキーホワイト(より明るく)
water_color = '#5dbbe3' # パステルブルー(鮮やかに)
road_color = '#64c2a6' # 明るいグリーン(白背景に映える)
railways_color = '#2e2e2e' # 鉄道:印刷で潰れないように濃いグレ
buildings_color = '#d8a976' # はっきりしたベージュオレンジ系
national_road_color = '#e6725b' # 国道:明るめレンガ色
サンプルコード(ライセンス表示)
# ライセンスを表示
credit_text = "Map data from OpenStreetMap."
ax.text(
0.03, 0.03,
credit_text,
transform=ax.transAxes,
fontsize=6,
color="black",
ha="left", va="bottom",
alpha=0.7,
path_effects=[pe.withStroke(linewidth=0.5, foreground="black")]
)
グッズらしい画像データになったのではないでしょうか。
4-3. 道路データのデザイン化
先ほど作成した画像は、国道を赤・県道(北海道のため道道)を太い緑・その他道路を細い緑に設定しています。
続いて、こちらの設定方法をまとめます。
OSMnxで道路データをダウンロードするには、featuresモジュールを使う場合とgraphモジュールを使用する場合の2通りが挙げられます。
1. feature モジュールを使用する場合
featuresモジュールは、OpenStreetMapのデータそのものに付けられた「タグ」を指定することで、データをダウンロードします。
今回は範囲指定の方法を緯度経度のボックスとしているため、 features_from_bbox()を使用します。
OpenStreetMapのデータに何のタグが付けられているかは、OpenStreetMapホームページから、データをクリックすることで確認できます。また、タグ一覧はOpenStreetMap 編集用タグ一覧表に公開されています。
国道と県道を出力した結果は以下の通りです。
サンプルコード(国道・県道の表示)
print('県道データ')
secondary_road = ox.features_from_bbox(bbox=[west,south,east,north],tags={'highway': 'secondary'})
secondary_road.plot(ax=ax, color=road_color, linewidth=1) # プロット時に色・幅を指定
print('国道データ')
trunk = ox.features_from_bbox(bbox=[west,south,east,north],tags={'highway': 'trunk'})
trunk.plot(ax=ax, color=national_road_color, linewidth=1.2, alpha=0.9) # プロット時に色・幅・透明度を指定
上記の画像では、高速道路や市道など、他のタグに割り振られたデータは出力できておりません。
OpenStreetMapのタグは細かく分かれており、道路関係だけで19種類。小道や交差点、坂などを含めると57種類になります。
これらを1つずつ指定して表示するのも大変なので、その他の道路はgraphモジュールを使用してまとめて出力することにしました。
2. graphモジュールを使用する場合
graphモジュールはOpenStreetMapの道路データを、PythonのパッケージであるNetworkXの構造でグラフ化したものです。今回使用した関数は、graph_from_bbox()です。
本来の使用用途はここでダウンロードしたデータから、経路探索やネットワーク解析を行うものですが、今回は道路データをまとめて出力するために使用します。
結果は以下の通りです。
サンプルコード(全道路データの表示)
road = ox.graph_from_bbox(bbox=[west,south,east,north],network_type='all')
ox.plot_graph(road, ax=ax, node_size=0, edge_color=road_color, edge_linewidth=0.5, show=False, close=False)
print('県道データ')
secondary_road = ox.features_from_bbox(bbox=[west,south,east,north],tags={'highway': 'secondary'})
secondary_road.plot(ax=ax, color=road_color, linewidth=1)
print('国道データ')
trunk = ox.features_from_bbox(bbox=[west,south,east,north],tags={'highway': 'trunk'})
trunk.plot(ax=ax, color=national_road_color, linewidth=1.2, alpha=0.9)
「network_type='all'」としたことで、林道や私道を含む、すべての道路が出力されました。
network_typeはall・all_public・bike・drive・drive_service・walkから指定できます。今回は車両が通行できる道路を指定するため、「network_type='drive'」とすることにしました。
最終形は以下の通りです。
サンプルコード
print("車両道路データ")
road = ox.graph_from_bbox(bbox=[west,south,east,north],network_type='drive')
ox.plot_graph(road, ax=ax, node_size=0, edge_color=road_color, edge_linewidth=0.5, show=False, close=False)
print('県道データ')
secondary_road = ox.features_from_bbox(bbox=[west,south,east,north],tags={'highway': 'secondary'})
secondary_road.plot(ax=ax, color=road_color, linewidth=1)
print('国道データ')
trunk = ox.features_from_bbox(bbox=[west,south,east,north],tags={'highway': 'trunk'})
trunk.plot(ax=ax, color=national_road_color, linewidth=1.2, alpha=0.9)
5. 終わりに
小規模ではありますが、人生初の個人開発が形になったことがとても面白かったです。
次はこれをブラウザ上で出力できるwebサイトを構築してみたいです。
参考リンク
Matplotlib plt.subplots()の使い方|FigureとAxesを同時生成!
【python】matplotlibで図の余白を調整する






