みどりのあくま

勉強したことをアウトプットしていきます。

けものフレンズ解析機作ってみた。 - 後編 -

前回、機械学習でフレンズを判定できるようになったので、次はこれをwebアプリとして使えるように実装していきたいと思います。 前回の記事はこちら

orange634.hatenablog.com

前回の機械学習部分において参考にさせていただいたこちらの記事の例だと、アップロードした画像を1回保存して、顔を切り出したり、線で囲った画像を別ディレクトリで保存して結果画面に表示するというものです。
この方法だと公開することを考えると、容量的な問題(金銭的に容量は最小にせざる負えない)で同じ方法は難しいと判断しました。
そこで、サーバ側ではあくまで判定だけ行いその結果をブラウザのcanvasで表示する方法をとりました。
以下のような流れをイメージして実装しようと思いました。

  • 画像アップロード
  • canvasで読み込んで表示
  • サーバへ送信
  • 判定、結果を返す
  • 結果を受けて顔の枠線と判定結果の描画

フロント

画面の切り替え

canvasでロードした画像を保持したまま通信や結果の表示させたいので、ブラウザの読みこみなしで表示の切り替えが行えるようにしました。
仕組みは簡単で各ページの要素にpage-xxxxのように命名したclassをつけます。
そのclassの全要素を表示・非表示にして操作します。
切り替えるのは以下の関数で行います。

  /* 定数 */
  const PAGES = {
    // ページクラス: ボスのメッセージ
    'page-top': 'ガゾウ ヲ エランデ ネ。',
    // ...(略)

  /* グローバル変数 */
  var now = 'page-top';

  // 次のページに遷移
  function movePage(targetPage) {
    // 現在のページの要素を非表示
    changeAllDisplayState(now, 'none');
    // 現在のページを次のページに変更
    now = targetPage;
    // 次のページの要素を表示
    changeAllDisplayState(now, 'block');
    // メッセージをセットする
    setMessage(PAGES[now]);
  }

  // 同一クラスの表示変更
  function changeAllDisplayState(className, displayState) {
    $('.' + className).css('display', displayState);
  }

  // ボスのメッセージを書き換え
  function setMessage(msg) {
    $('#boss-msg').html(msg);
  }

表示を切り替える時はmovePage('page-xxx')で行います。
また、ページを読み込んだ時一瞬要素が見えてしまうので、表示する必要ないpag-xxxがついた要素はdisplay: noneにして表示できないようにしておきます。

ファイルアップロード

ファイル選択のボタンがブラウザのデフォルトだと、統一感がないので他のボタンと同じcssを適用させたいと思います。
labelで囲って実装する方法をとりました。
下の例はわかりやすいように説明に不要なclassは消してあります。

<label for="sendFile" class="button">
  ファイル センタク
   <input type="file" name="file" accept="image/*" id="sendFile">
</label>

ファイルがアップロードされたら、そのファイルの画像をcanvasで読み込むようにします。

  // ファイルアップロード時
  $('#sendFile').on('change', function (evt) {
    handleFileSelect(evt);
  });

  // アップロードした画像から情報取得
  function handleFileSelect(evt) {
    var files = evt.target.files;
    if (!files.length) {
      failDetecting();
      return;
    }
    loadImageFile(files[0]);
    movePage('page-ready');
  }

  // アップロードした画像をcanvasで描画
  function loadImageFile(uploadFile) {
    var canvas = $('#cnvs');
    var ctx = canvas[0].getContext('2d');
    var image = new Image();
    var fr = new FileReader();
    // ファイルをロードしたらコールバック
    fr.onload = function(evt) {
      // 画像をロードしたらコールバック
      image.onload = function() {
        var cnvsW = image.width;
        var cnvsH = cnvsW*image.naturalHeight/image.naturalWidth;
        canvas.attr('width', cnvsW);
        canvas.attr('height', cnvsH);
        ctx.drawImage(image, 0, 0, cnvsW, cnvsH);
        // 画像の表示
        isUpload = true;
        displayCanvasImg(isUpload);
      }
      image.src = evt.target.result;
    }
    fr.readAsDataURL(uploadFile);
  }

file要素の変更を検知してcanvasに画像読み込みとページ遷移を行います。
また、canvasの幅を適切に設定して、画面外に画像が出ないよう、アスペクト比を保つようにしています。

ajaxでファイル送信・結果の描画

jQueryを使って行います。
解析に時間がかかることを考えてtimeoutを60000にしています。

  // 画像投稿
  $('#imgForm').on('submit', function(e) {
    e.preventDefault();
    var formData = new FormData($(this).get(0));
    $.ajax($(this).attr('action'), {
      type: 'post',
      timeout: 60000,
      processData: false,
      contentType: false,
      data: formData,
      success: movePage('page-detecting')
    }).done(function(response){
      drawResult(response['result']); // 結果の描画
      movePage('page-result')
    }).fail(function() {
      failDetecting();
    });
    return false;
  });

canvasで結果の描画

判定結果はcanvasの機能で描画しています。
特に難しいことはしていません。

リセット

初期状態に戻すために、以下のようにしています。

  • 選択したファイルの値を空にする
  • canvasの画像を全部書き換える
  • 変数のリセット
  • 表示ページを変更
  function resetAll() {
    // ファイルをリセット
    $('#sendFile').val('');
    // canvasの中の画像を削除
    var canvas = $('#cnvs');
    var ctx = canvas[0].getContext('2d');
    ctx.clearRect(0, 0, canvas.width(), canvas.height());
    // アップロード状態をリセット
    isUpload = false;
    displayCanvasImg(isUpload);
    // topへ遷移
    movePage('page-top');
  }

デザイン

cssライブラリとしてNormalize.cssMilligramを使用しています。
フロントは詳しくないので、chromeで動くことしか考えないでcssを書いていきました。
ieとかedgeとかは気にしないフレンズ。

デザインはできるだけ「けものフレンズ」ぽくしたかたので、本家のサイト等を参考にそれっぽく作ってみました。
華やかさも欲しかったのでこちらのロゴジェネレータを使ったり、ボスの画像やfaviconドット絵で作ってみました。
本当はここまでこだわる予定ではなかったのですが、時間をかけすぎてしましました。。。

バック

あんまり特殊なことはしていないので、箇条書きでざっくり書いていきます。 書いていないところは基本的に普通のflaskアプリと同じようなことをしてます。

  • アップロードファイルの読み込みはこちらの記事にあるようにopencvで扱えるようにしました。
  • 切り出した顔画像に関して、画像データ、元画像から抜き出した位置情報、解析結果を一緒に扱えるにclassにまとめました。
  • evalとmainで別れていたファイルを一つに結合して、扱いやすくしました。
  • detector.svmmodel.ckptlibというディレクトリに置いてstaticと分けています。 staticにおくと公開してしまい、誰でもダウンロード出来てしまうので、あまり良くないかなと思い分けています。

こちらのwebアプリ部分に関してはgithubに参考となるコードを上げておきます。
よかったらそちらも参照してください。

github.com

サーバにデプロイ

これも特別なことはしていません。
サーバは僕が書いた以下2記事と同じ方法を用いています。

qiita.com

qiita.com

またあらかじめvagrantで開発環境の構築を何回か練習しました。
サーバを立てること自体が、初だったので練習できてよかったと思います。

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

  config.vm.box = "centos7.1" # 公式からcentos7.1最小構成のboxを取ってきています

  config.vm.network "private_network", ip: "192.168.33.10"

  config.vm.synced_folder "./www", "/var/www/app", create: true
end

ただ今は更新したら、いちいちssh接続してgit pullしているのでjenkins等で簡略化できないか考えています。

全体的な感想

長い道のりでしたが、フレンズ解析機を作成できました。
仕事でwebアプリを作成したことがあったが、このように一から自分で作ったことがなかったので、色々考えが不足しているところや、他の人が担当してどうなっているのかいまいちわからないことがいくつも出てきて、制作を通して知見が広がったように思います。

一度完成させるのは難しいかも(サーバのセットップあたり)と諦めかけました。
しかし、職場の同僚や上司、友人などにすでに話していたのでやめるわけに行かないと、モチベーションを上げてなんとか完成にこぎつきました。

これからはこのアプリを作成して見えてきた問題点を改良していって、ちょっとずつ成長させていこうと思います。
直近の目標としては、解析できるキャラを増やしたり、顔認識の精度をあげたり、アニメ以外のイラストもキャラの推測を行えるようにしたいと思います。