概要
顔認識システムのFaceNetを使って顔の距離計算をしてみる。
バージョン情報
- FaceNet Latest commit 096ed77 on 17 Apr 2018
顔認証の実装について
顔認証が可能な実装として他にOpenFaceがある。アルゴリズムはFaceNetに近い。
FaceNetはTensorflowとPythonで実装されている。API等は整っておらずバージョン管理も行われていないので導入には少しハードルがある。
OpenFaceはAPIも整っており実行しやすい。実装はTorchとLuaでされている。
OpenFaceの方が手を出すのは優しそうだったが、個人的にLuaはあまり触って来なかったのとTensorflowとは馴染みがあるのでFaceNetを選択した。
導入
FaceNetのレポジトリから最新を落としてくる。
$ git clone https://github.com/davidsandberg/facenet.git
また同サイトからPre-trained modelsを落としてくる。どちらでも良いけど今回はVGGFace2を選択。
ダウンロードしたファイルはcloneしたディレクトリにmodelsというディレクトリを作り、そこに解凍しておく。
$ mkdir models $ mv 20180402-114759.zip models/ $ cd modesl $ unzip 20180402-114759.zip $ cd ../
condaでfacenet用の環境を用意して必要なライブラリをインストールする。
$ conda create -n facenet pip jupyter $ source activate facenet $ conda install -c conda-forge tensorflow scipy scikit-learn opencv h5py matplotlib Pillow requests psutil
これを書いている(2019年5月)時点でnumpyとkerasの間で少し問題が出ているようなので、何かしら問題が出た場合はnumpyのバージョンは1.16.1に下げる。
$ pip install numpy==1.16.1
具体的には下記のようなエラーが発生する。
ValueError: Object arrays cannot be loaded when allow_pickle=False
実行可能な機能
src以下には既存モデルを利用して実行可能なコードとして下記などが入っている。
ファイル名 | 機能 |
---|---|
compare.py | 2つの顔の距離を測る |
classifier.py | 誰の顔か判定する |
今回は学習済みモデルを利用してこの2つのコードを叩いてみる。
compare.py
引数の指定はこちら。
引数名 | 概要 |
---|---|
model | モデルのディレクトリパス |
image_files | 比較したい画像ファイル |
--image_size | 画像サイズ。デフォルトは160px |
--margin | 顔検知をした結果を表示する際のマージン。デフォルトは44px |
--gpu_memory_fraction | GPUメモリの使用量の設定。tf.GPUOptionsに渡される |
ということで落としてきたモデルと2枚の写真を引数に渡して実行してみる。
対象のファイルはlfwのデータセットからマドンナ(Madonna)とマライア・キャリー(Mariah Carey)の画像を5枚ずつ持ってくる。ファイルはimagesというディレクトリを作って配下に入れる。
落としてきた画像を表示する。
import cv2 from matplotlib import pylab as plt madonna = [cv2.imread('images/Madonna_{:04d}.jpg'.format(i)) for i in range(1, 6)] mariah = [cv2.imread('images/Mariah_Carey_{:04d}.jpg'.format(i)) for i in range(1, 6)] images = madonna + mariah f, ax_list = plt.subplots(2, 5, figsize=(10, 4)) for i, img in enumerate(images): ax = ax_list[int(i / 5)][i % 5] ax.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) ax.axis('off')
実行は下記のように1つ目の引数にモデルのディレクトリパス。2つ目以降の引数に写真のファイルを列強する。
$ python src/compare.py \ models/20180402-114759 \ images/Madonna_0001.jpg images/Madonna_0002.jpg images/Madonna_0003.jpg \ images/Madonna_0004.jpg images/Madonna_0005.jpg \ images/Mariah_Carey_0001.jpg images/Mariah_Carey_0002.jpg images/Mariah_Carey_0003.jpg \ images/Mariah_Carey_0004.jpg images/Mariah_Carey_0005.jpg
実行結果は下記。実際には小数点4桁まで表示されるがスペースの都合で2桁まで記載。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0.00 | 0.99 | 0.88 | 1.06 | 0.85 | 1.36 | 1.47 | 1.44 | 1.50 | 1.46 |
1 | 0.99 | 0.00 | 1.02 | 1.08 | 1.07 | 1.38 | 1.52 | 1.32 | 1.48 | 1.42 |
2 | 0.88 | 1.02 | 0.00 | 0.96 | 0.84 | 1.25 | 1.41 | 1.28 | 1.36 | 1.25 |
3 | 1.06 | 1.08 | 0.96 | 0.00 | 1.10 | 1.30 | 1.29 | 1.28 | 1.24 | 1.24 |
4 | 0.85 | 1.07 | 0.84 | 1.10 | 0.00 | 1.32 | 1.42 | 1.26 | 1.39 | 1.30 |
5 | 1.36 | 1.38 | 1.25 | 1.30 | 1.32 | 0.00 | 0.95 | 0.83 | 0.93 | 1.00 |
6 | 1.47 | 1.52 | 1.41 | 1.29 | 1.42 | 0.95 | 0.00 | 0.85 | 0.57 | 0.76 |
7 | 1.44 | 1.32 | 1.28 | 1.28 | 1.26 | 0.83 | 0.85 | 0.00 | 0.75 | 0.73 |
8 | 1.50 | 1.48 | 1.36 | 1.24 | 1.39 | 0.93 | 0.57 | 0.75 | 0.00 | 0.50 |
9 | 1.46 | 1.42 | 1.25 | 1.24 | 1.30 | 1.00 | 0.76 | 0.73 | 0.50 | 0.00 |
0〜4がマドンナ。5〜9がマライア・キャリー。
0.0〜1.1は本人、1.1以上は別人と判定すれば良いらしい。しっかりと0〜4が同一人物、5〜9も同一人物と判定されていることがわかる。
マドンナとマライア・キャリーは比較的顔が近いような気がしたのでマドンナとブッシュ大統領(George_W_Bush)で比較してみたが、多少距離が離れた程度だった。もっと大きく差が出るかと思っていた。
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 0.00 | 0.99 | 0.88 | 1.44 | 1.39 | 1.43 |
1 | 0.99 | 0.00 | 1.02 | 1.54 | 1.48 | 1.46 |
2 | 0.88 | 1.02 | 0.00 | 1.45 | 1.37 | 1.46 |
3 | 1.44 | 1.54 | 1.45 | 0.00 | 0.40 | 0.56 |
4 | 1.39 | 1.48 | 1.37 | 0.40 | 0.00 | 0.54 |
5 | 1.43 | 1.46 | 1.46 | 0.56 | 0.54 | 0.00 |
似ていそうな画像としてマナカナの画像をネットから拾ってきた。写真の立ち位置からしてたぶん0〜2がマナ、3〜5がカナだと思う。
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 0.00 | 0.96 | 0.79 | 1.15 | 1.0091 | 0.7574 |
1 | 0.96 | 0.00 | 0.55 | 1.13 | 0.7079 | 0.7608 |
2 | 0.79 | 0.55 | 0.00 | 1.10 | 0.7720 | 0.5349 |
3 | 1.15 | 1.13 | 1.10 | 0.00 | 1.1346 | 1.0972 |
4 | 1.00 | 0.70 | 0.77 | 1.13 | 0.0000 | 0.7755 |
5 | 0.75 | 0.76 | 0.53 | 1.09 | 0.7755 | 0.0000 |
だいぶ距離が近い。1.1を閾値とした場合、誤判定も発生している。顔認証で双子が突破されるのも納得の数字。
classifier.py
classifier.pyはまずTRAINを実行してベクトルを作り、その後CLASSIFIERを実行してその写真が誰であるかを判定する。
ディレクトリがクラス名(人物を表す)として扱われるので、パス構成は「人物名/ファイル名」の形式にする必要がある。
各人物の1〜3の画像をimagesディレクトリ配下に置き、4〜5をテスト用としてimages_testディレクトリ配下に置いて実行してみる。パス構成としては下記のようになる。
images ├── Madonna │ ├── Madonna_0001.jpg │ ├── Madonna_0002.jpg │ └── Madonna_0003.jpg └── Mariah_Carey ├── Mariah_Carey_0001.jpg ├── Mariah_Carey_0002.jpg └── Mariah_Carey_0003.jpg images_test/ ├── Madonna │ ├── Madonna_0004.jpg │ └── Madonna_0005.jpg └── Mariah_Carey ├── Mariah_Carey_0004.jpg └── Mariah_Carey_0005.jpg
classifier.pyを実行する際の引数の指定はこちら
引数名 | 概要 |
---|---|
mode | TRAIN or CLASSIFY |
data_dir | 画像データのディレクトリパス |
model | モデルのディレクトリパス |
classifier_filename | 分類対象データ。TRAINで生成し、CLASSIFIERで利用する。 |
--use_split_dataset | trainとtestにデータセットを分割する。default=True |
--test_data_dir | テストデータのディレクトリパス |
--batch_size | Tensorflowのバッチサイズ。default=90 |
--image_size | 画像サイズ。default=160 |
--seed | ランダムシード |
--min_nrof_images_per_class | |
--nrof_train_images_per_class | 1クラスにつき指定数をtrainに使い残りはtestに使う |
TRAINを実行してみる。実行結果はclassifier_filenameで指定したパスにpickleで生成される。
引数は上から、実行モード、画像ディレクトリのパス、モデルディレクトリのパス、出力するpickleのパス。
$ python src/classifier.py \ TRAIN \ images \ models/20180402-114759 \ classifier.pkl
これでimages配下においた6枚の画像(各人物3枚ずつ)に対してTRAINが実行された。
pickleなので実際にロードして値を見てみる。
with open('classifier.pkl', 'rb') as f: (model, class_names) = pickle.load(f) print(model) #=> SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0, #=> decision_function_shape='ovr', degree=3, gamma='auto_deprecated', #=> kernel='linear', max_iter=-1, probability=True, random_state=None, #=> shrinking=True, tol=0.001, verbose=False) print(class_names) #=> ['Madonna', 'Mariah Carey']
ベクトルそのものが入っているわけではなく、SVCの分類モデルが生成されている。
これを利用してCLASSIFYを実行する。
1つ目の引数にCLASSIFYを指定し、画像が入っているパスは4〜5の画像を入れたimages_test。FaceNetのモデルとTESTで生成したモデルのパスを指定して実行する。
$ python src/classifier.py \ CLASSIFY \ images_test \ models/20180402-114759 \ classifier.pkl
実行結果(見やすいように一部print文を修正)
0 label=Madonna pred=Mariah Carey: 0.810 1 label=Madonna pred=Mariah Carey: 0.630 2 label=Mariah Carey pred=Madonna: 0.750 3 label=Mariah Carey pred=Madonna: 0.807 Accuracy: 0.000
Accuracyが0になってしまった。何か間違っているかもしれないけどSVCで3件で学習している時点で危うそうなのでこのへんは詳しく追うのはやめておこう。実際に使う時は学習モデルは別途実装することになると思うし。
OpenCVのデモではLinearSvm、GridSearchSvm、GMM、RadialSvm、DecisionTree、GaussianNB、DBNなどが用意されている。
指定画像をベクトルに変換する
顔画像をベクトルに変換して保存するフローはよく使いそうなので、そのあたりの処理を抜き出して実行してみる。
face2vec.pyという名前で下記のようなファイルをsrc配下に作成する。classifier.pyから必要なところだけを切り出したようなコード。
# face2vec.py import os import sys import math import argparse import tensorflow as tf import numpy as np import pandas as pd import facenet def main(args): with tf.Graph().as_default(): with tf.Session() as sess: np.random.seed(seed=args.seed) # image path list paths = [os.path.join(args.data_dir, f) for f in os.listdir(args.data_dir)] # Load the model facenet.load_model(args.model) # Get input and output tensors images_placeholder = tf.get_default_graph().get_tensor_by_name("input:0") embeddings = tf.get_default_graph().get_tensor_by_name("embeddings:0") phase_train_placeholder = tf.get_default_graph().get_tensor_by_name("phase_train:0") embedding_size = embeddings.get_shape()[1] # Run forward pass to calculate embeddings nrof_images = len(paths) nrof_batches_per_epoch = int(math.ceil(1.0*nrof_images / args.batch_size)) emb_array = np.zeros((nrof_images, embedding_size)) for i in range(nrof_batches_per_epoch): start_index = i*args.batch_size end_index = min((i+1)*args.batch_size, nrof_images) paths_batch = paths[start_index:end_index] images = facenet.load_data(paths_batch, False, False, args.image_size) feed_dict = { images_placeholder:images, phase_train_placeholder:False } emb_array[start_index:end_index,:] = sess.run(embeddings, feed_dict=feed_dict) df = pd.DataFrame(emb_array, columns=[i for i in range(512)]) df['file_path'] = paths print(paths) df['person'] = df.file_path.apply(lambda x: '_'.join(os.path.basename(x).split('_')[:-1])) df.to_csv(args.output_file) def parse_arguments(argv): parser = argparse.ArgumentParser() parser.add_argument('data_dir', type=str, help='Path to the data directory containing aligned LFW face patches.') parser.add_argument('output_file', type=str, help='Path to the output vector file.') parser.add_argument('model', type=str, help='Could be either a directory containing the meta_file and ckpt_file or a model protobuf (.pb) file') parser.add_argument('--batch_size', type=int, help='Number of images to process in a batch.', default=90) parser.add_argument('--image_size', type=int, help='Image size (height, width) in pixels.', default=160) parser.add_argument('--seed', type=int, help='Random seed.', default=666) return parser.parse_args(argv) if __name__ == '__main__': main(parse_arguments(sys.argv[1:]))
これを実行するとベクトルファイルがCSV形式で出来上がる。
下記コマンドではimagesフォルダ直下に {人物名}_{:04d}.jpg なファイル名で画像ファイルが置いてある想定。
$ python src/face2vec.py images output.csv models/20180402-114759
中身を確認する。FaceNetの論文によると1つの顔が128次元の……
X.shape
#=> (155, 512)
512次元?
Wikiを見たらEmbedding sizeはPreviousは128DだったけどCurrentは512Dになっていた。
各画像に対して512次元のベクトルが出力される。
t-SNEで可視化する
512次元もあると距離関係を視覚的に掴むことができないので、うまいこと2次元や3次元に圧縮してくれるt-SNEを使って可視化してみる。
import numpy as np import pandas as pd from sklearn.manifold import TSNE import matplotlib from matplotlib import pylab as plt df = pd.read_csv('output.csv') names = sorted(df.person.unique()) DISP_SIZE = len(names) X = df[[str(i) for i in range(512)]].values X_2D = TSNE(n_components=2).fit_transform(X) df2 = pd.DataFrame(X_2D, columns=['x', 'y']) df2['person'] = df.person colors = [matplotlib.cm.hsv(i/DISP_SIZE) for i in range(DISP_SIZE)] f, ax = plt.subplots(figsize=(10, 10)) for color, name in zip(colors[:DISP_SIZE], names[DISP_SIZE]): tmp_df = df2[df2.person == name] ax.scatter(tmp_df.x, tmp_df.y, alpha=0.7, color=color, label=name) plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left')
5画像以上持つ人を18名ほど適当に集めてきてplotした結果が下記。
キレイに人ごとに位置が揃っている。また左側にいわゆる美女と呼ばれる人たちいて、右側に男性が多くなっているなどの分かれ方もしっかりできている。マイケル・ジャクソンがその中央に位置しているのもなるほど。
改定履歴
Author: Masato Watanabe, Date: 2019-05-06 記事投稿