OpenCVを使って、簡単顔認識Androidアプリを作ってみた

# はじめに
顔認識技術そのものは古くから存在していますが、最近ではSnowなど顔認識を利用したスマホアプリも登場し、より身近な存在になってきています。
以前、どのようにして実現しているのか？という興味もあり、自分でも顔写真を撮影したら、写真内の人の顔が別のものに置き換わるアプリを作ってみたい！と思い立ちました。
しかし、実際に顔認識や画像解析などネットで調べてみますと、機械学習をメインで取り上げられているものが多く、あまりの難しさに心が折れていました・・・
ただ今回AndroidアプリにてOpenCVを使って顔認識機能部分を作ってみたところ、想像以上に簡単にできたので、ご紹介したいと思います。
実際に作ってみてわかったことは、OpenCVを使って画像から顔の認識をさせることよりも、むしろカメラ機能の実装の方が大変ということでした。

# そもそもOpenCVってなに？
OpenCV（Open Source Computer Vision Library）は、動画や画像を処理するためのオープンソースライブラリです。OpenCVについては[この記事](http://www.buildinsider.net/small/opencv/001)が参考になりました。

# やりたかったこと
Androidのカメラ機能で撮影した写真に顔が存在するかをOpenCVを使って判定する
※今回紹介するのは、撮影後のBitmapの画像データに対して顔認識し、結果を取得する部分だけになります。結果（顔の座標、サイズ）が取得できれば、あとは元の画像に別の画像を載せたりするだけになります。
実際に独自に機械学習を行い、OpenCVを利用して顔認証を行う場合はおおよそ以下のような流れになります。
1. 機械学習を行い特徴量を取得する
2. Androidアプリにてカメラ機能を実装
3. カメラにて撮影した画像データの顔認識機能を実装←今回紹介する部分
4. 認識結果（顔の大きさ、位置情報）を利用して、画像の加工、判定

# OpenCVで顔認識機能を実現するにあたって使うもの
OpenCVにはC、C++、Python、Java用のインターフェースが用意されています。
スマホアプリに組み込む場合は、iOS、Android用に用意されているSDKがあり、このSDKをプロジェクトにインポートすることで、簡単にOpenCVを使う環境が用意できます。
また顔認識においては、「顔」を判定するために機械学習した結果が必要ですが、OpenCVがすでに用意しているものがいくつかあります。この機械学習した結果※は、SDKパッケージに同封されています。
※XMLファイル形式で同封されています。
同封されているxmlファイルの一覧は以下の記事にて紹介されています。
- https://qiita.com/hitomatagi/items/04b1b26c1bc2e8081427

# 利用環境・ライブラリ
- Android 6.0
- OpenCV 3.2.0
    - OpenCVのSDKは、以下のサイトよりダウンロードできます。
        - http://opencv.org/releases.html
 ![OpenCV_HP.png](https://qiita-image-store.s3.amazonaws.com/0/211031/5046ead1-b634-71f0-64a2-ad689be522bb.png)

# 処理の流れ
### OpenCVライブラリのロード
- OpenCVを利用するクラスに以下のコードを記載し、Android StudioにインポートしたOpenCVライブラリをロードします。
- Android Studioへのインポート方法は以下のサイトを参考
    - https://qiita.com/kodai100/items/6c9e8a34d0714913c017

```java:FaceClassifier.java

  // OpenCVライブラリのロード
  static {
    System.loadLibrary("opencv_java3");　
  }
```

### 画像データをMat形式に変換
- OpenCVのMat形式に画像データを変換します。
- サンプルでは、Bitmap形式からMat形式に変換しました。

```java:FaceClassifier.java

    // 画像データを変換（BitmapのMatファイル変換）
    Mat matImg = new Mat();
    Utils.bitmapToMat(image,matImg); 
```

### 顔認識を行うCascadeClassifierインスタンスの生成
- 顔のカスケードである「haarcascade_frontalface_alt.xml」を利用します。SDKダウンロード時に特徴量ファイルもダウンロードされるため、対象ファイルをAndroidプロジェクトのres/raw配下に格納しておきます。
- ちょっとここではまりました・・・
    - CascadeClassifierのコンストラクタはInputStreamを引数にできないため、xmlファイルのパスを引数にする必要があります。なので「一度xmlファイルを読み取り、FileOutputStreamで出力し、出力したファイルのパスを取得する。」という手順を踏む必要があります。

```java:FaceClassifier.java

    // 顔認識を行うカスケード分類器インスタンスの生成（一度ファイルを書き出してファイルのパスを取得する）
    // 一度raw配下に格納されたxmlファイルを取得
    InputStream inStream = this.activity.getResources().openRawResource(R.raw.haarcascade_frontalface_alt);
    File cascadeDir = this.activity.getDir("cascade", Context.MODE_PRIVATE);
    File cascadeFile = new File(cascadeDir, "haarcascade_frontalface_alt.xml");
    // 取得したxmlファイルを特定ディレクトリに出力
    FileOutputStream outStream = new FileOutputStream(cascadeFile);
    byte[] buf = new byte[2048];
    int rdBytes;
    while ((rdBytes = inStream.read(buf)) != -1) {
      outStream.write(buf, 0, rdBytes);
    }
    outStream.close();
    inStream.close();
    // 出力したxmlファイルのパスをCascadeClassifierの引数にする
    CascadeClassifier faceDetetcor = new CascadeClassifier(cascadeFile.getAbsolutePath());
    // CascadeClassifierインスタンスができたら出力したファイルはいらないので削除
    if (faceDetetcor.empty()) {
      faceDetetcor = null;
    } else {
      cascadeDir.delete();
      cascadeFile.delete();
    }
```

### CascadeClassifierに画像データを与え顔認識
- CascadeClassifier.detectMultiScaleメソッドには、Mat型の画像データとMatOfRectインスタンスを渡します。認証結果はMatOfRectに格納されます。

```java:FaceClassifier.java

    // カスケード分類器に画像データを与え顔認識
    MatOfRect faceRects = new MatOfRect();
    faceDetetcor.detectMultiScale(matImg, faceRects);
```

### MatOfRectインスタンスに格納された結果から、画像内で認識された顔の数や、顔の位置を取得
- MatOfRectインスタンスはRect型に変更できます。
- サンプルのソースとしてはログ出力させているだけですが、画像内の顔を別のものに置き換える場合は、顔の座標、顔の大きさに合わせて画像を重ねることで実現できます。

```java:FaceClassifier.java

    // 顔認識の結果の確認
    Log.i(TAG ,"認識された顔の数:" + faceRects.toArray().length); 
    if (faceRects.toArray().length > 0) {
      for (Rect face : faceRects.toArray()) {
        Log.i(TAG ,"顔の縦幅" + face.height);
        Log.i(TAG ,"顔の横幅" + face.width);
        Log.i(TAG ,"顔の位置（Y座標）" + face.y);
        Log.i(TAG ,"顔の位置（X座標）" + face.x);
      }
      return true;
    } else {
      Log.i(TAG ,"顔が検出されませんでした");
      return false;
    }
```

 # ソースコード

```java:FaceClassifier.java

import org.opencv.android.Utils;
import org.opencv.core.Mat;
import org.opencv.core.MatOfRect;
import org.opencv.core.Rect;
import org.opencv.objdetect.CascadeClassifier;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class FaceClassifier {
  // OpenCVライブラリのロード
  static {
    System.loadLibrary("opencv_java3");　
  }
  private Activity activity;
  public FaceClassifier (Activity activity) {
    this.activity = activity;
  }
  public boolean checkFaceExistence (Bitmap image) throws IOException {
    // 画像データを変換（BitmapのMatファイル変換）
    Mat matImg = new Mat();
    Utils.bitmapToMat(image,matImg); 
    // 顔認識を行うカスケード分類器インスタンスの生成（一度ファイルを書き出してファイルのパスを取得する）
    // 一度raw配下に格納されたxmlファイルを取得
    InputStream inStream = this.activity.getResources().openRawResource(R.raw.haarcascade_frontalface_alt);
    File cascadeDir = this.activity.getDir("cascade", Context.MODE_PRIVATE);
    File cascadeFile = new File(cascadeDir, "haarcascade_frontalface_alt.xml");
    // 取得したxmlファイルを特定ディレクトリに出力
    FileOutputStream outStream = new FileOutputStream(cascadeFile);
    byte[] buf = new byte[2048];
    int rdBytes;
    while ((rdBytes = inStream.read(buf)) != -1) {
      outStream.write(buf, 0, rdBytes);
    }
    outStream.close();
    inStream.close();
    // 出力したxmlファイルのパスをCascadeClassifierの引数にする
    CascadeClassifier faceDetetcor = new CascadeClassifier(cascadeFile.getAbsolutePath());
    // CascadeClassifierインスタンスができたら出力したファイルはいらないので削除
    if (faceDetetcor.empty()) {
      faceDetetcor = null;
    } else {
      cascadeDir.delete();
      cascadeFile.delete();
    }
    // カスケード分類器に画像データを与え顔認識
    MatOfRect faceRects = new MatOfRect();
    faceDetetcor.detectMultiScale(matImg, faceRects);
    // 顔認識の結果の確認
    Log.i(TAG ,"認識された顔の数:" + faceRects.toArray().length); 
    if (faceRects.toArray().length > 0) {
      for (Rect face : faceRects.toArray()) {
        Log.i(TAG ,"顔の縦幅" + face.height);
        Log.i(TAG ,"顔の横幅" + face.width);
        Log.i(TAG ,"顔の位置（Y座標）" + face.y);
        Log.i(TAG ,"顔の位置（X座標）" + face.x);
      }
      return true;
    } else {
      Log.i(TAG ,"顔が検出されませんでした");
      return false;
    }
  }
}

```

# まとめ
OpenCVのSDKがかなり使いやすくなっているので、実際に作ってみると思ったよりもすんなりできます。
もしも「顔以外のものの認識をしたい」という場合はOpenCVを使った機械学習を調べる必要がありますが、既にOpenCV用の機械学習した結果（XMLファイル）は色々と公開されているものがあるので一度チェックすることをお勧めします。
私と同じように顔認証技術を使ったアプリを作ってみたいという方は、是非OpenCVを使って見てください。