画像を識別をするwebアプリ (Django)
概要
東方Projectのキャラクター画像の識別器のWebアプリをDjangoを使って作成しました。
東方のキャラクター画像識別器
イントロ
画像識別器を作成してから(前回記事)、折角だから分かりやすい形で残したいと思ったのでWebアプリという形にしようかなと思いました。
思い立った時点では、Webアプリなんて一度も作ったこともなんてないしHTMLも勉強したこともない、なんかPythonを使えるWebフレームワークがあってそれを使えばWebアプリ作れるのは知ってるという程度の知識でした。
とりあえずDjangoの公式チュートリアルとDjango Girlsのチュートリアルをやってみました。
これらのチュートリアルを終えて、簡単な画像識別器アプリくらいなら作れるなと思ったのでやってみました。
目標
東方のキャラクター画像をアップロードしたらそのキャラクターの識別をするサイト。
アップロードした画像のプレビューが表示されて、識別結果としてキャラクター名とスコアの表が表示される感じにしたい。
実装
Djangoのフレームワークに沿って実装していきます。
Djangoのバージョンは4.0です。
urls.py
from django.urls import path from django.conf.urls.static import static from . import views app_name = 'classifier' urlpatterns = [ path('', views.ClassifierView.as_view(), name='classifier'), ]
app_name
はclassifier
としました。
データベースは使わないので、models.pyは無し。データベース使わないのにDjango使う意味あるの
views.py
from django.shortcuts import render from django.views import generic from django import http from django.conf import settings import json import io import base64 from PIL import Image from .forms import ImageForm from classifier import classifier # Create your views here. class ClassifierView(generic.TemplateView): template_name = 'classifier/classifier.html' classifier_model = classifier.init_model() def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['form'] = ImageForm() context['pred'] = '' context['done'] = False context['image'] = None context['alert'] = None self.classifier_model = classifier.init_model() return context def get(self, request, *args, **kwargs) -> http.HttpResponse: return super().get(request, *args, **kwargs) # label to name def label_to_name(self, labels, chara_table): # int or str if type(labels) == int or type(labels) == str: name = "" try: for k, v in chara_table.items(): if v['id'] == int(labels): name = k break except ValueError: pass return name else: return "" def post(self, request, *args, **kwargs): form = ImageForm(request.POST, request.FILES) if not form.is_valid(): context = self.get_context_data() # Only file size <= 50MB is acceptable if 'File size over' in form.errors['image']: context['alert'] = '50MB以下のファイルのみ実行します' else: context['alert'] = 'ファイルを正常に読み込めませんでした' # raise ValueError('invalid form') return render(request, self.template_name, context=context) image = form.cleaned_data['image'] # Preview image with io.BytesIO() as buf: Image.open(image, mode='r').save(buf, format='PNG') img = buf.getvalue() img = base64.b64encode(img) # encode the buffer valuess by base64 img = img.decode("utf-8") # decode to image self.kwargs['image'] = img # Character name string table_file = settings.STATIC_ROOT + '/classifier/chara_table.json' chara_table = dict() with open(table_file, "r") as f: chara_table = json.load(f) # Generate return values preds = classifier.pred(image, model=self.classifier_model) top_labels = {} value_prev = 1.0 for n, idx in enumerate(reversed(preds.argsort())): value = preds[idx] # Display at least 3 classes. Display classes which scores > 0.1. # Break when score is less than half of previous one or the difference is less than 0.005. if (n > 3) and (value < 0.1) and (((value_prev / value) > 2.0) or (value_prev - value < 0.005)): break top_labels[self.label_to_name(int(idx), chara_table)] = value value_prev = value self.kwargs['pred'] = top_labels self.kwargs['done'] = True return render(request, self.template_name, context=self.kwargs)
このviews.pyは主に画像投稿post
の挙動を定義しています。
post
を簡単に説明すると、forms.py(下に記載してます)に定義されたImageForm
クラスで画像を読み込んでから、画像をプレビュー。
chara_table.jsonというキャラクターのIDと名前を対応させたファイルを読み込み。
classifier
クラスの学習済の識別器モデルを呼び出して、pred
メソッドで推測結果を出力してます。
推測結果の予測値が高いものをcontext
に格納してテンプレートに渡し、render
実行。
推測結果のうち表示するクラスは、スコアの降順に並べ、最低3クラスを表示、4つ目以降はスコアが0.1より高く、スコアが一つ前のクラスの半分以上かつ差が0.005未満のクラスならば表示。
forms.py
from django import forms from django.forms import ValidationError def size_limit_validator(value): file_size= value.size if file_size > 52428800: # 50MB = 52428800 raise ValidationError('File size over') class ImageForm(forms.Form): image = forms.ImageField(validators=[size_limit_validator])
画像投稿Form。
過負荷防止のため、validatorsに50MB以上の画像を受け付けないようにする処理を追加。
classifier.html
<!-- ### 前略 ### --> <h1> 東方のキャラクター画像識別器 </h1> <p> 東方Projectのキャラクターが描かれた画像をアップロードするとどのキャラクターの画像かを識別します。 <br> 50MB以下のファイルのみ受け付けます。 </p> {% if not done %} <table> <form method="POST" enctype="multipart/form-data"> {% csrf_token %} {{ form }} <tr> <td></td><td><input type="submit" value="実行する" /></td> </tr> </form> </table> {% if alert %} <div class="alert alert-danger" role="alert"> <p>{{ alert }}</p> </div> {% endif %} {% else %} <p><img src="data:image/png;base64,{{ image | safe }}" class="img-fluid"></p> <div class="table-responsive"> <table style="max-width: 400px;" class="table table-light table-striped table-hover caption-top"> <thead class="table-light"> <tr> <th>Name</th> <th>Score(0~1)</th> </tr> </thead> <tbody> {% for key, value in pred.items %} <tr> <td>{{ key }}</td> <td class="w-25">{{ value |floatformat:3}}</td> </tr> {% endfor %} </tbody> </table> </div> <p> <a href="" class="btn btn-primary">もう一度する</a> </p> {% endif %} <!-- ### 後略 ### -->
説明文を記載して、未実行の場合は画像アップロードフォームと識別実行ボタンを表示。
識別器が動いた後はimage
に格納されたプレビュー用データを表示して、識別結果の表を表示(bootstrapを使ってるので簡単に書けます)。もう一度するボタンを表示。
デプロイ
2022.10.06 追記
初稿時はHerokuにデプロイしていましたが、Renderに移行しました。
さて、折角作ったのでwebに公開したいですが、これもやり方全然知りませんでした。
最も簡単なのはDjango Girlsチュートリアルで触れたPythonAnywhereですが、試してみたらPyTorchの容量が大きすぎるせいで入らない!
別の手段を探すとherokuというサービスでも同じことが出来るようなので、herokuで試してみます。
「Django deploy heroku」とかでググれば沢山解説記事出てくるので引っかかったところだけ書きます。
requirements.txtにPyTorchのインストール時のリンクを指定します。(サイズオーバーを回避できます。)
--find-links https://download.pytorch.org/whl/cpu/torch_stable.html torch==1.11.0+cpu torchvision==0.12.0+cpu
セキュリティ上、SECRET_KEY
をアプリ内部に置かないようにするため、HerokuのアプリのSettingのページからConfig VarsにSECRET_KEY
を設定し、settings.pyで環境変数から読み込むようにする。
SECRET_KEY = os.environ['SECRET_KEY']
herokuにデプロイしたのが、こちらのページです。
東方のキャラクター画像識別器
2022.10.06 追記
Renderに移行しました。
Renderへのデプロイ記事はこちら。
ウェブページは東方のキャラクター画像識別器。
動かしてみます。
ちゃんと動いてるようですね。良かった。