今回はWeb会議ツールに依存しないバーチャル背景を作成してみようと思います。
使用するパッケージ名とバージョンは以下の通りです。
パッケージ名 | バージョン | 説明 |
---|---|---|
pyserial | 3.5 | シリアル通信を簡単に実装できるパッケージで、PCに接続されているカメラのポートIDを探索・取得するために使用します。 |
mediapipe | 0.8.11 | 人間の顔や身体、表情などの検出を簡単に行うことができるパッケージです。https://google.github.io/mediapipe/ |
OpenCV | 4.6.0 | カメラから画像を取得・処理するために使用します。 |
pyvirtualcam | 0.9.1 | 仮想カメラにフレームを送るために使用します。 |
これに加えて、仮想カメラ機能を標準で搭載しているOBS Studioも使用します。
データの流れ
実装
background_cover.py
import argparse
import numpy as np
from enum import Enum
import cv2
import serial.tools.list_ports
import mediapipe as mp
import pyvirtualcam
from pyvirtualcam import PixelFormat
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument("--width", help="cap width", type=int, default=1280)
parser.add_argument("--height", help="cap height", type=int, default=720)
parser.add_argument("--model-type", help="model type", type=int, default=1)
parser.add_argument("--score-th", help="score threshold", type=float, default=0.5)
parser.add_argument(
"--bg-color",
help="background color ex.'BLACK','GRAY','WHITE'",
type=str,
default="GRAY",
)
parser.add_argument(
"--bg-path", help="background image path", type=str, default=None
)
args = parser.parse_args()
return args
def serial_find():
ports = []
ports = list(serial.tools.list_ports.comports())
for p in ports:
print(p.name, "- Port ID : " + str(p.hwid[-1]))
print(f"Number of connected serial: {len(ports)}")
class bg_color(Enum):
BLACK = (0, 0, 0)
GRAY = (192, 192, 192)
WHITE = (255, 255, 255)
if __name__ == "__main__":
args = get_args()
serial_find()
port_id = input("select port ID : ")
if not port_id.isnumeric():
raise ValueError("error port ID")
# windows only
cap = cv2.VideoCapture(int(port_id), cv2.CAP_DSHOW)
print("cap.isOpened() :", cap.isOpened())
if not cap.isOpened():
raise ValueError("error opening cam port")
cap.set(cv2.CAP_PROP_FRAME_WIDTH, args.width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, args.height)
length = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
BG_COLOR = None
for color in bg_color:
if color.name == args.bg_color:
BG_COLOR = color.value
# create background image
if args.bg_path == None:
bg_image = np.zeros((height, width, 3), dtype=np.uint8)
bg_image[:] = BG_COLOR
else:
bg_image = cv2.imread(args.bg_path)
bg_image = cv2.resize(bg_image, (width, height))
mp_drawing = mp.solutions.drawing_utils
mp_selfie_segmentation = mp.solutions.selfie_segmentation
with mp_selfie_segmentation.SelfieSegmentation(
model_selection=args.model_type
) as selfie_segmentation:
with pyvirtualcam.Camera(width, height, fps=30, fmt=PixelFormat.BGR) as cam:
print(
f"Virtual cam started: {cam.device} ({cam.width}x{cam.height} @ {cam.fps}fps)"
)
count = 0
while cap.isOpened():
# Restart video on last frame
if count == length:
count = 0
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
ret, frame = cap.read()
if not ret:
raise RuntimeError("Error fetching frame")
frame = cv2.cvtColor(cv2.flip(frame, 1), cv2.COLOR_BGR2RGB)
frame.flags.writeable = False
results = selfie_segmentation.process(frame)
frame.flags.writeable = True
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
condition = (
np.stack((results.segmentation_mask,) * 3, axis=-1) > args.score_th
)
output_image = np.where(condition, frame, bg_image)
cam.send(output_image)
cam.sleep_until_next_frame()
count += 1
cap.release()
オプション引数
usage: background_cover.py [-h] [--width WIDTH] [--height HEIGHT] [--model-type MODEL_TYPE]
[--score-th SCORE_TH] [--bg-color BG_COLOR] [--bg-path BG_PATH]
options:
-h, --help show this help message and exit
--width WIDTH cap width
--height HEIGHT cap height
--model-type MODEL_TYPE
model type
--score-th SCORE_TH score threshold
--bg-color BG_COLOR background color ex.'BLACK','GRAY','WHITE'
--bg-path BG_PATH background image path
解説
1. PCに接続されているWebカメラのポート番号を検出(serial.tools.list_ports)
serial_find()
def serial_find():
ports = []
ports = list(serial.tools.list_ports.comports())
for p in ports:
print(p.name, "- Port ID : " + str(p.hwid[-1]))
print(f"Number of connected serial: {len(ports)}")
2. 指定したカメラから映像を取得
cap = cv2.VideoCapture(int(port_id), cv2.CAP_DSHOW)
print("cap.isOpened() :", cap.isOpened())
if not cap.isOpened():
raise ValueError("error opening cam port")
3. 取得した画像から人間を検出
results = selfie_segmentation.process(frame)
4. 検出された人間以外の部分にバーチャル背景を描画
condition = (
np.stack((results.segmentation_mask,) * 3, axis=-1) > args.score_th
)
output_image = np.where(condition, frame, bg_image)
5. 描画した画像を仮想カメラに出力
cam.send(output_image)
6. 任意のWeb会議ツールで仮想カメラを入力に指定
試しにteamsのデバイス設定から仮想カメラの映像を確認してみました。
結果
無事にWebカメラの映像から人間を検出して、バーチャル背景を適用することができました。
また、仮想カメラの映像を取得することでアプリに依存せずにバーチャル背景を作成することが可能になりました。
ただし、人間の検出精度がバーチャル背景の輪郭精度に大きく影響するため、今回使用したmediapipeはteamsやzoomに標準搭載されているバーチャル背景よりも精度は落ちました。
さいごに
今回は、「Web会議ツールに依存しないバーチャル背景」について解説しました。
目次は以下の記事からご覧になれます。