RICOH THETA + OpenCV で 360° 顔検出

## はじめに

こんにちは、リコーの @yomura_ です。

今回は RICOH THETA V に顔検出処理をさせてみました。
また、せっかくの 360° カメラなので、顔を検出した方向に応じて内蔵 LED の点灯色を変えるようにしてみました。

<img width="640" alt="image1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381074/4477ecd3-fffa-28d3-17d7-cc59ed423827.png">


## RICOH THETA プラグインについて

THETA プラグインをご存じない方はこちらをご覧ください。
興味を持たれた方は [Twitter](https://twitter.com/thetaplugin) のフォローと [THETA プラグイン開発コミュニティ(Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU) への参加もよろしくお願いします｡

## 準備

### OpenCV 環境の準備

顔検出には画像処理ライブラリ OpenCV のバージョン 3.4.5 を使用しました。

THETA の中で OpenCV を動かすための環境は [THETAの中でOpenCVを動かす【プレビューフレーム取得編】](https://qiita.com/roohii_3/items/f96918cdb52fd84e1cea) の記事で詳しく紹介されています。今回はそちらの記事の環境とサンプルコードを使用し、顔検出のための処理を追加しました。

顔検出には `Cascade Classifier` クラスを使いました。
このクラスは映像ストリーム内からオブジェクトを検出するためのクラスです。

顔や目などの特徴の検出には haar cascades 分類器を使いました。
`Cascade Classifier` による顔検出の実装については [公式チュートリアル](https://docs.opencv.org/3.4/db/d28/tutorial_cascade_classifier.html) に分かりやすい例とともに紹介されています。

### カスケードファイルの用意

`Cascade Classifier` を使うには、カスケードファイルと呼ばれる検出対象の特徴がまとめて記述されているファイルを読み込む必要があります。

カスケードファイルは OpenCV の公式 SDK の中に含まれているのでこれを使います。以下の手順でダウンロードして配置しました。

1. [OpenCV 3.4.5](https://github.com/opencv/opencv/releases/tag/3.4.5) から `opencv-3.4.5-android-sdk.zip` をダウンロードして解凍する
2. `/OpenCV-android-sdk/sdk/etc/haarcascades` 以下にある XML ファイルをサンプルコードの `app/src/main/assets` 以下に配置する

これで準備完了です。

## 実装

### 解像度とフレームレートの設定

サンプルコードの`MainActivity.java` には `onCameraFrame()` メソッドが用意されており、プレビュー映像のフレーム毎の処理を書くことができます。

解像度とフレームレートを高くすると顔検出の計算量が大きくなり、動作が不安定になってしまうため、以下のように設定しました。

- 解像度: `640x320 (検出範囲は640x160)`
- フレームレート: `4fps`

フレームレートを指定するには `ThetaView.java` に設定を追加します。

```java:ThetaView.java
...
private boolean mStopThread;
// 追加
private int fps = 4;

protected boolean initializeCamera(int width, int height) {
    ...
    params.setPreviewSize((int) frameSize.width, (int) frameSize.height);
    // 追加
    params.set("RIC_SHOOTING_MODE", Constants.RIC_MOVIE_PREVIEW);
    // 追加
    params.setPreviewFpsRange(fps * 1000, fps * 1000);

    mCamera.setParameters(params);
    ...
}
```

解像度については、サンプルでもともと `640x320` となっています。
設定値は `Constants.java` で定義されています。

```java:Constans.java
// the preview size according to the "RicMoviePreviewXXXX" parameter
public static final int PREVIEW_SIZE_WIDTH = 640;
public static final int PREVIEW_SIZE_HEIGHT = 320;
```

### 分類器の生成

カスケードファイルは 顔, 目, 笑顔, 上半身など 検出対象毎にファイルが分かれています。
今回は正面の顔用の `haarcascade_frontalface_default.xml` を使います。

`onCreate()` でカスケードファイルのパスを取得し、`onCameraFrame()` で一度だけ `CascadeClassifier` のインスタンスを生成します。 (`onCreate()` で生成すると実行時にエラーが発生します)

```java:MainActivity.java
private String filepath;
CascadeClassifier faceDetector;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // カスケードファイルのパスを取得
    filepath = getApplicationContext().getFilesDir().getAbsolutePath()
        + "/haarcascades/haarcascade_frontalface_default.xml";
    ...
}

public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
    if (faceDetector == null) {
        // 顔検出のための分類器を生成
        faceDetector = new CascadeClassifier(filepath);
    }
    ...
}
```

### 顔の検出

各プレビューフレームで呼ばれるメソッドに顔検出の処理を追加します。

顔検出の処理自体は `detectMultiScale` メソッドで行うことができます。顔が検出されると引数に渡した `faces` に顔の座標が格納されます。これを使って検出した顔の周りに楕円を描画してみます。

```java:MainActivity.java
// プレビューフレームごとに呼ばれるメソッド
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
    if (faceDetector == null) {
        // 顔検出のための分類器を生成
        faceDetector = new CascadeClassifier(filepath);
    }
    // 検出対象マトリクスを準備
    // 計算量を減らすため画像のrowを全体の半分に絞る
    Mat targetMat = inputFrame.gray().rowRange(80, 240);
    // 検出結果(矩形)の格納先を準備
    MatOfRect faces = new MatOfRect();
    // 顔検出を実行
    faceDetector.detectMultiScale(targetMat, faces);

    mOutputFrame = inputFrame.gray();
    for(Rect face : faces.toList()) {
        Point center = new Point(face.x+face.width/2, face.y+face.height/2);
        // 顔の周りに円を描画
        Imgproc.ellipse(mOutputFrame, center, new Size(face.width/2, face.height/2 ), 0, 0, 360, new Scalar( 255, 0, 255 ), 4 );
    }
    return mOutputFrame;
}
```

実際に試してみたところ、検出精度はあまり良くありませんが、自分の顔を検出できました。
カメラと自分の距離が遠すぎると顔として検出されず、かと言って近すぎると大きく歪んでしまうので、ある程度の距離を保つ必要がありました。

<img width="640" alt="image3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381074/425aadd9-52ff-b4a2-67d3-50b1cdf2c484.png">


### LED の点灯

次に、顔を検出した場合の LED の点灯/消灯の処理を追加します。

顔検出処理を行い、検出できなかった場合は LED を消灯、顔を複数検出した場合は `WHITE` に点灯とします。

```java
faceDetector.detectMultiScale(targetMat, faces);

if (faces.empty()) {
    notificationLedHide(LedTarget.LED3);
    return mOutputFrame;
}

// show white LED when multiple faces are detected
if (faces.toList().size() > 1) {
    notificationLed3Show(LedColor.WHITE);
    return mOutputFrame;
}
```

顔を一つだけ検出した場合は、顔を検出した水平方向の座標に応じて点灯する LED の色を変えます。

図のように、360° 画像の両端のつなぎ目は顔検出できないのでそこを除いた範囲を 6 等分してそれぞれ `RED` , `YELLOW` , `GREEEN` , `CYAN` , `BLUE` , `MAGENTA`に割り当てます。

<img width="640" alt="image2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381074/02e848b9-405b-2db8-e400-04277ca23150.png">

今回は画像の横幅を 640px、検出可能範囲を 600px として、検出位置の x 座標を 100 で割ったときの商によって点灯色を変えています。

まとめると`onCameraFrame()`の実装は以下のようになります。

```java:MainActivity.java
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
    if (faceDetector == null) {
        Log.i(TAG, "detector is not found");
        faceDetector = new CascadeClassifier(filename);
    }
    mOutputFrame = inputFrame.gray();

    // detect faces
    Mat targetMat = inputFrame.gray().rowRange(80, 240);
    MatOfRect faces = new MatOfRect();
    faceDetector.detectMultiScale(targetMat, faces);
    if (faces.empty()) {
        notificationLedHide(LedTarget.LED3);
        return mOutputFrame;
    }

    // show white LED when multiple faces are detected
    if (faces.toList().size() > 1) {
        notificationLed3Show(LedColor.WHITE);
        return mOutputFrame;
    }

    // change LED color by angle
    int faceArea = (int)(Math.floor(faces.toList().get(0).x / 100));
    switch (faceArea) {
        case 0:
            notificationLed3Show(LedColor.RED);
            break;
        case 1:
            notificationLed3Show(LedColor.YELLOW);
            break;
        case 2:
            notificationLed3Show(LedColor.GREEN);
            break;
        case 3:
            notificationLed3Show(LedColor.CYAN);
            break;
        case 4:
            notificationLed3Show(LedColor.BLUE);
            break;
        case 5:
            notificationLed3Show(LedColor.MAGENTA);
            break;
    }

    // draw ellipse
    for(Rect face : faces.toList()) {
        Point center = new Point(face.x+face.width/2, 80+face.y+face.height/2);
        Imgproc.ellipse(mOutputFrame, center, new Size(face.width/2, face.height/2 ), 0, 0, 360, new Scalar( 255, 0, 255 ), 4 );
    }

    return mOutputFrame;
}
```

## 結果

プラグインを起動し、顔のイラストの前に THETA を置くと、イラストの顔を検出しました。
また、顔の位置を変えると顔検出位置が代わり、LED の点灯色が変わりました。

下の GIF は検出していないときは LED が OFF、左後方で顔を検出したときは `GREEN` で点灯、右後方で顔を検出したときは `CYAN` で点灯している様子です。

(※ 顔の代わりにいらすとやの[吉田兼好の似顔絵イラスト](https://www.irasutoya.com/2015/10/blog-post_733.html)を使いました)

![image4_v2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/381074/94a59da4-1d0e-9148-d286-719689b88f00.gif)


## おわりに

以下の条件で THETA 上で顔検出処理を行うことができました。
また、顔の検出数と検出位置によって LED の点灯色を変えることができました。

- 画像の形式: Equirectangular 形式
- 画像サイズ: 640x320 (検出範囲は 640x160)
- フレームレート: 4fps

今回はお試し的な記事でしたが、複数人が等間隔に並んだことを検出したらシャッターを切るなど実用的なプラグインが作れると楽しそうです。

## 参考

- [OpenCV: Cascade Classifier](https://docs.opencv.org/3.4.5/db/d28/tutorial_cascade_classifier.html)
- [THETA の中で OpenCV を動かす【プレビューフレーム取得編】 - Qiita](https://qiita.com/roohii_3/items/f96918cdb52fd84e1cea)
- [THETA の中で OpenCV を動かす【プレビューフレーム応用編】 - Qiita](https://qiita.com/roohii_3/items/75c3e73cee0ed6d53b4b)
- [Android で OpenCV 3.2 を使って顔検出をする](https://blogs.osdn.jp/2017/02/10/opencv.html)