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


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

実現してほしいひみつ道具アンケートNo1。
今回は、これをpythonで作ることに挑戦する。

## 先に結果 ⇒ 「どこでもドア気分」

**任意の地点の接続ドラレコ動画が作れるツール**を作った。

![](https://media.giphy.com/media/23cYHjFt0jhM6uoyr3/giphy.gif)

## 要件定義

ホンモノを作るのは、私の科学力ではちょっとだけ難しいので、
どんな ~~欲望~~ 要望 を満たすツールにするのか、要件定義を行った。

> 青狸ノ元ニ伸太在リ、何処出門扉ヲ使ウ
> 則ラバ声聞ユ 「嗚呼伸太氏ノ卑猥」
> 静曰ク「何スレゾ伸太氏此処ニ或ル乎」ト
> 伸太答ヘテ曰ク「誤解也」ト
> 　　　　　　～ 天道蟲連画集第拾参巻 ～

常に浴室に通じるのは古来より最重視される仕様の一つである。
しかし、使用者起因バグの可能性も否めないため、
今回は任意の地点間の移動方法として考えた。

原作のホンモノは、「**空間歪曲装置**」である。
~~決して、使用者の精巧なコピーを移動先に作成し~~
~~移動元の使用者を抹殺する恐怖の装置ではない。~~

pythonには残念だが空間歪曲機能は無い。
ホンモノを作ることを目指す途中段階として、
「**移動した気分**」を重視したツールを目指した。

**現実的な移動方法（空間歪曲のルート）を定義**し、
その**２地点の空間を接続**するツールにしよう！


## 顧客が本当に必要だったもの

最初の動画は、東京から横浜まで
（正確には、東京タワーからアンパンマンミュージアムまで）
のドライビングルートの連続動画Gifである。

が、**最先端の空間歪曲装置**を使って、
途中を**大幅にショートカット**することに成功した。
（見逃した人は再度よ～く見てください。）

* **「移動している感」が出ること**
* **移動の途中で「空間歪曲」を自然に利用**

という２点の要件を満たすことが出来た。

なお、原作では「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として保存するような関数を作る。


```py:静的なストリートビューイメージの保存

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キーは不要！


```py:道案内/ナビデータの取得
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"]のところに、各運転操作が必要な箇所ごとの、
緯度経度が格納されていることが分かる。
だが良く見て欲しい。長い所では、
１０分や１３分に一箇所、ポイントされているだけである。

１０分も車が走ったら全く別な場所になってしまうので、
単純にこの緯度経度のリストを使うと、**枚数が足りなすぎる**。
「角度」情報も無い。写真を撮る間隔も違い過ぎる。

**道案内/ナビデータと、StreetViewを単純に**
**組み合わせるだけでは全くサマにならない！**


## 地点情報を増やす（間隔を補完する）方法

緯度経度のリストに対して、その間の地点を
出来るだけ等間隔で埋めるようにしないと、アニメにならない。

任意の緯度経度のリストと、撮影間隔（メートル）が与えられたときに、
与えられた地点同士の距離を計算して、
緯度経度のリストの間を補完する関数を作る。

例えば、
「A地点, B地点, C地点」の３つの座標リストが与えられたとして、
　A地点　～　B地点　＝　９００ｍ
　B地点　～　C地点　＝　５００ｍ
で、撮影したい間隔が３００ｍ以下ならば、
　A地点　～　B地点　⇒ ３００ｍ×３に分割（A, a-1, a-2 ,B）
　B地点　～　C地点　⇒ ２５０ｍ×２に分割（B, b-1, C）
となるように、合計６点の座標リストにするということ。

緯度経度が与えられた時に、その距離を計算することが必要で、
その方法は以下のQiita記事をご参照：
　[緯度経度より距離を（地球の丸さも考えて）求める。](https://qiita.com/s-wakaba/items/e12f2a575b6885579df7)

この関数を使って、作った補完関数は以下。


```py:地点の補完処理
#######距離計算
#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単位の２点距離を算出
        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」で静的画像を得るためには、
**「角度」も必要**になる。２地点の緯度経度の座標から、
数学的な計算で「角度」を求める関数を用意する。

緯度経度の連続リストから、それぞれの地点間の角度を求め、
APIを呼ぶためのURLのリストを作っていく。

```py:
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](https://qiita-image-store.s3.amazonaws.com/0/193512/1c540b9c-5153-0e88-56f1-450b52815753.png)

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

![254.png](https://qiita-image-store.s3.amazonaws.com/0/193512/98cbfbc7-3950-6fb0-a3d9-ac78f6fb7ae2.png)

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

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

> 伸太、部屋ニ駆ケ込ミ戻リテ、号泣シテ曰ク 
> 「青狸、我将二静二嫌レント成ス」 
> 青狸嘆息シ、部屋ヲ出デテ、雌猫ノ元ヘ去ル
> 　　　　　　～ 天道蟲連画集第拾参巻 ～

一歩間違えれば、彼と同じように、浴室に通じてしまい、
嫌レント成ス、ところであったかもしれない重大な課題だ。

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


## 原因

原因は、道案内/ナビ機能（Google Map Directions API）で
取得出来る各地点は「**直線で結んでも道にならない**」ためだった。

![hokan_sippai_zu.PNG](https://qiita-image-store.s3.amazonaws.com/0/193512/df415c90-2c70-3b4a-c966-73880848ef74.png)

上図のように、ちょっと道が曲がっていたり、
ナビ機能が示す地点同士が離れている場合、
**全く道の上に乗っていない**。

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

いや、まてよ。Directions APIの結果をよく見ると、
通る道路が太線で示されている！
**道路に近いデータは存在しているのだ**。

そのデータは、json形式での戻り値で見た時に、
下記のような「polyline」のコードに対応している。

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

**！？** 一見、これが座標データだと全く分からないが、
実は緯度経度のリストをエンコードした形式であり、
例えば、長くてくねくねしている「polyline」は、桁数が多い。

このpolylineを、緯度経度の連続リストにデコードできれば、
**曲がりくねった道を行く**場合にも対応できる！
~~きっと想像した以上に騒がしい未来が僕を待っているに違いない。~~

さきほどの図で言うと、元々の座標データ：２点間の補完ではなく、
灰色の道の形状情報＝「polyline」のデータから座標データを得て、
より細かい座標を得てから補完処理をしよう、ということ。


## 「polyline」のデコード方法

「polyline」は、主な使われ方として、
座標（緯度経度）の連続情報を、地図上に線として表示する目的が多い。

そのため、緯度経度リスト⇒「polyline」はすぐに方法が見つかるが、
逆は見つけにくかった。
最終的に以下の**polylineライブラリ**を見つけ、目的を達成できた。

```py:polylineのデコード
import polyline
#（https://pypi.org/project/polyline/）
lonlatlist = polyline.decode(polyline_points)
```

最初のDirectionsAPIの結果から、直接緯度経度のリストを作るのではなく、
まず、「polyline」の文字列を参照して、
それをデコードして緯度経度リストにすることで、
かなり道に沿ったリストが作成出来る。
作ったリストに関して、中間点の補完を実施するところは以前と同様。


## GIFアニメ化の方法

画像が取得できたので、GIFアニメを作る。
Pillow/PILを使って、以下のようにGIFアニメを生成できる。
画像加工の方法については、本稿の主目的ではないため、
説明は割愛させていただく。

```py:画像フォルダを元に、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キーを取得してから実行）

最後の**空間歪曲装置**の実装は単純であり、下記の３ステップ。

①東京から横浜まで、例えば1400枚ほどの画像取得予定地点のうち、
　冒頭100枚と、最後100枚の画像を実際にダウンロードしておく。
　中間のURLにはアクセスしない。

②No1300画像（後半100枚の最初の画像）のデータと、
　桃色のドアの画像を合成した画像を作る。

③前半100枚の最後のNo90～No100の画像に対して、
　②で生成した合成画像を、サイズを変えながら貼り付けていく。

①～③までで作った画像を、GIFアニメとして繋げればOK

No90～No100の時点でドアとワープ先が見え始め、
No100の次にNo1300になるために、
ドアをくぐってワープしたように見える、という仕組みだ。

縮小のための倍率、貼り付ける場所（左上からの距離）を
良いバランスに調整するのが非常に面倒であった。

なお、Street View Image APIからの取得画像サイズは固定しているため、
画像や取得箇所に依存した再調整は発生しない。
どの区間の画像でも同じ関数に通すだけである。

画像のリサイズ/貼り付け合成は、本稿の主目的ではないため、
一番キーになる箇所だけ記載しておく。

```py:画像のりサイズ/貼り付け部分（イメージ）
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)
```

# どこでもドア気分の完成！

![](https://media.giphy.com/media/57SRxyjPYG30vmAUXi/giphy.gif)

上記は、横浜から熱海までの実行例。
（正確には、カップヌードルミュージアムから、
　アタミロープウェイまで）
いきなり海辺までワープするのが楽しい。

これで様々な場所に ~~行ける~~ 行った気分になれる。

**こんなこといいな**　**出来たらいいな**
と思ったら、
**不思議なpythonで**作ってみよう！！

> 我此夢或夢大量持テリ
> 全テ全テ全テ夢実現使ル
> 不可思議ナ布袋備エタ青狸有リ 
> 我欲空飛自在 
> 青狸応エテ曰ク 
> 是竹製回転羽根也 
> 餡餡餡我大好青狸
> 　　　　　～ 青狸賛歌 ～

~~いつかは、ホンモノもできるかもしれない~~

## 発展

「角度」をくるくる回して観光バスにしたり、
道無き場所を移動してネコバス化させたり、
現在地から常にローマにワープさせて、
「全ての道はローマに通ず」を作ってみたり、
まじめに道案内ツールにしたり、
自分の走った道をGIF化して思い出にしたり、
様々な活用方法が考えられる。

また、良い画像が取れないケースもまだかなりあるため、
（アップしたGIFアニメも、稀におかしなコマが入っている）
他のAPIを組み合わせて精度向上するのも良いかもしれない。

位置（イチ）を聞いて、住所（ジュウショ）を知る時代、
APIはまさに1を10にするツールであり、
Google先生に感謝しつつ、面白い活用を創りたい。

以上。

---

この物語はフィクションです。
登場する人物・団体・名称等は架空であり、
実在のものとは関係ありません。
天道蟲連画集は架空の書物であり、
青狸賛歌は架空の歌です。