機械学習モデルBodyPixを使ってAmazon Chime SDKのビデオ会議の背景をぼかしてみた(前編)

2020.11.12

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部の若槻です。

AWSのコラボレーションツールであるAmazon Chimeでは、2020/11現在のところ、ビデオ会議で人物の背景のみをぼかす「背景ぼかし機能」は提供されておらず、同様にAmazon Chime SDKでも標準の実装としては未提供となります。

そこで今回は、ない機能は自分で作っちゃおうということで、オープンソースの機械学習モデルであるBodyPixを使ってAmazon Chime SDKのビデオ会議の背景をぼかせることを簡単に確認してみました。

BodyPixとは

BodyPixとは、ブラウザとTensorFlow.jsを使って人や体の部位が映っている領域を分割(セグメンテーション)できるGoogleのオープンソース機械学習モデルです。

image

ちなみに、このBodyPixを使用してAmazon Chime SDKのビデオ会議のバーチャル背景を実装している方も既にいらっしゃいます。

アウトプット

Amazon Chime SDKへの背景ぼかし機能の実装は、AWS公式が公開している下記のデモアプリ(コミットバージョン39a4e73)を修正する形で行いました。

実際にビデオ会議アプリとして使うには不完全ですが、「videoタグ領域」と「canvasタグ領域」を会議実施画面に追加して、BodyPixによる背景ぼかしを行い、Amazon Chime SDKの「ビデオタイル」に出力するようにしています。処理のシーケンスは次のようになります。

  1. カメラからvideoタグ領域への映像入力
  2. videoタグ領域の映像の背景をぼかし処理してcanvasタグ領域へ描画
  3. canvasタグ領域の描画をMediaStreamとして取得しビデオタイルに入力する

image

ビデオ会議の背景のぼかしが行われている様子です。少し粗くて見にくいですが、身体の動きに合わせてぼかし領域もちゃんと追随できています。
bodypix-chime

やってみる

Amazon Chime SDKのデモアプリのソースコードのダウンロード

% git clone https://github.com/aws/amazon-chime-sdk-js.git
% cd amazon-chime-sdk-js/demos/browser

AWSの認証

今回使用する/demos/browserのデモアプリはローカルで動作するため、バックエンドを構築する必要はありません。ただし、 会議利用時にアプリが会議の作成、削除、出席者の作成をするためにAmazon Chime APIにアクセスするため、その際にAWSの認証情報が必要となります。まだの場合は認証を設定してください。

修正前のデモアプリの確認

npm run startを実行して、依存関係のインストールとアプリのローカルでの起動を行います。

% npm run start

ターミナルに次のように表示されたらアプリの起動は成功です。

2020-11-11T13:04:42.423Z server running at http://127.0.0.1:8080/

http://127.0.0.1:8080/にアクセスすると会議参加画面が開きます。 image

デバイス準備画面です。 image

修正前のデモアプリの会議実施画面です。ビデオ会議に参加できています。 image

デモアプリを修正してビデオ会議の背景をぼかす

BodyPixの依存関係のインストール

% npm install @tensorflow-models/body-pix @tensorflow/tfjs-converter @tensorflow/tfjs-core @tensorflow/tfjs

それぞれ次の用途でインストールしています。

  • @tensorflow-models/body-pix:BodyPix本体。
  • @tensorflow/tfjs-converter@tensorflow/tfjs-core:インストールせずにnpm run startを実行するとエラーとなりこれらのインストールを促されたため。
  • @tensorflow/tfjs:インストールせずにアプリ上でBodyPixによる処理を行おうとするとエラーとなるため。(詳細は後述)

「videoタグ領域」と「canvasタグ領域」の実装

demos/browser/app/meetingV2/meetingV2.html<div id="flow-meeting">配下に下記の記述を追加して、BodyPixによるぼかし前とぼかし後の映像を会議実施画面に表示するようにします。

meetingV2.html

  <p><button  id='pix-start'>start</button></p>
  <div>
  <video id='input' width="320" height="240" autoplay muted playsinline style="border:solid 1px black;box-sizing:content-box;display:inline-block;transform:scaleX(-1);"></video><span style="position:relative;display:inline-block;"><span id='loadingicon' class="fa fa-spinner fa-spin" style="position:absolute;font-size:100px;margin:70px 110px;display:none;"></span><canvas id='output' width="320" height="240" style="border:solid 1px black;box-sizing:content-box;"></canvas></span>
  </div>

BodyPixのインポート

demos/browser/app/meetingV2/meetingV2.tsに次の記述を追加して、BodyPixをインポートして使えるようにします。

meetingV2.ts

import * as bodyPix from '@tensorflow-models/body-pix';

@tensorflow/tfjsのインポート

demos/browser/app/meetingV2/meetingV2.tsに次の記述を追加します。

meetingV2.ts

import * as tf from '@tensorflow/tfjs';
console.log('Using TensorFlow backend: ', tf.getBackend());

@tensorflow/tfjsをインポートしない場合、アプリ上でBodyPixによる処理を行おうとすると次のエラーが発生したため、必要な記述となっています。

engine.js:269 Uncaught (in promise) Error: No backend found in registry.
    at Engine.getSortedBackends (engine.js:269)
    at Engine.initializeBackendsAndReturnBest (engine.js:278)
    at Engine.get backend [as backend] (engine.js:110)
    at Engine.makeTensor (engine.js:577)
    at makeTensor (tensor_ops_util.js:61)
    at tensor (tensor.js:53)
    at Module.decodeWeights (io_utils.js:229)
    at GraphModel.loadSync (graph_model.js:131)
    at GraphModel.load (graph_model.js:114)
    at async loadGraphModel (graph_model.js:382)

次の投稿によるとこのエラーを解消するためには@tensorflow/tfjsのインポートが必要とのことです。

これによりエラーを解消することができました。

処理1および2を開始するイベントリスナーの追加

DemoMeetingAppクラスのinitEventListenersメソッドに以下の記述を追加して、[start]ボタンをクリックすると、処理「1. カメラからvideoタグ領域への映像入力」および「2. videoタグ領域の映像の背景をぼかし処理してcanvasタグ領域へ描画」を開始するイベントリスナーがDOMに追加されるようにします。

meetingV2.ts

    document.getElementById('pix-start').addEventListener('click', e => {
      e.preventDefault();
      this.startDrawBokehEffect();
    });

処理3の実装

DemoMeetingAppクラスのopenVideoInputFromSelectionメソッドを修正して、既存のドロップダウンリストで選択したデバイスからの入力ではなく、「3. canvasタグ領域の描画をMediaStreamとして取得しビデオタイルに入力する」が行われるようにします。

次の既存の記述をコメントアウトして削除します。

meetingV2.ts

    //const device = this.videoInputSelectionToDevice(this.selectedVideoInput);

次の記述を追加して、BodyPixによる背景ぼかし映像のcanvasタグ(id:output)への描写をMediaStreamとして取得し、ビデオタイルへの入力に渡すようにします。

meetingV2.ts

    interface CanvasElement extends HTMLCanvasElement {
      captureStream(frameRate?: number): MediaStream;
    }
    const device = (<CanvasElement>document.getElementById('output')).captureStream();

CanvasElement型をあえて定義しているのは、定義しない場合だとcaptureStream()によるMediaStreamの取得でTypeScriptの静的エラーとなるためです。 image

HTMLCanvasElement.captureStream() - Web API | MDNによると、メソッドしては持ってはいるのですが、実験的な機能であるためまだ利用中のTypeScriptバージョンで対応していないようです。よって、次のStack Overflowの投稿を参考に型定義を行いました。

videoInputSelectionToDevice()メソッドの削除

ビデオタイルへの入力はドロップダウンリストでの選択でなく既定で背景ぼかし映像が入力されるようにしたため、使用しなくなったDemoMeetingAppクラスのvideoInputSelectionToDevice()メソッドをコメントアウトにより削除します。

meetingV2.ts

  /*
  private videoInputSelectionToDevice(value: string): Device {
    if (this.isRecorder() || this.isBroadcaster()) {
      return null;
    }
    if (value === 'Blue') {
      return DefaultDeviceController.synthesizeVideoDevice('blue');
    } else if (value === 'SMPTE Color Bars') {
      return DefaultDeviceController.synthesizeVideoDevice('smpte');
    } else if (value === 'None') {
      return null;
    }
    return value;
  }
  */

処理1および2の実装

このBodyPixによる背景ぼかし処理の部分は、次の記事を大いに参考にさせて頂きました。記事内にコードの詳しい解説もあるので合わせて参照ください。

DemoMeetingAppクラスに次のsetupCamera()segmentBody()loadingc()およびstartDrawBokehEffect()メソッドの記述を追加します。これらメソッドにより「1. カメラからvideoタグ領域への映像入力」および「2. videoタグ領域の映像の背景をぼかし処理してcanvasタグ領域へ描画」の処理を実装します。

meetingV2.ts

  async setupCamera(videoElement: any) {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: {
        width: 320,
        height: 240,
      },
      audio: false,
    });
    videoElement.srcObject = stream;
    return new Promise(resolve => {
      videoElement.onloadedmetadata = () => {
        videoElement.play();
        resolve();
      };
    });
  }

  segmentBody(input: any, output: any, bodypixnet: any) {
    async function renderFrame() {
      const segmentation = await bodypixnet.segmentPerson(input);
      const backgroundBlurAmount = 3;
      const edgeBlurAmount = 3;
      const flipHorizontal = true;
      bodyPix.drawBokehEffect(
        output,
        input,
        segmentation,
        backgroundBlurAmount,
        edgeBlurAmount,
        flipHorizontal
      );
      requestAnimationFrame(renderFrame);
    }
    renderFrame();
  }

  loading(onoff: any) {
    document.getElementById('loadingicon').style.display = onoff ? 'inline' : 'none';
  }

  async startDrawBokehEffect() {
    this.loading(true);
    const input = document.getElementById('input');
    if ((input as HTMLMediaElement).srcObject) {
      (input as HTMLMediaElement).srcObject = null;
    } else {
      const output = document.getElementById('output');
      await this.setupCamera(input);
      const bodypixnet = await bodyPix.load();
      this.segmentBody(input, output, bodypixnet);
    }
    this.loading(false);
  }

修正したデモアプリの実行

% npm run start

ビデオ会議の背景をぼかすことができました。 image

次回(後編)に行いたい対応

今回(前編)は背景ぼかしができることを簡単に確認することが目的の記事でした。次回(後編)は次のような実装対応を行いたいと思います。

videoタグ領域、canvasタグ領域を使わずビデオタイルに映像入力する

今回はカメラ - videoタグ領域 - 背景ぼかし処理 - canvasタグ領域 - ビデオタイルという順の処理でビデオタイルに背景ぼかしした映像を入力しましたが、処理が冗長で、そもそもvideoタグ領域とcanvasタグ領域は実際のアプリでは不要のため、次回はビデオタイルに直接映像入力するようにしたいです。

ビデオ会議をすると相手の映像が固まる事象の解消

今回修正したデモアプリを使い複数人でビデオ会議をすると、相手のビデオ出力映像がなぜか固まってしまうため、次回はこの事象を解消したいと思います。

背景ぼかしをするかどうか選択できるようにする

アプリの実際の利用としては必要なのでこれは実装したいです。

(2020/11/18)後編を投稿しました。

おわりに

オープンソースの機械学習モデルであるBodyPixを使ってAmazon Chime SDKのビデオ会議の背景をぼかせることを簡単に確認してみました。

BodyPixを今回初めて使いましたがとても便利ですね。他にも複数人の場合や身体のパーツごとにセグメントしたり、顔だけにぼかしをかけたりと色々できるようなので今後機会があれば使っていきたいと思います。

参考

以上