表情認識アプリをつくってみた


こんにちは、馬場です。

最近弊社R&Dでは脳の活動情報をコミュニケーションに活かす研究がホットですが、他にも観測できる生体反応はあります。例えば、声や表情、心拍や身振り手振りなどです。これらの情報は近年安価な機器で比較的簡単に測定ができるようになってきており、Intel はカメラの情報から手や指、顔の動きを記録するSDK(Software Development Kit)を公開しています。そこで、今回はこのSDKに含まれる表情認識機能を利用して簡単なアプリケーションをつくってみます。

Intel Perceptual Computing SDK とは

Intel Perceptual Computing SDK (以下、PerC)は、顔や音声、手や指の動作によってアプリケーションを操作するNUI(Natural User Interface)を開発するためのSDKです。手の指の認識などにはSenz3D などの深度を認識するカメラ(Depth カメラ)が必要なものの、顔の認識は通常のカメラ、同様に音声認識と合成は通常のマイクやスピーカーがあれば作成したアプリを動かしてみることができます。

このPerC、今年リリースされた最新版には表情認識機能が追加されました。これは、認識した顔ごとに、怒り、侮蔑、不快、恐れ、喜び、悲しみ、驚きの7種類の表情である度合いを推定するものです。せっかくなのでこの機能を使ってアプリを作ってみました。

アプリケーションを作る 

今回は表情認識機能をつかって、にっこりするとアフロになるアプリケーションを作ろうと思います。
ざっくりとしたプログラムの流れは以下の通りです。

  1. 表情認識機能を利用可能に設定する
  2. カメラで撮っている画像を表示する。
  3. 顔情報を取得する
  4. 表情情報を取得する
  5. 7種の表情のうち「喜び」である度合いが最も高くかつそれが0.4以上のときは、2の顔の上の位置にアフロ画像を表示する。

開発にあたり、Percに関してはこの連載を、opencvに関してはこの記事を参考、というよりはまるっとコピーしました。本当にありがとうございます。

それでは順にみていきましょう。

開発環境の構築

まずは、開発環境の作成です。今回、カメラからの顔や表情などの情報の取得はPerC、出力はOpenCVを利用しました。開発言語はC++です。これらを利用したアプリの開発環境をVisual Studio Express 2012 に用意しました。開発環境の作り方は、記事「Intel Perceptual Computing(PerC) SDKの概要と環境構築」および記事「Colorカメラを使って顔を検出する」の通りに実行しましたので、そちらを参考にしてください。

プログラム

プログラムの全体は以下になります。実行ファイルはここからダウンロードできます。少し面倒ですが、Windows のPC にPerC/ Visual C++ 再頒布可能パッケージをインストールし、解凍したフォルダ内のHappyAfro.exeを実行すれば試してみることができます。

// HappyAfro.cpp 
//

#include "stdafx.h"
#include "util_pipeline.h"
#include "opencv2\opencv.hpp"

const int NUM_PRIMARY_EMOTIONS = 7;

const int JOY_INDEX =4;

class Pipeline: public UtilPipeline {
protected:

  int Width;
  int Height; 
  cv::Mat afro ;
  PXCImage::ColorFormat colorFormat;

public:
  Pipeline()
    : UtilPipeline()
    , colorFormat( PXCImage::COLOR_FORMAT_RGB32 )
    , afro( cv::imread("afro.png", -1))
  {
     // 表情認識機能を有効にする
    EnableEmotion();
  }

  virtual bool OnNewFrame()
  {
    try {
      // カメラが映した画像情報、顔・表情情報を取得する
      auto colorFrame = QueryImage( PXCImage::IMAGE_TYPE_COLOR );
      auto emotionalFrame = QueryEmotion();

      // データを解析し表示画像を作成する
      cv::Mat colorImage =  getColorData( colorFrame );
      drawFaceDetection( colorImage, emotionalFrame );

      // 表示する
      cv::imshow( "Color Camera", colorImage );

    }
    catch ( std::exception& ex ) {
        std::cout << ex.what() << std::endl;
    }
  
    auto key = cv::waitKey( 10 );
    return key != 'q';
  }
  
  //画像データを取得する
  cv::Mat getColorData(PXCImage* colorFrame )
  {
    //画像の高さを取得する
    PXCImage::ImageInfo info;
    colorFrame->QueryInfo(&info);
    Height = info.height;
  
    // 画像データを取得する
    PXCImage::ImageData data = { 0 };
    auto sts = colorFrame->AcquireAccess( PXCImage::ACCESS_READ, colorFormat, &data );
    if ( sts < PXC_STATUS_NO_ERROR ) {
      cv::Mat colorImage(0,0,CV_8UC4);
      return colorImage;
    }
  
    Width = data.pitches[0] /4 ;
    cv::Mat colorImage( Height, Width, CV_8UC4 );
  
    // 画像データを可視化する
    memcpy( colorImage.data, data.planes[0], data.pitches[0] * Height );
  
    // 画像データを解放する
    colorFrame->ReleaseAccess( &data );
    return colorImage;
  }
  

  // 顔、表情認識データを取得する
  void drawFaceDetection( cv::Mat& colorImage, PXCEmotion* emotionDet )
  {
    //認識した顔の数を取得する
    int numFaces = 0;
    PXCEmotion::EmotionData arrData[10];
    numFaces = emotionDet->QueryNumFaces();
    for(int i=0; i < numFaces; i++){
      //顔ごとに表情情報を取得する
      emotionDet->QueryAllEmotionData(i, &arrData[0]);
  
      //どの表情の度合いが最も高いか
      int epidx= -1; pxcI32 maxscoreE = -3; pxcF32 maxscoreI = 0;
      for (int i=0;i<NUM_PRIMARY_EMOTIONS;i++) {
        if (arrData[i].evidence < maxscoreE)  continue;
        if (arrData[i].intensity < maxscoreI) continue;
        maxscoreE=arrData[i].evidence;
        maxscoreI=arrData[i].intensity;
        epidx=i;
      }
  
      //喜びの度合いが一番高い場合
      if(epidx == JOY_INDEX && maxscoreI > 0.4){
        //アフロ画像の大きさを整え、顔の上に表示する
        //アフロの大きさを、顔の幅の2.5倍、顔の高さの2倍に設定
        cv::Mat fineAfro(arrData->rectangle.h * 2,  arrData->rectangle.w * 2.5, afro.type());
        cv::resize(afro, fineAfro, fineAfro.size(), cv::INTER_CUBIC);
                  

        //アフロの透過PNG を透過させるために、アルファチャネルの処理を行う。
        cv::Mat maskMat;
        int left = arrData->rectangle.x - (arrData->rectangle.w * 0.7);
        int top = arrData->rectangle.y - (arrData->rectangle.h);
        maskMat = fineAfro.clone();
        setAlpha(maskMat);                                                            

        //加工したアフロ画像を重ねる  
        cv::Mat colorImageROI(colorImage, cv::Rect(left, top, fineAfro.cols, fineAfro.rows));
        fineAfro.copyTo(colorImageROI,maskMat);
      }
    }
  }
  
  void setAlpha(cv::Mat& srcMat){
    int width = srcMat.cols;
    int height = srcMat.rows;
       
    for(int i = 0; i < height;i++){
      for(int j = 0; j < width ;j++){
        cv::Vec4b &p = srcMat.at<cv::Vec4b>(i, j);
        if(p[3] == 255){
          p[0] = 255 - p[0];
          p[1] = 255 - p[1];
          p[2] = 255 - p[2];
          p[3] = 255 - p[3];
        }else{
          p[0] = 0;
          p[1] = 0;
          p[2] = 0;
          p[3] = 0;
        }
      }
    }
  }
  
};

//メインメソッド
int _tmain(int argc, _TCHAR* argv[])
{
  try {
    Pipeline pipeline;
    pipeline.LoopFrames();
  }
  catch ( std::exception& ex ) {
    std::cout << ex.what() << std::endl;
  }

  return 0;
}

PerC を利用した開発では、1. UtilPipelineを拡張したクラスを作成し、2. カメラの情報を取得するたびに実行されるonNewFrameメソッドを実装 3. mainメソッドでPipelineインスタンスを生成しLoopFrame()メソッドを実行することが最も手軽ですので、今回もその手順で行いました。

21-28行目のUtilPipelineを拡張したPipelineクラスのコンストラクタ内で表情認識機能をオンし、あらかじめ作成した透過PNGのアフロ画像読み込んでいます(EnableImage()など他のメソッドを実行するとなぜか動かなくなります。)

次にonNewFrame()メソッドを実装します(30-52行目)。PerC よりカメラ画像および認識した顔・表情の情報を取得します(34-35行目)。

取得した情報を表示します。取得した画像情報はgetColorData()メソッドで解析し、OpenCVのcv::Mat インスタンスを作成しています(54-78行目)。この処理はほぼ、記事「Colorカメラを使って顔を検出する」に記載の通りですが、1点。表示画像の大きさ(Height とWidth) はPerCから取得した情報から計算しています。

そして、このアプリの肝である表情認識です。

認識された各顔の表情情報を取得するには、QueryAllEmotionData()メソッドを実行します(90行目)。
arrDataに全7つ表情の信頼度(この推論がどれだけ確からしいか)と強度(その感情の強さ)が取得できたので、そこから最も信頼度が高く強度が強い表情を見つけます(93-110行目)。

今回のアプリでは喜んでいるときにアフロにするので、信頼度が高い表情のインデックスが喜びの4である場合、かつ強度が0.4 を超える場合に、アフロ画像をもとのカメラ画像に重ねて表示します(103-117行目)。顔の位置や大きさはarrData->rectangleでわかるので、その情報からアフロの表示位置や表示の大きさを算出しています。また透過PNGの表示方法に関しては記事「OpenCV 2.0 でマスク処理してみた。」を参考にしました。

できました。結果。うかない気分のときでも。

happy_afro1

笑うとアフロ!!

happy_afro2

息子もアフロ!!!

happy_afro3

できました。

感想:PerC の表情認識機能は使えるのか?

作成したアプリですが、アフロが枠外にはみ出る場合はエラーになったり(ウィンドウの真ん中あたりでアフロ余白を残してにっこりしないといけない)、アフロ画像のクオリティが低かったりと細かい部分が実装しきれていませんが、そこは私のC++やOpenCVの力のなさによるもので、PerC自体 はシンプルで使いやすいと感じました。ただ、表情認識機能はまだPreview機能で正式機能ではいため、ドキュメントがありません。サンプルだけを頼りに、APIを解読し実装するのに少し苦労しました。

実は、もともとは息子の要望で「怒った顔をしたらスーパーサイアサイヤ人になる」アプリケーションをつくろうと思っていました。ただその時はいくら息子に「怒れー」といっても、私もカメラの前で怒りの表情を作ろうとしても、PerC が「怒っている」と認識する表情をつくることができませんでした。そこで一番多く認識された「喜び」の表情をきっかけにするアプリケーション開発に切り替えたのです。ところが、このアプリ開発中に何をやってもうまく動かずかなりイライラしていた時間があったのですが、そのとき表情認識アプリを起動したらPerC が「怒り」の表情をしているというではありませんか。。認識結果は結構信頼できるのではないか、と思った瞬間です。

今年は、他にもウェアラブル機器なども数多く発売されています。引き続きこの分野も注視していきたいと思います。


This entry was posted in 技術. Bookmark the permalink.