こんにちは、イシイです。
最近Node.jsに触れる機会が増えてきて、Node.jsで顔認識でもやってみようかと触り始めた矢先にCloud Vision APIの画像認識のニュースが出てきてしまい、ちょっと梯子を外された感じではありますが、せっかくなのでNode.jsでやってみました。

準備

顔認識にはnode-opencvを使ってみました。また、画像合成にはGraphicsMagickとImageMagickが使えるgmを使っています。

node-opencvの取得

まずは、GitHubからnode-opencvを持ってきます。

git clone https://github.com/peterbraden/node-opencv

ライブラリのインストール

次に、MacでOpenCVを使えるように、brewでインストールします。

brew tap homebrew/science
brew install opencv

インストールの際にエラーが出たのですが、

cd /usr/local/lib
sudo ln -s ../../lib/libSystem.B.dylib libgcc_s.10.5.dylib

こんな感じで動作するようです。次に先程GitHubからクローンしてきたnode-opencvのディレクトリに移動して

npm install

でモジュールをインストールします。
これで、node-opencv内のexamplesのものは大体動くのですが、画像合成用に追加で

brew install imagemagick
brew install graphicsmagick

をしておきます。

顔認識と画像合成プログラム

MacのWebカメラから取得した画像の顔認識を行い、認識した箇所にヨンマルマルキューのロゴを被せて画像をファイル出力する、ということをしています。今回はWebカメラから取得した画像を使っていますが、アップロードしてもらった画像の顔認識を行って画像合成するような仕組みにも転用できるようなイメージで作成しました。

app.js

キーイベントをフックにして顔認識処理をするようにしているので、プログラムが実行されてWebカメラの映像がスクリーンに表示されたら、Cキーを押して認識処理を開始します。Cキーを押したタイミングのフレーム画像に対して顔認識を行うようにしていて、スクリーン上では今どういうステータスなのかをメッセージとして表示するようにしています。画像のファイル出力まで終わったら、プログラムは自動で終了します。

const logger = require('./logger');

const cv = require('../lib/opencv');

const gm = require('gm').subClass({imageMagick: true});
var capture;

const fs = require('fs');
const compFile = './tmp/logo.png';
var compWidth, compHeight;
const dstFile = './tmp/composition.png';
const tmpFile = './tmp/detect_face.jpg';

gm(compFile)
  .size((err, size) => {
    if(!err) {
      compWidth = size.width;
      compHeight = size.height;
    }
  });

const async = require('async');

const keypress = require('keypress');
keypress(process.stdin);
process.stdin.setRawMode(true);
process.stdin.resume();

var detectStart = false;
var msg = 'Press C Key';

try {

  const camera = new cv.VideoCapture(0);
  const window = new cv.NamedWindow('Video', 0)

  setInterval(() => {

    camera.read((err, img) => {

      if (err) throw err;

      if (img.size()[0] > 0 && img.size()[1] > 0){

      if (!detectStart) {

          outMsg(img, msg);

      } else {

          capture = img.copy();

          detectStart = false;
          msg = 'Detecting...';

          var opts = {};

          //detect start
          capture.detectObject(cv.FACE_CASCADE, opts, (err, faces) => {

              //detect check
              if (faces.length) {

              //draw circle according to the number of face
              faces.forEach ((item, index, array) => {
                  if (isTarget(item.width, capture.width())) {
                      capture.ellipse(item.x + item.width / 2, item.y + item.height / 2, item.width / 2, item.height / 2);
                      msg = 'Detected!';
                      logger.system.debug(array);
                  } else {
                      return;
                  }
              });

              //screen capture -> analyze capture -> composition -> file export
              async.waterfall(
                      [
                       (callback) => {//screnn capture
                           capture.save(tmpFile);
                           callback(null);
                       },
                       (callback) => {//analyze capture
                           msg = 'Composing...';

                           var steps = gm();

                           steps.in('-page', '+0+0').in(tmpFile);

                           faces.forEach ((item, index) => {
                               var compCenterX = (item.x + item.width / 2 - compWidth / 2);
                               var compCenterY = (item.y + item.height / 2 - compHeight / 2);
                               var compPos = '+' + compCenterX + '+' + compCenterY;
                               steps.in('-page', compPos).in(compFile);
                           });

                           steps
                            .mosaic()
                            .toBuffer('PNG', (err, buffer) => {
                                   callback(null, buffer);
                             });

                       },
                       (buffer, callback) => {//composition
                           msg = 'Writing...';
                           fs.writeFile(dstFile, buffer, 'binary', (err) => {
                               callback(null);
                           });
                       },
                       (callback) => {//file export
                          logger.system.debug('Composed file is ' + dstFile);
                          msg = 'Done.';
                          fs.unlink(tmpFile);
                          callback(null);
                       }
                       ],
                      (err, result) => {//exit
                          if(err) {
                            throw err;
                          }
                          process.stdin.pause();
                          process.exit();
                      });

              } else {
                msg = 'Not detected. Retry. Press C Key.';
              }

          });

      }

      window.show(img);
      window.blockingWaitKey(0, 50);

      }

    });
  },50);

} catch (e){
    logger.system.error("Camera Open Error", e)
}

//Status Message
function outMsg (img, msg) {
    //Text, xPos, yPos, font, BGR, scale, bold
    img.putText(msg, 100, 100, cv.FONT_HERSHEY_COMPLEX_SMALL, [0, 0, 255], 1, 2);
};

//Aptitude check
function isTarget (targetWidth, screenWidth) {
    if ((targetWidth == 0) || (screenWidth == 0)) {
      return false;
    } else {
      return true;
    }
};

//KeyPress Event
process.stdin.on('keypress', (ch, key) => {

    //Detect Start
    if (key.name === 'c') {
        detectStart = true;
    }

    //Process Exit
    if (key.name === 'x') {
        process.stdin.pause();
        process.exit();
    }

});

package.json

node-opencvのライブラリの他、今回のプログラム用に必要なパッケージはこんな感じです。log4jsについては必須ではないため、必要に応じて入れてください。

{
    "name": "opencv_composite"
  , "version": "0.1.0"
  , "private": true
  , "scripts": {
    "start": "node app"
  }
  , "dependencies": {
      "gm": "1.21.1",
      "async": "1.5.0",
      "keypress": "0.2.1",
      "log4js": "0.6.29"
  }
}

あとがき

合成するロゴ画像を顔のサイズに合わせてスケールさせる処理を入れていなかったり、顔認識から画像合成・出力の処理を子プロセスで処理するようにした方が良さそうだったりと、プログラムとしては実装すべきことはまだまだありそうですが、Node.jsだけでもそれなりに顔認識・画像合成はできそうです。