みどりのあくま

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

顔画像周りのデータを扱うアプリをelectron-vueで作ってみた

この前けものフレンズ解析機を作った際にdlibやtensorflowを利用したのですが、その時顔の位置に関した情報を扱うことが多かったです。
dlibだと顔の位置判別の学習に、tensorflowは学習データを作成する際に顔画像を切り出すのに使いました。

orange634.hatenablog.com

なので、後々のデータセットを準備する作業のことを考えて、そういった顔の位置の情報を上手く扱えるようなアプリがあると便利だろうなーと思い今回作成しました。

概要

f:id:orange634:20170908131738p:plain

起動した画面はこんな感じです。
「Load」ボタンから画像が入ってるフォルダを選択して、フォルダ内部画像データを取得します。
再帰的にファイルを取得しないのでフォルダ直下の画像のみ読み込まれます。

f:id:orange634:20170908131850p:plain

実際にロードしたらこんな感じになります。

f:id:orange634:20170908131943g:plain

各画像の上に四角の描画、ドラッグ、リサイズ、削除が行うことができ、複数の四角を描画することも可能です。

描画が完了したら「SAVE」ボタンを押して保存します。
保存しないでページを変えると描画したデータが消えてしまいます。

f:id:orange634:20170908132245p:plain

「Publish」ボタンを押すと、上の画像のように画面に書かれた正方形の位置(x, y)と大きさ(rectLength)のデータをdata.jsonとして選択したフォルダ内部に出力します。
また、ロードした際にディレクトリにdata.jsonがあった場合はそのデータを元に画像に正方形を描画します。 なので、何回でも調整可能ということです。
また、dlibで検出した顔の位置の情報をdata.jsonと同じ形式にすれば、そのデータも利用可能です。 (ただし画像に合わせて倍率を修正する必要があります)

f:id:orange634:20170908132228p:plain

mustacheというテンプレートエンジンを利用してtemplate.xmlからdata.xmlを出力できるようにしました。
出力される位置とうの情報は実際に画像に合わせて倍率をかけて四捨五入しています。

ただ、今回あくまで機械学習がメインであまり時間をかけるのは適切でないと判断して以下の部分で妥協しています。

  • 画像の横幅は1000px固定(実際の大きさに戻す時に倍率の計算が楽)
  • 縦長の画像は途切れて実質使えない(アニメの画像のみ使う想定なのでこれで横長画像しか使わない想定なので)
  • リサイズするのに右下一箇所でしか出来ない
  • vuexが上手く扱えず厳格モードではない

技術

今回アプリ作成に置いて、electron-vueを利用しました。
仕事で一度使ったことがあり、なんとなくですが知見があるからです。

vueではなくreactを使おうかと考えましたが、あくまで機械学習がメインでアプリ制作は手っ取り早く(それでも1ヶ月かかりましたが…)作ってしまいたかったので、経験のあるvueを採用しました。

四角の描画

顔を囲む四角はsvgを利用して記述しています。
svgを扱うのにライブラリは一切利用していません。
本当はsvg.jsを使いたかったのですが、vueと相性が悪いようで正しく動かなかったの(どうやらDOMを直接いじるのがvueだと上手く動かない)で諦めました。
正直正方形を記述するだけなので簡単だと思っていましたが、そんなことはなく苦労しました。

fabricを利用してのcanvasでの描画も考えたが、リサイズした時に大きさによっては四角にジャギが入るので、謎のこだわりでリサイズしても綺麗なsvgにしました。
正直canvasでよかったように思います…

またsvgはvueでhtmlのタグを扱うように扱えるので、vueの単一コンポーネントも使えます。

四角の描画部分はmouseup、mousedown、mousemove等と今は何を選択しているかで描画、ドラッグ、リサイズ等を行なっています。
正直コードは実験しながら行なっていたので、あまりよくありません。

ファイルの取得と画像の描画

nodeのfs(FileSystem)を利用しています。
画像はsvgのimageタグで表示させました。
file://<url>を使えるようんするためにelectronのwebPreferenceswebSecurityfalseにしています。
canvasで読み込んで表示させる方法も考えましたが、個人利用の範囲ならこの設定でも問題ないかなと思い、楽しています。

画像の切り出し方法

こちらはpythonのコードを利用する想定です。
nodeで画像を切り取ることは可能ですが、pythonのコードがすでにあるのでそちらを使ったほうが早いと思い切り出す機能はつけていません。
時間に余裕があればつけたいと思っています。
なので「画像を切り出しソフト」はあまり正しくないのですが、いいネーミングがないので暫定でこの名前にしています。

問題

正しくビルドされない

現在なぜかビルドが正しく出来ないでいます。原因は不明です。 ビルドは正しく終わるのですが起動してみると真っ白のままです。
mainWindow.openDevTools()を追記して、ビルドされたもののデバッグツールを確認すると、axiosというモジュールが見つかんないとエラーがでます。
これは素のelectron-vue(つまりvue init simulatedgreg/electron-vue applicationのテンプレートのままのこと)でも上手く行かないようです。
webpack等のフロントのビルドに関してはテンプレートを信じて使っており、あまり詳しくないので一旦諦めました。
原因がわかれば追記します。

Vuexの厳格モードでのデータの扱い

Vuexを利用してgetterで受け取ったarrayを変更すると(今回の場合は正方形の情報)Vuexの厳格モードだとエラーが発生します。
jsは普段あまりつかわないのでうまい解決策がわからず、今回は厳格モードオフで作成しました。
後ほどVuexのベストプラクティスを調べてここら辺に理解を深めたいと思っています。

感想

だらだらっと箇条書きで感想を書いていきます。

svgは初めて利用しましたがvue扱うのに全く問題はありませんでした。
ずっと昔から簡易イラレ版みたいなの欲しいけど、インクスケープ操作感が悪く使いづらかたので、時間がある時にsvgを利用して作れないかな考えています。(時間ないけど)

electron-veuは昔使った時よりさらにドキュメントやツール周りがさらに強化されているなーという印象でした。
vue周りの発展の速度がすごい早いので驚きです。

Vuexを利用する想定がある場合は早めに利用したほうがいいです。
中盤くらいまでVuexを使わないで作っており、いざVuexを使うことを考えると書き直しが発生して、余計に作業が増えてしまいました…

mustacheはシンプルが故に簡単な演算は無理みたいなので、作業的に必要になったら他のテンプレートエンジンの利用も考えてみます。 でも今はこれで問題ないです。

これできっと機械学習のデータセットを作るのが多少楽になるはず…!

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

前回、機械学習でフレンズを判定できるようになったので、次はこれを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アプリを作成したことがあったが、このように一から自分で作ったことがなかったので、色々考えが不足しているところや、他の人が担当してどうなっているのかいまいちわからないことがいくつも出てきて、制作を通して知見が広がったように思います。

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

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

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

はじめに

会社で学べること以外に自分で勉強して行かないといけないと思い、今流行りの機械学習でwebアプリを作ることに挑戦してみました。
何番煎じかわかりませんが、自分が好きな「けものフレンズ」の顔認識+判定機を作成してみることにしました。

記事公開しようと思っている矢先にネタが被った上になんかもっといい感じのやつが出ていました。。。
yokoeworld.hatenablog.com フ、フレンズによって得意なことが違うから!

ちなみに、僕は機械学習に関してはほとんどわからない状態です。
色々な記事を追ったり、たまにコードをコピペして動くのをみていた程度です。
開発にはmacを使用しました。

目的

今回は以下2つを目的に作成しました。

  • とりあえず機械学習を用いてなんらかのwebアプリを作成する
  • サーバを立てて、公開しても問題ないようにする

作業をしていく上でつまるところが出てくる(特に機械学習部分)のでそこは、動けばとして次に進むことにします。
というのも、今まで期間をかけすぎてダレてしまって最終的に完成しなかったことが何回もあり、繰り返さないためにも自分でこういったルールを設けます。

また、今まで自分一人でサーバを立てて公開したことがないので、今後のことを考えると公開できるよにセットアップできるようにすることも学習の目的です。

製作物

f:id:orange634:20170812132020p:plain

こちらに公開しています。よかったら試してみてください。

ボクニマカセテ! フレンズ解析機

現在は「サーバル」「カバン」「アライさん」「フェネック」の4種類しか判定できません。
その他はフレンズと判定されるはずです。

最終的に製作に約2ヶ月かかりました。
作成期間中、空いている時間のほとんどを作業に当てていました。
なんとか満足がいくものができてよかったです。

構成

大まかの構成は以下のようになっています。
細かい内容に関しては後で解説していきます。

  • 判定・学習:dlib、tensorflow、opencv
  • webアプリ:flask、jQuery
  • サーバ:nginx、gunicorn

機械学習

初心者にも優しそうなでかつ今回のアプリを作るのに役に立ちそうな、以下の2記事を読み込んで実行してみました。

qiita.com

bohemia.hatenablog.com

今回機械学習において、ほとんどコードを参考にしたサイトから拝借してます。
一応参考にしたコードの多くはpython2だったのでそれをpython3に直して使っています。

フレンズの顔画像収集

まず機械学習に必要な顔画像を収集していきます。
事例を調べた限りだと、大量のスクショを集める→顔を判定して画像を切り出す→画像をラベルごとに整理する、という流れで作業を行うようです。
もし顔認識がうまくできないと、手動で全部切り出していくみたいです。
なので、まず顔認識を行えるか確認していきます。

顔認識について

アニメ顔に近いとlbpcascade_animefaceというもの先人の方々の知恵が使えるのですが、残念ながらフレンズの顔は一般的なアニメ顔とは異なるようで、判定はできるものの精度はそこまで高くありませんでした。
そのため自分で分類機を作成する必要が出てきて、こちらの記事にあるようにdlibにて判定機を作成することにしました。
作成用のコードも同じ記事から拝借して使用しています。

インストール

以下の4つのライブラリをインストールする必要があります。

調べてところopencvを入れるのにanacondaを使用している例が多かったので、pyenvでanaconda3-4.0.0を入れました。
ただ、この際にanacondaのroot環境に直で入れてしまいました。一般的に複数バージョンを扱えるように環境を分けることが多いので、本当は分けたほうがいいです。
当時はそこまで気が回っていなかったのでそのままインストールしました。
opencvはそのまま入れただけでは動かないかも(メモを紛失してしまい定かではありませんが)しれないのでエラーでググって解決しましょう。
他のライブラリも全部anacondaからインストールしました。

brew等でもboostやpython-boostは入れられるのですが対象としているpythonのバージョンが異なったり、実行してもエラーが多発して、悩んだ挙句最終的に全部アンインストールしてanacondaでいいれたらすんなり入ったかからです。。。
詳しくない方はanacondaに任せるのがいいでしょう。

学習と確認

約150枚の画像から顔の位置をだいたい正方形になるようにして取ってきて、ファイルに保存、xmlファイル生成しました。
120枚を学習、30枚をチェックに使用して学習させました。
軽くチェックを行なった結果、かなりの精度で取れるのを確認しました。

また、学習に使ったのはサーバル、カバン、アライさん、フェネックの画像であったが、精度は高くないが他のフレンズの顔も検出できていました。
ただ、なぜかプリンセスのヘッドフォンの赤い丸を検出していました。。。
原因は不明ですが、目視でデータ整理する時に省くので問題はありませんでした。

顔画像の収集と水増し

次にdlibにて作成した判定装置を使って、大量のスクショから顔の画像を取り出しました。
スクショの画像は1から12話でなるべくシーンが被らないようにスクショしていきました。
この際、フレンズの顔以外のものを検出していたり、画像ファイルが破損している物があったので削除して整理しました。
集まった画像の枚数は以下の通りになります。

フレンズ 画像枚数
サーバル 208
カバン 167
アライさん 40
フェネック 42

どうしても出番が少ないアライさんとフェネックの画像数が減ってしまいました。
そこで、画像の水増しを行い6倍にします。

pillowを用いて一つの画像から

  • 元の画像より明るいもの
  • 元の画像より暗いもの
  • 元の画像を反転したもの
  • 元の画像を反転して明るくしたもの
  • 元の画像を反転して暗くしたもの

を生成した水増しすることにしました。
これでそれぞれアライさんが236枚、フェネックが248枚になり十分な量の画像が用意できました。
何枚か画像が破損しているのもがあったので枚数は6倍ぴったりではないです。

カバンの画像が他に比べ少ないですが、多分大丈夫だろうと信じて水増しはしていません。
たつきを信じろ。

tensorflowでの学習

顔の画像が集まったのでtensorflowを用いた学習に入ります。
基本的にこちらの記事の手法とコードを拝借して行いました。
インストールしたtensorflowのバージョンが参考にしたサイトのものと異なるようで所々警告やエラーが出ました。
こちらの解説にあるように対応表を確認しながら修正しました。

ちなみに、学習の何回か行なったのですがうまくいかない時もありました。
ここの原因は不明です。

学習が終わるといくつかの中間データや最終生成物が出てきます。
以下の3つのファイルを判定に用います。

model.ckpt.data-00000-of-00001
model.ckpt.index
model.ckpt.meta

バージョンの違いから参考にしたサイトの生成物が異なり、使用する際も以下のようにコードを修正するが必要があります。

# 前
saver.restore(sess, "/tmp/model.ckpt")
# 後
saver = tf.train.import_meta_graph('/tmp/model.ckpt.meta')
saver.restore(sess, "/tmp/model.ckpt")

参考 : https://stackoverflow.com/questions/41265035/tensorflow-why-there-are-3-files-after-saving-the-model
また、こちらのevalを実行する際に再帰的に判定する場合にはリセットが必要らしいので下記のリンクを参考に修正しました。
ただ、本質的に理解をしていないのでもしかしたら正しくないかもしれません。
参考 : https://stackoverflow.com/questions/41400391/tensorflow-saving-and-resoring-session-multiple-variables
元のeval.pyをdlibで判定できるように修正しました。

f:id:orange634:20170812142353p:plain

画像が排出され、ターミナルで判定結果が出ていることが確認でき正常に動いていることがわかりました。

これで機械学習部分は終わりです。

一応動くものはできましたが、コードをかけるようになるにはまだ勉強が必要だと思いました。
実際、学習させる顔画像の解像度をあげる方すらわかりません。。。
今回は動くものを作るのが目的なので、次にステップに進むことにしました。

あと、内容と関係ないのですが、長文を書くのは難しいなと痛感しました。
下手な小学生の日記みたいな「~でした。」の繰り返しで、すごく単調な文章になってしまいました。。。
ここら辺も勉強が必要なようです。

次はwebアプリの実装を行なっていこうと思います。

orange634.hatenablog.com

ブログはじめてみました。

どうもはじめまして。 某ソシャゲ会社で働く3年目のバックエンジニアです。

何か作ったらその分アウトプットしていこうと思いブログを始めることにしました。

技術系の記事に関してはQiitaの方に書いていきます。 よければこちらもご覧ください。 qiita.com

ブログには自分の作ったアプリやツールを使ってみた感想など、Qittaに書くか微妙なものはこちらに書いていきます。 なのでそこまで記事更新は多くないと思います。

主に使用する言語はpythonとswiftです。javascriptも多少かけます。 今は仕事でphpを使っています。 最近は機械学習に興味が出てきているのそこらへんに関しても記事にしていこうと思います。