あらまし

顔認識を実際に動かしながら試すことで顔認識がどういうものかを体験します。
実装に使ったのはdlibの畳み込みニューラルネットワークベースの認識器です

顔認識の必要性

顔認識の必要性については今や説明するまでもないほど身近なものになっていると思います。
監視カメラにおいては顔だけではなく、他の特徴も含めて利用し複数台のカメラの画像を高速に処理できる時代が到来しています。
例えば渋谷で発生した事件ではスマートフォンの画像から犯人を突き止めたようです。
今回は顔認識の精度を大幅に向上させた深層学習ベースの手法を試したいと思います。

利用するライブラリと処理概要

今回実装に使うのはface_recognitionというpythonのパッケージです。わかりやすい名前ですね。
このライブラリは画像全体からアライメントされた顔画像領域を取り出す関数と個人識別のための関数がそれぞれ含まれています。
このパッケージはdlibを利用しています。dlibの顔認識に関する詳細はこちらに記載されていました。
ResNetをベースに開発されたモデルのようです。

このモデルはFaceNetにインスパイアされているようなので、詳しい仕組みや理論的な背景はその論文を読むと良いかと思います。

今回顔認識を行う処理は大まかに以下の流れになります。
  1. 顔領域の特定とアライメント
  2. 1.の画像から特徴ベクトルを取得
  3. 2で得られた特徴ベクトルの比較
特徴ベクトルはResNetの最終層から出てくる128次元のベクトルであり、同じ個人の画像から算出される特徴ベクトルの距離が小さくなるようにResNetが学習されています。
つまり同じ個人の顔画像かどうかを調べるにはベクトル間の距離を測り、閾値より小さいかどうかを計算すればよい、ということになります。

実装と検証

実際に動かしてみましょう。
今回の実験では対象の画像は私と私の所属する技術研究所の丸山所長、私の上司である綾塚さんの合計3名としました。
同じ個人の画像を識別できることを検証するために各人それぞれ二枚ずつの写真を用意しています。
画像を読み込みます。
import numpy as np
from PIL import Image
from PIL import ImageDraw
import face_recognition
# 丸山所長
image_maruyama_1 = face_recognition.load_image_file("maruyama_san_1.png")
image_maruyama_2 = face_recognition.load_image_file("maruyama_san_2.jpg")
# 加藤
image_kato_1 = face_recognition.load_image_file("kato_1.png")
image_kato_2 = face_recognition.load_image_file("kato_2.jpg")
# 綾塚さん
image_ayatuka_1 = face_recognition.load_image_file("ayatuka_san_1.jpg")
image_ayatuka_2 = face_recognition.load_image_file("ayatuka_san_2.jpg")
アライメントされた顔領域を取得します。
# 顔を囲む領域を抽出する
# model=cnnにしないとbboxがかなりずれる。結果としてencodingの値もおかしくなるっぽい。
# 丸山所長
img_maruyama_1 = Image.fromarray(np.array(image_maruyama_1))
loc_maruyama_1 = face_recognition.face_locations(image_maruyama_1, model="cnn")
top, right, bottom, left = loc_maruyama_1[0]
draw = ImageDraw.Draw(img_maruyama_1)
draw.rectangle((right, top, left, bottom))
img_maruyama_1
正しく丸山所長の顔領域(白枠)を抜き出すことができました。
最後まで(個人を識別するところまで)実装してみてわかったのですが、顔領域を取り出す処理  face_recognition.face_locations  は深層学習ベース model=”cnn” を使ったほうがよい精度がでるようです。

すべての画像に対する結果を並べるとこのようになりました。
顔領域が口を含んでいないことがありますね。

次に顔領域を
face_encodings() に入力し、特徴ベクトルを計算します。
顔領域の切り出しと認識器への入力は face_recognitionが面倒を見てくれるので楽ちんです。
# 128bytesの顔の特徴ベクトルを取得する
encoding_maruyama_1 = face_recognition.face_encodings(image_maruyama_1, known_face_locations=loc_maruyama_1)[0]
encoding_maruyama_2 = face_recognition.face_encodings(image_maruyama_2, known_face_locations=loc_maruyama_2)[0]
encoding_kato_1 = face_recognition.face_encodings(image_kato_1, known_face_locations=loc_kato_1)[0]
encoding_kato_2 = face_recognition.face_encodings(image_kato_2, known_face_locations=loc_kato_2)[0]
encoding_ayatuka_1 = face_recognition.face_encodings(image_ayatuka_1, known_face_locations=loc_ayatuka_1)[0]
encoding_ayatuka_2 = face_recognition.face_encodings(image_ayatuka_2, known_face_locations=loc_ayatuka_2)[0]
合計6(各個人2枚ずつ x 3名)の特徴ベクトルを取り出すことができました。

次に、これらについてすべての組み合わせの距離を求めます。
これは np.meshgrid で総当りの組み合わせを求めた後、 np.linalg.norm にその組み合わせを入力することで実行できます。
最後にこれをヒートマップにすることで可視化しました。
# 特徴ベクトル間の距離をメッシュで計算した表を可視化する
encodings = np.array([encoding_maruyama_1, encoding_maruyama_2, encoding_kato_1, encoding_kato_2, encoding_ayatuka_1, encoding_ayatuka_2])
index = np.arange(encodings.shape[0])
ix, iy = np.meshgrid(index, index)
distances = np.linalg.norm(encodings[ix]-encodings[iy], axis=2)
import seaborn as sns
import pandas as pd
df = pd.DataFrame(distances,
index=["maruyama", "maruyama", "kato", "kato", "ayatuka", "ayatuka"],
columns=["maruyama", "maruyama", "kato", "kato", "ayatuka", "ayatuka"],
)
sns.heatmap(df, annot=True, fmt=".2f")
少しわかりづらいと思いますが、各個人の画像の組み合せが他の方の画像に比べて距離が近いことがお分かりいただけると思います。
同じ人の画像同士はおおよそ0.3付近にあり、他人の画像は0.5以上となっていました。
思ったほど他人間の距離の差がでていないのが気になりますが、これは後日調査しようと思っています。
ちなみに今回は利用していない face_recognition.compare_faces() は上記のスクリプトと同じように顔の同一判定を行いますが、閾値のデフォルトは0.6でした。

最後に

撮影条件が大きく異なる画像を読み込ませても、ベクトルの距離が大体期待通りになっていることが確認できました。
ちょっとした顔認識用途では十分利用できるのではないでしょうか?会社の受付の自動化や勤怠登録の自動化など身近な用途にも使えると思います。