Edited at

どこでもドアを作ってみた物語


どこでもドアは人類の夢である

実現してほしいひみつ道具アンケート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


第一段階の完成!と大きな課題

ここまでで、道順に沿って画像をダウンロード出来る。

と、思って実行した。

指定する区間によっては上手く動いている部分もあるのだが、

途中で水上を走っていたり、住宅街に入ったり、壁を向いていたり、

めちゃくちゃな動きをする箇所が多発

195.png

公園の中を走っていたり、

254.png

水上を走っていたりする。

Googleさまはこんなところもキャプチャしているんですね。すごいな~。


伸太、部屋ニ駆ケ込ミ戻リテ、号泣シテ曰ク

「青狸、我将二静二嫌レント成ス」

青狸嘆息シ、部屋ヲ出デテ、雌猫ノ元ヘ去ル

      ~ 天道蟲連画集第拾参巻 ~


一歩間違えれば、彼と同じように、浴室に通じてしまい、

嫌レント成ス、ところであったかもしれない重大な課題だ。

「青狸」は助けてくれないので、原因を考えてみよう。


原因

原因は、道案内/ナビ機能(Google Map Directions API)で

取得出来る各地点は「直線で結んでも道にならない」ためだった。

hokan_sippai_zu.PNG

上図のように、ちょっと道が曲がっていたり、

ナビ機能が示す地点同士が離れている場合、

全く道の上に乗っていない

正直、結構困った。

「RoadsAPI」という道路系APIで補正する方法も考えたが、イマイチ。

いや、まてよ。Directions APIの結果をよく見ると、

通る道路が太線で示されている!

道路に近いデータは存在しているのだ

そのデータは、json形式での戻り値で見た時に、

下記のような「polyline」のコードに対応している。

"polyline" : {

"points" : "yxwxEez`tYHi@HObBaAZIVCr@LfHjB"
},

!? 一見、これが座標データだと全く分からないが、

実は緯度経度のリストをエンコードした形式であり、

例えば、長くてくねくねしている「polyline」は、桁数が多い。

このpolylineを、緯度経度の連続リストにデコードできれば、

曲がりくねった道を行く場合にも対応できる!

きっと想像した以上に騒がしい未来が僕を待っているに違いない。

さきほどの図で言うと、元々の座標データ:2点間の補完ではなく、

灰色の道の形状情報=「polyline」のデータから座標データを得て、

より細かい座標を得てから補完処理をしよう、ということ。


「polyline」のデコード方法

「polyline」は、主な使われ方として、

座標(緯度経度)の連続情報を、地図上に線として表示する目的が多い。

そのため、緯度経度リスト⇒「polyline」はすぐに方法が見つかるが、

逆は見つけにくかった。

最終的に以下のpolylineライブラリを見つけ、目的を達成できた。


polylineのデコード

import polyline

#(https://pypi.org/project/polyline/)
lonlatlist = polyline.decode(polyline_points)

最初のDirectionsAPIの結果から、直接緯度経度のリストを作るのではなく、

まず、「polyline」の文字列を参照して、

それをデコードして緯度経度リストにすることで、

かなり道に沿ったリストが作成出来る。

作ったリストに関して、中間点の補完を実施するところは以前と同様。


GIFアニメ化の方法

画像が取得できたので、GIFアニメを作る。

Pillow/PILを使って、以下のようにGIFアニメを生成できる。

画像加工の方法については、本稿の主目的ではないため、

説明は割愛させていただく。


画像フォルダを元に、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先生に感謝しつつ、面白い活用を創りたい。

以上。


この物語はフィクションです。

登場する人物・団体・名称等は架空であり、

実在のものとは関係ありません。

天道蟲連画集は架空の書物であり、

青狸賛歌は架空の歌です。