👬

OpenCV + PyTorch + faissで学習なし顔識別?

2022/04/19に公開1

はじめに

お久しぶりです!前回はゼロからYOLOv3を作るという無謀なことをしていましたね・・・。
https://zenn.dev/opamp/articles/5198d6bf369b8e

普段は
https://kdl-di.hatenablog.com/
で技術ブログを書いたりしています!良ければ読んでください~~~~!

前回もCenterNetの記事を次は書く!と言っていたのですが、なぜ違うものを書いています。
今回に至ってはタイトルに?が入っちゃってますからね、何なんでしょう・・・。

さて、今回はAIモデルの学習無しで顔認証的な何かを作れないか?ということでチャレンジしてみました。
先に結果をお見せします!

入力した画像と、マッチした画像を並べています。1番目は入力した画像の人になっていることが多いので、割といい感じじゃないでしょうか?
画角と背景をちゃんと合わせればもっとうまくいきそうな気がしますね!

では、どうやって ごみを作ったのかこのアルゴリズムを作ったのか説明していきます!

学習なし顔識別の考え方

まずは今回作成したアルゴリズムの概念図を示します。

大きく使っているアプリケーションは3つです。

  • OpenCV:画像処理をpython
  • PyTorch:学習済みモデルの利用・特徴量抽出
  • faiss:ベクトル検索

学習をしなくて済む最大のポイントは、学習済みモデルが抽出した特徴量を利用することです。
学習済みモデルは基本1000クラス分類ができるモデルです。
すなわち、1000クラスをきめ細かに分けられるほど、丁寧に画像から特徴を抽出しているとも考えることができます。

つまり学習済みモデルを特徴抽出器として利用すれば、同じものは似た特徴を、違うものは違った特徴が抽出されることになると考えられます。

今回はResNeXtを特徴抽出器として利用しています。

特徴を抽出したはいいが、類似度マッチングはどうしましょう。1から書くこともできますが、ちょっと処理が重たくなりそうですね。

そんな時に便利なのがfaiss。検索ライブラリの最強さんです。これもfacebookが作ってるんですよ。PyTorchといいfaissといい、もうfacebookに足を向けて寝られません・・・。(facebook自体は5年以上更新してないのですが)

https://github.com/facebookresearch/faiss

このfaissというライブラリは、ベクトルを入力するとその関係性を良しなにして保持してくれます。

その後、検索したいベクトルを入力すると、高速で(0.1msくらい)でそのインデックスを返却します。

今回はfaissに山田孝弘さんとムロツヨシさんの顔をResNeXtでベクトル化し、それをぶちんでみました!

実装

では実際に実装してみましょう!

顔認識

まずは顔認識から実装します。

顔のみを切り取る理由として、顔以外の領域を持ったまま特徴抽出器に入れてしまうと、関係のない部分の特徴も抽出してしまうことになります。
そうすると、同じ人でも服が違うと抽出されるベクトルが変わってしまい、別物判定されます。
こういった問題を解決するために、顔だけを切り抜いてしまいます!

環境準備はこの記事を参考にしてください!

https://zenn.dev/opamp/articles/e0c2f8acbfd504

まずはスクリーンに映っている内容から顔認識してみましょう!

face_detection.py
import cv2
from PIL import ImageGrab ,Image
from cv2 import bilateralFilter
import numpy as np
import random
import copy

cascade = cv2.CascadeClassifier('./haarcascade_frontalface_default.xml')
def random_face_opencv():
    while True:
        image = ImageGrab.grab()
        image = cv2.cvtColor(np.array(image),cv2.COLOR_RGB2BGR)
        image = cv2.resize(image,(int(image.shape[1]/2),int(image.shape[0]/2)))
        gray = cv2.cvtColor(copy.deepcopy(image),cv2.COLOR_BGR2GRAY)
        
        faces = cascade.detectMultiScale(gray)
        for (x, y, w, h) in faces:
            image = cv2.rectangle(image,(x,y),(x+w,y+h) , color = (0,0,255),thickness=3)
        cv2.imshow('test',image)
        k = cv2.waitKey(1)&0xFF

        if k == ord('q'):
            break
if __name__ == "__main__": 
    random_face_opencv()

私はGoogleで「かお」と調べて検出してみました!かなりきれいに検出できてますね!
多分これで十分でしょう!

データの準備

続いて顔認識したい人の写真を収集します。
私はGoogle検索で「山田孝弘」「ムロツヨシ」と入力して、取れるだけ取りました。

もっと楽な方法で取得できる方は人力で行わず、コードで行うほうがいいと思います~~~

収集した画像を以下のように保存しましょう!

顔領域のカッティング

では!お待ちかね!顔カッティングのお時間です!

まずは保存先を作りましょう!私は「cut」というフォルダを作成しました。お好きなように構築してください!

ではカットします!

import cv2
import copy

# 識別したい画像分だけ準備してください!
muro_list = glob.glob(os.path.join("./face/muro/","*"))
yamada_list = glob.glob(os.path.join("./face/yamada/","*"))

cascade = cv2.CascadeClassifier('./haarcascade_frontalface_default.xml')

# 上に合わせて書き換えてください!!
data_list = [muro_list , yamada_list]

# クラス名を記載してください!
name_list = ["muro" , "yamada"]
for name , l in zip(name_list,data_list):
    count = 1
    for idx , path in enumerate(l):
        image =cv2.imread(path)
        if image is None:
            continue
        gray = cv2.cvtColor(copy.deepcopy(image),cv2.COLOR_BGR2GRAY)
        
        faces = cascade.detectMultiScale(gray)
        for (x, y, w, h) in faces:
            cv2.imwrite(f"./cut/{name}_{count}.png",image[y:y+h,x:x+w])
            count += 1

結果は・・・・

んーーー、圧が凄い。ちょこちょこ誤検出が混じってますね。手で削除しちゃいましょう!

きれいになりましたね!

凄い圧が強い・・・。なぜかわいい女の子とかにしなかったのか・・・。
好きなアイドルやキャラクターの顔を題材にした方は、たぶんこの段階で満足してるんじゃないでしょうか? ごはん何杯でも行けそうですね

特徴抽出器による特徴抽出

続いて特徴抽出器を用いて、先ほどの顔画像から特徴を抽出します。

早速準備しましょう!

import torch
import torchvision.models as models
import torch.nn as nn

model = models.resnext50_32x4d()

vec_model = nn.Sequential(
    *(
        list(model.children())[:-1] 
        )
)

完成です! たった数行で準備できちゃうんです。びっくり・・・。

では特徴抽出しちゃいましょう~~

from PIL import Image
from torchvision import transforms
import torch

# 各顔画像の呼び出しを行っています。yamada_やmuro_を各自の名前で変更してください。
yamada_face = glob.glob(os.path.join("./cut/","yamada_*"))
muro_face = glob.glob(os.path.join("./cut/","muro_*"))
len(yamada_face),len(muro_face)

# faissに保存する画像と検証に利用する画像を決めています。
# trainがfaiss用。valが検証用です。
yamada_train = yamada_face[:int(len(yamada_face)*0.5)]
yamada_val = yamada_face[int(len(yamada_face)*0.5):]
muro_train = muro_face[:int(len(muro_face)*0.5)]
muro_val = muro_face[int(len(muro_face)*0.5):]

vec_list = []

face_data = [yamada_train , muro_train]
preprocess = transforms.Compose([
    transforms.Resize((224,224)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
for data in face_data:
    for path in data:
        input_image = Image.open(path)
        input_image = input_image.convert("RGB")
        with torch.no_grad():
            img = preprocess(input_image)
            vec = vec_model(img.unsqueeze(0))
        vec_list.append(vec.detach().cpu().numpy()[0,:,0,0])

faissに登録する

抽出したベクトルたちをfaissに登録します。
faissは

pip install faiss-cpu

でインストールできるので、まだの方は入れちゃってください!

では、faissにベクトルを登録していきましょう!

import faiss                   
import numpy as np

# 山田孝弘の画像かムロツヨシの画像かわかるようにデータを残す
ans = ["yamada"]*len(yamada_train) + ["muro"]*len(muro_list)
# 登録
index = faiss.IndexFlatL2(vec_list[0].shape[0])  
index.add(np.array(vec_list, dtype=np.float32) )                 
print(index.ntotal)

では、登録した内容で検索してみましょう。

# 試したい画像を選択
search_id = 0
print(f"その画像は{ans[search_id]}です")
k = 3                         
D, I = index.search(np.array(vec_list[search_id:search_id+1]), k) 

print(f"結果は")
for n,i in enumerate(I[0]):
    print(f"No{n+1}{ans[i]}")
 
#その画像はyamadaです
#結果は
#No0はyamada
#No1はmuro
#No2はyamada

どうでしょうか?

順位が高いほうが類似しています!なんかわからんけど楽しい!(狂気)

実際に識別してみる

では!お待ちかねの識別タイムです!
さて、どんな感じになるでしょうか~~~~?

from PIL import Image
from torchvision import transforms
import torch
vec_list = []

face_data = [yamada_val , muro_val]
preprocess = transforms.Compose([
    transforms.Resize((224,224)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
for data in face_data:
    for path in data:
        input_image = Image.open(path)
        input_image = input_image.convert("RGB")
        with torch.no_grad():
            img = preprocess(input_image)
            vec = vec_model(img.unsqueeze(0))
        vec_list.append(vec.detach().cpu().numpy()[0,:,0,0])
import matplotlib.pyplot as plt
%matplotlib inline

def create_fig(id,path_list):
    fig = plt.Figure(figsize=(30,10))
    for i,path in enumerate(path_list):
        plt.subplot(1,4,i+1)
        img = Image.open(path)
        
        plt.imshow(img)
        plt.title("input "if i == 0 else f"No {i}")
        plt.axis('off')
    plt.savefig(f"{id}.png")

train_path_list = yamada_train + muro_train
val_path_list = yamada_val + muro_val
    
k = 3                          # we want to see 4 nearest neighbors

for i in range(len(vec_list)):
    D, I = index.search(np.array(vec_list[i:i+1]), k) # sanity check
    I = I[0]
    D = D[0]
    print(I,D)
    create_fig(i,val_path_list[i:i+1]+[train_path_list[idx] for idx in I])

山田孝弘の場合

どうでしょう?多くの場合で山田孝弘が上位に出てきていることがわかります!(ちょこっとムロツヨシがNo1の時もありますが・・・)

ムロツヨシの場合

え!山田孝弘の顔強すぎ? 5/9で山田孝弘じゃないですか・・・。

まとめ

今回は学習なしで顔識別に取り組んでみました!
思ってたより難しかったですね・・・。
でも、思いついた実装ができたので満足です!

特徴を利用した画像検索はよく行われているので、何かの参考になれば幸いです!
ではまた!次回は何の記事になるでしょうか~~~。(もうCenterNetとは言いません・・・)

Discussion

yKesamaruyKesamaru

素晴らしい記事をありがとうございます!😊🌟 faiss、面白そうですね!