読んでください

OBS Studioを使ってZoomで笑い男になってみる

東大のある講義で、教授が自分の顔が少し微笑んでいるように見えるソフトを使って話していたので私もやりたくなった。

今は友人に薦められて攻殻機動隊Amazon Primeで見ており、笑い男さんがこのネタにぴったりだと思ったので私も笑い男さんになってみようじゃないか。
ただ、今回作ったものは笑い男さんが作ったものより何倍もレベルが低いので、その技術力に敬意を表し、笑い男「さん」と呼ばせていただく。

ちなみに攻殻機動隊STAND ALONE COMPLEXは初回放送が2002年で私が生まれた年である。

動作環境

MacBook Air (M1, 2020)

この先必要になるもの

都度インストールやセットアップの方法は述べていくが先にまとめておいた方が見やすいだろう。
Windowsでは動作確認していないのでお手数だが調べて欲しい。
OBS Studio (https://obsproject.com/)
Python3 (https://www.python.org/downloads/)
Homebrew (https://brew.sh/index_ja)
Zoomなど、自分の使いたいソフト

OBS Studioとは

OBS(Open Broadcaster Software)とはオープンソースで開発されている無料の画面配信・録画のソフトウェアである。
いろいろ機能があるらしいのだがまだ使いこなせていない。詳しいことはOBSのwiki(https://obsproject.com/wiki/)を見ていただくなどして、今回はこのソフトウェアの「仮想カメラ」を使っていく。

とりあえずはOBS Studioをインストールしていこう。
https://obsproject.com/にアクセスし、自分の使っているOSに適したものをダウンロードする。
Macの場合はdmgファイルがダウンロードされるのでダブルクリックして出てきたウィンドウでOBS.appをApplicationsのエイリアスにぶち込んでおけばOK。

Homebrewを使ってOpenCVをダウンロードする

HomebrewはMacで使えるパッケージマネージャーで、かなり便利。ターミナル.appを開いて(LaunchPadのその他にある)、以下をペースト。

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

homebrewの準備が終わったらOpenCVの準備をしていこう。
OpenCVとは画像処理のためのつよつよライブラリである。こちらも機能がめちゃくちゃ豊富だが今回はほんの一部しか使わない。
以下をターミナルにペースト。

brew install opencv


これでOpenCVの準備は完成。

Python3を準備

Pythonプログラミング言語である。めちゃくちゃ簡単に書けるので書いたことがない方は少し挑戦してみるといいと思う。機械学習分野でもホットであり、Pythonで大体のことができる。すごいね。

Python3のダウンロードページhttps://www.python.org/downloads/にアクセスしてインストールする。
ターミナルで

python3 -c "print('hello world')"

を実行してhello worldと出力されればOK。

Python3でOpenCVを使うために以下のコマンドを実行。

python3 -m pip install opencv-python opencv-contrib-python


これで全ての準備が揃った。それではプログラムを書いていこう。

Pythonでプログラムを書く

手っ取り早く試したいので今回はOpenCVの分類器を使って顔検出をする。
https://github.com/opencv/opencv/tree/master/data/haarcascadesにアクセスするといくつかファイルがある。
haarcascade_frontalface_default.xmlをクリック。
rawと書かれたボタンを押して開いたページで右クリックすると(control押しながらクリックでもOK)、
別名で保存というオプションが出てくるのでクリック。
face_cascade.xmlなど適当な名前をつけて保存。

smile-man-zoomなどと適当な名前のフォルダを作って先ほどダウンロードしたface_cascade.xmlを入れておく。

今回は笑い男になりたいので笑い男の画像を探してこよう。Googleで検索すれば何個か出てくる。
このままだと背景が白で邪魔なので私はペイントアプリで透明に塗りつぶした。

著作権に配慮してここでは配布しないから自分で頑張って。

smileface.pngなどと名前をつけておいて、smile-man-zoomフォルダにぶち込んでおこう。

次にsmile-man-zoomフォルダ内にsmile.pyという名前のファイルを作成する。
ソフトはなんでもいいんだけど、私はVSCodeを愛用しているので布教しておこう。
VSCode (https://azure.microsoft.com/ja-jp/products/visual-studio-code/)

以下のコードをコピペ

import cv2

face_cascade = cv2.CascadeClassifier("./face_cascade.xml") 
camera = cv2.VideoCapture(0)

smile_face = cv2.imread("./smileface.png", cv2.IMREAD_UNCHANGED) # α値まで取得

MAX_FRAME = 10 # これ以下では取得できなくても前回の位置に画像を貼る。
k = 0
image_detected = False
last = [] # 最後に取得された位置

rate = 1.5 # 少し大きめに画像を貼る

# ループ中にフレームを1枚ずつ取得(qキーで撮影終了)
while True:
    ret, img = camera.read()
    img = cv2.flip(img, 1)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    faces = face_cascade.detectMultiScale(gray,minSize=(100,100))
    image_detected = False

    #モザイク
    for x, y, w, h in faces:
        #image detected
        image_detected = True
        k = 0

        # 少し大きめに取得したいのでrate倍に調整
        if x - w * (rate - 1) / 2 >= 0:
            x = x - w * (rate - 1) / 2 
        else:
            x = 0
        if y - h * (rate - 1) / 2 >= 0:
            y = y - h * (rate - 1) / 2 
        else:
            y = 0
        
        if x + rate * w < img.shape[1]:
            w = w * rate
        else:
            w = img.shape[1] - x
        
        if y + rate * h < img.shape[0]:
            h = h * rate
        else:
            h = img.shape[0] - y
        x, y, w, h = int(x), int(y), int(w), int(h)
        last = (x, y, w, h) # ストレージしておく

        smile = cv2.resize(smile_face, (w, h))
        img[y:y+h, x:x+w] = img[y:y+h, x:x+w] * (1 - smile[:, :, 3:] / 255) + smile[:, :, :3] * (smile[:, :, 3:] / 255) # α値を考慮した足し算
        

    if not image_detected:
        k += 1
        #顔が検出されなくてもMAX_FRAMEまでは同じ場所に画像を出しておく
        if k < MAX_FRAME and len(last) > 0:
            x, y, w, h = last
            smile = cv2.resize(smile_face, (w, h))
            img[y:y+h, x:x+w] = img[y:y+h, x:x+w] * (1 - smile[:, :, 3:] / 255) + smile[:, :, :3] * (smile[:, :, 3:] / 255)
    
    cv2.imshow('smile face', img) # 画像を表示

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
 
camera.release()
cv2.destroyAllWindows()

これで準備完了。

動作確認

再びターミナルを開き、今作業していたフォルダを開く。
cdと入力した後に一つ空白を開けて、Finderからフォルダをターミナルにドラッグしてくれば開ける。

python3 smile.py

と実行すると自分の顔に笑い男のアイコンが被さった動画が映し出されるはずだ。
qキーを入力すると終了できる。

Zoomで使う

ここまで来れば後もう少し。OBS.appを開いてソースと書かれたセクションの+ボタンを押す。
ウィンドウキャプチャを選択し、ウィンドウを[python3] smile faceにする。

出てきた画面をいい感じに枠に合わせれば設定完了。

普段Zoomの画面を反転させて使用しているならOBS上でも画面を反転させる必要がある。
右クリック→変換→水平反転を押せば良い。

あとは仮想カメラ開始を押す。

Zoomを開いてカメラのソースをOBS Virtual Cameraに変更する(ビデオマークの横の^を押すと出てくる)。
これでZoom上で笑い男さんになれたはずである。

f:id:manato1fg:20220214150855p:plain
笑い男になった僕

YuNetを使って顔検出を行う

ここまでで笑い男さんにはなれるのだが、精度が低いと不満を持っている方もいると思う。
というわけで新しく追加されたOpenCVAPIを使ってさらに精度を上げて笑い男さんになってみよう。
https://github.com/opencv/opencv_zoo/tree/master/models/face_detection_yunetからコードとモデルを拝借して書いてみる。
yunet.pyとface_detection_yunet_2021dec.onnxをsmile-man-zoomフォルダに入れる。

長いのでyunet.onnxとリネームしておいた。

smile2.pyを作成して以下のコードをコピペする。

import cv2
from yunet import YuNet
import numpy as np

camera = cv2.VideoCapture(0)
width = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))
model = YuNet("./yunet.onnx", inputSize=(width, height), confThreshold=.8)


smile_face = cv2.imread("./smileface.png", cv2.IMREAD_UNCHANGED)

MAX_FRAME = 30 # これ以下では取得できなくても前回の位置に画像を貼る。
k = 0
image_detected = False
last = [] # 最後に取得された位置

rate = 1.5

# 撮影=ループ中にフレームを1枚ずつ取得(qキーで撮影終了)
while True:
    ret, img = camera.read()
    img = cv2.flip(img, 1)
    results = model.infer(img)
    image_detected = False

    for det in (results if results is not None else []):
        bbox = det[0:4].astype(np.int32)
        x, y, w, h = bbox[0], bbox[1], bbox[2], bbox[3]

        w = max([w, h])
        h = max([w, h]) # 正方形に変換

        #image detected
        image_detected = True
        k = 0

        # 少し大きめに取得したいので少しrate倍に調整
        if x - w * (rate - 1) / 2 >= 0:
            x = x - w * (rate - 1) / 2 
        else:
            x = 0
        if y - h * (rate - 1) / 2 >= 0:
            y = y - h * (rate - 1) / 2 
        else:
            y = 0
        
        if x + rate * w < img.shape[1]:
            w = w * rate
        else:
            w = img.shape[1] - x
        
        if y + rate * h < img.shape[0]:
            h = h * rate
        else:
            h = img.shape[0] - y
        x, y, w, h = int(x), int(y), int(w), int(h)
        last = (x, y, w, h)
        smile = cv2.resize(smile_face, (w, h))
        img[y:y+h, x:x+w] = img[y:y+h, x:x+w] * (1 - smile[:, :, 3:] / 255) + smile[:, :, :3] * (smile[:, :, 3:] / 255)

    if not image_detected:
        k += 1
        if k < MAX_FRAME and len(last) > 0:
            x, y, w, h = last
            smile = cv2.resize(smile_face, (w, h))
            img[y:y+h, x:x+w] = img[y:y+h, x:x+w] * (1 - smile[:, :, 3:] / 255) + smile[:, :, :3] * (smile[:, :, 3:] / 255)
    
    cv2.imshow('smile face', img)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
 
camera.release()
cv2.destroyAllWindows()

おそらくnumpyがないと怒られるので、ターミナルで

python3 -m pip install numpy

と入力してから、

python3 smile2.py

を実行する。あとは先述した手順でOBSから読み込めば良い。

体感だがface cascadeを使うよりも精度がいい感じがする。お好みということで。

GCPのGPUでTensorflowを動かす

自分でGPUを買うお金はないのでGCPでサクッと機械学習をしたいなと思ったが、Linuxを初めて触ったのでかなり手こずった。備忘録として残す。

 

この記事の対象者: 初心者

VMインスタンスの作成

無料枠だとA2シリーズは許可がもらえず、N1シリーズでもV100は作れなかったのでNVIDIA Tesla P4を選択した。初めてGPUを使うときは割り当てを増やす申請をしなくてはならないが結構すぐに許可をくれる。

ブートディスクはLinux18.04の50GBにした。この辺は用途に合わせて上手に設定する。

 


f:id:manato1fg:20210915232244p:plain

VMインスタンスの設定画面

これでVMインスタンスの作成は完了。数分すると起動するようになる。

 

CLIの設定

cloud.google.com

このページを参考にCLIでいじれるようにする。ブラウザでもアクセスできるが、なかなか繋がりづらい時があって不便だったのでなるべくCLIを使おうと思った。

 

「リモート アクセス」のドロップダウンで「gcloud コマンドを表示」を押してコピペすれば一瞬でアクセスできる。

 

GPU周りの設定

qiita.com

こちらの記事を参考にしてGPU周りの設定をしていく。

うまく設定できていれば


f:id:manato1fg:20210915233329p:plain

結果

こんな風になると思う。

 

Docker周りの設定

www.tensorflow.org

ここを参考に設定していく。

まずはdockerイメージのダウンロード。

sudo docker pull tensorflow/tensorflow:latest-gpu

続いて起動。

sudo docker run --gpus all -it --rm tensorflow/tensorflow:latest-gpu

Google DriveCLIから使えるようにする

Google Driveだと爆速でファイルの管理ができる。
qiita.com
こちらを参考に設定。ただ、私の環境のせいか、curlコマンドがうまく動いてくれなかったのでブラウザでダウンロードしてからdockerイメージ内に送った。

sudo docker cp gdrive-linux-x64 <コンテナID>:/home/gdrive

あとは参考ページ通り。

まとめ

こんな感じでセットアップできる。
なお、デフォルトではvimが入っていないので

apt-get install vim

をして、インストール。
また、GCPを閉じて自分は他のことをしたいときはtmuxを使う。