LoginSignup
11
13

More than 3 years have passed since last update.

DjangoとTensorFlowを利用した、顔画像の推論

Last updated at Posted at 2019-12-21

はじめに

  • DjangoTensorFlow を使い、顔画像の推論を実施します。
  • 最終的に Heroku へデプロイする事を考慮しました。Django なら、データベースや CSRF 対策など、機能が充実しているからですね。
  • ソース一式は ここ です。

概要

ライブラリ

  • 以下を 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/ にアクセスし、設定したユーザー名とパスワードを入力します。

image.png

  • ブラウザで http://127.0.0.1:8000/admin にアクセスします。

image.png

image.png

トップ画面を作成

  • app/views.py にトップ画面のテンプレートを設定します。
app/views.py
def index(request):
    return render(request, 'index.html')
  • 上記の index.htmltemplates/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 で送信されているかチェックします。
  • また、事前に DjangoCSRF チェックをしている形になります。
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/ 下記のような画面が準備できました。

image.png

S3、IAMの設定

IAM

  • Django の推論API の def save_image(filedata, filename): でファイルを保存しています。
  • AWS_ACCESS_KEY_IDAWS_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 にしました。
  • リージョンは、米国東部(バージニア北部)にしました。
  • Herokuus リージョンにデプロイする予定です。us は、バージニアなので、距離的に近い方が良いでしょう。

起動シェル

  • AWSIAM 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

推論のテスト

  • 画像をドラッグ&ドロップしてみます。

image.png

download.jpg

download-1.jpg

  • 管理サイトでログを確認してみます。
  • 左上では、MESSAGE の内容を検索できます。右側では、推論APIへのアクセス日時で絞り込んだり、ステータスで絞り込んだりできます。

image.png

  • S3 にも保存できました。

image.png

おわりに

  • TensorFlow で学習したモデルを使い、Django で推論アプリケーションを作成しました。
  • Flask よりは、難しいですが、CSRF 対策、データベース等の機能が使えるのが便利ですね。
  • 次は、Heroku へデプロイを実施する予定です。
11
13
0

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
11
13