デプスカメラを「バーチャル背景」用 Web カメラとして使う
先日ちょっとしたきっかけから Intel 製 RealSense D415 を買いました。デプスカメラに触れるのは初めてでしたが、Microsoft Azure Kinect の国内販売がまもなく開始される旨もアナウンスされておりこの分野の今後の動向が楽しみです。
試作の内容
さっそくこの D415 をあれこれ試しています。公式の RealSense SDK 2.0 の勉強をかね習作のひとつとして次の内容のプログラムを作ってみました。
静止状態でも常に微妙な揺れを伴う深度情報を RGB 画像切り抜き領域の判定に用いているため静的なクロマキー合成とは異なり境界にノイズが発生しがちですが、デプスカメラ利用の一例として紹介します。
動作の様子
デモ動画:1分4秒 無音
ソースコード
プログラムでは画像処理と表示に OpenCV を併用しています。
//
// 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 カメラとして認識されるため汎用的に扱えそうです。
以下にざっくりと組み合わせの手順を示します。(クリックで大きく表示)
2. ミーティング用クライアントからカメラとして SplitCam を選択
(tanabe)










