LoginSignup
2
0

More than 1 year has passed since last update.

【画像処理】Web会議ツールに依存しないバーチャル背景

Posted at

今回はWeb会議ツールに依存しないバーチャル背景を作成してみようと思います。

out_2.gif

使用するパッケージ名とバージョンは以下の通りです。

パッケージ名 バージョン 説明
pyserial 3.5 シリアル通信を簡単に実装できるパッケージで、PCに接続されているカメラのポートIDを探索・取得するために使用します。
mediapipe 0.8.11 人間の顔や身体、表情などの検出を簡単に行うことができるパッケージです。https://google.github.io/mediapipe/
OpenCV 4.6.0 カメラから画像を取得・処理するために使用します。
pyvirtualcam 0.9.1 仮想カメラにフレームを送るために使用します。

これに加えて、仮想カメラ機能を標準で搭載しているOBS Studioも使用します。

データの流れ

構成図.png

実装

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のデバイス設定から仮想カメラの映像を確認してみました。
teams_サンプル.png

結果

無事にWebカメラの映像から人間を検出して、バーチャル背景を適用することができました。
また、仮想カメラの映像を取得することでアプリに依存せずにバーチャル背景を作成することが可能になりました。
ただし、人間の検出精度がバーチャル背景の輪郭精度に大きく影響するため、今回使用したmediapipeはteamsやzoomに標準搭載されているバーチャル背景よりも精度は落ちました。

sunshine.gif

さいごに

今回は、「Web会議ツールに依存しないバーチャル背景」について解説しました。

目次は以下の記事からご覧になれます。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0