2020年03月04日

デプスカメラを「バーチャル背景」用 Web カメラとして使う

はてなブックマークに登録

先日ちょっとしたきっかけから Intel 製 RealSense D415 を買いました。デプスカメラに触れるのは初めてでしたが、Microsoft Azure Kinect の国内販売がまもなく開始される旨もアナウンスされておりこの分野の今後の動向が楽しみです。

試作の内容

さっそくこの D415 をあれこれ試しています。公式の RealSense SDK 2.0 の勉強をかね習作のひとつとして次の内容のプログラムを作ってみました。

  • RGB カラーフレームの画角へ Depth フレームを位置合わせして両方の情報を使用
  • カメラのスコープから「1m 以内」にあるものの RGB 画像を抽出して表示する
  • スペースキー押下で背景画像を切り替え上の画像との合成を行う

静止状態でも常に微妙な揺れを伴う深度情報を RGB 画像切り抜き領域の判定に用いているため静的なクロマキー合成とは異なり境界にノイズが発生しがちですが、デプスカメラ利用の一例として紹介します。

動作の様子

デモ動画:1分4秒 無音

ソースコード

プログラムでは画像処理と表示に OpenCV を併用しています。

  • SDK 内の以下のサンプルプログラムを参考にしておりコードの一部を引用しています
  • 手元の開発環境は Windows 10 + Visual Studio 2019 Community です
  • プロジェクトファイルは上記サンプルのものをコピー〜編集して利用するのが手早いでしょう
//
// Intel RealSense D400 シリーズ
//
// カメラから所定の距離内の RGB 画像を抽出して表示.
// スペースキー押下で背景画像を差し替え上の画像と合成する.
//
// 2020-02
//

#include <librealsense2/rs.hpp>
#include <opencv2/dnn.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>
#include <cmath>

#define Width  640
#define Height 480
#define Fps     15

// RGB 画像抽出対象圏内の距離
#define Depth_Clipping_Distance 1.0f  // 1メートル

// 背景画像ファイル数
#define NumBgImages 8

// Depth スケール情報を取得
float get_depth_scale(rs2::device dev)
{
  // センサ情報を走査
  for (rs2::sensor& sensor : dev.query_sensors()) {
    // 深度センサなら Depth スケール情報を返す
    if (rs2::depth_sensor dpt = sensor.as<rs2::depth_sensor>()) {
      return dpt.get_depth_scale();
    }
  }
  throw std::runtime_error("Device does not have a depth sensor");
}

// RGB フレーム中の Depth_Clipping_Distance 圏外を塗りつぶし
// 圏内 = 255, 圏外 = 0 のマスクイメージを得る
cv::Mat remove_background(rs2::video_frame& video_frame,
            const rs2::depth_frame& depth_frame,
            float depth_scale)
{
  const uint16_t* p_depth_frame = (const uint16_t*)(depth_frame.get_data());
  uint8_t* p_video_frame = (uint8_t*)((void*)(video_frame.get_data()));

  // マスク用の Matrix をモノクロで用意
  cv::Mat m = cv::Mat(cv::Size(Width, Height), CV_8UC1);

  // 当該 video_frame の幅, 高さ, 1ピクセルのバイト長
  int width = video_frame.get_width();
  int height = video_frame.get_height();
  int other_bpp = video_frame.get_bytes_per_pixel(); // RGB につき 3

  // Depth フレームを走査して RGB フレームの画像情報を加工
  //   OpenMP で二重 for ループを並列に処理
  //   VC++ ではコンパイルオプション /openmp が必要
  //   https://docs.microsoft.com/ja-jp/cpp/parallel/openmp/openmp-in-visual-cpp?view=vs-2019
  //   https://docs.microsoft.com/ja-jp/cpp/parallel/openmp/d-using-the-schedule-clause?view=vs-2019
  #pragma omp parallel for schedule(dynamic)
  for (int y = 0; y < height; y++) {
    auto depth_pixel_index = y * width;
    for (int x = 0; x < width; x++, ++depth_pixel_index) {
      // 現座標箇所のセンサからのメートル距離を得る
      auto pixels_distance = depth_scale * p_depth_frame[depth_pixel_index];
      // Depth の死角領域 (<=0) および 対象圏外に該当の場合
      if (pixels_distance <= 0.f || pixels_distance >
Depth_Clipping_Distance) {
        // RGB フレーム内の対象オフセット
        auto offset = depth_pixel_index * other_bpp;
        // 0x999999 で塗りつぶす
        std::memset(&p_video_frame[offset], 0x99, other_bpp);
        // 背景部分としてマーク
        m.at<uchar>(y, x) = 0;
      } else {
        // 前景部分としてマーク
        m.at<uchar>(y, x) = 255;
      }
    }
  }
  // 作成したマスクイメージを CV_8UC1 から CV_8UC3 に変換して返す
  cv::Mat mask;
  cv::cvtColor(m, mask, CV_GRAY2BGR);
  return mask;
}

// 所定の画像データをロード
cv::Mat loadImage(int n)
{
  char fn[16];
  sprintf(fn, "%02d.jpg", n);
  return cv::imread(fn);
}

int main(int argc, char * argv[]) try
{
  rs2::log_to_console(RS2_LOG_SEVERITY_ERROR);

  int photoNumber = 0;
  // ストリーミング用のパイプライン
  rs2::pipeline pipeline;
  rs2::config cfg;
  cfg.enable_stream(RS2_STREAM_COLOR); // RGB ストリーム
  cfg.enable_stream(RS2_STREAM_DEPTH); // 深度ストリーム
  rs2::pipeline_profile profile = pipeline.start(cfg);
  // このカメラの Depth スケール情報を取得
  // "Depth スケール * Depth フレーム内の各ピクセルの値" がセンサからのメートル距離
  float depth_scale = get_depth_scale(profile.get_device());

  // info
  rs2::device rs_dev = profile.get_device();
  std::cout << "Device Name" << ": " <<
rs_dev.get_info(RS2_CAMERA_INFO_NAME) << std::endl;
  std::cout << "Firmware Version" << ": " <<
rs_dev.get_info(RS2_CAMERA_INFO_FIRMWARE_VERSION) << std::endl;
  std::cout << "Recomended Firmware Version" << ": "
<< rs_dev.get_info(RS2_CAMERA_INFO_RECOMMENDED_FIRMWARE_VERSION)
<< std::endl;
  std::cout << "Serial Number" << ": " <<
rs_dev.get_info(RS2_CAMERA_INFO_SERIAL_NUMBER) << std::endl;
  std::cout << "Product Id" << ": " <<
rs_dev.get_info(RS2_CAMERA_INFO_PRODUCT_ID) << std::endl;
  std::cout << "USB Type" << ": " <<
rs_dev.get_info(RS2_CAMERA_INFO_USB_TYPE_DESCRIPTOR) <<
std::endl;
  std::cout << "Depth Scale" << ": " << depth_scale
<< std::endl;

  // RGB ストリーム分の align オブジェクトを用意
  rs2::align align(RS2_STREAM_COLOR);

  // 最初の背景画像をロード
  cv::Mat photo = loadImage(photoNumber);

  while (1) {
    // カメラからのフレームセット受信を待つ
    rs2::frameset frameset = pipeline.wait_for_frames();
    // アライメントを RGB ストリーム分のビューポートに揃える
    frameset = align.process(frameset);
    // RGB フレームを取得 (video_frame クラスに注意)
    rs2::video_frame video_frame = frameset.get_color_frame();
    // Depth フレームを取得
    rs2::depth_frame depth_frame = frameset.get_depth_frame();

    // RGB フレーム中の Depth_Clipping_Distance 圏外を塗りつぶし
    // 圏内 = 255, 圏外 = 0 のマスクイメージを得る
    cv::Mat mask = remove_background(video_frame, depth_frame, depth_scale);

    cv::Mat rgbCvMatsrc, rgbCvMatDst;
    // RGB フレームからピクセルデータを取得し OpenCV のマトリックスに変換
    rgbCvMatsrc = cv::Mat(cv::Size(Width, Height), CV_8UC3,
(void*)video_frame.get_data(), cv::Mat::AUTO_STEP);
    // チャネル並びを RGB から BGR に
    cv::cvtColor(rgbCvMatsrc, rgbCvMatDst, cv::COLOR_RGB2BGR, 0);

    // 抽出した RGB 画像
    //cv::imshow("Src", rgbCvMatDst);

    // 背景画像
    //cv::imshow("pic", photo);

    // マスクと反転マスク
    cv::Mat mask_inv, fg, bg, mix;
    cv::bitwise_not(mask, mask_inv);
    //cv::imshow("mask", mask);
    //cv::imshow("mask_inv", mask_inv);

    // 前景となる抽出画像にマスクをかけて背景色を 0 に
    cv::bitwise_and(rgbCvMatDst, mask, fg);
    //cv::imshow("fg", fg);

    // 背景画像に反転マスクをかけて前景部分を 0 に
    cv::bitwise_and(photo, mask_inv, bg);
    //cv::imshow("bg", bg);

    // 加工した前景と背景をマージして表示
    //cv::bitwise_or(fg, bg, mix);
    cv::add(fg, bg, mix);
    cv::imshow("Enter", mix);

    // キー押下チェック
    int key = cv::waitKey(1);
    if (key == 32) { // SPACE
      // 背景画像を変更
      if (++photoNumber > NumBgImages) {
        photoNumber = 0;
      }
      photo = loadImage(photoNumber);
    } else if (key == 'q' || key == 27) { // 'q' or ESC
      cv::destroyAllWindows();
      break;
    }
  }
  return EXIT_SUCCESS;
}
catch (const rs2::error & e)
{
  std::cerr << "RealSense error calling " <<
e.get_failed_function() << "(" << e.get_failed_args()
<< "):\n  " << e.what() << std::endl;
  return EXIT_FAILURE;
}
catch (const std::exception& e)
{
  std::cerr << e.what() << std::endl;
  return EXIT_FAILURE;
}

メモ:マスク合成の手順

A: 圏内の RGB 画像
B: 0, 255 のマスク
C: 元画像 & マスク C: + c:
a: 背景画像
b: 255, 0 のマスク
c: 背景 & マスク

仮想 Web カメラと組み合わせてみる

上のプログラムはあくまでも習作ですが、ふと、以下のような仮想 Web カメラソフトウェアと組み合わせれば、たとえばテレワーク環境からのビデオミーティング参加時に背後のプライベート空間の露出を避ける目的などに利用できるのではないかと考えました。

ちなみに所属部署ではリモート会議に Google Hangouts Meet を利用しています。上のデモを部内で紹介した折に Zoom にはこれと同様のことをスマートに実現できる「バーチャル背景」機能が用意されていることを知り、この手の需要が少なくないことをあらためて実感しました。

上記の SplitCam を使えば PC 上の所定のウィンドウの任意の領域をローカルのカメラ映像として流すことができます。最新版では領域選択方法が不明でしたが、「SplitCam links to download OLD VERSIONS!」ページから「SPLITCAM 8.1.4.1」をダウンロードして試すと期待する結果が得られました。他のプログラムからは普通の Web カメラとして認識されるため汎用的に扱えそうです。

以下にざっくりと組み合わせの手順を示します。(クリックで大きく表示)

1. SplitCam を起動して領域を設定

2. ミーティング用クライアントからカメラとして SplitCam を選択

3. こんな感じで利用できる


(tanabe)
klab_gijutsu2 at 17:46│Comments(0)その他 

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント