Python+OpenCVで仮想カメラを自作しオリジナルエフェクトをかける

仮想カメラとは，Webカメラのような画像入力デバイスとしてPCに認識させることのできるソフトウェアの事です．
これを利用することにより，最近よく用いられるZoom，WebEx，Meetといった<b>ビデオ会議システム</b>に（フロントカメラの入力ではなく）好きな映像を流すことができます．

一般的に用いられる仮想カメラには[OBS Studio](https://obsproject.com/ja)があり，私もよく利用しています（最近プラグインなしの標準で仮想カメラが使えるようになったらしいですね）．しかしこれは機能が非常に充実している分，とんでもなく重いためあまり日常的に利用したくはありません．（RTX2028 SUPERでGPU使用率8割くらいになるんですが私だけですかね....？）

そこで，自分でも仮想カメラを作成し，エフェクトをかけられるのではないかと思い，いろいろと試行錯誤をしてみました．まだ原因不明のバグがありますが，一応は動くようになったので紹介したいと思います．

# 前提
<b>このシステムは`Windows`を対象にしています．</b>
グラフィックの低レイヤーを扱う関係で，`Mac`や`Linux`といった別OSでは手段が大きく異なります．
調査をする中で別OSで利用できそうなツールも見かけたので紹介はしようと思いますが，未検証なので信用しないでください．

# 今回の構成
始めに結論を述べますが，完全な仮想カメラの自作は難しかったので，先ほども話に上がった<b>OBS</b>に搭載されている仮想カメラに自作ソフトウェアの入力を流す形で実現しました．
構成図は以下のようになります．
![構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433211/38676224-a785-3758-7a26-e381d7dde0dd.png)
本格的な仮想カメラの自作には，`Direct Show API`といったグラフィック関連の低レベルAPIを勉強する必要がありそうです．それはそれで興味深いと思ったので，今度やってみようと思います．

# 実装
## 入力を取得し，毎フレーム表示する
まずは一般的な話ですが，`OpenCV`を利用してデータソースから毎フレーム画像を取得し，表示する部分の実装を行います．すごく一般的な部分なのでそれぞれの処理はコード中に書くのみとします．

```py
import cv2

# 取得するデータソース（Webカメラ）を選択
cap = cv2.VideoCapture(0)

while True:
    # 各フレームの画像を取得
    ret, frame = cap.read()

    # ここで何らかのエフェクトをかける

    # 画像を表示
    # 表示先を仮想Webカメラにしたい
    cv2.imshow("Window Name", frame)

    # 30フレーム表示して，Enterキーが入力されたら表示を終了する
    if cv2.waitkey(30) == 13:
        break

# 終了処理
cv2.destroyAllWindows()
cap.release()
```

これでWebカメラの映像をウィンドウで表示する部分の実装ができました．
次にこの表示をOBSの仮想カメラに入力することを考えます．

## 仮想カメラに情報を流す
### Windowsの場合
[pyvirtualcam](https://github.com/letmaik/pyvirtualcam)というパッケージを利用させてもらいます．
GitHubリポジトリにもあるように，pipが入っている環境だと`pip install pyvirtualcam`で入れることができます（が，現行のリポジトリには<b>不具合</b>があり，ソースを再ビルドしないとバグが発生する可能性があります．これの対策については本記事の一番最後に載せておきます．）．

これも↑のリポジトリページに書いてありますが，すでにOBS（にVirtual Camera）が入っているなら行う必要はありませんが，OBSの仮想Webカメラを有効にする必要があります．
方法は2つあり，

1. OBSをインストールする（勝手にインストールされます）
1. https://github.com/CatxFish/obs-virtual-cam/releases からVirtual Camera部分だけをダウンロードし，リポジトリで`egsvr32 /n /i:1 "obs-virtualcam\bin\64bit\obs-virtualsource.dll"`と打って有効化する

1の方法ですが，私は確認できていません．最近のOBSではVirtual Cameraが標準で入ったのでこれだけで大丈夫かなと思うのですが，もし何らかのエラーが出るようなら2の方法を試してください．


パッケージをインポートしたうえで，上のコードを以下のように置き換えます．

```py
import cv2
import pyvirtualcam

# 取得するデータソース（Webカメラ）を選択
cap = cv2.VideoCapture(0)

# 最初のフレームから画像のサイズを取得
ret, frame = cap.read()
with pyvirtualcam.Camera(width=frame.shape[1], height=frame.shape[0], fps=30, delay=0) as cam:
    while True:
        # 各フレームの画像を取得
        ret, frame = cap.read()

        # ここで何らかのエフェクトをかける

        # 色空間を変更
        # αチャンネルを有効にして，RGB順にする
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)

        # 画像を仮想カメラに流す
        cam.send(frame)

        # 画像をスクリーンに表示しなくなったので，pyvirtualcamの機能を使って次のフレームまで待機する
        cam.sleep_until_next_frame()

# 終了処理
cap.release()
```

これでOBSの仮想カメラに画像を流せるようになりました．
キー入力で終了する方法がなくなったので`Ctrl + C`などで終了してください．

あとはWeb会議システム側で利用するWebカメラを`OBS-Camera`に指定すると，このプログラムで取得したWebカメラの映像が表示されるはずです．
まだこの状態ではWebカメラの映像をただ垂れ流しているだけなので，ここからエフェクトをかけていきます．

### Mac，Linuxの場合（未確認）
#### Macの場合
[OBSVirtualCameraのMac用プラグイン](https://github.com/johnboiles/obs-mac-virtualcam)があったので，これを利用すればWindowsの方法と同じようにできるかも....しれません（Windowsの方法に書かれたリポジトリに書いてありました）

#### Linuxの場合
Linuxには，[v4l2loopback](https://github.com/umlaeute/v4l2loopback)という仮想カメラを簡単に作成できるライブラリがあります．これを利用した[pyfakewebcam](https://github.com/jremmons/pyfakewebcam)のようなpythonラッパーも多く存在するので，これらを利用すればWindowsよりはるかに簡単に実装できると思います．
確認はしていませんが，`pyfakewebcam`を利用したい場合，上のコードを以下のように変更すればよさそうです．

```py
import cv2
import pyfakewebcam

# 取得するデータソース（Webカメラ）を選択
cap = cv2.VideoCapture(0)

# 最初のフレームから画像のサイズを取得
ret, frame = cap.read()
camera = pyfakewebcam.FakeWebcam('/dev/video1', frame.shape[1], frame.shape[0])
while True:
    # 各フレームの画像を取得
    ret, frame = cap.read()

    # ここで何らかのエフェクトをかける

    # 色空間を変更
    # RGB順にする
    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

    # 画像を仮想カメラに流す
    camera.schedule_frame(frame)

    # 画像をスクリーンに表示しなくなったので，次のフレーム（30fps）まで待機する
    time.sleep(0.033)

# 終了処理
cap.release()
```

# おわりに
PythonとOpenCVを利用して仮想Webカメラを自作する方法を紹介しました．
エフェクトに関してはここでは述べませんでしたが，OpenCVの各機能を使って好きなエフェクトをかけることができます．
最後に，エフェクトのサンプルとしてグリーンバックの背景抜きの処理と上で述べたpyvirtualcamのバグとその一応の解決方法について紹介しようと思います．

# 付録
## エフェクト例：グリーンバックの背景抜き
ここまでのサンプルコードの中で`# ここで何らかのエフェクトをかける`とした部分の下に追記してください．

```py
frame = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
# 第2変数と第3変数はHSV色空間の最小値と最大値で，各環境で適宜変更
mask = cv.inRange(frame, (48, 158, 32), (88, 255, 255))
mask_inv = cv.bitwise_not(mask)
bg = np.zeros(frame.shape, dtype=np.uint8)
# 背景にしたい色を設定
bg[:] = [255, 255, 255]
front_result = cv.bitwise_and(frame, frame, mask=mask_in)
back_result = cv.bitwise_and(bg, bg, mask=mask)
frame = cv.add(front_result, back_result)
```

## pyvirtualcamの修正
これはIssueにも建てられていたのですが，どうやら終了処理が記述されていないようです．
リポジトリを各自でクローンし，修正箇所を直したうえで`setup.py`を実行することでビルドとインストールができます．
修正箇所は，`pyvirtualcam/pyvirtualcam/native_windows/main.cpp`で，以下の関数を追記します．

```cpp
void stop(){
    virtual_output_stop();
}
```

これで正しく関数が呼び出され，終了処理が正しく呼ばれる....と思います．
でもなぜか時々OBS-Cameraがブロックされ，再起動しないと治らなくなるバグが残っており原因を調査しています．