LoginSignup
172
121

More than 1 year has passed since last update.

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

Last updated at Posted at 2020-08-16

はじめに

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

完成品

1. Webサイトを開いて処理したい画像を選択して送信すると、
image.png
選択された元画像
処理前.jpg

2. 絵画っぽく変換された画像が返ってきます。
処理後.PNG

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

image.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) 安全に無料枠を使い倒せ

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

2. Cloud shellの起動方法

GCE環境を構築したあとは、インスタンスのページを開いて右上のcloud shellをアクティブにするをクリックしてターミナルを起動します。ターミナルが起動出来たら、Linuxのコマンドを使って構築したGCEのインスタンスを操作していくことができます。 ちなみに公式サイトによると、初めて VM インスタンスに接続したタイミングでCompute Engine によって 自動的にSSH 認証鍵ペアが生成されているらしいです。

pic.jpg

3. ローカル環境とのデータ受け渡し方法

ローカル環境で作成したWebサイトや画像処理をGCEで実行するには、ローカル環境のデータをGCEと共有する必要があります。コマンドでカッコよく置く方法もありますが、設定がいろいろ手間なので最も理解しやすいCloud Storageを使う方法を採用しました。
GCPのタブの中からStrage→ブラウザと選択していきます。ストレージブラウザで適当な名前でバケットを作成したら、GCEと共有したいファイルをローカルからアップロードしておきます。
image.png

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

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

【参考記事】
【無料】Qiitaの殿堂を作った物語【簡単】

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のダッシュボードから内容を確認できます。

6. 画像処理の手法

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

元画像
a.jpeg

フィルタ適用後
b.jpe

7. PythonからHTMLへの画像の渡し方で苦戦

Pythonで画像処理後の画像をHTMLに渡すのに手こずりました。はじめは普通に画像をstaticファイルに一度保存して、そのパスをHTMLに渡してHTMLから画像を読み込むような仕様にしていました。しかし、GCE環境でアプリ実行中はPythonからフォルダへの書き込みは制限されておりエラーとなりました。公式サイトによるとランタイム環境下でのデータの保存は以下の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】

8. メモリ不足による不具合

色々な画像を試していると、画像サイズによっては無料版GCEの最大メモリ(256MB)をオーバーしてしまってエラーがでているようです(1000×600より大きいとダメでした)。おそらくプログラムやデータ構造を改良したらマシンをアップグレードせずとも対応可能と思いますが、まだやっていません。GCPは最初1年間で300$の無料クレジットがあるのでアップグレードして、試してみるのも良いかもしれません。エラーの内容はGCPのタブの中のApp Engineのダッシュボードから確認できます。
メモリ不足エラー.PNG
(2020/08/17 22:30追記)
GCEのメモリ不足に関して、GCPの無料クレジットを使ってマシンのcpuコア数とメモリ容量を上げてみました。マシンのレベルを上げたにも関わらず最初はメモリのエラーが出ていたので一瞬エッとなったのですが、公式サイトによると各インスタンスで使えるメモリ容量はデフォルトで256MBとなっているようです。 app.yamlファイル内で下記のコードを追加することでメモリ容量を変更することができます。2048MBに上げてみたところ、4000×1800の画像まで処理できました。

app.yaml
instance_class: F4_1G

9. ソースコード

app.yaml
runtime: python37
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アプリのお手軽構築

requirements.txt
Flask==1.1.2
Pillow==7.2.0
Numpy==1.16.4
opencv-python==4.2.0.34
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アプリの作成は一筋縄とはいかなかったのですが、何とか動くものを作れました。

172
121
4

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
172
121