openCVの顔認識で自分の顔をTwitterアイコンに置き換えてZOOMをする

久しぶりの投稿になります。
facerigなどで簡単にバ美肉はできますが、自分の顔だけを他の画像に置き換える方法がググっても見つからなかったのでササっと作りました。
100%顔を置き換える保証はないので内輪ネタとしてお使いください。

表示される画像はこんな感じ。
![ANf8EAbN.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508704/29856705-58f3-28ee-c953-2ac63d3f2b08.png)

使用環境はWindows10、Python3.6.5です。
このプログラムと置き換えたい画像と顔認識モデルのxmlファイルは同じファイルに入れてください。

参考にさせていただいたサイトはこちらです。

Haar Cascadesを使った顔検出
http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_objdetect/py_face_detection/py_face_detection.html
OpenCVで画像上に別の画像を描画する
https://note.com/npaka/n/nddb33be1b782
Python+OpenCVでWebカメラの画像を取り込んで処理して表示する話
https://ensekitt.hatenablog.com/entry/2017/12/19/200000

では作っていきましょう。
完成したコードは一番下に載せてあるので、それだけ見たい方は下までスクロール。


<br>
<h1>必要なライブラリのインポート</h1>

```python
# OpenCVのインポート
import cv2
import numpy as np
from PIL import Image
```
もしインストールしていないライブラリがあれば各自インストールしてください。
コマンドプロンプトを開き、

```
pip install opencv-python
pip install numpy
pip install Pillow
```
と入力すれば入れられるはず。
<br>
<h1>諸々の設定</h1>

```python
# カメラの解像度設定
WIDTH = 1920
HEIGHT = 1080

# 読み込みする画像の指定
img = "vtuber_may.jpg"
cv2_img = cv2.imread(img, cv2.IMREAD_UNCHANGED)

#顔認識モデルの指定
face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_alt2.xml')

# 引数でカメラを選べれる。
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)

```
顔認識のカスケード型分類器は自分で作成してもいいですが、面倒だったのでOpencv提供の分類器を使いました。pip install opencvでopencvをインストールした際に付属されてます。
僕の環境では C:\Users\{ユーザ名}\AppData\Local\Programs\Python\Python36-32\Lib\site-packages\cv2\data フォルダ内にありました。

<br>
<h1>画像を合成する関数</h1>

```python
def overlayImage(src, overlay, location, size):
    overlay_height, overlay_width = overlay.shape[:2]

    # webカメラの画像をPIL形式に変換
    src = cv2.cvtColor(src, cv2.COLOR_BGR2RGB)
    pil_src = Image.fromarray(src)
    pil_src = pil_src.convert('RGBA')

    # 合成したい画像をPIL形式に変換
    overlay = cv2.cvtColor(overlay, cv2.COLOR_BGRA2RGBA)
    pil_overlay = Image.fromarray(overlay)
    pil_overlay = pil_overlay.convert('RGBA')
    #顏の大きさに合わせてリサイズ
    pil_overlay = pil_overlay.resize(size)

    # 画像を合成
    pil_tmp = Image.new('RGBA', pil_src.size, (255, 255, 255, 0))
    pil_tmp.paste(pil_overlay, location, pil_overlay)
    result_image = Image.alpha_composite(pil_src, pil_tmp)

    # OpenCV形式に変換
    return cv2.cvtColor(np.asarray(result_image), cv2.COLOR_RGBA2BGRA)
```
Opencvでは画像を合成できないので、PIL形式に変換してから画像を合成する関数を作ります。
ついでに顔の高さと幅にあわせてリサイズもします。
<br>
<h1>メイン関数</h1>

```python
def main(): 
    # 顔認識の左上角の座標を格納する変数
    x = 0
    y = 0

    # 顔の幅と高さを格納する変数
    w = 0
    h = 0
    while True:
        # VideoCaptureから1フレーム読み込む
        ret, frame = cap.read()

        #フレームをグレー形式に変換し、顔認識
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        face = face_cascade.detectMultiScale(gray, 1.3, 5)

        # 顔が認識されていればx, y, w, hを更新
        if face != ():
            (x, y, w, h) = face[0]

        # 変数が初期値以外ならフレームに画像を合成
        if w != 0:
            frame = overlayImage(frame, cv2_img, (x, y), (w, h))


        # 加工したフレームを表示する
        cv2.imshow('Frame', frame)

        # キー入力を1ms待って、ESCを押されたらBreakする
        k = cv2.waitKey(1)
        if k == 27:
            break

    # キャプチャをリリースして、ウィンドウを閉じる
    cap.release()
    cv2.destroyAllWindows()
```
webカメラから1フレーム読み込み、その画像に対して処理を行います。
フレームによっては顔を認識しないこともあるので、認識しなかった場合は以前認識した位置に合成するようにしています。
もし画面内に二人以上の顔が認識された場合はどちらかの顔にしか画像は合成されないので注意。
改良しようかとも思いましたが、そもそも二人同時にZOOMに映ることなんでまずないのでその改良はしませんでした。
<br><br>
<h1>メイン関数を実行する処理</h1>

```python
if __name__ == '__main__':
    main()
```
<br><br>
<h1>まとめるとこんな感じ</h1>

```python
# OpenCVのインポート
import cv2
import numpy as np
from PIL import Image

# カメラの解像度設定
WIDTH = 1920
HEIGHT = 1080

# 読み込みする画像の指定
img = "vtuber_may.jpg"
cv2_img = cv2.imread(img, cv2.IMREAD_UNCHANGED)

#顔認識モデルの指定
face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_alt2.xml')

# 引数でカメラを選べれる。
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)

def overlayImage(src, overlay, location, size):
    overlay_height, overlay_width = overlay.shape[:2]

    # webカメラの画像をPIL形式に変換
    src = cv2.cvtColor(src, cv2.COLOR_BGR2RGB)
    pil_src = Image.fromarray(src)
    pil_src = pil_src.convert('RGBA')

    # 合成したい画像をPIL形式に変換
    overlay = cv2.cvtColor(overlay, cv2.COLOR_BGRA2RGBA)
    pil_overlay = Image.fromarray(overlay)
    pil_overlay = pil_overlay.convert('RGBA')
    #顏の大きさに合わせてリサイズ
    pil_overlay = pil_overlay.resize(size)

    # 画像を合成
    pil_tmp = Image.new('RGBA', pil_src.size, (255, 255, 255, 0))
    pil_tmp.paste(pil_overlay, location, pil_overlay)
    result_image = Image.alpha_composite(pil_src, pil_tmp)

    # OpenCV形式に変換
    return cv2.cvtColor(np.asarray(result_image), cv2.COLOR_RGBA2BGRA)


def main():
    # 顔認識の左上角の座標を格納する変数
    x = 0
    y = 0

    # 顔の幅と高さを格納する変数
    w = 0
    h = 0
    while True:
        # VideoCaptureから1フレーム読み込む
        ret, frame = cap.read()

        #フレームをグレー形式に変換し、顔認識
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        face = face_cascade.detectMultiScale(gray, 1.3, 5)

        # 顔が認識されていればx, y, w, hを更新
        if face != ():
            (x, y, w, h) = face[0]

        # 変数が初期値以外ならフレームに画像を合成
        if w != 0:
            frame = overlayImage(frame, cv2_img, (x, y), (w, h))


        # 加工したフレームを表示する
        cv2.imshow('Frame', frame)

        # キー入力を1ms待って、ESCを押されたらBreakする
        k = cv2.waitKey(1)
        if k == 27:
            break

    # キャプチャをリリースして、ウィンドウを閉じる
    cap.release()
    cv2.destroyAllWindows()

if __name__ == '__main__':
    main()
```

あとはこのプログラムで表示されたwebカメラの画像をOBS Studioで読み込み、virtualcamで仮想カメラとして出力させればいいだけです。
ここでは解説しませんが、こちらのサイトなどを見て設定をしてください。

OBS を仮想カメラとして認識させる VirtualCam を利用する方法 
https://loumo.jp/archives/24912

![43LjUnYe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508704/e918dc86-d270-3820-3213-21945c2ca5b2.png)
Zoomで使用するとこんな感じ。オタク顔をTwitterのアイコンで隠してミーティングができますね。
<br>
<h1>さいごに</h1>

もっとコードの解説をするつもりで少しづつ分けてコード載せたんですが、解説はコメントでほとんど書いてあるしコメント以外のことはopencvのカスケード型分類器の場所くらいしか書くことありませんでした。