cloudvisionApi + google App engine + python3でゲームのリザルト画像解析APIを作ってみた。

ある日突然、私がはまっているゲームの画像から、テキストを抽出したいという衝動にかられた。
勉強もかねて個人的メモを公開することにした。
結論から言うと、全ての文字を抽出することは出来なかったが、９割ほどは抽出が出来た。

## 目的
ゲームの画像からテキストを抽出する。

以下の画像から、機械学習を使い、「ドワーフ、　ナイト」などの種族名を抽出する。
gcpのapp engineや、python3など、普段使わない技術の習得も目的とする。
![Screenshot_20190917-101012 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183835/350bbb46-098d-74d4-d6a6-f692abe72787.jpeg)

最終ソースコード
https://github.com/t-kabaya/autochessTextExtracter

# 使用技術
### cloudvision api
google が開発した画像解析APIだ。エンドポイントに画像をPOSTするだけで、画像解析を行うことが出来る。

以下のサイトで、cloud vision apiを試すことが出来る。試しにゲームの結果画面で画像解析をする。
[cloud vision apiを試す。](https://cloud.google.com/vision/docs/drag-and-drop)

テキストを抽出
<img width="822" alt="スクリーンショット 2019-10-02 11.14.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183835/da489f96-d189-8331-1a1d-2cdd68fae979.png">

ラベル付け
文字、スクリーンショット、PCゲーム、テクノロジー、ソフトウェア、電子機器というラベルが付いた。
有名人の画像をアップロードすると、人物判定も行える。凄すぎる。
<img width="867" alt="スクリーンショット 2019-10-02 11.14.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183835/9688d0a5-5cc5-e034-f03c-a742eebabae5.png">

### google app engine
google platformのサービスの一つ。
Awsのelastic beanstalkや、helokuと全く同じfass
elastic beanstalkや、helokuの辛さは知っていたので、新しい技術に挑戦してみた。
結果から言うと、これは大成功で、gaeなしでは生きられない体になってしまった。
cloud functionなどのサーバレスでも十分だったのだが、インフラを勉強したかったので、gaeを選択。

### python3
機械学習ならpython3と言う事で、選択。

### Flask
pythonで、ミニマルなAPIを作るにはこれ一択。(app engineの公式チュートリアルでも使用されている。)
とても使いやすく、５分で理解出来る。

## 実装準備
まず、macには、python２が標準インストールされているが、python3は、インストールされていない。

そのため、python3をmacにインストールし、その後、

pip3 install -U flask
を実行する。
python3 xxxx
pip3 xxxx
のように、python3を実行するには、各種コマンドに３をつけないといけないのが煩わしい。

pythonには、標準ライブラリに、unittestと言うライブラリがあり、単体テストを、pythonのみで記述出来る。
今回は、モックが必要な箇所以外は、このunittestでテストを記述した。

cloudvision apiを使うには二つの選択肢がある。
1 REST
2 クライアントSDKを導入する。
今回は、よりパフォーマンスの良いクライアントSDKを採用した。
以下のコマンドで、ライブラリをインポートする。
pip install --upgrade google-cloud-vision
[ドキュメント](https://cloud.google.com/vision/docs/libraries#client-libraries-install-python)

## 実装

```
mkdir awesomeProject
cd awesomeProject
```

google app engineは、app.yamlと言うファイルで、構成を管理する。
といっても、python3を使う場合、一行追加するだけだ。

```
touch app.yaml
```

``` app.yaml
runtime: python37
```

次に、flaskでエンドポイントを作成する。google app engineのデフォルトのポートは、8080だ。
flaskのデフォルトポートは、5000のため、flaskに８０８０を使うよう指示する。
またgoogle app engineでは、エントリーポイントは、main.pyと指定されているため、ルーティングは、main.py内で行う。

```
touch main.py
```

``` main.py
# [START gae_python37_app]
from flask import Flask

@app.route("/")
def extract_text_from_autochess_image():
    return 'hello world!'

if __name__ == '__main__':
  app.run(host='127.0.0.1', port=8080, debug=True)
# [END gae_python37_app]
```

ローカルで、python3 main.py
と実行した後、localhost:8080にアクセスして、'hello world!'と帰って来れば成功だ。

## deploy
まず、deployには二つの方法がある。gcloudコマンドを使用して、ローカルマシンからデプロイする方法と、cloud shellを使用して、gcpコンソールからデプロイする方法だ。

今回は、より簡易的な、gcpコンソールからデプロイを試した。（gcloud コマンドを使用したデプロイの方が安全で早い）

まず。gaeコンソールから、gaeペインをクリックし、cloud shellを有効化する。すると、リモートサーバーにssh接続され、bashでサーバーを操作可能になる。

次に、現れたターミナルに、
```
git clone origin master
cd project
pip3 install -r requirements.txt
python3 main.py
```

と打ち、リモートサーバーにローカルマシンの環境を再現する。次は、flaskが動くかテストしてみよう。

```python
python3 main.py
```

APIが立ち上がったら、テストはOK

作成したアプリのルートフォルダに移動し、以下の二個のコマンドを打つだけで、デプロイが完了する。
```
gcloud app create
gcloud app deploy
```

とても簡単だ。

アプリを更新するなら、以下の２コマンドで更新。

```
git pull
gcloud app deploy
```

## 実装編

さて、前回までで、google app engineへのデプロイが完了した。

次は画像解析の実装に移る。
コードは、以下に置いた。
https://github.com/t-kabaya/autochessTextExtracter

cloud vision apiを使って画像解析を行うには、以下の４行だけで良い。そうすると、画像内の文字列と、その位置がリターンされる。

```extract_text_from_image.py
from google.cloud import vision

def detect_text_uri(image_uri):
    client = vision.ImageAnnotatorClient()
    image = vision.types.Image()
    image.source.image_uri = image_uri
    response = client.text_detection(image=image)

    texts = response.text_annotations

    return texts
```

このリターンされたobjectを、jsonに加工するのが割と手がかかる。
最終的に以下のエンドポイントで、jsonをリターンする。
詳しくはgithubのソースコードを読んで欲しい
flask慣れしていないとは言え酷いコードだ。
```
# [START gae_python37_app]
from flask import Flask
from flask import jsonify
from flask import request
from index import create_synergy_text

app = Flask(__name__)
# 文字化け防止。flaskは、defaultではutf8をサポートしない。
app.config['JSON_AS_ASCII'] = False

# TODO: 本来は、image_uriが存在しない場合、imageが、autochess_resultの物で無かった場合に個別のエラーメッセージを返してやるのが正しい。
# エラーが帰っているのに、successというstatusが返っている。404, 200など、status codedを
# GET param: image_uri: string
@app.route("/")
def extract_text_from_autochess_image():
  try:
    image_uri = request.args.get('image_uri')
    if image_uri is None:
      return 'image_uriパラメーターが必要です。'
    
    response = create_synergy_text(image_uri)

    return jsonify(response)
  except:
    return 'エラー　autochessの画像を投稿してください'

if __name__ == '__main__':
  app.run(host='127.0.0.1', port=8080, debug=True)
# [END gae_python37_app]
```


## おまけ　pythonで驚いたこと。
### List Comprehensions syntax
日本語ではリスト内包表記と言われる。

例えば、以下のfor文で、new_listを作成していたのを、

```python
new_list = []
for i in old_list:
    if filter(i):
        new_list.append(expressions(i))
```

以下のように一行で書ける。最初は、気持ち悪かったが、慣れるととても便利だ。インデント崩れを気にしなくて良いしね。

```python
new_list = [expression(i) for i in old_list if filter(i)]
````

### __init__.py
pythonでは、subdirectoryからファイルをインポートが出来ない。これを解決するには、__init__.pyという名前の空ファイルを置かないといけない。
この仕様は酷すぎる。

### requirements.txt
このファイルで、依存関係を書き込む。wheelなどを使った方が良いよ。venvを使用して、仮想環境ごとに、ライブラリをインストールしてねと言われたが、今回は小規模apiなので、requirements.txtで事足りた。