flaskを使ったWebアプリ作成 画像処理サーバー化

python

 flaskを使って複数の画像ファイルをアップロード、からのそれを処理して返す方法の覚書です。何手かつまづいたので。

 アップロードに関しては

ファイルのアップロード — Flask Documentation (2.2.x)

 公式の和訳はこれだと思われます。こいつをトレースします。複数ファイルのアップロードし、それらに対して自前の画像処理関数を通し、結果を表示、ダウンロードまでの過程を残しておきます。

スポンサーリンク

ファイルのアップロード

 まずはアップロードのための最低限のhtml記述をします。(ここでは実験的に文字列の受け取りも併記しています。)

<!DOCTYPE html>
<html lang="ja">

<body>
 <form method="POST" enctype="multipart/form-data">
  <label>文字列の入力<input type="text" name="text"></label>
  <label>
   画像 <input type="file" name="img_name" accept="image/*" multiple>
  </label></br>
  <input type="submit" name="confirm" value="実行"></br></br>
 </form>
</body>

</html>

 ここでfileのformに対して、multipleをつけておくと複数ファイルが指定できるようになります。

 あともう一つ。formに対してenctype=”multipart/form-data”をつけておきます。いったんおまじないみたいなものと理解しときます。

 これをindex.htmlとしてstaticのフォルダに置いておきます。bodyしか記述していないので気持ち悪ければ適宜headはつけてあげてください。

 そしたらコードの方

from flask import Flask, request, render_template

UPLOAD_FOLDER = './uploads'

app = Flask(__name__, static_folder='./html/', static_url_path='/files', template_folder='./temps')

@app.route('/', methods=['GET'])
def root_func_get():
    return app.send_static_file('index.html')

 ここまでで上記index.htmlの表示を行います。問題はこのファイル群の受け取り

@app.route('/', methods=['POST'])
def root_func_post():
    print("POST")
    
    if 'text' in request.form:
        print("has text in form") # textはここにある

    if 'img_name' in request.form:
        print("has img_name in form") # img_nameはここには無い
    
    if 'img_name' in request.files: # ここにある
        files = request.files.getlist('img_name')

        for file in files:
            file.save(os.path.join(UPLOAD_FOLDER, file.filename))
        
    return app.send_static_file('index.html')

 今回は”img_name”のラベルがつくファイルリストが欲しいのですが、前回試したrequest.formの辞書ではなく、request.filesという変数にへ入ってくるみたいです。そこからファイル群(list)を取得します。

 ここで使用するgetlistという関数ですが、通常の辞書型(クラス)の関数にはありません。formやfilesはMultiDictというクラスになっていて、ひとつのkeyに対して複数の値が入れられる特殊な辞書の様で、getlistというメソッドで同名の要素群をリストとして取得できるようです。多分これ。

multidict
multidict implementation

 file.saveで*.pyが置いてあるフォルダを起点にした場所にファイルを保存してくれます。今回はUPLOAD_FOLDERの下に同じファイル名で保存するようにしています。

 これがシンプルな複数ファイルのアップロードのサンプルになります。

安全なファイルのアップロード

 ただこれだとファイル名がセキュアではない(いじわるな名前でファイルがアップロードされるとサーバ上の大事なファイルを上書きされる恐れがある。)ので、保存するファイル名をセキュアなファイル名に変換するのが定番の様です。

 また他のアクセスで同名のファイルをアップロードされると後のアクセスに上書きされてしまいます。これもいまいち。

 なので各アクセスに対してユニークな名前のフォルダに対してセキュアなファイル名で保存するようにコードを変更してみます。

from werkzeug.utils import secure_filename

 これを使うと安全なファイル名に変更してくれるみたいです。2バイト文字やスペースを使ったファイル名だとそれらの文字が消えています。日本語だけのファイルを送ったらファイル名無しになってしまいました。(拡張子だけのファイル)セキュア過ぎやろ。

import datetime

 これを使って現在時刻を取得してこれをひとまず識別子とします。秒単位で同時アクセスがあるとまずいでしょうけど。

 コードをこのように書き換えます。

@app.route('/', methods=['POST'])
def root_func_post():
    print("POST")
    
    if 'img_name' in request.files: # ここにある
        files = request.files.getlist('img_name')        
        
        now = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
        os.makedirs(os.path.join(UPLOAD_FOLDER, now), exist_ok=True)

        for file in files:
            filename = secure_filename(file.filename)
            if(filename != ''):
                file.save(os.path.join(UPLOAD_FOLDER, now, filename))
        
    return app.send_static_file('index.html')

 これでユニークなフォルダに対して、セキュアなファイル名でアップロードされます。(盤石ではないですがまぁ普通に使う分には)

画像処理

 アップロードされたファイルに対して適当な処理をしてみます。ファイルはサーバサイドに持ってこれたので、こいつらに対して処理を書けば良いだけです。

 今回は適当にアップロードされたファイルを単純平均を取る関数を書いてみます。

def img_sum(src_dir, dst_name):
    flist = glob.glob(src_dir + "/*.jpg")
    sums = cv2.imread(flist.pop()).astype(np.float64)
    cnt=1

    for fname in flist:
        sums += cv2.imread(fname).astype(np.float64)
        cnt += 1
    
    sums = (sums / cnt).astype(np.uint8)
    cv2.imwrite(dst_name, sums)

 画像ファイルの大きさがすべて同じサイズであることを前提にした適当な処理です。関数の引数でファイルをアップロードしたフォルダと、出力ファイルのセットにしています。この関数を

img_sum(os.path.join(UPLOAD_FOLDER, now), os.path.join("./html", now + ".jpg"))

 こんな感じで呼ぶだけです。staticフォルダにjpgが出来上がっています。このファイルを

return app.send_static_file(now + ".jpg")

 してあげるとブラウザに結果が返ってきます。

コード

from flask import Flask, request
from werkzeug.utils import secure_filename

import datetime
import os
import cv2
import glob
import numpy as np

UPLOAD_FOLDER = './uploads'

app = Flask(__name__, static_folder='./html/', static_url_path='/files', template_folder='./temps')


def img_sum(src_dir, dst_name):
    flist = glob.glob(src_dir + "/*.jpg")
    sums = cv2.imread(flist.pop()).astype(np.float64)
    cnt=1

    for fname in flist:
        sums += cv2.imread(fname).astype(np.float64)
        cnt += 1
    
    sums = (sums / cnt).astype(np.uint8)
    cv2.imwrite(dst_name, sums)
    

@app.route('/', methods=['GET'])
def root_func_get():
    print("GET")
    return app.send_static_file('index.html')


@app.route('/', methods=['POST'])
def root_func_post():
    print("POST")
    
    if 'img_name' in request.files: # ここにある
        files = request.files.getlist('img_name')        
        
        now = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
        os.makedirs(os.path.join(UPLOAD_FOLDER, now), exist_ok=True)

        for file in files:
            filename = secure_filename(file.filename)
            if(filename != ''):
                file.save(os.path.join(UPLOAD_FOLDER, now, filename))
        
        img_sum(os.path.join(UPLOAD_FOLDER, now), os.path.join("./html", now + ".jpg"))

    return app.send_static_file(now + ".jpg")

app.run(host="0.0.0.0", port=80, debug=False)

まとめ

 ひとまずクライアントサイドから画像を選んでサーバサイドで処理をさせ答えを返す簡単なサンプルを作ってみました。

 もともとの動機がスマホにはちと重たい処理(というかスマホでコードを書きたくなかった)をパソコンでさせて、答えを返してもらう事をやりたかったので、これでやりたいことは実現できました。もちろんスマホからのアクセスも問題なく受けられました。

 すべてちょっと調べたらわかる内容なのですが、自分がまた今度やりたくなった時に思い出せるように記録を残しときました。あっさりできてしまったので今度はフロントエンド側も少し勉強してみたくなりました。

python
スポンサーリンク
キャンプ工学

コメント

タイトルとURLをコピーしました