日本コンピュータビジョン株式会社 (JCV) よりご提供いただいた 顔認証 SDK を使用して、顔認証アプリを実装し、その機能をご紹介します。
この SDK の概要は「前々回の記事」、今回作成するアプリの前段となる「顔情報登録アプリ」の実装は「前回の記事」をご覧ください。
顔認証アプリを作成する
今回作成する「顔認証アプリ」は、ウェブカメラで捉えた人物の顔と、事前に「顔情報登録アプリ」で取得しておいた人物の顔とを比較し、ウェブカメラに映り込んだのが誰なのかを識別します。
↑ 「顔認証アプリ」の完成イメージはこんな感じです。
あわせて、SDK が提供する 画質チェック 機能についても、チェック項目や精度を確認していきます。
プロジェクト作成 〜 初期処理 〜 ウェブカメラのキャプチャ
それでは実装解説です。
- Gradle を使用した Java プロジェクトの雛形作成
- アプリ起動時の初期処理 : OpenCV ライブラリの読み込み、SDK のライセンス認証、JavaFX ウィンドウの表示
- ウェブカメラのキャプチャ処理 :
Executors
によるキャプチャ処理のループ実行
などは、前回の「顔情報登録アプリ」と同様なので割愛します。
ここまでで、アプリを起動するとウェブカメラの映像が JavaFX ウィンドウにプレビューされている状態とします。
映像から顔をトラッキングする
ウェブカメラでキャプチャした映像から、人物の顔をトラッキング (追跡) するには、 track()
メソッドを使用します。
// トラッキング処理を行う
final TrackResults trackResults = faceProLibrary.track(trackerHandle.getHandle(), stidImage, FaceproLibrary.STID_ORIENTATION_UP, System.currentTimeMillis());
// トラッキング結果から、特定した顔情報のリストを取得する
final List<TargetResult> targetResults = trackResults.getDetectionResults();
トラッキング処理用の「ハンドル」と呼ばれるオブジェクト (trackerHandle
) が必要になるのは、前回の「顔情報登録アプリ」と同様です。
OpenCV がキャプチャした Mat イメージ (ウェブカメラの映像のコマ画像) を、SDK の独自形式である StidImage
に変換して、引数で渡しています。それと一緒に、システム時刻を渡しています。どうやらこの情報のセットで、前回のコマ画像との差分を計算し、顔のトラッキングを実現しているようです。ということで、「映像データ」をそのまま処理しているのではなく、「フレーム (コマ)」ごとに処理しているわけですね。
track()
メソッドで同時に捉えられる人物の顔は、最大で32人分までだそうです。32人までは、一度にカメラに映り込んでも個別に認識できるということですね。カメラを設置した会場を広く映して認証できそうです。
画質チェック
ここまでで、「画像中に顔が映り込んでいる」ことは確認できました。ここで、顔認証処理に移る前に、SDK が提供する 画質チェック機能 を確認してみましょう。
// 顔情報のリストから1人分の「顔特徴点データ」を取り出す
final TargetResult targetResult = targetResults.get(index);
final Landmarks landmarks = targetResult.getLandmarks();
// 画質チェックを行う
final FaceQuality faceQuality = faceProLibrary.imageQualityAssessment(stidImage, landmarks, FaceproLibrary.STID_FACEPRO_QUALITY_FLAG_ALL);
このようにすると、次のような情報が取得できます。
- 顔が画像の中央からどのくらい離れているか
- 顔の大きさ
- 顔の前に遮蔽物がある度合い (マスクをしているなど、顔の一部が隠れている場合の度合いが分かる)
- 画像の明るさ
- 画像の鮮明さ (シャープネス)
- 人物がどのくらい口を開けているか
- 人物がフレームから見切れている度合い
こうした情報が取得できるので、映りが不鮮明な場合は顔認証処理を実行しないようハンドリングできます。
「顔情報登録アプリ」のように、一人の人物をハッキリと捉えた時だけ処理したい場合は、
- 顔が画像の中央に位置していること
- 顔が一定以上大きく映り込んでいること
といったチェックに利用しても良いですし、認証精度を極限まで高める必要がある場合は
- 顔の前に遮蔽物がないこと = マスクをしている人にはマスクを取ってもらうなど
- 人物がフレームから見切れていないこと
といったチェックをしても良いでしょう。
↑ マスクで顔が隠れている場合は NG (顔認証を行わない) とする設定例。
顔の傾きチェック
画質チェック処理の一環で、顔の傾きが確認できます。
final Pose pose = faceProLibrary.calcposeGetHeadPose(calcPoseHandle.getHandle(), landmarks);
一人の顔の特徴点データ (landmarks
) のみで、顔がどのくらい傾いているか、3軸 (ロール・ヨー・ピッチ) の度数が取得できます。
この SDK は、顔が斜めに映り込んだりしても高い精度で認証できますが、「真正面を向いた人物しか認証したくない」といった場合は、顔の傾き度合いをチェックしてハンドリングすると良いでしょう。
↑ 斜めを向いた人は NG (顔認証を行わない) とする設定例。
生体検出チェック
さらに、捉えた人物が「生体」かどうかをチェックするメソッドもあります。チェック処理は以下の1行で実現できます。
final FloatResult floatResult = faceProLibrary.singlelivenessDetect(singleLivenessHandle.getHandle(), stidImage, landmarks);
例えばスマホで表示した人物の画像や動画だったり、ポスターをウェブカメラにかざしたりした場合は、このメソッドでの判定結果が 1.00
となり、「確実に 非生体 である」と見なされます。
↑ 非生体の場合は NG (顔認証を行わない) とする設定例
カメラの設置場所に応じて、背景に映り込むポスターを誤認識しないようにするとか、成りすましを防いだりするために使用できます。
こうしたチェックメソッドを利用し、その結果の値を見て、次の「顔認証」メソッドを実行するかどうかを決めるわけです。
顔認証を行う
いよいよ顔認証です。このメソッドで、「映り込んだ人物が誰なのか」を判別します。
// ウェブカメラで捉えた映像から、一人分の顔特徴データを取得する
final StringResult stringResult = faceProLibrary.getFeature(featureExtractionHandle.getHandle(), stidImage, landmarks);
final String targetFeature = stringResult.getString();
// JSON ファイルから「顔情報」文字列の配列を生成する (gson 使用・詳細は割愛します)
final String[] features = this.getFeaturesFromJson("./features.json");
// 顔認証 (1:N 検索) を行う
final SearchResult searchResult = faceProLibrary.searchFaceFromList(
featureComparisonHandle.getHandle(),
features,
features.length,
targetFeature,
1,
targetFeature.length()
);
// 最も類似した人物の類似度を取得する
final float mostSimilarScore = searchResult.getTopScores()[0];
// 「最も類似した人物」が、配列 features の何番目のデータであるか、添字を取得する
final int mostSimilarIndex = searchResult.getTopIdxs()[0] - 1;
// 「最も類似した人物の添字」を基に、JSON ファイルからその人物の名前情報を取得する
final String mostSimilarName = this.getNameFromJson("./features.json", mostSimilarIndex);
ウェブカメラが捉えた一人の人物につき、JSON ファイルに蓄えた「顔情報」のどれに類似するか、という 1:N 検索を行っているのが searchFaceFromList()
メソッドです。引数が多いですが、要するに「検索したい1人の情報 vs. 検索対象の N 人の情報」を渡します。
なお、前述の各種チェック機能は必ずしも実行する必要はありません。最初の track()
メソッドで「顔らしきもの」が特定できていれば、映りが悪くても、非生体でも、強引に 1:N 検索を行います。その代わり、「どのくらい似ているか」という 類似度 の精度は若干落ちることがあります。
顔情報を蓄えていた JSON ファイルは、次のように作っていました。
{
"features": [
{
"name": "Person 1",
"feature": "【Person 1 の顔特徴…】",
"note": "備考 1"
},
{
"name": "Person 2",
"feature": "【Person 2 の顔特徴…】",
"note": "備考 2"
}
……
]
}
配列 features
は、このデータを次のような配列に変換しています。
// ベタ書きした場合のイメージ
final String[] features = { "【Person 1 の顔特徴…】", "【Person 2 の顔特徴…】" };
これを searchFaceFromList()
メソッドに渡すと、2種類のデータが受け取れます。
getTopScores()
は、類似度の float 値を配列で保持しています。最も類似している人物から順に並べられています。getTopIdxs()
は、getTopScores()
の配列の順序と対応していて、その類似度の人物が配列features
のどこに位置するのか、が分かります。
例えば「Person 2」の人物がウェブカメラに映り込んだ場合は、
getTopScores()[0]
の値は0.96
など (96% 類似している、といった類似度の値)getTopIdxs()[0]
の値は2
となっており、マイナス 1 することで、features
の添字を示します- つまり、
features[1]
の人物 = 「Person 2」が 96% 類似している
というように情報が取得できます。String の配列で扱うので若干分かりづらいですね…。
そしてこのままだと、 features[1]
には Base64 文字列しか入っていません。それが「誰なのか」という、氏名などの情報を得るには、取得した添字を利用して自分で取得する必要があります。
先程の、ベタ書きでコーディングした例でお見せしましょう。
// 顔情報をベタ書きしたイメージ
final String[] features = { "【Person 1 の顔特徴…】", "【Person 2 の顔特徴…】" };
// ↑ に対応する形で「氏名」を配列にベタ書きしたイメージ
final String[] names = { "Person 1", "Person 2" };
// 顔認証 (1:N 検索) を行う
final SearchResult searchResult = faceProLibrary.searchFaceFromList(……);
// 「最も類似した人物」が、配列 features の何番目のデータであるか、添字を取得する
final int mostSimilarIndex = searchResult.getTopIdxs()[0] - 1;
// 「最も類似した人物の名前」を取得する
final String mostSimilarName = names[mostSimilarIndex];
// → 「Person 2」という氏名が取得できる
…このように実装します。
配列の添字でアレコレするので少々分かりにくい感はありますね。ただ、SDK はあくまで「顔情報データ」しか扱わないので、検索結果をいかようにも利用できるという意味では柔軟性が高いです。
顔認証結果を描画する
ここまでで、SDK が持つ顔認証処理の紹介は終わりです。ここからは、画質チェックや顔認証の結果を、ウェブカメラのプレビュー上に描画していきましょう。
ウェブカメラのプレビュー表示には OpenCV を使っていますので、 Mat
イメージに対して、顔を四角枠で囲んでみたり、特徴点を描画したりしましょう。
1:N 検索処理 (searchFaceFromList()
) を実行した時、検索対象の中に該当する人物がいなかった場合は、「最も類似する人物の類似度」が極端に低い値になっています。だいたい 0.4
(40% の類似度) 以下の値だと確実に別人なので、その場合は別人であると結果表示すると良いでしょう。
// 最も類似した人物の類似度を取得する
final float mostSimilarScore = searchResult.getTopScores()[0];
if(mostSimilarScore >= 0.4) {
// 類似度が一定以上の値なので、同一人物として描画する
Imgproc.rectangle(mat, point1, point2, scalar, 2);
Imgproc.putText(mat, mostSimilarName, ……); // 名前を描画する
}
else {
// 類似度が一定値に達しない場合は、別人として描画する
Imgproc.rectangle(mat, point1, point2, scala, 1);
Imgproc.putText(mat, "Unknown Person", ……);
}
Imgproc
というのは OpenCV のメソッドで、引数の mat
イメージに対して、四角枠を書いたり、文字を書き込んだりできます。
顔の特徴点データを描画する際は、SDK が保持している Point3D32F
クラスの値を利用して、次のように小さな円を描画してやるとそれらしくなります。
// 顔情報のリストから1人分の「顔特徴点データ」を取り出す
final TargetResult targetResult = targetResults.get(index);
final Landmarks landmarks = targetResult.getLandmarks();
// 「顔特徴点データ」から座標情報を取り出し、OpenCV で描画する
List<Point3D32F> points = landmarks.getPointsArray();
for(final Point3D32F point : points) {
Imgproc.circle(mat, new Point(point.x, point.y), 3, scalar);
}
「顔認証アプリ」完成
このように実装していくことで、顔認証アプリが完成しました。実際に動きを見てみましょう。
アプリを起動し、カメラプレビューが立ち上がると、すぐに顔のトラッキング処理が始まります。JSON ファイルに一致する人物の顔があれば、このように氏名が表示されます。
もしも JSON ファイルに一致する人物が登録されていなかった場合は、「Unknown」と表示されます。
マスクをしたりしている場合は「顔の遮蔽物」の値が小さくなっていきます。任意にしきい値を設けることで、不鮮明な顔画像を排除するかどうか決められます。
生体検出チェックの様子です。スマホで表示した人物の顔は 1:N 検索より前に弾かれますので、成りすましも防止できます。
複数の人物がカメラに映り込んだ場合も、それぞれ検知できました。 上のスクショは生体検出チェックを無効化し、スマホに映した人物と生体とで2人分の顔認証を行っています。
まとめ
JCV 社の顔認証 SDK を使って、リアルタイムに顔認識・顔認証するアプリが実装できました。
それぞれの API は簡単に呼び出せて、結果も分かりやすく取得できます。オーバーヘッドの大きい VirtualBox 経由でも 30fps 程度でプレビューを描画できており、動作も快適です。
画質チェック機能の精度や、1:N 検索の詳細について紹介しきれていない感がありますので、機会がありましたらまたご紹介したいと思います。
現状、この SDK はクラウド VM 上での動作保証がされていません。クラウドに対応すれば、複数のウェブカメラの映像をサーバサイドで同時に処理したりできそうですので、今後に期待ですね。