YOLO v8で笑い男もどきを実現

## 目的
映像に映った人の顔をリアルタイムで`笑い男`[^1]の画像に差し替えるプログラムを作成する
([この記事](https://qiita.com/k_sui_14/items/5386db1a118103b1828f) が面白そうだったので、それをYOLOでやったみた記事です)
[^1]:笑い男については、以下参照。https://ja.wikipedia.org/wiki/%E7%AC%91%E3%81%84%E7%94%B7_(%E6%94%BB%E6%AE%BB%E6%A9%9F%E5%8B%95%E9%9A%8A

## 環境
### HW
+ PC：Lenovo ThinkBook 13s Gen 3
+ カメラ：PC内蔵のインカメラ

### SW
+ Python：v3.11.5
+ Anaconda：conda v23.7.4
+ 物体検出モデル：YOLO v8

## 作業メモ
### 環境整備
<b>参考サイト</b>
https://qiita.com/marumen/items/481ffac7cc0c54948652

#### Anaconda の導入
以前、[光学迷彩もどきプログラム](https://qiita.com/marumen/items/481ffac7cc0c54948652) にて導入したため、そちらを参照

#### YOLO v8 の導入
同上

### YOLOで顔認識できるようにする
<b>参考サイト</b>
https://github.com/akanametov/yolo-face

[参考サイト](https://github.com/akanametov/yolo-face)に顔認識できるモデルが公開されていたので、そちらを使えるようにセットアップする

#### 顔認識モデルのダウンロード
+ `Installation` に書かれている内容をそのまま実施
```
# clone repo
$ git clone https://github.com/akanametov/yolov8-face

# pip install required packages
$ pip install ultralytics			# これは依然やったので省略

# go to code folder
$ cd yolov8-face
```

+ 顔認識用のモデルをダウンロード
[参考サイト](https://github.com/akanametov/yolo-face) から `yolov8n-face.pt` をダウンロード

#### 顔認識をお試し
※ Jupyter Notebook上で確認
```
$ from ultralytics import YOLO

$ model = YOLO("yolov8n-face.pt")
WARNING ⚠️ yolov8n-face.pt appears to require 'omegaconf', which is not in ultralytics requirements.
AutoInstall will run now for 'omegaconf' but this feature will be removed in the future.
Recommend fixes are to train a new model using the latest 'ultralytics' package or to run a command with an official YOLOv8 model, i.e. 'yolo predict model=yolov8n.pt'
requirements: Ultralytics requirement ['omegaconf'] not found, attempting AutoUpdate...
requirements: ❌ Command 'pip install --no-cache "omegaconf" ' returned non-zero exit status 101.
```
→ `omegaconf` というパッケージが足りない模様。将来的には削除されると書かれているが、とりあえずの対策としてインストールする

+ 不足パッケージをインストール
```
$ pip install omegaconf
Collecting omegaconf
・・・
Successfully installed antlr4-python3-runtime-4.9.3 omegaconf-2.3.0
Note: you may need to restart the kernel to use updated packages.
```

+ 顔認識を試す
```
# from ultralytics import YOLO

# model = YOLO("yolov8n-face.pt")

# results = model(0 , show=True)
```
<font color="red">→ webカメラの映像に対して、顔認識できる事を確認</font>

### 笑い男もどきプログラムを作成
<b>参考サイト</b>
http://ledmyway.seesaa.net/article/181382754.html

#### 考え方
+ (1) マスク用の画像1を作成
  + 顔認識できた範囲に笑い男画像[^2]をリサイズし(黒色の部分)、それ以外の背景(白色の部分)をWebカメラ画像のサイズになるように補完
+ (2) (1)の画像でWebカメラ画像をマスクする(`cv2.bitwise_and`)
+ (3) マスク用の画像2を作成
  + 顔認識できた範囲に笑い男画像をリサイズする
+ (4) (2)と(3)から黒の部分を消すように`cv2.bitwise_or`する
  + <font color="red">笑い男画像でのマスクができる</font>

[^2]: 画像は以下から拝借(http://ledmyway.seesaa.net/article/181382754.html)
  
![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248487/4eb16e82-6800-b663-6343-065d3b1d44c0.png)


#### プログラム
プログラムの説明は[考え方](#考え方)の項目を参照

```Python
#!/usr/bin/env python
# coding: utf-8

from ultralytics import YOLO
import cv2
import numpy as np

model = YOLO("yolov8n-face.pt")

cap = cv2.VideoCapture(0)

org_img_mask = cv2.imread("image\warai.png")

while True:
    ret, img_main = cap.read()
    results = model(img_main)

    for r in results:
        boxes_xyxy = r.boxes.numpy().xyxy
#        print(boxes_xyxy)
#        print(boxes_xyxy[0][0])
#        print(boxes_xyxy[0][1])
#        print(boxes_xyxy[0][2])
#        print(boxes_xyxy[0][3])

    # 顔認識できた場合に以下の処理を実施
    if r.boxes.numpy().cls.size > 0 and r.boxes.numpy().cls[0] == 0:

        # 笑い男画像を顔のサイズにリサイズ
        # intにしないとエラーになるので、小数点以下が切り捨てになる
        dsize_x = int(boxes_xyxy[0][2] - boxes_xyxy[0][0])
        dsize_y = int(boxes_xyxy[0][3] - boxes_xyxy[0][1])
        resize_img_mask = cv2.resize(org_img_mask, (dsize_x, dsize_y))

        # mask画像作成
        h, w = r.boxes.numpy().orig_shape[:2]

        th, th_resize_img_mask = cv2.threshold(resize_img_mask, 1, 255, cv2.THRESH_BINARY)
        th_resize_img_mask = cv2.bitwise_not(th_resize_img_mask)

        # pythonの除算はfloat型で返却らしいので、intへ型変換
        top = int(boxes_xyxy[0][1])
        bottom = (h - dsize_y) - top
        left = int(boxes_xyxy[0][0])
        right = (w - dsize_x) - left

        # webカメラ画像のサイズが合うように、mask画像を補完
        # img_main_mask("考え方"の(1)の画像)、img_mask("考え方"の(3)の画像)
        img_main_mask = cv2.copyMakeBorder(th_resize_img_mask, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(255,255,255))

        img_mask = cv2.copyMakeBorder(resize_img_mask, top, bottom, left, right, cv2.BORDER_CONSTANT, (0,0,0))    

        # 笑い男画像でのmaskを実行("考え方"の(2)の画像)
        img_main_masked = cv2.bitwise_and(img_main, img_main_mask)
    
        # お試しに顔認識している範囲を見える化
        cv2.rectangle(img_main_masked, (int(boxes_xyxy[0][0]), int(boxes_xyxy[0][1])), (int(boxes_xyxy[0][2]), int(boxes_xyxy[0][3])), (255, 255, 0))

        # "考え方"の(4)の画像
        img_last = cv2.bitwise_or(img_main_masked, img_mask)
    
    # 顔認識できない場合はWebカメラの画像をそのまま映す
    else:
        img_last = img_main.copy()

    cv2.imshow("img_last",img_last)
    
    # 処理を終了する場合は、Escキーで抜ける
    if cv2.waitKey(1) == 27:
        break
        
cv2.destroyAllWindows()
cap.release()
```
### デモ
実際に動作させたときのデモ動画が以下になる
+ プログラムを起動して、Webカメラ画像で顔認識できればその範囲が笑い男画像になる
+ プログラム終了は`Esc`キー

![output4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248487/68c771bf-be48-7ced-4acc-3787286fb13e.gif)


## まとめ
課題はあるが、目標達成！

>映像に映った人の顔をリアルタイムで`笑い男`の画像に差し替えるプログラムを作成する

課題としては以下が挙げられる
+ 人の動きに追従しきれず、顔認識ができないケースがある
(今回のデモでも一瞬笑い男画像が外れることがあった。ただし、マスクをしているため、顔認識の難易度が上がっている可能性もある)
+ 複数人の顔を笑い男に置き換えることはできない