Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

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

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

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

前提

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

今回の構成

始めに結論を述べますが,完全な仮想カメラの自作は難しかったので,先ほども話に上がったOBSに搭載されている仮想カメラに自作ソフトウェアの入力を流す形で実現しました.
構成図は以下のようになります.
構成図.png
本格的な仮想カメラの自作には,Direct Show APIといったグラフィック関連の低レベルAPIを勉強する必要がありそうです.それはそれで興味深いと思ったので,今度やってみようと思います.

実装

入力を取得し,毎フレーム表示する

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

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というパッケージを利用させてもらいます.
GitHubリポジトリにもあるように,pipが入っている環境だとpip install pyvirtualcamで入れることができます(が,現行のリポジトリには不具合があり,ソースを再ビルドしないとバグが発生する可能性があります.これの対策については本記事の一番最後に載せておきます.).

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

  1. OBSをインストールする(勝手にインストールされます)
  2. 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の方法を試してください.

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

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用プラグインがあったので,これを利用すればWindowsの方法と同じようにできるかも....しれません(Windowsの方法に書かれたリポジトリに書いてありました)

Linuxの場合

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

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のバグとその一応の解決方法について紹介しようと思います.

付録

エフェクト例:グリーンバックの背景抜き

ここまでのサンプルコードの中で# ここで何らかのエフェクトをかけるとした部分の下に追記してください.

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で,以下の関数を追記します.

void stop(){
    virtual_output_stop();
}

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

Asalato
UnityでCG作ったりフロントやったり基幹システムやったりしています。あとハードウェアとかWeb開発とかも。 世界で自分くらいにしか需要がなさそうな記事を投稿することでウェブの多様性に貢献していき
https://portfolio.asalato.net
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away