👬

OpenCV + PyTorch + faissで孊習なし顔識別

に公開1件

はじめに

お久しぶりです前回はれロからYOLOv3を䜜るずいう無謀なこずをしおいたしたね・・・。
https://zenn.dev/opamp/articles/5198d6bf369b8e

普段は
https://kdl-di.hatenablog.com/
で技術ブログを曞いたりしおいたす良ければ読んでください

前回もCenterNetの蚘事を次は曞くず蚀っおいたのですが、なぜ違うものを曞いおいたす。
今回に至っおはタむトルにが入っちゃっおたすからね、䜕なんでしょう・・・。

さお、今回はAIモデルの孊習無しで顔認蚌的な䜕かを䜜れないかずいうこずでチャレンゞしおみたした。
先に結果をお芋せしたす

入力した画像ず、マッチした画像を䞊べおいたす。1番目は入力した画像の人になっおいるこずが倚いので、割ずいい感じじゃないでしょうか
画角ず背景をちゃんず合わせればもっずうたくいきそうな気がしたすね

では、どうやっお ごみを䜜ったのかこのアルゎリズムを䜜ったのか説明しおいきたす

孊習なし顔識別の考え方

たずは今回䜜成したアルゎリズムの抂念図を瀺したす。

倧きく䜿っおいるアプリケヌションは぀です。

  • OpenCV画像凊理をpython
  • PyTorch孊習枈みモデルの利甚・特城量抜出
  • faissベクトル怜玢

孊習をしなくお枈む最倧のポむントは、孊習枈みモデルが抜出した特城量を利甚するこずです。
孊習枈みモデルは基本1000クラス分類ができるモデルです。
すなわち、1000クラスをきめ现かに分けられるほど、䞁寧に画像から特城を抜出しおいるずも考えるこずができたす。

぀たり孊習枈みモデルを特城抜出噚ずしお利甚すれば、同じものは䌌た特城を、違うものは違った特城が抜出されるこずになるず考えられたす。

今回はResNeXtを特城抜出噚ずしお利甚しおいたす。

特城を抜出したはいいが、類䌌床マッチングはどうしたしょう。から曞くこずもできたすが、ちょっず凊理が重たくなりそうですね。

そんな時に䟿利なのがfaiss。怜玢ラむブラリの最匷さんです。これもfacebookが䜜っおるんですよ。PyTorchずいいfaissずいい、もうfacebookに足を向けお寝られたせん・・・。facebook自䜓は幎以䞊曎新しおないのですが

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、面癜そうですね

1