はじめに
こんにちは、リコーの @yomura_ です。
今回は RICOH THETA V に顔検出処理をさせてみました。
また、せっかくの 360° カメラなので、顔を検出した方向に応じて内蔵 LED の点灯色を変えるようにしてみました。
RICOH THETA プラグインについて
THETA プラグインをご存じない方はこちらをご覧ください。
興味を持たれた方は Twitter のフォローと THETA プラグイン開発コミュニティ(Slack) への参加もよろしくお願いします。
準備
OpenCV 環境の準備
顔検出には画像処理ライブラリ OpenCV のバージョン 3.4.5 を使用しました。
THETA の中で OpenCV を動かすための環境は THETAの中でOpenCVを動かす【プレビューフレーム取得編】 の記事で詳しく紹介されています。今回はそちらの記事の環境とサンプルコードを使用し、顔検出のための処理を追加しました。
顔検出には Cascade Classifier
クラスを使いました。
このクラスは映像ストリーム内からオブジェクトを検出するためのクラスです。
顔や目などの特徴の検出には haar cascades 分類器を使いました。
Cascade Classifier
による顔検出の実装については 公式チュートリアル に分かりやすい例とともに紹介されています。
カスケードファイルの用意
Cascade Classifier
を使うには、カスケードファイルと呼ばれる検出対象の特徴がまとめて記述されているファイルを読み込む必要があります。
カスケードファイルは OpenCV の公式 SDK の中に含まれているのでこれを使います。以下の手順でダウンロードして配置しました。
-
OpenCV 3.4.5 から
opencv-3.4.5-android-sdk.zip
をダウンロードして解凍する -
/OpenCV-android-sdk/sdk/etc/haarcascades
以下にある XML ファイルをサンプルコードのapp/src/main/assets
以下に配置する
これで準備完了です。
実装
解像度とフレームレートの設定
サンプルコードのMainActivity.java
には onCameraFrame()
メソッドが用意されており、プレビュー映像のフレーム毎の処理を書くことができます。
解像度とフレームレートを高くすると顔検出の計算量が大きくなり、動作が不安定になってしまうため、以下のように設定しました。
- 解像度:
640x320 (検出範囲は640x160)
- フレームレート:
4fps
フレームレートを指定するには 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
で定義されています。
// 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()
で生成すると実行時にエラーが発生します)
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
に顔の座標が格納されます。これを使って検出した顔の周りに楕円を描画してみます。
// プレビューフレームごとに呼ばれるメソッド
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;
}
実際に試してみたところ、検出精度はあまり良くありませんが、自分の顔を検出できました。
カメラと自分の距離が遠すぎると顔として検出されず、かと言って近すぎると大きく歪んでしまうので、ある程度の距離を保つ必要がありました。
LED の点灯
次に、顔を検出した場合の LED の点灯/消灯の処理を追加します。
顔検出処理を行い、検出できなかった場合は LED を消灯、顔を複数検出した場合は WHITE
に点灯とします。
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
に割り当てます。
今回は画像の横幅を 640px、検出可能範囲を 600px として、検出位置の x 座標を 100 で割ったときの商によって点灯色を変えています。
まとめるとonCameraFrame()
の実装は以下のようになります。
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
で点灯している様子です。
(※ 顔の代わりにいらすとやの吉田兼好の似顔絵イラストを使いました)
おわりに
以下の条件で THETA 上で顔検出処理を行うことができました。
また、顔の検出数と検出位置によって LED の点灯色を変えることができました。
- 画像の形式: Equirectangular 形式
- 画像サイズ: 640x320 (検出範囲は 640x160)
- フレームレート: 4fps
今回はお試し的な記事でしたが、複数人が等間隔に並んだことを検出したらシャッターを切るなど実用的なプラグインが作れると楽しそうです。