たった1枚の画像から顔認識してみる (在室検知)

この記事は [coins Advent Calendar 2023](https://adventar.org/calendars/9226) 10日目の記事です。

どうも、Nameless です。最近は AI が流行りのようですね。今回は割と実用的な AI の使い方を紹介します。

## 導入

AI を活用したい事例として、顔認識があると思います。ここでは、「ある写真から写っている人物を顔から判別する」ことを考えます。

しかしこんなことを聞いたことがあるでしょう。「AI の学習には、大量のデータ(この場合は学習させたい人物の写真) が必要である！」と。

これは確かに正しいのですが、今回のようなタスクだと実は必要ありません。**問題設定を適切にすることで、追加学習なしでたった一枚の画像から人物を識別できるようになります。**

## 今回想定するユースケース

- **AI を利用して在室検知したい**状況を考えます。
- web カメラから部屋を定期的に撮影し、誰が写っているのかを判別して記録します。
- 検知したい人物の顔写真は 1 枚程度しか持っていないものとします。


## 実装

目標は 1 枚の画像から、そこに映る人物が誰なのかを特定することです。しかし、私たちは判別したい人物の顔写真を何万枚も持っていません (逆にそれだけの写真を持っていたら怖いですね(笑))。よって、分類問題として AI にゼロから学習させるのは不可能でしょう。では、回帰問題としてならどうでしょうか？ここで、顔をベクトルに変換することを考えます。

```mermaid
graph LR
A((顔画像)) --> B{{機械学習\nモデル}} --> C((ベクトル))
```
ここで、同じ人物の顔画像から変換されたベクトル同士のコサイン類似度が 1 になるように、また別の人物の顔画像から変換されたベクトル同士のコサイン類似度が 0 になるように学習させるとどうなるでしょうか？こうすると、AI は渡された画像から人物ごとの普遍的な特徴を精確に捉えたベクトルを出力できるようになります。こうすることで、データセットに含まれていない人物に対してもたった 1 枚の画像を事前に用意すれば同一人物かどうかの判定が出来てしまうのです。

形的には、事前に用意した識別したい人物の顔写真から出来たベクトルと、推論させたい顔写真から出来たベクトルを照合します。これが 1 に近ければ同一人物、0 に近ければ別人として判定します。

とはいえ自分で 0 から学習させるのは骨が折れるので、事前学習済みモデルを使います。今回は insightface というモデルを使用しました。(ちなみにインストールがちょっと大変です)

以下はwebカメラから画像を取得して顔認識し、json に書き込むコードです。

```python:main.py
import numpy as np
import cv2
from insightface.app import FaceAnalysis
import db
import time
import os

app = FaceAnalysis()
app.prepare(ctx_id=1, det_size=(640, 640))

def cos_sim(feat1, feat2):
    return np.dot(feat1, feat2) / (np.linalg.norm(feat1) * np.linalg.norm(feat2))

faces_dir = './faces'
folders = {}
with os.scandir(faces_dir) as fs:
    for entry in fs:
        if entry.is_dir():
            folders[entry.name] = []
            with os.scandir(faces_dir+'/'+entry.name) as folder:
                for face in folder:
                    img = cv2.imread(faces_dir+'/'+entry.name+'/'+face.name)
                    tmp = app.get(img)
                    vec = tmp[0].embedding
                    folders[entry.name].append(vec)

def match(vec):
    ans = [0, -1]
    for name, faces in folders.items():
        for face in faces:
            if cos_sim(vec, face) > ans[1]:
                ans = [name, cos_sim(vec, face)]
    if ans[1] > 0.5:
        return ans[0]
    else:
        return -1

capture = cv2.VideoCapture(0)

while True:
    ret, flame = capture.read()

    if(ret == False):
        break

    embs = app.get(flame)

    for emb in embs:
        vec = emb.embedding
        m = match(vec)
        if m != -1:
            db.write(m, int(time.time()))
            print(m, int(time.time()))

    detect = app.draw_on(flame, embs)
    cv2.imshow("flame", detect)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
```
```python:db.py
import json
import time
import datetime
import zoneinfo
import os

db_path = './db/'

def write(user_id: str, date: int):
    d = datetime.datetime.fromtimestamp(date, datetime.timezone.utc).astimezone(datetime.timezone(datetime.timedelta(hours=9)))
    file_name = db_path+f'{str(d)[:10]}.json'
    if not os.path.isfile(file_name):
        with open(file_name, mode='w') as f:
            f.write(r'{}')
    with open(file_name) as f:
        db = json.load(f)
    if user_id not in db.keys():
        db[user_id] = []
    db[user_id].append(date)
    with open(file_name, 'w') as f:
        json.dump(db, f, indent=2)
```

検出したい人物の登録には特に難しい操作は必要なく、`faces / [検出したい人物の名前] / hogehoge.jpg` を作成することで簡単にいくらでも顔認識の対象者を登録できるようにしています。以下は実際のフォルダです。(私以外の名前は伏せています)

<img width=60% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3571388/5b3e5dbb-4f9b-dd9b-4584-ad5bb486019d.png">

`db` フォルダに自動で日付ごとに json ファイルが作成されます。

<img width=60% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3571388/1b69bafa-3822-4291-7380-585d38901ae1.png">

以下は実際の json ファイルの中身です。UNIX 時間で記録しています。(私以外の名前は伏せています)

<img width=60% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3571388/71d5b3b1-7b54-6a7d-3138-5ccd55028882.png">

ここまでの実装を、以下に置いています。

https://github.com/Nameless-itf23/face_detection

あとはこの json ファイルをパースしていい感じに表示するアプリを作れば、無事目標達成です。

## おわりに

今回は、追加学習なしで 1 枚の画像から顔認識をする方法を紹介しました。問題設定次第で目的を比較的簡単に達成できるようになるのはとても面白いですね。