naritoブログ

【お知らせ】
・コメントで質問等をしたが返事が返ってこない場合、私はそれを見落としています。
その場合は再度コメントをするかメールをしてください(toritoritorina@gmail.com)。

・近いうちに新しいブログが作成されます。わーお!

Djangoで、けものフレンズキャラの顔を認識させる(Deep Learning)

約565日前 2017年3月7日21:27
プログラミング関連
Bootstrap4 Django OpenCV 画像認識 ディープラーニング Python
Githubに、ソースをおきました。
https://github.com/naritotakizawa/kemono

このように画像をアップロードし


その画像で、誰が移っているかと、キャラの部分に白線を引いた結果を表示します。


元のデータが少ないため、サーバルとかばんちゃん以外は上手く認識されません。


はぶられるカワウソ


アニメのキャラで学習させていたので、右下のようなデフォルメキャラも認識されるとは思いませんでした。良いですね。



Djangoで、手書き数字の画像認識(Deep Learning)
https://torina.top/detail/332/
を基に、手書き数字の画像をけものフレンズキャラにしたものになります。
フレームワークは使わず
「ゼロから作るDeep Learning」の7章、畳み込みニューラルネットワークの実装サンプルを利用しています。

書籍のソースコードは以下です。
https://github.com/oreilly-japan/deep-learning-from-scratch

Pythonは3.5です。
pip freeze


Django、numpy、Pillow、opencvを入れれば大丈夫です。
Django==1.10.6
numpy==1.12.0
olefile==0.44
opencv-python==3.2.0
Pillow==4.0.0
pytz==2016.10


opencvだけ上手くWindowsにインストールできなかったため、以下サイトからwhlをもらいました。
http://www.lfd.uci.edu/~gohlke/pythonlibs/#opencv

プロジェクト名は「kemono」
アプリケーションは「image」

ディレクトリはこのような感じ。


params.pklは、学習済みの重みパラメータです。

lbpcascade_animeface.xmlは、アップロードされた画像からアニメ顔を検出するのに使用します。この検出された顔を、ディープラーニングを使って「誰だ?」と推論する訳です。
ダウンロードは以下のサイト様からです。
http://ultraist.hatenablog.com/entry/20110718/1310965532
https://github.com/nagadomi/lbpcascade_animeface
deepディレクトリは、ディープラーニング関連のプログラムが入っています。

main.py以外は、以下にあります。
https://github.com/oreilly-japan/deep-learning-from-scratch/tree/master/ch07
https://github.com/oreilly-japan/deep-learning-from-scratch/tree/master/common


今回はDjangoに埋め込むため、importのパスを変更しています。変更はimport部分のみです。
common.layers等を.layersに変更するだけです。

kemono/deep/simple_convnet.py
import pickle
import numpy as np
from collections import OrderedDict
from .layers import *  # 変更
from .gradient import numerical_gradient  # 変更


kemono/deep/layers.py
import numpy as np
from .functions import *  # 変更
from .util import im2col, col2im  # 変更



kemono/kemono/settings.py
INSTALLED_APPSにimageアプリを足し、MEDIA_ROOT、MEDIA_URLの設定です。
"""
Django settings for kemono project.

Generated by 'django-admin startproject' using Django 1.10.6.

For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'nc9=10tm(d#xmxa_q745#9f+hqhmg8x#@wh=!77e&8tr%61-=8'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'image',  # 作った奴
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'kemono.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'kemono.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/

STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'


kemono/kemono/urls.py
ローカル環境でのメディアURLの設定と、メインアプリのurls.pyをincludeします。
from django.conf import settings
from django.conf.urls import url, include
from django.conf.urls.static import static
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('image.urls', namespace='image')),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,
                          document_root=settings.MEDIA_ROOT)


kemono/image/urls.py
画像の一覧画面と、画像のアップロード画面です。
from django.conf.urls import url
from django.contrib import admin
from image import views
 
urlpatterns = [
    url(r'^$', views.ImageList.as_view(), name='list'),
    url(r'^create/$', views.ImageForm.as_view(), name='create'),
]


kemono/image/models.py
モデルはシンプルです。画像と、映っているキャラ達の名前です。
from django.db import models
from django.utils import timezone


class Image(models.Model):

    file = models.ImageField('画像', upload_to='img/')
    names = models.CharField('映っているキャラ達', max_length=255)
    created_at = models.DateTimeField('作成日', default=timezone.now)

    def __str__(self):
        return self.names



kemono/image/forms.py
これはファイルアップロード用のフォームです。
from django import forms


class UploadForm(forms.Form):
    file = forms.ImageField(label='画像ファイル')



kemono/image/views.py
画像一覧のListViewと、アップロードのFormViewです。
from django.shortcuts import redirect
from django.views import generic
from .forms import UploadForm
from .models import Image
from deep.main import predict


class ImageList(generic.ListView):
    model = Image


class ImageForm(generic.FormView):
    form_class = UploadForm
    template_name = 'image/image_form.html'

    def form_valid(self, form):
        file = form.cleaned_data['file']
        result_img, char_names = predict(file)
        image_model = Image(file=result_img, names=char_names)
        image_model.save()
        return redirect('image:list')


ちゃんとファイルがアップロードされれば、そのファイルをdeep.mainのpredict関数へ渡します。
元画像の顔部分に白線が引かれた、加工された画像と映っているキャラ達の名前が文字列でかえってきます。
それを引数に、Imageモデルをインスタンス化しセーブ。
    def form_valid(self, form):
        file = form.cleaned_data['file']
        result_img, char_names = predict(file)
        image_model = Image(file=result_img, names=char_names)
        image_model.save()



処理が終わればトップぺージである一覧画面へリダイレクトさせます。
HttpResponseRedirectよりもredirectのほうがやっぱり楽ですね。
return redirect('image:list')



kemono/image/templates/image/base.html
Bootstrap4です
<!DOCTYPE html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
 
  </head>
  <body>
      {% block content %}{% endblock %}
    <!-- jQuery first, then Tether, then Bootstrap JS. -->
    <script
      src="https://code.jquery.com/jquery-3.1.1.min.js"
      integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
      crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
 
  </body>
</html>


kemono/image/templates/image/image_list.html
{% extends "image/base.html" %}
{% block content %}
<div class="container">
  <div class="mt-5 text-center">
    <h1 class="display-4">フレンズ検出</h1>
    <p>
      <a href="{% url 'image:create' %}" class="btn btn-primary">画像アップロード</a>
    </p>
  </div>
  <hr>
  <div class="row">
  {% for img in image_list %}
    <div class="col-3">
      <a href="{{ img.file.url }}"><img class="img-thumbnail" src="{{ img.file.url }}"></a>
    </div>
    <div class="col-3">
      <h2>映っているキャラ</h2>
      <h5>{{ img.names }}</h5>
      {% if user.is_authenticated %}
      <p><a href="{% url 'admin:image_image_delete' img.id %}">削除</a></p>
      {% endif %}
    </div>
  {% endfor %}
  </div>

</div>
{% endblock %}


mt-5は、margin-top:3remです。
https://v4-alpha.getbootstrap.com/utilities/spacing/
<div class="mt-5 text-center">


今回は使っていませんが、width:100%;のw-100などもあります。
https://v4-alpha.getbootstrap.com/utilities/sizing/
<div class="w-100">Width 100%</div>


{% if user.is_authenticated %}は、ログインユーザにだけ見えるやつです。
hrefに指定しているのは、管理画面の削除ページへのリンクですね。
Django、管理画面へのリンク
https://torina.top/detail/255/
      {% if user.is_authenticated %}
      <p><a href="{% url 'admin:image_image_delete' img.id %}">削除</a></p>
      {% endif %}



kemono/image/templates/image/image_form.html
アップロード画面
{% extends "image/base.html" %}
{% block content %}
<div class="container">
  <div class="mt-5 text-center">
    <h1 class="display-4">アニメキャラ検出アプリ(けものフレンズ)</h1>
    <p>
      <a href="{% url 'image:list' %}" class="btn btn-primary">一覧へ</a>
    </p>
  </div>
  <hr>
  <div class="col-sm-6 offset-sm-3">
  <form action="" method="POST" enctype='multipart/form-data'>
    <div class="form-group row">
      <label for="{{ form.file.id_for_label }}" class="col-sm-3 form-control-label">
        {{ form.file.label }}
      </label>
      <div class="col-sm-9">
        {{ form.file }}
        {{ form.file.errors }}
      </div>
    </div>
    {% csrf_token %}
    <input type="submit" class="btn btn-outline-primary btn-lg btn-block" value="送信する">
  </form>
  </div>
</div>
{% endblock %}


kemono/deep/main.py
import io
from django.core.files.uploadedfile import InMemoryUploadedFile

import cv2
import numpy as np
from PIL import Image
from .simple_convnet import SimpleConvNet

# ネットワークのインスタンス化と、重みの読み込み
network = SimpleConvNet(
    input_dim=(3, 100, 100), hidden_size=50, output_size=10)
network.load_params('params.pkl')

# 推論の結果となる配列。画像がサーバルなら0って帰る
labels = np.array(['サーバル', 'かばんちゃん', 'かば', 'かわうそ', 'ジャガー',
                   'トキ', 'アライ', 'フェネック', 'すなねこ', 'つちのこ'])


def detect(image):
    """画像を受け取り、顔部分を白線で囲み、顔部分を全て返す"""

    cascade = cv2.CascadeClassifier('lbpcascade_animeface.xml')
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray = cv2.equalizeHist(gray)

    faces = cascade.detectMultiScale(gray,
                                     # detector options
                                     scaleFactor=1.1,
                                     minNeighbors=5,
                                     minSize=(24, 24))
    for (x, y, w, h) in faces:
        cv2.rectangle(image, (x, y), (x + w, y + h), (255, 255, 255), 2)

    return [cv2.resize(image[y:y+h, x:x+w], (100, 100)) for x, y, w, h in faces]


def predict(upload_file):
    """推論した結果の画像と内容を返す"""

    # 画像をnumpy配列に
    image_array = np.asarray(Image.open(upload_file))

    # 画像の顔部分が帰ってくる
    faces = detect(image_array)

    # 顔が検出できなかった
    if not faces:
        return upload_file, '誰も検出できませんでした'

    # 顔の格納されたリストをnumpy変換し、形を整え
    faces_array = np.array(faces).reshape(
        (len(faces), 3, 100, 100))

    # 推論
    result = network.predict(faces_array).argmax(axis=1)

    # 映っているキャラクターの名前
    char_names = labels[result]

    # 画像のnumpy配列を、PilowのImageオブジェクトへ
    image = Image.fromarray(np.uint8(image_array))

    # Imageオブジェクトを、Djangoのファイルっぽいオブジェクトへ治す処理
    image_io = io.BytesIO()
    image.save(image_io, format='JPEG')
    image_file = InMemoryUploadedFile(image_io, None, 'foo.jpg', 'image/jpeg',
                                      image_io.getbuffer().nbytes, None)

    return image_file, ','.join(char_names)


(3, 100, 100)は、RGB、縦100、横100の画像ということです。グレースケールなら(1, 100, 100)になります。
hidden_sizeは隠れ層、
output_sizeは10個に分類なので10です。
# ネットワークのインスタンス化と、重みの読み込み
network = SimpleConvNet(
    input_dim=(3, 100, 100), hidden_size=50, output_size=10)
network.load_params('params.pkl')


パスは丁寧に設定するなら、例えば以下のような感じになるでしょう...
import os
from django.conf import settings
...
...
params_path = os.path.join(settings.BASE_DIR, 'params.pkl')
network.load_params(params_path)


このlabelsもnumpy配列にすることで、一度で計算がおわります。numpyすごい
# 推論の結果となる配列。画像がサーバルなら0って帰る
labels = np.array(['サーバル', 'かばんちゃん', 'かば', 'かわうそ', 'ジャガー',
                   'トキ', 'アライ', 'フェネック', 'すなねこ', 'つちのこ'])


このモジュールの、メインとなる関数です。
form.cleaned_data['file']とか、そういったものを引数に渡します。
def predict(upload_file):
    """推論した結果の画像と内容を返す"""


アップロードされたファイルはInMemoryUploadedFileというオブジェクトに格納されていますが、こいつはImage.open()に渡せます。
それをasarrayでnumpy配列にし、このnumpy配列をdetectへ渡します。
    # 画像をnumpy配列に
    image_array = np.asarray(Image.open(upload_file))

    # 画像の顔部分が帰ってくる
    faces = detect(image_array)



この関数は一枚の画像から顔部分(アニメな顔)を抜き出し、それを返します。
ついでに、顔部分を白い線で囲んでくれます。
def detect(image):
    """画像を受け取り、顔部分を白線で囲み、顔部分を全て返す"""


顔の検出はこの部分です。
一枚の画像にアニメキャラが100人いれば、上手くいけばその100人の顔の部分がfacesに格納されます。
    cascade = cv2.CascadeClassifier('lbpcascade_animeface.xml')
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray = cv2.equalizeHist(gray)

    faces = cascade.detectMultiScale(gray,
                                     # detector options
                                     scaleFactor=1.1,
                                     minNeighbors=5,
                                     minSize=(24, 24))


minSizeは、検出する物体の最小サイズで、これより小さい物は無視します。
今回の例だと、顔っぽいけど23*23サイズなんかだと無視されるってことです。
逆のmaxSizeもあります。

上手く検出できなければ、minNeighborsも色々変えると良いでしょう。以下のページが参考になります。
http://workpiles.com/2015/04/opencv-detectmultiscale-scalefactor/
http://workpiles.com/2015/04/opencv-detectmultiscale-minneighbors/



顔部分を白い線で囲んでいるのが、ここです。
    for (x, y, w, h) in faces:
        cv2.rectangle(image, (x, y), (x + w, y + h), (255, 255, 255), 2)


これはちょっと複雑ですが...
    return [cv2.resize(image[y:y+h, x:x+w], (100, 100)) for x, y, w, h in faces]


丁寧に書くとこのような処理です。
顔部分を抜き出し、100*100にリサイズし、リストに追加します。
    faces = []
    for (x, y, w, h) in faces:
        face = image[y:y+h, x:x+w]
        face_100_100 = cv2.resize(face, (100, 100))
        faces.append(face_100_100)
    return faces


predictへ戻ります...
facesが空の場合は、顔が検出できなかった場合です。
当然顔が検出できなければ、どのアニメキャラか?を判断することもできかねます。
    # 顔が検出できなかった
    if not faces:
        return upload_file, '誰も検出できませんでした'



顔が検出されてれば、どのキャラかを推論します!
    # 顔の格納されたリストをnumpy変換し、形を整え
    faces_array = np.array(faces).reshape(
        (len(faces), 3, 100, 100))

    # 推論
    result = network.predict(faces_array).argmax(axis=1)

    # 映っているキャラクターの名前
    char_names = labels[result]


推論結果(キャラの名前)とともに、顔部分を白く囲んだ画像も当然返したいわけです。
最終的には、ここに渡ってきた時のようにInMemoryUploadedFileオブジェクトにしたいです。
まずは画像をImageオブジェクトへ変換します。
    # 画像のnumpy配列を、PILのImageオブジェクトへ
    image = Image.fromarray(np.uint8(image_array))


PILのImageからは、こんな感じで変換できるようです...
http://stackoverflow.com/questions/3723220/how-do-you-convert-a-pil-image-to-a-django-file
今度調べます。
    # Imageオブジェクトを、Djangoのファイルっぽいオブジェクトへ治す処理
    image_io = io.BytesIO()
    image.save(image_io, format='JPEG')
    image_file = InMemoryUploadedFile(image_io, None, 'foo.jpg', 'image/jpeg',
                                      image_io.getbuffer().nbytes, None)


そして、画像と映っているキャラの名前を返す
return image_file, ','.join(char_names)