OAK-D-LiteDepthAI を用いてバーチャル背景合成プログラムを自作してみようという企画の1回目です。

やりたいこと (ゴール)

OAK-D-Lite をWebカメラとして用いてバーチャル背景合成を行い、Web会議サービス (ZoomとかGoogle Meetとか) に出席したい。

動機

この2年間ぐらい 完全リモートワーク生活@台所 をやっているので、バーチャル背景は必須アイテムです。 が、Web会議サービスで利用できるバーチャル背景機能には以下の不満がありました。

  • 会議中に家族が後ろを通り過ぎるとその姿が映ってしまう。
  • 自分が持つ書籍等の小物をWebカメラ越しに見せようとした際に背景として認識されてしまう。

おそらくこれはWeb会議サービスでは 「『人物』と推論したものを前景、それ以外を背景」 というロジックで処理が行われていることがその理由なんじゃないかなーという気がしてます。なので、これを 「ある一定以上の近さにあるものを前景とし、それよりも遠いものを背景とする」 というロジックに置き換えられれば、上記2つの問題はクリアできるのではなかろうか…と。そして「DepthAIを使えば距離による前景・背景の識別もできそうじゃないか!!!」という安直な発想が今回の作成動機です(あともう一つの目的としては「DepthAIはおろか画像処理もあんまり知らないのでこの機会に勉強してみたい」てのもありますが…)

実現までの段取り

いきなり最終ゴールまでやりきるのもハードル高いので、ステップバイステップで徐々に完成に近づける、って感じで行ってみたいと思います。

ひとまずこんな順番で:

  1. カラーカメラの画像を画面に表示できる
  2. 視差データを取得して表示する
  3. カラー画像に対して視差データでマスクをかける
  4. 背景画像を読み込みこんで合成する
  5. Web会議サービスに合成画像を連携する

ステップ1の実装

ということで今回は最初のステップ 「カラーカメラの画像を画面に表示できる」 をやってみます。

前回紹介したデモプログラムのコードを眺めてみると、どのプログラムも基本的には以下の流れでできているようです。

  1. パイプラインを作成して、開始する
  2. (ループ処理の中で) パイプラインの出力キューからメッセージを取り出して画面表示する

ということで、デモプログラムと同様にこの2種類の処理をコーディングすれば今回の目標は達成できそうです。

1. パイプラインの作成

OAK-D-Lite など DepthAI対応デバイスは、デバイス内の様々な機能モジュール(ノード)の接続をプログラムして自分の欲しい結果を得ることができます。この「ノードの接続状態」がパイプラインです。すなわち、パイプラインの作成とは「自分の使いたいノードを必要な分だけ生成して、各ノード間の接続状態を定義する」ということになります (このあたりの説明は DepthAI API Documentation に詳しく書かれています)。

APIドキュメント をざっと眺めると、以下のようなノードがあります。

  • ColorCamera (カラーカメラ)
  • MonoCamera (モノクロカメラ)
  • StereoDepth (ステレオ視差/深度計算)
  • NewralNetwork (ニューラルネットワーク推論)
  • XLinkIn (入力ノード: デバイスへの入力)
  • XLinkOut (出力ノード: デバイスからの出力)
  • ほかにもたくさん …

ステップ1のゴール「カラーカメラの画像を画面に表示できる」には ColorCameraXLinkOut の2つのノードを使ってパイプラインを作成します。

import cv2
import depthai as dai
import numpy as np

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

    # カラーカメラを作成
    cam = pipeline.createColorCamera()

    # 出力ノードを作成。
    isp_xout = pipeline.createXLinkOut()
    isp_xout.setStreamName("cam")

    # カラーカメラに出力ノードを接続
    cam.isp.link(isp_xout.input)

    return pipeline

出力ノードのストリームには、isp_xout.setStreamName で “cam” という名前を付けておきます。

パイプラインの出来上がりイメージはこんな感じです:

パイプラインのイメージ

参考ページ

2. メッセージを取り出し~画面表示

デバイス上にあるパイプラインの出力ノードから出力されたデータはホスト(PC)側のキューに格納されます。 キューは、device.getOutputQueue(name="ストリーム名") で取得できます。

あとは、

  1. キュー内のメッセージを取り出す
  2. メッセージの中から画像データ(フレーム)を取り出す
  3. 取り出したフレームを表示する

でやりたいことができます。

with dai.Device() as device:
    # パイプラインを作成
    pipeline = create_pipeline()

    # パイプラインを開始
    device.startPipeline(pipeline)

    # 出力キューを取得
    q_color = device.getOutputQueue(name="cam", maxSize=1, blocking=False)

    while True:
      # キューからメッセージを取り出す
      color = q_color.get()

      # メッセージからフレームを取り出す
      frame = color.getCvFrame()

      # フレームを画面に表示する
      cv2.imshow("Preview", frame)

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

コードはこんな感じでとてもシンプルです。getOutputQueue のオプションについてはこちらのページ が参考になります。

これでひとまずカラー画像を表示できるようになるのですが、このままだと表示画像が大きすぎて少し不便です。 最終的なアウトプットも高解像度にする必要はないので (Web会議向けなので) 360*360 の小さな画像に加工します。

「# メッセージからフレームを取り出す」と「# フレームを画面に表示する」の間に以下の処理を追加します。

      # フレームを 360*360 に縮小する
      frame = resize(frame)

resize 関数はこんな感じ:

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

    #  360*360 に縮小する
    return cv2.resize(frame, (360, 360))

こんな感じになりました:

カラーカメラからの出力画像

コード全体

import cv2
import depthai as dai
import numpy as np

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

    # カラーカメラを作成
    cam = pipeline.createColorCamera()
    isp_xout = pipeline.createXLinkOut()
    isp_xout.setStreamName("cam")
    cam.isp.link(isp_xout.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))

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

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

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

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

次回

次回は ステップ2: 視差データを取得して表示する です。

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