初めてのWebアプリとして「画像を絵画っぽく変換するアプリ」をFlaskとGCPで作成しようとしたら苦戦した話

# はじめに
初めてのWebアプリとして画像処理アプリの作成を試みて、画像データの取り扱いに手こずった話です。ネットを探しても意外と**Python・GCP・Flaskを使った画像処理Webアプリ**についての記事が少なかったので、**とりあえず動いてくれる画像処理Webアプリ**の作成手順と苦戦した部分を紹介しようと思います。

## 完成品

**1.　Webサイトを開いて処理したい画像を選択して送信すると、**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624261/a0af7c5b-39d9-a0cd-47d2-58836bc9f386.png)
**選択された元画像**
![処理前.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624261/100c7943-7bc9-339f-8e5f-aa2ca40393c3.jpeg)


**2.　絵画っぽく変換された画像が返ってきます。**
![処理後.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624261/3122937f-5fa3-3127-fe7c-6c7d582006bc.png)

#Webアプリの概要
作成したWebアプリの概要を図にまとめてみました。今回はWebサーバとしてGoogleのクラウドである**GCP**を、無料サービスの範囲で使用しました。実際の画像処理の部分は**Python**を使って作成し、Webサイトの見た目は**HTML**だけを使って簡単に作成しました。そして、PythonのAPIの一種である**Flask**を使用してPythonとHTMLの連携部分を作成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624261/c5cebff9-747e-d1ba-2cc7-99ffd9cbd51f.png)


# 1. GCE(Google Cloud Engine)の環境構築
まず、GCP内のGCEというサービスを利用してWebアプリを公開する環境を構築していきます。**Googleアカウント・クレジットカードはGCP利用に必須**ですので、用意しておきます。また本記事では**Google ChromeからGCEにSSH接続**してクラウドの仮想環境を遠隔操作しています。
GCEサービスを使うためには以下の3つの作業を行う必要がありますが、分かりやすく説明してくれている記事があるので参照ください。私はGoogle Cloud Platform Console（ブラウザはGoogle Chrome）から接続するので記事のSSH認証鍵の部分は飛ばしました。MacのターミナルからSSH接続したい場合などは設定が必要です。
【参考記事】
[これから始めるGCP（GCE）　安全に無料枠を使い倒せ](https://qiita.com/Brutus/items/22dfd31a681b67837a74)

【必要な作業】
・GCPの無料トライアルに登録
・プロジェクトの作成
・仮想マシンインスタンスの作成

# 2. Cloud shellの起動方法
GCE環境を構築したあとは、[インスタンスのページ](https://console.cloud.google.com/compute/instances)を開いて右上のcloud shellをアクティブにするをクリックしてターミナルを起動します。**ターミナルが起動出来たら、Linuxのコマンドを使って構築したGCEのインスタンスを操作していくことができます。** ちなみに[公式サイト](https://cloud.google.com/compute/docs/instances/connecting-to-instance?hl=ja)によると、初めて VM インスタンスに接続したタイミングでCompute Engine によって 自動的にSSH 認証鍵ペアが生成されているらしいです。

![pic.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624261/6a20533c-d85c-45e6-1b74-678ce094cb3f.jpeg)



# 3. ローカル環境とのデータ受け渡し方法
ローカル環境で作成したWebサイトや画像処理をGCEで実行するには、ローカル環境のデータをGCEと共有する必要があります。コマンドでカッコよく置く方法もありますが、設定がいろいろ手間なので**最も理解しやすいCloud Storageを使う方法**を採用しました。
GCPのタブの中からStrage→ブラウザと選択していきます。ストレージブラウザで適当な名前でバケットを作成したら、GCEと共有したいファイルをローカルからアップロードしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624261/72ef3d18-1d05-52a4-ba47-34a2ad895243.png)

そして下記コードをcloud shellで実行することでストレージに保存したデータをGCEの任意の場所に保存できます。gsutilは、コマンドラインからCloud Storageにアクセスするために用意されているアプリケーションです。 

```
gsutil cp -r gs://｛バケット名｝/ ./｛GCEのフォルダ名など｝
```

【参考記事】
[【無料】Qiitaの殿堂を作った物語【簡単】](https://qiita.com/youwht/items/9851c2ac9024633fc04e)


# 4. Webアプリのファイル構成(GCE・Python・Flaskを使う場合)
GCE上でPythonとFlaskを使ってWebアプリを作成する場合、以下のようなファイルとディレクトリの構成にする必要があります。**逆に言うと、作成したプログラムを以下の構成で保存して、GCP上でいくつかコマンドを実行しさえすればすぐにWebアプリを作成することができます。**my folder name以外の各ファイルとフォルダは下記と違う名前にするとエラーが出てしまうので気を付ける必要があります。

my folder name(名前何でもOK)/
　├ static/
　├ templates/
　│　    └ index.html
　├ app.yaml
　├ main.py
　└ requirements.txt

それぞれの役割を簡単に書いていきます。
・**static**：アプリ実行中にアクセスしたい静的データ(css・Javascript・画像・学習済みDLモデルなど)を入れておきます。今回空です。
・**templates**：htmlファイルを保存しておきます。
・**app.yaml**：GAEの挙動が記載されている指示書です。
・**main.py**：データ処理やhtmlとの連携について書かれたpythonファイルです。
・**requirements.txt**：main.pyの実行に必要なpythonライブラリとバージョンが書かれたファイルです。デプロイ時にこれ通り環境構築してくれます。

# 5. Webアプリの作成
上記のファイル構成さえGCE上に作ってしまえば、残りの作業は簡単です。
以下のコマンドを実行していきます。

アプリをデプロイするには、まずリージョン内にアプリを作成する必要があります。
※すでに一度アプリを作成している場合は、このコマンドをスキップできます。

```
gcloud app create
```
アプリをデプロイします。

```
gcloud app deploy app.yaml --project {my project name}
```
{my project name}に自分のプロジェクト名を入れてください。Cloud Shellの中に書かれていると思います。
本記事の9章にあるソースコードをコピペしてプログラムファイルを作成して4章通りのディレクトリ構成になってたら動作すると思います。エラーが出てしまうときは[App Engineのダッシュボード](https://console.cloud.google.com/appengine)から内容を確認できます。

# 6. 画像処理の手法
下の記事で紹介されている**桑原フィルター**というものを採用して、コードもそのまま使わせていただきました。**簡単な式で表せる割に、手の込んだ深層学習の画像処理かのような絵画っぽい画像**が得られるってすごいですね。
詳しくは下の記事をご覧ください。
[Kuwahara filterとかいう明らかに日本人の名前な画像フィルターに出会い、試してみたらすごかったので紹介する。](https://qiita.com/Cartelet/items/5c1c012c132be3aa9608)



**元画像**
![a.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624261/b230717f-ccf1-07a6-b69d-fb07e79fb6ee.jpeg)

**フィルタ適用後**
![b.jpe](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624261/d14de21e-0148-d808-c2f2-2ca5d89f9cba.jpeg)




# 7. PythonからHTMLへの画像の渡し方で苦戦
Pythonで画像処理後の画像をHTMLに渡すのに手こずりました。はじめは普通に画像をstaticファイルに一度保存して、そのパスをHTMLに渡してHTMLから画像を読み込むような仕様にしていました。**しかし、GCE環境でアプリ実行中はPythonからフォルダへの書き込みは制限されておりエラーとなりました。**[公式サイト](https://cloud.google.com/appengine/docs/standard/python3/storage-options?hl=ja)によるとランタイム環境下でのデータの保存は以下の4つが推奨されていました。
・Datastore モードの Cloud Firestore
・Cloud SQL for MySQL
・Cloud SQL for PostgreSQL
・Cloud Storage

他にも、PythonのAPIであるtempfileであればデータ保存が可能で、Pythonからのデータの保存はできましたが、逆にHTMLからのアクセスができなくて詰みました。よく考えると、そもそも画像を一度どこかに保存する必要がないので、**PythonからHTMLに直接画像データを渡す方法**がないか探しました。
Python側で画像データをbase64でエンコードして文字列としてHTMLに渡し、HTML側で文字列をデコードして画像化するという方法があったので以下の記事を参考に実装すると上手くいきました。
[pillowを使って生成した画像を、ファイルに保存せずにhtml表示【Django】](https://qiita.com/miler0528/items/59caf9bd1ed4212edd3a)

# 8. メモリ不足による不具合
色々な画像を試していると、画像サイズによっては無料版GCEの最大メモリ(256MB)をオーバーしてしまってエラーがでているようです（1000×600より大きいとダメでした）。おそらくプログラムやデータ構造を改良したらマシンをアップグレードせずとも対応可能と思いますが、まだやっていません。GCPは最初1年間で300＄の無料クレジットがあるのでアップグレードして、試してみるのも良いかもしれません。エラーの内容はGCPのタブの中のApp Engineのダッシュボードから確認できます。
![メモリ不足エラー.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624261/06aea121-1319-73b7-9c40-c0b8eec2842c.png)
（2020/08/17 22:30追記）
GCEのメモリ不足に関して、GCPの無料クレジットを使ってマシンのcpuコア数とメモリ容量を上げてみました。マシンのレベルを上げたにも関わらず最初はメモリのエラーが出ていたので一瞬エッとなったのですが、**[公式サイト](https://cloud.google.com/appengine/docs/standard?hl=ja#second-gen-runtimes)によると各インスタンスで使えるメモリ容量はデフォルトで256MBとなっているようです。** app.yamlファイル内で下記のコードを追加することでメモリ容量を変更することができます。2048MBに上げてみたところ、4000×1800の画像まで処理できました。

```yaml:app.yaml
instance_class: F4_1G
```

# 9. ソースコード

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

```python:main.py
# 必要なモジュールを読み込む
# Flask関連
from flask import Flask, render_template, request, redirect, url_for, abort
import base64
import tempfile
from PIL import Image
import io
import numpy as np
import cv2

app = Flask(__name__)

def kuwahara(pic,r=5,resize=False,rate=0.5): #元画像、正方形領域の一辺、リサイズするか、リサイズする場合の比率
    h,w,_=pic.shape
    if resize:pic=cv2.resize(pic,(int(w*rate),int(h*rate)));h,w,_=pic.shape
    pic=np.pad(pic,((r,r),(r,r),(0,0)),"edge")
    ave,var=cv2.integral2(pic)
    ave=(ave[:-r-1,:-r-1]+ave[r+1:,r+1:]-ave[r+1:,:-r-1]-ave[:-r-1,r+1:])/(r+1)**2 #平均値の一括計算
    var=((var[:-r-1,:-r-1]+var[r+1:,r+1:]-var[r+1:,:-r-1]-var[:-r-1,r+1:])/(r+1)**2-ave**2).sum(axis=2) #分散の一括計算
    
    def filt(i,j):
        return np.array([ave[i,j],ave[i+r,j],ave[i,j+r],ave[i+r,j+r]])[(np.array([var[i,j],var[i+r,j],var[i,j+r],var[i+r,j+r]]).argmin(axis=0).flatten(),j.flatten(),i.flatten())].reshape(w,h,_).transpose(1,0,2)
    filtered_pic = filt(*np.meshgrid(np.arange(h),np.arange(w))).astype(pic.dtype) #色の決定
    return filtered_pic
    
@app.route("/", methods=["GET", "POST"])
def upload_file():
    if request.method == "GET":
        return render_template("index.html")
    if request.method == "POST":
        # アプロードされたファイルをいったん保存する
        f = request.files["file"]
        filepath = tempfile.mkdtemp()+"/a.png"
        #filepath = "{}/".format(tempfile.gettempdir()) + datetime.now().strftime("%Y%m%d%H%M%S") + ".png"
        
        f.save(filepath)
        image = Image.open(filepath)
        
        # 画像処理部分
        image = np.asarray(image)
        filtered = kuwahara(image, r=7)        
        filtered = Image.fromarray(filtered)
        
        # base64でエンコード
        buffer = io.BytesIO()
        filtered.save(buffer, format="PNG")
        img_string = base64.b64encode(buffer.getvalue()).decode().replace("'", "")
        
        result = "image size {}×{}".format(len(image[0]), len(image))
        return render_template("index.html", filepath=filepath, result=result, img_data=img_string)
    
    
if __name__ == "__main__":
     app.run(host="127.0.0.1", port=8080, debug=True)
```

下記の記事はFlaskのコードを書く上で参考にさせて頂きました。
[Python × Flask × PyTorch 数字認識Webアプリのお手軽構築](https://qiita.com/oyngtmhr/items/433974829015abac753c)

```txt:requirements.txt
Flask==1.1.2
Pillow==7.2.0
Numpy==1.16.4
opencv-python==4.2.0.34
```

```html:index.html
<html>
    <body>
        {% if result %}
	<IMG SRC="data:image/png;base64,{{img_data}}" alt="img_data"  id="imgslot"/>
        <div>{{result}}</div>
	<HR>
        {% endif %}
        ファイルを選択して送信してください<BR>
        <form action = "./" method = "POST" 
           enctype = "multipart/form-data">
           <input type = "file" name = "file" />
           <input type = "submit"/>
        </form>
     </body>
</html>
```

# 10. さいごに
最後まで記事を読んで頂きありがとうございました。Webアプリの作成は一筋縄とはいかなかったのですが、何とか動くものを作れました。