はじめに
-
Django
とTensorFlow
を使い、顔画像の推論を実施します。 - 最終的に
Heroku
へデプロイする事を考慮しました。Django
なら、データベースや CSRF 対策など、機能が充実しているからですね。 - ソース一式は ここ です。
概要
- フロント
- Bootstrap
- jQuery
- SpinKit https://github.com/tobiasahlin/SpinKit
- バックエンド
- Django
- NumPy
- OpenCV
- Pillow
- TensorFlow
- ストレージ
- S3
ライブラリ
- 以下を
pip
でインストールします。
requirements.txt
boto3
django
numpy==1.16.5
opencv-python
pillow
tensorflow==1.14.0
Django
セットアップ
- まず、プロジェクトとアプリの雛形を作成します。
$ django-admin startproject project .
$ python manage.py startapp app
- 上記で作成した
app
を追加する。 - テンプレートのフォルダを追加する。
- 日本語への修正をする。
- タイムゾーンの修正をする。
- スタティックファイルのフォルダを追加する。
project/settings.py
INSTALLED_APPS = [
'app.apps.AppConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
-
manage.py
が存在するフォルダと同じレベルで、上記で設定したスタティックファイルとテンプレートのフォルダを作成する。
$ mkdir static templates
- デバッグ用のデータベースを作成します。
- デフォルトでは、
SQLite
が利用され、db.sqlite3
が作成されます。
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying sessions.0001_initial... OK
- 管理者ユーザーを作成する。
$ python manage.py createsuperuser
ユーザー名 (leave blank to use 'maeda_mikio'):
メールアドレス:
Password:
Password (again):
Superuser created successfully.
- 管理画面を起動します。
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
December 21, 2019 - 14:09:05
Django version 3.0.1, using settings 'project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
- ブラウザで
http://127.0.0.1:8000/
にアクセスし、設定したユーザー名とパスワードを入力します。
- ブラウザで
http://127.0.0.1:8000/admin
にアクセスします。
トップ画面を作成
-
app/views.py
にトップ画面のテンプレートを設定します。
app/views.py
def index(request):
return render(request, 'index.html')
- 上記の
index.html
はtemplates/index.html
に作成します。 - テンプレートは、
Bootstrap
を利用しました。 - 画像をドラッグ&ドロップ出来る様にしています。
static/index.js
で処理をしています。 - また、推論の処理中は、SpinKit でアニメ処理をしています。
static/spinner.css
で処理をしています。
templates/index.html
<head>
省略
<!-- Spinner CSS -->
<link rel="stylesheet" href="/static/spinner.css">
省略
</head>
<body>
省略
<div class="starter-template container">
<img src="/static/title.png" class="img-fluid"><br />
<img id="img" src="/static/abe_or_ishihara.png" class="img-fluid" height="400">
<div class="spinner" style="display: none;">
<div class="rect1"></div>
<div class="rect2"></div>
<div class="rect3"></div>
<div class="rect4"></div>
<div class="rect5"></div>
</div>
</div>
<input type="file" id="file" name="file" accept="image/*" style="display: none;" required>
{% csrf_token %}
省略
<script src="/static/index.js"></script>
</body>
画像の送信
- Django の
CSRF
対策を利用しています。 -
index.html
内の{% csrf_token %}
からトークンを取得します。 - こちらは、Django公式ドキュメントをそのまま利用しています。
static/index.js
// https://docs.djangoproject.com/en/3.0/ref/csrf/
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var csrftoken = getCookie('csrftoken');
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
- 画像は、クリックしてファイルを選択する、もしくはドラッグ&ドロップで選択することにしました。
- スマホなら、タップが使えるはずです。
static/index.js
// クリックしてファイルを選択
$('#img').on('click', function() {
$('#file').click();
});
$('#file').change(function() {
var files = this.files;
if (checkImg(files)) {
file = files[0];
readImg(file);
predict(file);
}
});
// ファイルをドラッグ&ドロップ
var img;
img = document.getElementById('img');
img.addEventListener('dragenter', dragenter, false);
img.addEventListener('dragover', dragover, false);
img.addEventListener('drop', drop, false);
function dragenter(e) {
e.stopPropagation();
e.preventDefault();
}
function dragover(e) {
e.stopPropagation();
e.preventDefault();
}
function drop(e) {
e.stopPropagation();
e.preventDefault();
var dt = e.dataTransfer;
var files = dt.files;
if (checkImg(files)) {
file = files[0];
readImg(file);
predict(file);
}
}
- 画像は、同時に複数選択出来るため、1ファイルに限定する様にしています。
- また、
jpeg
png
のみとしました。 - サイズも 10MB 以上は処理をしないことにします。
static/index.js
// 1ファイル以上、jpeg、png、10MB以上の場合は処理をしない
function checkImg(files) {
if (files.length != 1 ) {
return false;
}
var file = files[0];
console.log(file.name, file.size, file.type);
if (file.type != 'image/jpeg' && file.type != 'image/png') {
return false;
}
if (file.size > 10000000) {
return false;
}
return true;
}
- ファイルが条件が問題なければ、ファイルを読み込みます。
static/index.js
// ファイルの読み込み
function readImg(file) {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function() {
$('#img').attr('src', reader.result);
}
}
- Django で作成する
/api/
パスに画像を送信します。 - 結果を受信するまでは、SpinKit でアニメ処理をします。
static/index.js
// 推論API
function predict(file) {
$('#img').css('display', 'none');
$('.spinner').css('display', '');
var formData = new FormData();
formData.append('file', file);
$.ajax({
type: 'POST',
url: '/api/',
data: formData,
processData: false,
contentType: false,
success: function(response) {
console.log(response);
$('#img').attr('src', response);
$('.spinner').css('display', 'none');
$('#img').css('display', '');
},
error: function(response) {
console.log(response);
$('#img').attr('src', '/static/abe_or_ishihara.png');
$('.spinner').css('display', 'none');
$('#img').css('display', '');
}
});
}
推論API
- 処理中のログをデータベースに保存するためのモデルを生成します。
app/views.py
def api(request):
log = Log()
- 画像は、
POST
で送信されているかチェックします。 - また、事前に
Django
がCSRF
チェックをしている形になります。
app/views.py
if request.method != 'POST':
log.status = 'post error'
log.save()
raise Http404
- 画像を読み込みます。ファイル名、ファイルサイズも取得します。
app/views.py
try:
formdata = request.FILES['file']
filename = formdata.name
filesize = formdata.size
except Exception as err:
log.message = err
log.status = 'formdata error'
log.save()
return server_error(request)
log.filename = filename
log.filesize = filesize
log.save()
- ファイルサイズは、フロント側でチェックしていますが、もう一度チェックします。
app/views.py
if filesize > 10000000:
log.status = 'filesize error'
log.save()
return server_error(request)
- ファイルデータを取得し、
jpeg
png
のチェックをします。
app/views.py
try:
filedata = formdata.open().read()
except Exception as err:
log.message = err
log.status = 'filedata error'
log.save()
return server_error(request)
ext = imghdr.what(None, h=filedata)
if ext not in ['jpeg', 'png']:
log.message = ext
log.status = 'filetype error'
log.save()
return server_error(request)
- 後で
Heroku
で公開をする予定です。 -
Heroku
いわゆるオブジェクトストレージがないため、AWS S3
へ保存することにしました。
app/views.py
try:
s3_key = save_image(filedata, filename)
except Exception as err:
log.message = err
log.status = 's3 error'
log.save()
return server_error(request)
log.s3_key = s3_key
log.save()
-
S3
には、日付とファイル名で保存します。 - アクセスキー、バケット等は、環境変数から読みだす様にします。
app/views.py
def save_image(filedata, filename):
now = (datetime.datetime.utcnow() + datetime.timedelta(hours=9)).strftime('%Y-%m-%dT%H:%M:%S+09:00')
key = '{}_{}'.format(now, filename)
resource = boto3.resource('s3', aws_access_key_id=os.environ['AWS_ACCESS_KEY_ID'], aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY'])
resource.Object(os.environ['BUCKET'], key).put(Body=filedata)
return key
- 画像データを
OpenCV
で扱える様にNumPy
で変換します。 - その後、
HAAR Cascade
で顔認識をします。 - また、
app/haarcascade_frontalface_default.xml
に保存しています。
app/views.py
image = np.fromstring(filedata, np.uint8)
image = cv2.imdecode(image, 1)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
face_cascade = cv2.CascadeClassifier(os.path.join(os.path.dirname(__file__), 'haarcascade_frontalface_default.xml'))
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=5)
if type(faces) != np.ndarray:
log.status = 'faces error'
log.save()
return server_error(request)
- 顔認識出来た場合は、
TensorFlow
で推論出来る様にデータをリサイズ、グレースケール等に変換します。 - 複数の顔に対応しています。
app/views.py
face_list = []
for (x, y, w, h) in faces:
face = image[y:y+h, x:x+w]
face = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)
face = cv2.resize(face, (IMG_ROWS, IMG_COLS))
face = np.array(face, dtype=np.float32) / 255.0
face = np.ravel(face)
face_list.append(face)
-
TensorFlow
で学習したモデルで、推論を実施します。 - 学習したモデルは、
app/data/
に保存しています。
app/views.py
try:
percent_list = predict(face_list, dtype='int')
except Exception as err:
log.message = err
log.status = 'predict error'
log.save()
return server_error(request)
- 顔の推論結果に基づいて、元の画像に修正を加えます。
- 今回、10人の顔画像の分類があって、0番目と1番目は特別に、青色、赤色で識別出来る様にしました。
- 顔は、
cv2.rectangle
で四角で選択され、下側にwrite_text
で推論結果を表示します。
app/views.py
predict_list = []
for (x, y, w, h), percent in zip(faces, percent_list):
max_index = np.argmax(percent)
max_value = np.amax(percent)
if max_index == 0:
color = (177, 107, 1)
elif max_index == 1:
color = (15, 30, 236)
else:
color = (0, 0, 0)
text = '{} {}%'.format(CLASSES[max_index], max_value)
image = write_text(image, text, (x, y+h+10), color, int(h/10))
cv2.rectangle(image, (x, y), (x+w, y+h), color, thickness=2)
predict_list.append(text)
log.message = ','.join(predict_list)
- 推論結果を画像に描きたいのですが、
OpenCV
では、日本語が利用できません。 - そこで、
Pillow
を利用します。 - 最初に、
OpenCV
からPillow
の形式へ変換します。 - フォントサイズも、顔のサイズに合わせて、適当に修正します。
- フォントは、コーポレート・ロゴ のフリーフォントを利用しました。
- フォントは
app/font.ttf
に保存しています。
app/views.py
def write_text(image, text, xy, color, size):
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image = Image.fromarray(image)
fill = (color[2], color[1], color[0])
size = size if size > 16 else 16
font = ImageFont.truetype(os.path.join(os.path.dirname(__file__), 'font.ttf'), size)
draw = ImageDraw.Draw(image)
draw.text(xy, text, font=font, fill=fill)
image = np.array(image, dtype=np.uint8)
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
return image
- 最後に、画像データを
BASE64
に変換し、フロントへ送信します。
app/views.py
image = cv2.imencode('.jpeg', image)[1].tostring()
response = 'data:image/jpeg;base64,' + base64.b64encode(image).decode('utf-8')
log.status = 'success'
log.save()
return HttpResponse(response, status=200)
URLのマッピング
- トップページの
/
と 推論APIの/api/
を追加しました。
project/urls.py
from app import views
urlpatterns = [
path('', views.index, name='index'),
path('api/', views.api, name='api'),
path('admin/', admin.site.urls),
]
ログ保存のモデルを作成
- 今回は、ログの保存のためのモデルを作成します。
- 推論APIで
log = Log()
やlog.save()
などで記載していた箇所です。 - ログの作成日時、更新日時、受信したファイル名、ファイルサイズ、S3のファイルキー、ステータスと状況に応じてメッセージを追加出来るようにしました。
app/models.py
class Log(models.Model):
create = models.DateTimeField(auto_now_add=True)
update = models.DateTimeField(auto_now=True)
filename = models.CharField(max_length=100, null=True, blank=True)
filesize = models.IntegerField(null=True, blank=True)
s3_key = models.CharField(max_length=100, null=True, blank=True)
message = models.TextField(null=True, blank=True)
status = models.CharField(max_length=100, null=True, blank=True)
- ログを管理画面から確認出来るようにします。
- カラム、カラムのフィルター、メッセージに関しては検索出来るようにしました。
app/admin.py
class LogAdmin(admin.ModelAdmin):
list_display = ('id', 'create', 'status', 'filename', 'filesize', 's3_key', 'message')
list_filter = ['create', 'status']
search_fields = ['message']
admin.site.register(Log, LogAdmin)
ログのモデルのマイグレーション
- 作成したログのモデルをデータベースへ反映します。
$ python manage.py makemigrations app
Migrations for 'app':
app/migrations/0001_initial.py
- Create model Log
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, app, auth, contenttypes, sessions
Running migrations:
Applying app.0001_initial... OK
管理サイトでログを確認
-
http://127.0.0.1:8000//admin/app/log/
下記のような画面が準備できました。
S3、IAMの設定
IAM
-
Django
の推論API のdef save_image(filedata, filename):
でファイルを保存しています。 -
AWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
を取得します。 -
IAM
のポリシーは、データの保存(PutObject) のみ許可しました。 - 大量のバケットは、
abe-or-ishihara
にしました。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::abe-or-ishihara/*"
]
}
]
}
S3
- バケットは、
abe-or-ishihara
にしました。 - リージョンは、米国東部(バージニア北部)にしました。
-
Heroku
のus
リージョンにデプロイする予定です。us
は、バージニアなので、距離的に近い方が良いでしょう。
起動シェル
-
AWS
のIAM
S3
の情報を環境変数に設定した上で、Django
の開発サーバーを起動します。
runserver.sh
#!/bin/bash
export AWS_ACCESS_KEY_ID='your-aws-access-key'
export AWS_SECRET_ACCESS_KEY='your-aws-secret-access-key'
export BUCKET=abe-or-ishihara
python manage.py runserver 0.0.0.0:5000
推論のテスト
- 画像をドラッグ&ドロップしてみます。
- 管理サイトでログを確認してみます。
- 左上では、
MESSAGE
の内容を検索できます。右側では、推論APIへのアクセス日時で絞り込んだり、ステータスで絞り込んだりできます。
-
S3
にも保存できました。
おわりに
-
TensorFlow
で学習したモデルを使い、Django
で推論アプリケーションを作成しました。 -
Flask
よりは、難しいですが、CSRF
対策、データベース等の機能が使えるのが便利ですね。 - 次は、
Heroku
へデプロイを実施する予定です。