どこでもドアは人類の夢である
実現してほしいひみつ道具アンケートNo1。
今回は、これをpythonで作ることに挑戦する。
先に結果 ⇒ 「どこでもドア気分」
任意の地点の接続ドラレコ動画が作れるツールを作った。
要件定義
ホンモノを作るのは、私の科学力ではちょっとだけ難しいので、
どんな 欲望 要望 を満たすツールにするのか、要件定義を行った。
青狸ノ元ニ伸太在リ、何処出門扉ヲ使ウ
則ラバ声聞ユ 「嗚呼伸太氏ノ卑猥」
静曰ク「何スレゾ伸太氏此処ニ或ル乎」ト
伸太答ヘテ曰ク「誤解也」ト
~ 天道蟲連画集第拾参巻 ~
常に浴室に通じるのは古来より最重視される仕様の一つである。
しかし、使用者起因バグの可能性も否めないため、
今回は任意の地点間の移動方法として考えた。
原作のホンモノは、「空間歪曲装置」である。
決して、使用者の精巧なコピーを移動先に作成し
移動元の使用者を抹殺する恐怖の装置ではない。
pythonには残念だが空間歪曲機能は無い。
ホンモノを作ることを目指す途中段階として、
「移動した気分」を重視したツールを目指した。
現実的な移動方法(空間歪曲のルート)を定義し、
その2地点の空間を接続するツールにしよう!
顧客が本当に必要だったもの
最初の動画は、東京から横浜まで
(正確には、東京タワーからアンパンマンミュージアムまで)
のドライビングルートの連続動画Gifである。
が、最先端の空間歪曲装置を使って、
途中を大幅にショートカットすることに成功した。
(見逃した人は再度よ~く見てください。)
- 「移動している感」が出ること
- 移動の途中で「空間歪曲」を自然に利用
という2点の要件を満たすことが出来た。
なお、原作では「10光年以内の距離しか移動できない」
という制限があるが、
今回の制限は「車で行ける道が存在する範囲」としている。
地点の入力を変えれば、京都から東京まで、
自宅から会社まで、など任意の移動気分を味わえる。
なお、もちろん、最初から最後まで「ワープ」抜きで、
ドライブ画像で通すことも可能であり、
道案内として活用することもできる。
その場合、「グウぐる」のAPIキーを入手し、
リクエスト数の制限を緩和、または、
もっと長距離ならば課金する必要がある。
最先端の空間歪曲装置は、APIキー無し&無料の範囲で、
本ツールを実現することにも貢献している。
「どこでもドア気分」の作り方
「GoogleMapのストリートビューのイメージを繋げただけでしょ?」
と思われた方も多いかもしれない。半分正解で半分ハズレ。
手動でストリートビューをグリグリやれば、似たような感じに見えるが、
操作が面倒だし、画像のロードに時間がかかる。爽快感に乏しい。
「重い」サービスなので、直接は利用できない。
また、道案内/ナビ機能では、左折右折など、要所ごとの案内が基本。
取得できるポイント間の距離が離れすぎており、
短距離ごとの地点/角度を得るには工夫が必要である。
(類似先行サービスも見たが、地点と角度の精度が悪く、見栄えが悪い)
綺麗にイメージがつながるようにして、その上、
GoogleMapのAPIの無料枠(しかもAPIキー不要)の範囲で、
Google側にも過度な負荷をかけないようにするには、
どんなことに取り組んだのか、
ジオ・コーディング(geocoding)のノウハウとして、
各要素ごとに分割利用できるように、まとめてみる。
似たものを作るまでならそう難しくないけれど、
運転しているような雰囲気が出るように精度をあげようとすると、
結構いろいろな工夫が必要になってくるよ、ということ。
現時点の結果でも、いくつかイレギュラー的な画像が混ざっている。
(が、敢えて手動で調整せずに、自動出力のままupしている)
作り方の概要(目次的な)
- 静的なストリートビューイメージの取得
- 道案内/ナビAPIの利用方法
- 地点情報を増やす(間隔を補完する)方法
- 進行方向の「角度」の算出方法
- 「polyline」のデコード方法
- GIFアニメ化の方法
- 空間歪曲装置の実装
具体的なコード/ノウハウ
静的なストリートビューイメージの取得
ストリートビューは、良く知られているグリグリ動くやつ以外に、
静的な画像データを取得できるAPIもある。
「Street View Image API」
Webサイトにお店の入り口画像を表示する時などに利用できるもので、
APIキーの取得がなくても、千回くらいまでなら連続で使える。
今回は、静的な画像を取得し、繋げてGIFアニメにする方針。
具体的には、例えば以下のURLにアクセスすれば、画像が見れる。
https://maps.googleapis.com/maps/api/streetview?size=600x300&location=46.414382,10.013988&fov=90&heading=151.78&pitch=-0.76
headingは、カメラのコンパスの方向を示している。
fov(デフォルトは90)は、画像ビューの水平視野。最大で120まで。
pitchはカメラの角度(上下)を示している。
これを利用して、緯度経度と角度を入力すれば、
画像データをpngとして保存するような関数を作る。
import requests
def make_one_url(lon, lat, heading):
url_str = r"https://maps.googleapis.com/maps/api/streetview?size=600x300"
url_str += r"&location=" + str(lon) + r"," + str(lat)
url_str += r"&heading="+ str(heading)
#以下のオプションをつけたほうが望ましいと考えられる
url_str += r"&source=outdoor"
return url_str
# 画像をダウンロードする
# 指定したURLの画像データを返す関数
def download_image(url, timeout = 30):
response = requests.get(url, allow_redirects=False, timeout=timeout)
if response.status_code != 200:
e = Exception("HTTP status: " + response.status_code)
raise e
content_type = response.headers["content-type"]
if 'image' not in content_type:
e = Exception("Content-Type: " + content_type)
raise e
return response.content
# 画像を保存する関数
def save_image(filename, image):
with open(filename, "wb") as fout:
fout.write(image)
# メイン(使用例)
if __name__ == "__main__":
url = make_one_url(46.414382, 10.013988, 151.78)
try:
print("GET: " + url)
#イメージをURLから落としてくる
image = download_image(url)
save_image("static-dl-test.png", image)
except Exception as err:
print (err)
緯度経度や角度などのパラメータの作り方/与え方によって、
様々な応用が可能になる関数ができた。便利。
注意点として、取得できるデータは「Googleが撮影したデータ」なので、
変な座標を与えてしまうと、容赦無く灰色のエラー画像になる。
あらゆる座標に対応しているわけではないし、
「道路上」の画像に限定しているわけでもない。
そのために「緯度経度角度をどうやって綺麗に与えるのか?」が
一番難しい問題になってくる。
また、Googleの仕様上、「標高」の指定が出来ないようで、
複数階層の道路や、高架道路の交差部分などは、
望み通りの道の画像が得られないなど、イレギュラー要素は多い。
夜間のデータになったりすることもある。
施設内などの室内画像が入ることもある。
(&source=outdoor のオプションを付与してある程度回避)
GIFアニメ中に稀に変な画像が入る主な理由は、
標高等の撮影ポイントの自動補正によって、
近辺の道や、別の高さの画像が含まれるため。
どうしても綺麗なアニメにしたい場合は、
おかしな画像は手動で取り直す必要がある。
(今回は実施していない)
道案内/ナビAPIの利用方法
今度は、緯度経度角度の情報を作るため、
東京から横浜までの行き方を調べるAPIを叩く。
「Google Map Directions API」
Chromeで以下のURLにアクセスすると、
JSON形式でデータが返ってくるのが確認できる。
https://maps.googleapis.com/maps/api/directions/json?origin=$東京駅&destination=$みなとみらい&travelmode=driving
人間サマに分かりやすい表示を求めるならば、以下。
https://www.google.com/maps/dir/?api=1&origin=$東京駅&destination=$みなとみらい&travelmode=driving
こちらも、大量に叩くのでなければ、APIキーは不要!
import json
import urllib.request
def makeNaviURL(startName, endName):
#URLに日本語が入るのはいまいちなので修正
startName = urllib.parse.quote_plus(startName, encoding='utf-8')
endName = urllib.parse.quote_plus(endName, encoding='utf-8')
result_str = r"https://maps.googleapis.com/maps/api/directions/json?origin=$"+startName+"&destination=$"+endName
print(result_str)
#人間に分かる形:
view_str = r"https://www.google.com/maps/dir/?api=1&origin=$"+startName+"&destination=$"+endName+"&travelmode=driving"
print(view_str)
return result_str
#APIに実際にアクセスして、結果をprint
def printNaviRes(url, timeout = 10):
try:
with urllib.request.urlopen(url) as url_opend:
html = url_opend.read().decode('utf-8')
#print(html)
print("----------------------------------")
json_content = json.loads(html)
print(json_content)
print("----------------------------------")
print(json_content["status"])
# OK
print("----------------------------------")
print(json_content["routes"][0]["legs"])
# リストが返ってくる
except:
print("ERR!")
import traceback
traceback.print_exc()
# メイン(使用例)
if __name__ == "__main__":
start_point_name = u"東京駅"
end_point_name = u"みなとみらい"
url = makeNaviURL(start_point_name, end_point_name)
printNaviRes(url, timeout = 10)
さて、URLにアクセスしたjson結果を見ると、
["steps"]のところに、各運転操作が必要な箇所ごとの、
緯度経度が格納されていることが分かる。
だが良く見て欲しい。長い所では、
10分や13分に一箇所、ポイントされているだけである。
10分も車が走ったら全く別な場所になってしまうので、
単純にこの緯度経度のリストを使うと、枚数が足りなすぎる。
「角度」情報も無い。写真を撮る間隔も違い過ぎる。
道案内/ナビデータと、StreetViewを単純に
組み合わせるだけでは全くサマにならない!
地点情報を増やす(間隔を補完する)方法
緯度経度のリストに対して、その間の地点を
出来るだけ等間隔で埋めるようにしないと、アニメにならない。
任意の緯度経度のリストと、撮影間隔(メートル)が与えられたときに、
与えられた地点同士の距離を計算して、
緯度経度のリストの間を補完する関数を作る。
例えば、
「A地点, B地点, C地点」の3つの座標リストが与えられたとして、
A地点 ~ B地点 = 900m
B地点 ~ C地点 = 500m
で、撮影したい間隔が300m以下ならば、
A地点 ~ B地点 ⇒ 300m×3に分割(A, a-1, a-2 ,B)
B地点 ~ C地点 ⇒ 250m×2に分割(B, b-1, C)
となるように、合計6点の座標リストにするということ。
緯度経度が与えられた時に、その距離を計算することが必要で、
その方法は以下のQiita記事をご参照:
緯度経度より距離を(地球の丸さも考えて)求める。
この関数を使って、作った補完関数は以下。
#######距離計算
#https://qiita.com/s-wakaba/items/e12f2a575b6885579df7
from math import sin, cos, acos, radians
earth_rad = 6378.137
def latlng_to_xyz(lat, lng):
rlat, rlng = radians(lat), radians(lng)
coslat = cos(rlat)
return coslat*cos(rlng), coslat*sin(rlng), sin(rlat)
def dist_on_sphere(pos0, pos1, radius=earth_rad):
xyz0, xyz1 = latlng_to_xyz(*pos0), latlng_to_xyz(*pos1)
return acos(sum(x * y for x, y in zip(xyz0, xyz1)))*radius
# 距離に応じて、分割を実施し、中間点を補完する
#中間点の補完処理によって、lonlatlistを補完する
def hokanlatlist(input_lonlatlist, bunkatu_kyori):
lonlatlist = []
befor_lonlat = input_lonlatlist[0]
for input_lonlat in input_lonlatlist[1:] :
#m単位の2点距離を算出
distance = dist_on_sphere(befor_lonlat, input_lonlat) * 1000
#距離に応じて、何分割するか決める
bunkatu_suu = int(distance/bunkatu_kyori)+1
befor_lat = befor_lonlat[0]
befor_lng = befor_lonlat[1]
now_lat = input_lonlat[0]
now_lng = input_lonlat[1]
#分割数分繰り返して、リストを作る
#終点は、+1しているために、この処理で含んで登録される。
for num in range(bunkatu_suu):
lat = befor_lat + ((num+1) * (now_lat - befor_lat)) / bunkatu_suu
lon = befor_lng + ((num+1) * (now_lng - befor_lng)) / bunkatu_suu
lonlatlist.append( [lat, lon])
#次の処理に行く前に出発点を移動する
befor_lonlat = input_lonlat
return lonlatlist
進行方向の「角度」の算出方法
「Street View Image API」で静的画像を得るためには、
「角度」も必要になる。2地点の緯度経度の座標から、
数学的な計算で「角度」を求める関数を用意する。
緯度経度の連続リストから、それぞれの地点間の角度を求め、
APIを呼ぶためのURLのリストを作っていく。
import math
from math import sin, con, tan, atan2, acos, radians, degrees
# 緯度経度の差分から角度を計算する関数:下記サイトからのコピー
# X-Yなのでlon-latからの代入方法に注意
# https://teratail.com/questions/90662
def getHeading(x1,y1,x2,y2):
# 経度・緯度を度からラジアンに変換 sin,cos,tan,etc はラジアンを使う
x1=radians(x1)
y1=radians(y1)
x2=radians(x2)
y2=radians(y2)
deltax = x2 - x1
#角度計算
heading = degrees(atan2(sin(deltax),(cos(y1)*tan(y2)-sin(y1)*cos(deltax))))%360
# %360 : マイナスの場合は正の角度にする
# degrees : atan2の返り値はラジアンなので度になおす
return heading
# URLのリストを作る場合の使い方
def make_url_list(lonlatlist):
url_list = []
#最後の地点は無視する(方角決定用)
for i in range(0,len(lonlatlist)-1):
lonlat = lonlatlist[i]
nextlonlat = lonlatlist[i+1]
#向かっている進行方向の角度の計算
heading = getHeading(lonlat[1], lonlat[0], nextlonlat[1], nextlonlat[0])
url_list.append( make_one_url(lonlat[0], lonlat[1], heading) )
return url_list
第一段階の完成!と大きな課題
ここまでで、道順に沿って画像をダウンロード出来る。
と、思って実行した。
指定する区間によっては上手く動いている部分もあるのだが、
途中で水上を走っていたり、住宅街に入ったり、壁を向いていたり、
めちゃくちゃな動きをする箇所が多発。
公園の中を走っていたり、
水上を走っていたりする。
Googleさまはこんなところもキャプチャしているんですね。すごいな~。
伸太、部屋ニ駆ケ込ミ戻リテ、号泣シテ曰ク
「青狸、我将二静二嫌レント成ス」
青狸嘆息シ、部屋ヲ出デテ、雌猫ノ元ヘ去ル
~ 天道蟲連画集第拾参巻 ~
一歩間違えれば、彼と同じように、浴室に通じてしまい、
嫌レント成ス、ところであったかもしれない重大な課題だ。
「青狸」は助けてくれないので、原因を考えてみよう。
原因
原因は、道案内/ナビ機能(Google Map Directions API)で
取得出来る各地点は「直線で結んでも道にならない」ためだった。
上図のように、ちょっと道が曲がっていたり、
ナビ機能が示す地点同士が離れている場合、
全く道の上に乗っていない。
正直、結構困った。
「RoadsAPI」という道路系APIで補正する方法も考えたが、イマイチ。
いや、まてよ。Directions APIの結果をよく見ると、
通る道路が太線で示されている!
道路に近いデータは存在しているのだ。
そのデータは、json形式での戻り値で見た時に、
下記のような「polyline」のコードに対応している。
"polyline" : {
"points" : "yxwxEez`tYHi@HObBaAZIVCr@LfHjB"
},
!? 一見、これが座標データだと全く分からないが、
実は緯度経度のリストをエンコードした形式であり、
例えば、長くてくねくねしている「polyline」は、桁数が多い。
このpolylineを、緯度経度の連続リストにデコードできれば、
曲がりくねった道を行く場合にも対応できる!
きっと想像した以上に騒がしい未来が僕を待っているに違いない。
さきほどの図で言うと、元々の座標データ:2点間の補完ではなく、
灰色の道の形状情報=「polyline」のデータから座標データを得て、
より細かい座標を得てから補完処理をしよう、ということ。
「polyline」のデコード方法
「polyline」は、主な使われ方として、
座標(緯度経度)の連続情報を、地図上に線として表示する目的が多い。
そのため、緯度経度リスト⇒「polyline」はすぐに方法が見つかるが、
逆は見つけにくかった。
最終的に以下のpolylineライブラリを見つけ、目的を達成できた。
import polyline
#(https://pypi.org/project/polyline/)
lonlatlist = polyline.decode(polyline_points)
最初のDirectionsAPIの結果から、直接緯度経度のリストを作るのではなく、
まず、「polyline」の文字列を参照して、
それをデコードして緯度経度リストにすることで、
かなり道に沿ったリストが作成出来る。
作ったリストに関して、中間点の補完を実施するところは以前と同様。
GIFアニメ化の方法
画像が取得できたので、GIFアニメを作る。
Pillow/PILを使って、以下のようにGIFアニメを生成できる。
画像加工の方法については、本稿の主目的ではないため、
説明は割愛させていただく。
from PIL import Image, ImageDraw, ImageFilter
#特定のフォルダの画像をくっつけてGIFにする関数
def makeGifFromPicDict(images_dir, maisuu, file_name):
picPathList=[]
#連番で画像が作られているため、何番まで読み込むか指定するだけ
# ファイルのリストを得る
for i in range(maisuu):
picPathList.append(images_dir+os.sep+str(i)+".png")
images = []
#画像ファイルを順々に読み込んでいく
for picPath in picPathList:
#1枚1枚のグラフを描き、appendしていく
#ファイルが存在しない場合はスルーする
try:
tmp = Image.open(picPath)
images.append(tmp)
except:
pass
#以下の方法でくっ付けてgif化出来る
images[0].save( file_name+'.gif',
save_all=True, append_images = images[1:],
optimize=False, duration=70, loop=0)
return 0
空間歪曲装置の実装
さて、上記までの部品を繋げて、
ドライブシュミレーター的なGIFアニメ作成ツールが実装できた。
(※長距離運転をする場合は、ちゃんとAPIキーを取得してから実行)
最後の空間歪曲装置の実装は単純であり、下記の3ステップ。
①東京から横浜まで、例えば1400枚ほどの画像取得予定地点のうち、
冒頭100枚と、最後100枚の画像を実際にダウンロードしておく。
中間のURLにはアクセスしない。
②No1300画像(後半100枚の最初の画像)のデータと、
桃色のドアの画像を合成した画像を作る。
③前半100枚の最後のNo90~No100の画像に対して、
②で生成した合成画像を、サイズを変えながら貼り付けていく。
①~③までで作った画像を、GIFアニメとして繋げればOK
No90~No100の時点でドアとワープ先が見え始め、
No100の次にNo1300になるために、
ドアをくぐってワープしたように見える、という仕組みだ。
縮小のための倍率、貼り付ける場所(左上からの距離)を
良いバランスに調整するのが非常に面倒であった。
なお、Street View Image APIからの取得画像サイズは固定しているため、
画像や取得箇所に依存した再調整は発生しない。
どの区間の画像でも同じ関数に通すだけである。
画像のリサイズ/貼り付け合成は、本稿の主目的ではないため、
一番キーになる箇所だけ記載しておく。
door_img_resized = door_img.resize((int(door_img.width * bairitu), int(door_img.height * bairitu) ), Image.LANCZOS)
mask = door_img_resized.split()[3]
DouroImg.paste(door_img_resized, (50+(127-kyori)*2, 20+130-kyori) , mask)
どこでもドア気分の完成!
上記は、横浜から熱海までの実行例。
(正確には、カップヌードルミュージアムから、
アタミロープウェイまで)
いきなり海辺までワープするのが楽しい。
これで様々な場所に 行ける 行った気分になれる。
こんなこといいな 出来たらいいな
と思ったら、
不思議なpythonで作ってみよう!!
我此夢或夢大量持テリ
全テ全テ全テ夢実現使ル
不可思議ナ布袋備エタ青狸有リ
我欲空飛自在
青狸応エテ曰ク
是竹製回転羽根也
餡餡餡我大好青狸
~ 青狸賛歌 ~
いつかは、ホンモノもできるかもしれない
発展
「角度」をくるくる回して観光バスにしたり、
道無き場所を移動してネコバス化させたり、
現在地から常にローマにワープさせて、
「全ての道はローマに通ず」を作ってみたり、
まじめに道案内ツールにしたり、
自分の走った道をGIF化して思い出にしたり、
様々な活用方法が考えられる。
また、良い画像が取れないケースもまだかなりあるため、
(アップしたGIFアニメも、稀におかしなコマが入っている)
他のAPIを組み合わせて精度向上するのも良いかもしれない。
位置(イチ)を聞いて、住所(ジュウショ)を知る時代、
APIはまさに1を10にするツールであり、
Google先生に感謝しつつ、面白い活用を創りたい。
以上。
この物語はフィクションです。
登場する人物・団体・名称等は架空であり、
実在のものとは関係ありません。
天道蟲連画集は架空の書物であり、
青狸賛歌は架空の歌です。