iMind Developers Blog

iMind開発者ブログ

FaceNetを動かしてみる

概要

顔認識システムの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')

f:id:imind:20190501184829p:plain

実行は下記のように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した結果が下記。

f:id:imind:20190504022349p:plain

キレイに人ごとに位置が揃っている。また左側にいわゆる美女と呼ばれる人たちいて、右側に男性が多くなっているなどの分かれ方もしっかりできている。マイケル・ジャクソンがその中央に位置しているのもなるほど。

改定履歴

Author: Masato Watanabe, Date: 2019-05-06 記事投稿