OAK-D-Lite と DepthAI を用いてバーチャル背景合成プログラムを自作してみようという企画の4回目です。 ステップ4「背景画像を読み込みこんで合成する」 の実装です。

ゴールと段取り (おさらい)

ステップ4「背景画像を読み込みこんで合成する」の実装

今回やることは以下3つです。

  1. 背景用画像ファイルの読み込み
  2. 背景用画像にマスクを適用する
  3. 前景と合成する

1. 背景用画像ファイルの読み込み

背景用画像ファイルはコマンドラインオプションで指定可能にしてみます。

import argparse

# 背景の読み込みを行う
def read_background_image(background_image_path):
    background = cv2.imread(background_image_path)
    return resize(background)

# コマンドラインで背景画像指定を可能にする
parser = argparse.ArgumentParser()
parser.add_argument('-b', '--background', help="Path to background image file")
args = parser.parse_args()

# コマンドラインオプション '--background' が指定されていればファイルを読み込む
background_image = None
if args.background is not None:
    background_image = read_background_image(args.background)

コマンドラインで背景画像ファイルが指定されなかった場合には、カラーカメラの画像にぼかしをかけたものを代用できるようにします。

# 背景画像を取得する
def get_background(frame):
    if background_image is not None:
        return background_image

    # 背景画像指定がない場合はぼかしたカメラ画像を代用
    return cv2.GaussianBlur(frame, (99, 99), 0)

2. 背景用画像にマスクを適用する

背景の切り抜き対象は前景で切り抜いた部分とは逆になるため、マスクの値を反転する必要があります。 マスクの値は 0~1.0 の範囲なので単純に 1.0 - mask とするだけで反転できます。

with dai.Device() as device:
    (...)
    while True:
        (...)
        # 背景画像を取得する
        background = get_background(frame)

        # 反転したマスクを背景画像に適用して表示する
        background = apply_mask(background, 1.0 - mask)
        cv2.imshow("Background", background)

こんな背景画像に対して

背景画像

反転したマスクを適用すると、こうなります。

マスク適用後の背景画像

3. 前景と合成する

画像の合成はとっても簡単で foreground + background とするだけです。

with dai.Device() as device:
    (...)
    while True:
        (...)
        # 合成した画像を表示する
        composite_image = foreground + background
        cv2.imshow("Composite Image", composite_image)

前回作成した前景画像はこちら:

前景画像

これと先ほどのマスク適用後背景画像を合成すると完成です。

合成した画像

コード全体

import argparse
import cv2
import depthai as dai
import numpy as np

# マスク調整用パラメタ
params = {
  'blur': 67,             # ボケの強さ
  'dilate': 4,            # 輪郭拡張サイズ
  'max_brightness': 100,  # この濃度を上回るピクセルは 255 にする
  'min_brightness': 60,   # この濃度を下回るピクセルは 0 にする
}

# パイプラインを作成する
def create_pipeline():
    pipeline = dai.Pipeline()

    # カラーカメラを作成
    cam = pipeline.createColorCamera()
    isp_xout = pipeline.createXLinkOut()
    isp_xout.setStreamName("cam")
    cam.isp.link(isp_xout.input)

    # モノクロカメラ(左)を作成
    left = pipeline.createMonoCamera()
    left.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
    left.setBoardSocket(dai.CameraBoardSocket.LEFT)

    # モノクロカメラ(右)を作成
    right = pipeline.createMonoCamera()
    right.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
    right.setBoardSocket(dai.CameraBoardSocket.RIGHT)

    # ステレオ深度ノードを作成
    stereo = pipeline.createStereoDepth()
    stereo.setDepthAlign(dai.CameraBoardSocket.RGB)

    # ステレオ深度ノードに左右カメラを接続
    left.out.link(stereo.left)
    right.out.link(stereo.right)

    # 視差データ出力用ノードを作成
    xout_disp = pipeline.createXLinkOut()
    xout_disp.setStreamName("disparity")

    # 視差データ出力用ノードにステレオ深度ノードを接続
    stereo.disparity.link(xout_disp.input)

    return pipeline

# フレームを 360*360 に縮小する
def resize(frame):
    h = frame.shape[0]  # 高さ
    w  = frame.shape[1] # 幅
    d = int((w-h) / 2)
    return cv2.resize(frame[0:h, d:w-d], (360, 360))

# 視差データの各ピクセルの値を 0 ~ 255 に変換する
def to_grayscale(frame):
    multiplier = 255 / 95
    frame = (frame * multiplier).astype(np.uint8)
    return frame

# マスクデータを作成する
def to_mask(frame):
    frame = cv2.medianBlur(frame, params['blur'])
    frame = cv2.dilate(frame, np.empty(0, np.uint8), iterations=params['dilate'])
    frame = np.where(frame > params['max_brightness'], 255, frame)
    frame = np.where(frame < params['min_brightness'], 0, frame)
    return frame / 255.0

# 変換元画像の全ピクセルに対してマスクデータを掛け合わせる
def apply_mask(img, mask):
    masked = np.zeros(img.shape)
    for i in range(3):
      masked[:,:,i] = img[:,:,i] * mask

    return masked.astype('uint8')

# 入力されたキーでパラメタ調整する
def edit_params(key):
    if key == ord('p'):
        print('blur:', params['blur'])
        print('dilate:', params['dilate'])
        print('max_brightness:', params['max_brightness'])
        print('min_brightness:', params['min_brightness'])

    if key == ord('b'):
        params['blur'] += 2
        print('blur:', params['blur'])

    if key == ord('B') and params['blur'] > 1:
        params['blur'] -= 2
        print('blur:', params['blur'])

    if key == ord('d'):
        params['dilate'] += 1
        print('dilate:', params['dilate'])

    if key == ord('D') and params['dilate'] > 0:
        params['dilate'] -= 1
        print('dilate:', params['dilate'])

    if key == ord('x') and params['max_brightness'] < 256:
        params['max_brightness'] += 10
        print('max_brightness:', params['max_brightness'])

    if key == ord('X') and params['max_brightness'] > 10:
        params['max_brightness'] -= 10
        print('max_brightness:', params['max_brightness'])

    if key == ord('n') and params['min_brightness'] < 256:
        params['min_brightness'] += 10
        print('min_brightness:', params['min_brightness'])

    if key == ord('N') and params['min_brightness'] > 0:
        params['min_brightness'] -= 10
        print('min_brightness:', params['min_brightness'])

# 背景の読み込みを行う
def read_background_image(background_image_path):
    background = cv2.imread(background_image_path)
    return resize(background)

# 背景画像を取得する
def get_background(frame):
    if background_image is not None:
        return background_image

    # 背景画像指定がない場合はぼかしたカメラ画像を代用
    return cv2.GaussianBlur(frame, (99, 99), 0)

# コマンドラインで背景画像指定を可能にする
parser = argparse.ArgumentParser()
parser.add_argument('-b', '--background', help="Path to background image file")
args = parser.parse_args()

background_image = None
if args.background is not None:
    background_image = read_background_image(args.background)


with dai.Device() as device:
    pipeline = create_pipeline()
    device.startPipeline(pipeline)

    q_color = device.getOutputQueue(name="cam", maxSize=1, blocking=False)
    q_disp = device.getOutputQueue(name="disparity", maxSize=1, blocking=False)

    while True:
        # カラーカメラからフレーム取得して表示する
        frame = resize(q_color.get().getCvFrame())
        cv2.imshow("Preview", frame)

        # 視差データを取得して表示する
        disp_frame = to_grayscale(resize(q_disp.get().getFrame()))
        cv2.imshow("Disp", disp_frame)

        # マスク用画像を作成して表示する
        mask = to_mask(disp_frame)
        cv2.imshow("Mask", mask)

        # カラーフレームにマスクを適用して前景を切り取って表示する
        foreground = apply_mask(frame, mask)
        cv2.imshow("Foreground", foreground)

        # 背景画像を取得する
        background = get_background(frame)

        # 反転したマスクを背景画像に適用して表示する
        background = apply_mask(background, 1.0 - mask)
        cv2.imshow("Background", background)

        # 合成した画像を表示する
        composite_image = foreground + background
        cv2.imshow("Composite Image", composite_image)

        # 画面で 'q' が入力されたら終了する
        key = cv2.waitKey(1)
        if key == ord('q'):
            break

        # パラメタ調整
        edit_params(key)

次回

次回いよいよ完成! ステップ5: Web会議サービスに合成画像を連携する です。

かわかみしんいち。島根県津和野町在住のフリーランスエンジニア。複合現実(Mixed Reality)と3DUXでおもちゃを作るのが趣味。 https://github.com/ototadana