今回は、Stravaのheatmapを使って、自転車経路の最適選択するというテーマです。2年ほど前からアイディアはあったのですが、最近OSRMのドキュメントを見ていたら、意外と簡単に実現できそうだったのでやってみました。
自転車で実際走っていると、「実は並行している別の道路の方が走りやすかった」「そもそも自転車通行禁止だった」等に遭遇します。道路の属性データの質・量を上げていくのも1つの解決策ですが、そこは地元の自転車乗りが一番知っているはず。Stravaのheatmapを眺めていると「ああやっぱりみんなこっち走っているのね」とか「あれ?こんなルートあったの?」とか思うことが多々あるので、このデータを使うと満足度の高いルート探索が実現できそうと思ったのが始まり。
構築手順
まず基本になるのが、こちらのOSRMのドキュメント。最後の方にある”Using Raster Data”というところ。予め用意しておいたラスターデータを使って経路の重み付けをできるというものです。例では標高差を考慮した最適化が紹介されていますが、今回はStrava Heatmap のデータを食わせればいいということになります。(そういう意味で汎用的に作ってあるんだなぁ、と感心)
さて、次なる課題は、以下になります。
- 入力データはどうやって作る?
- 入力データはどうやってルート最適化ロジックに反映させるの?
入力データ作成
ベースとなる入力データは当然Strava Heatmap サイトからダウンロードしてきます。
http://globalheat.strava.com/tiles/cycling/color7/${z}/${x}/${y}.png
こちらはXYZ形式の座標指定のPNG形式となっています。こちらを地図上で確認しやすいようにGeoTIFF形式に変換します。
$ gdal_translate -of Gtiff -a_ullr [xmin] [ymin] [xmax] [ymax] -a_srs EPSG:4326 src.png dst.tif
重み付けにあまり詳細なデータもいらないので、ある程度道路をカバーしているzoomlevel=10のデータを使うことにしました。作成したデータはQGIS等で確認することができます。サンプルは以下。わかりやすいように zoomlevel=10 のタイル 2×2 で伊豆半島をカバーするGeoTIFFを作成してみました。
ちなみにGeoTIFFのマージは、以下でできます。
$ gdal_merge.py -o <output_file> <input_files ...>
ここで作成したファイルはRGBA形式(4層)ですが、OSRMが処理しやすように1層にします。本来は、RGBAの4層の値から評価値を算出すべきなのですが、データを眺めてみたら緑(G)だけ抜き出してもそれなりに使えそうなデータになっていたので、とりあえずこれで行ってみることにしました。抜き出したデータを沼津〜富士のルートを地図上に重ねてみるとわかりますが、自動車のメジャーのルートである国道1号バイパスに比べ、海岸に近い県道380号(通称:旧国一)が自転車のメジャールートになっています。
このようにして、ひとまず入力するデータは用意できました。
本来は、これをASCII形式のラスターデータに変換する必要があるのですが、GeoTIFFのまま扱えた方が何か便利かと思い、osrmのraster読み込み部分を修正してみました。現状はやっつけ修正ですが、GeoTIFF対応版のソースはgithubに上げておきます(branch:charilab-tuned)。
最適化ロジックの作成
こちらは、先述のドキュメントにあるとおり、lua言語で記述されているprofileを修正するこで対応します。キーとなるのは process_segment()の部分。少し試した結果、以下のようにしてみました。
function process_segment (profile, segment)
local sourceData = raster:query(profile.raster_source, segment.source.lon, segment.source.lat)
local targetData = raster:query(profile.raster_source, segment.target.lon, segment.target.lat)
local invalid = sourceData.invalid_data()
local scaled_weight = segment.weight
if sourceData.datum ~= invalid and targetData.datum ~= invalid then
if sourceData.datum > 80 then
local rate = sourceData.datum / 1000.0
scaled_weight = scaled_weight * (1.0 - rate)
end
if targetData.datum > 80 then
local rate = targetData.datum / 1000.0
scaled_weight = scaled_weight * (1.0 - rate)
end
end
segment.weight = scaled_weight
end
お試し
先ほどの例で上げた沼津〜富士のルートを試してみるとこんな感じ。左側がheatmapの重み付けを加えたエンジンでのルート。
一般的にもわかりやすいルートとして、横浜から小田原までのルート。自転車乗りは国道134号をメインで使いますよね。strava heatmap と重ねてみてもメジャーなルートが選択されているのがわかります。
ちなみに、以前のエンジンでもそこそこいい線いっているのは、すでに自転車禁止区間を考慮するエンジンになっているため(この記事参照)。Googleやルートラボでどうなるかは各自お試しください。
まとめ
とりあえず、Strava Heatmap を使ったルート最適化のプラットフォームは構築でき、ある程度の効果が見込めることはわかりましたが、実際サービスで使うとなると大変なのはここから。課題としてはこんな感じ。
- チューニングはエンジンのチューニング(入力データの評価値の改善、重み付けの評価式の改善)には、現状経験と勘に頼らざるを得ない。
- パラメータ変更には検索用のデータの再構築(数時間かかる)が必要なので、試行錯誤の効率が悪い。
- そもそも最適ルートは、人によって違う。(アップダウン量、交通量、景色、トンネルの有無とか好みが多種多様)
まあ、こんなことを始めた理由は、「大手サービスが提示したルートではなく、自分好みのルートを探したい」だったので、ここで汎用的な1つのチューニングを模索するよりは、今後は、いかに個人の嗜好をルート探索に取り入れていくかを考えていきたい。
まずは一歩前進したということで。