宙畑 Sorabatake

衛星データ

Tellus搭載データを使ってパズルアプリ作ってみた

TellusでSARデータと光学データに馴染んでもらえるようなアプリを作ろう、ということで今回のアプリは誕生しました。

衛星データプラットフォームであるTellusは、今回の最新リリースで、Pythonを使ったデータ分析で便利なJupyter Notebookに対応しました。Tellusについて詳しくはこちらの記事をご覧ください。

Tellusに搭載されているAPIを引っ張ることができるようになったので、株式会社プラハさん協力のもとパズルアプリを作成しながらJupyter Notebookの機能をご紹介していきたいと思います!

※本記事の内容を実践するためにはTellusの利用登録が必要になります。利用登録はこちらから

Tellusには、大きく2種類のアクセス方法があります。
1つはブラウザ上に表示した地図上に様々なデータを重ね、操作できるTellus OS、もう1つはプログラミングでデータを解析する開発環境(以下、Jupyter Notebook)です。

Tellusの開発環境では、各種データとJupyter NotebookがAPIで連携しています。そのため、Jupyter Notebook上からプログラミングによりAPIで引っ張ることで、データを引っ張り、解析することができます。

この機能を使いながら衛星データパズルを作っていきます!

1. 要件定義

1.1 こんなアプリを作りたい!

Tellusは、他の衛星データプラットフォームに比べると、SARデータが多く搭載される、という特徴があります。光学衛星は太陽からの反射をカメラで捉えるのに対して、SAR衛星は自ら電波を発して観測します。そのため、光学衛星とは対照的に、雲を透過し、夜間にも観測できることから、SARデータは有用なデータであると言われています。

しかしながら、SARデータは一般的にはあまり知られていません。

そこで、SARデータと光学データに馴染んでもらえるようなアプリを作ろうということに。今回はその作り方について、解説します。

完成したアプリはこちら

1.2 使用技術の選定

基本的な画像解析系のアプリでは、プログラミング言語としてPythonを利用することが多いです。Pythonには画像分割処理のライブラリがあり、今回のアプリ開発も効率よく行えそう、ということで、言語はPythonを選定しました。

フロントエンドのフレームワーク選定については、iOSやらAndroidのアプリではなく、多くの方に応用してもらいやすいだろう、ということを考えてPWAを導入しました。PWAの導入が容易そうなものという観点で、今回はNuxt.jsと@nuxjs/pwaを使うことにしました。

 

パズル画像処理プロセス Python:  Pllow(画像処理), Flask(API)
フロントエンドのフレームワーク Nuxt.js, @nuxtjs/pwa

2. 事前準備

2.1 環境構築

開発環境の管理にはDockerを利用しました。

2.2 画面デザイン

実装方法を大まかに決めたので、次はパズルアプリのデザインを考えます。ゲームらしさを醸成しつつ、宇宙モチーフのディテールにこだわりました。衛星パズルアプリのスタート画面には、画像を引っ張っている陸域観測衛星だいち2号(ALOS-2)をモチーフにしたイラストを入れてみました。

スタート画面
難易度選択画面
パズル画面

3. 実装

3.1 ライブラリの選定

開発にあたって、以下のライブラリを利用しました。

簡易的なAPIとしてflaskを使用しました。画像を分割する処理のため、pillowというライブラリを入れました。

参考記事:

https://qiita.com/zaburo/items/5091041a5afb2a7dffc8

 

ライブラリ 解説
flask

flask-api

API作成を簡易的にするため
pillow 画像を分割する処理をするため
requests httpのリクエストを作成するため

毎回画像変換して直接APIを叩くわけにもいかないので、SAR画像をAPIから取得してきて、pngに変換してストレージに保持することにしました。

# SAR画像(.tif)をAPIから取得してきて、それを画像クロップして、pngに変換してストレージに保持している
_img = Image.open(BytesIO(_res.content))
_img.point(lambda i:i*(1./256)).convert('L').crop(_box).resize((op_size, op_size)).save(save_sar_png_img_path, 'PNG', quality=True)

画面分割を取得しやすいAPIにしておくため、flaskを使ってAPIを作成しました。flaskでは、getクエリでパズルの分割情報を与えて、画像の分割をするようにしました。

cd image_processing
make devrun

SAR画像だけだととてもではないけどパズルが完成しないので、比較として光学画像も確認したい、というわけで、光学画像も取得しました。

# _req_url='https://gisapi.opf-dev.jp/true/9/449/202.png'
_req_url = '%s/%s/%d/%d/%d.png' % (img_url, map_kinds[map_kind], z, x, y)
_res = requests.get(_req_url, stream=True)
_img = Image.open(BytesIO(_res.content))

※2019年2月21日時点では、こちらのAPIは未公開となっています。正式なAPI発表をお待ちください。

見やすさ、パズルとして面白そう、という感覚で、琵琶湖・東京・佐渡島を引っ張ってみました。それぞれのパラメータは以下。

kind 画像種類
z ズーム値(大きければ大きいほど、ズームになる)
x 地図のx軸の値
y 地図のy軸の値
split_n 分割数(パズルの分割数、3の場合は3×3の9つに分割される)

 

琵琶湖

curl http://localhost:5000\?z\=9\&x\=449\&y\=202\&kind\=true\&split_n\=3
curl http://localhost:5000\?z\=9\&x\=449\&y\=202\&kind\=true\&split_n\=4
curl http://localhost:5000\?z\=9\&x\=449\&y\=202\&kind\=true\&split_n\=5

東京

curl http://localhost:5000\?z\=10\&x\=909\&y\=403\&kind\=true\&split_n\=3
curl http://localhost:5000\?z\=10\&x\=909\&y\=403\&kind\=true\&split_n\=4
curl http://localhost:5000\?z\=10\&x\=909\&y\=403\&kind\=true\&split_n\=5

佐渡島

curl http://localhost:5000\?z\=8\&x\=226\&y\=98\&kind\=true\&split_n\=3
curl http://localhost:5000\?z\=8\&x\=226\&y\=98\&kind\=true\&split_n\=4
curl http://localhost:5000\?z\=8\&x\=226\&y\=98\&kind\=true\&split_n\=5

フロントとのつなぎ込みが必要だったため、当初batchでやろうと思っていましたが、nuxtからcallできるようにAPIにしました。またjsonで取得してくる画像の位置情報を管理し、そのパラメータをAPIに送るような、コンフィグラブルにできるようにしました。

3.2 画面のコーディング

画面全体の構成はこんな感じで書きました。

 

src
├ components(画面を構成するパーツ)
│ ├ puzzle
│ │ └ Puzzle.vue(パズル画面)
│ └ Result.vue(パズル完成画面に表示する結果モーダル)

├ layouts
│ ├ default.vue(共通のテンプレート?)
│ └ error.vue(エラーページ)

├ pages(画面の実装ファイルがあるフォルダ)
│ ├ difficulty
│ │ ├ _difficulty
│ │ │ └ map
│ │ │ ├ _map
│ │ │ │ ├ complete.vue(パズル完成時の画面)
│ │ │ │ └ index.vue(パズルの画面)
│ │ │ └ index.vue(マップ選択の画面)
│ │ └ index.vue(難易度選択の画面)
│ └ index.vue(トップ画面)

├ static
│ └ tile.json(パズルのタイル定義)
├ store
│ └ index.js(エントリーポイント)
└ puzzle.json(難易度ごとのパズルの説明)

 

PWAにするため、 @nuxtjs/pwa を使用しました。ライブラリをインストールして、nuxt.config.js でインポートするだけで簡単に使うことができます。@nuxtjs/pwaは単一ファイルコンポーネント(Vueファイル)でコンポーネント単位でのファイル分割が可能です。

 

今回フロントのデザインと密に連携するので、必要な部品については単一コンポーネント化して実装することで、デザイン確定後に呼出元の調整のみでいいようにしました。

 

またパズルで使う画像は、動的に簡単に変えられるようにしました。

方法としては、jsonで設定ファイルを書き、それをgetのrequest parameterとしてpythonのAPIをcallすることで、動的に画像を変更できるようにしました。

そうすることでjsonファイルを修正するだけで。tellusから取得する画像を変えることができ、パズルの画像を容易に変更できるようにしました。

{
  "puzzles": [
    {
      "id": "lake-biwako",
      "name": "琵琶湖",
      "description": "日本最大の面積と貯水量を誇る湖",
      "sar": [1000, 7800, 23000, 33000],
      "parameters": [
        {
          "kind": "true",
          "z": 9,
          "x": 449,
          "y": 202,
          "split_n": 3
        },
        {
          "kind": "true",
          "z": 9,
          "x": 449,
          "y": 202,
          "split_n": 4
        },
        {
          "kind": "true",
          "z": 9,
          "x": 449,
          "y": 202,
          "split_n": 5
        }
      ]
    },
    {
      "id": "city-tokyo",
      "name": "東京",
      "description": "世界最大級のメトロポリス",
      "parameters": [
        {
          "kind": "true",
          "z": 10,
          "x": 909,
          "y": 403,
          "split_n": 3
        },
        {
          "kind": "true",
          "z": 10,
          "x": 909,
          "y": 403,
          "split_n": 4
        },
        {
          "kind": "true",
          "z": 10,
          "x": 909,
          "y": 403,
          "split_n": 5
        }
      ]
    },
    {
      "id": "island-sado",
      "name": "佐渡ヶ島",
      "description": "金・銀で栄えた島",
      "parameters": [
        {
          "kind": "true",
          "z": 8,
          "x": 226,
          "y": 98,
          "split_n": 3
        },
        {
          "kind": "true",
          "z": 8,
          "x": 226,
          "y": 98,
          "split_n": 4
        },
        {
          "kind": "true",
          "z": 8,
          "x": 226,
          "y": 98,
          "split_n": 5
        }
      ]
    }
  ]
}

SAR画像も似たような作りにしようと思いましたが、色々と仕様変更が有りハードコーディングされている状態になっています。

またSAR画像は光学画像と違い、tifという拡張子で画像が配信されるため、PNGに変換して、サイズを光学画像と同じ大きさにする必要があり、その処理も入っています。

_img.point(lambda i:i*(1./256)).convert('L').crop(_box).resize((op_size, op_size)).save(save_sar_png_img_path, 'PNG', quality=True)

3.3 パズルのロジック

あまりにも難しいのだけだと困るので、パズルのマス目は3×3、4×4、5×5の3パターン用意することにしました。あまりにも難しいと、永遠に時間がかかるので、たまにヒントで可視光を見せるように。完成画像を見るというボタンで、最終形態も見ることができるようにしました。

4. できたものの紹介・まとめ

で、最終的に完成したものがこちらです。結構難しいので、ぜひお試しください!

https://satellite-puzzle.app.tellusxdp.com

※Tellusの利用登録はこちらから