268
176

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-08-25

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

実現してほしいひみつ道具アンケート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先生に感謝しつつ、面白い活用を創りたい。

以上。


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

268
176
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
268
176

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?