Python
画像処理
GoogleMapsAPI
python3
GoogleCloudPlatform

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

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

実現してほしいひみつ道具アンケート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, radious=earth_rad):
    xyz0, xyz1 = latlng_to_xyz(*pos0), latlng_to_xyz(*pos1)
    return acos(sum(x * y for x, y in zip(xyz0, xyz1)))*radious

# 距離に応じて、分割を実施し、中間点を補完する
#中間点の補完処理によって、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先生に感謝しつつ、面白い活用を創りたい。

以上。


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