顔認証を使った「顔が似てる芸能人ランキング」

約650人の芸能人の顔データを使って、芸能人(と著名人)の似ている組み合わせ(なんと20万組以上!)を顔認証エンジンを使用したプログラムを書いて検証してみました。最初に結論を載せて、後半にソースコードとその説明、最後に目的を解説します。

検証結果

同一人物の違う写真がある場合、当たり前ですが本人同士がマッチしてしまいます。ここでは強制的に「他人どうし」になるように調整してあります。

1 位 大森南朋_default.png 新井浩文_default.png 99.44 %
2 位 新川優愛_default.png 内田理央_default.png 99.43 %
3 位 金正恩_10.png 馬場園梓_default.png 99.38 %
4 位 西村康稔_5.png 池田清彦_default.png 99.37 %
5 位 金正恩_10.png 畑岡奈紗_default.png 99.37 %
6 位 椎名林檎_3.png 有働由美子_default.png 99.37 %
7 位 波瑠_default.png 入山杏奈_default.png 99.36 %
8 位 浅田舞_default.png 浅田真央_default.png 99.36 %
9 位 麻木久仁子_default.png 有働由美子_default.png 99.35 %
10 位 加藤勝信_001.png 萩生田光一_001.png 99.31 %

「実はこのふたり、いとこなんです」って言われたら納得してしまうかもしれません。(実際は違いますが…)

1 位 大森南朋 新井浩文 99.44 %

2 位 新川優愛 内田理央 99.43 %

3 位 金正恩 馬場園梓 99.38 %

すごく綺麗にお化粧して変装したら似ているかもしれません。

4 位 西村康稔 池田清彦 99.37 %

30年後はそっくりかもしれません。顔のパーツはそっくりですのでありえるかも。

5 位 金正恩 畑岡奈紗 99.37 %

目鼻立ちのパーツはイケメンなのかもしれません。中性的なのでしょうか。

6 位 椎名林檎 有働由美子 99.37 %

7 位 波瑠 入山杏奈 99.36 %

8 位 浅田舞 浅田真央 99.36 %

姉妹であられるとのことです。

追検証

後日画像数を増やして追検証してみました。

本人同士ではどうなるか

他人の空似より本人同士の類似度は当然高くなります。本人の写真が2枚以上存在する人は14人です。検証してみましょう。

1 位 小泉進次郎_4.png 小泉進次郎_6.png 99.71 %
2 位 金正恩_3.png 金正恩_2.png 99.67 %
3 位 金正恩_4.png 金正恩_default.png 99.66 %
4 位 金正恩_1.png 金正恩_4.png 99.57 %
5 位 神木隆之介_2.png 神木隆之介_1.png 99.52 %
6 位 金正恩_4.png 金正恩_6.png 99.5 %
7 位 西村康稔_5.png 西村康稔_3.png 99.5 %
8 位 尾身茂_default.png 尾身茂_1.png 99.47 %
9 位 安倍晋三_face_learning_1.png 安倍晋三_default.png 99.47 %
10 位 神木隆之介_default.png 神木隆之介_1.png 99.46 %

他人の空似の場合より、パーセントの数値が高くなっていることに注目してください。

1 位 小泉進次郎 小泉進次郎 99.71 %

2 位 金正恩 金正恩 99.67 %

3 位 金正恩 金正恩 99.66 %

4 位 金正恩 金正恩 99.57 %

5 位 神木隆之介 神木隆之介 99.52 %

6 位 金正恩 金正恩 99.50 %

7 位 西村康稔 西村康稔 99.50 %

8 位 尾身茂 尾身茂 99.47 %

9 位 安倍晋三 安倍晋三 99.47 %

10 位 神木隆之介 神木隆之介 99.46 %

ソースコードと解説

ここからは技術的なお話です。

ソースコード

import itertools

import numpy as np

import similar_percentage_to_tolerance
from load_priset_image import load_priset_image


def make_dict(kaoninshoDir):
    known_face_encodings_list, known_face_names_list = load_priset_image(
        kaoninshoDir,
        priset_face_imagesDir=kaoninshoDir + '/priset_face_images', 
        upsampling=0, 
        jitters=100, 
        mode='hog', 
        model='small'
    )

    name_encoding_dict = {}

    # 辞書へ要素を追加
    for counter in range(0, len(known_face_names_list), 1):
        name_encoding_dict[known_face_names_list[counter]] = known_face_encodings_list[counter]

    return name_encoding_dict

def distances_per_pair(name_encoding_dict):
    pair_distance_dict = {}  # pair_distance_dict -> pair : distance
    distance_list = []

    print(
        len(list(itertools.combinations(name_encoding_dict.keys(), 2))),
        'の組み合わせから探します'
    )
    for pair in list(itertools.combinations(name_encoding_dict.keys(), 2)):
        distance = np.linalg.norm(name_encoding_dict[pair[0]] - name_encoding_dict[pair[1]], ord=2, axis=None)
        
        pair_distance_dict[pair] = distance
        
        distance_list.append(distance)

    distance_list.sort(reverse=False)

    cnt = 0
    for distance_item in distance_list:
        
        for key, value in pair_distance_dict.items():
            if value==distance_item:

                first, second = key[0], key[1]

                first, _first = first.split('_', 1)
                second, _second = second.split('_', 1)

                # if not first==second:
                if first==second:
                    first = first + '_' + _first
                    second = second + '_' + _second

                    print(
                        cnt + 1, '位',
                        first,
                        second,
                        round(similar_percentage_to_tolerance.to_percentage(value), 2),'%'
                    )
                    cnt+=1
                    if cnt==10:
                        exit()
        
# 検証
if __name__ == '__main__':
    name_encoding_dict = make_dict('/home/***/***')

    distances_per_pair(name_encoding_dict)

ソースコード解説

import itertools

import numpy as np

import similar_percentage_to_tolerance
from load_priset_image import load_priset_image

組み合わせ問題にitertoolsライブラリを使います。similar_percentage_to_toleranceとload_priset_imageライブラリはFACE01のライブラリを使用します。

def make_dict(kaoninshoDir):
    known_face_encodings_list, known_face_names_list = load_priset_image(
        kaoninshoDir,
        priset_face_imagesDir=kaoninshoDir + '/priset_face_images', 
        upsampling=0, 
        jitters=100, 
        mode='hog', 
        model='small'
    )

make_dict()ではFACE01のライブラリであるload_priset_imageメソッドを呼び出しています。引数はiniファイルから引っ張ってくる予定ですが、ここではコードが長くなるので変数に固定値を代入しています。

    name_encoding_dict = {}

    # 辞書へ要素を追加
    for counter in range(0, len(known_face_names_list), 1):
        name_encoding_dict[known_face_names_list[counter]] = known_face_encodings_list[counter]

    return name_encoding_dict

name_encoding_dictという辞書変数を作成して名前のリストとエンコーディングされた顔データを辞書の要素として追加していきます。len(known_face_names_list)分、つまり約650人全てを辞書データとして登録します。

def distances_per_pair(name_encoding_dict):
    pair_distance_dict = {}  # pair_distance_dict -> pair : distance
    distance_list = []

distances_per_pair()では先のmake_dict()の返り値、即ち辞書オブジェクトを引数にとります。

    print(
        len(list(itertools.combinations(name_encoding_dict.keys(), 2))),
        'の組み合わせから探します'
    )
    for pair in list(itertools.combinations(name_encoding_dict.keys(), 2)):
        distance = np.linalg.norm(name_encoding_dict[pair[0]] - name_encoding_dict[pair[1]], ord=2, axis=None)

itertools.combinations()で順番関係なしの組み合わせを作成しています。続くnp.linalg.norm()ではエンコードされた顔データ同士のユークリッド距離を計算しています。L2ノルムなのでord=2、それぞれひとつづつの比較なのでaxis=Noneです。これらの構文は機械学習で度々用いられます。

            pair_distance_dict[pair] = distance
        
        distance_list.append(distance)

    distance_list.sort(reverse=False)

    cnt = 0
    for distance_item in distance_list:
        
        for key, value in pair_distance_dict.items():
            if value==distance_item:

                first, second = key[0], key[1]

                first, _first = first.split('_', 1)
                second, _second = second.split('_', 1)

                # if not first==second:
                if first==second:
                    first = first + '_' + _first
                    second = second + '_' + _second

                    print(
                        cnt + 1, '位',
                        first,
                        second,
                        round(similar_percentage_to_tolerance.to_percentage(value), 2),'%'
                    )
                    cnt+=1
                    if cnt==10:
                        exit()

書捨てのようになっておりますが。顔距離のリストを短い順からソートして、顔距離を値として辞書のkeyを検索し、10位までを標準出力しています。見やすいようにパーセント表示は四捨五入しています。

最後に

いかがだったでしょうか。

今回の検証実験は一見興味本位の様に見えますが目的がしっかりとあります。顔サンプル中に顔距離の小さい顔どうしがあった場合、映像データのクオリティや表情によっては誤検出が発生します。似た顔を予め把握して対策をとることは顔認証に於いて非常に重要な前処理です。あまりにも顔距離が近い場合、サンプルとなる顔データをとりなおしたり、機械学習にかけて分類する必要があるかもしれません。

以上です。最後までお読み頂きありがとうございました。