Web会議が多くなっていくなかで
・画面共有のためだけに、配信用のソフトウェア設定をするのが面倒だ!
・マシンがVPN接続中の作業用マシンなので、経路的に直接会話に参加ができない!
・クロームキャストをPCの画面上でサクッと確認したい!
などと思っている方がたくさんいることでしょう。
そこで今回は、配信でよくある画面構成をWebブラウザの方でどこまでできるのかというのを題材に
・ワイプ
・クロマキー、人物抽出
あたりを、やってみました。
機器構成
クロームキャスト => キャプチャボード(UVC認識) => PC <= Webカメラ
ひとつめ 複数のカメラの取り扱い
基本的には複数のカメラデバイス利用可能です スマホなら例えば、フロントとリアのカメラ両方を利用することができます。
よくでてくるフロントとリアのカメラのサンプル
navigator.mediaDevices.getUserMedia({
video: {facingMode: "user"}
}).then((stream) => {
navigator.mediaDevices.getUserMedia({
video: {facingMode: {exact: "environment"}}
}).then((stream) => {
getUserMediaではデバイスIDを指定してstreamを取得することもできますので
そちらを利用して、他のデバイスから映像を取得することが可能です。
機器の一覧に関してはnavigator.mediaDevices.enumerateDevices()を利用します。
navigator.mediaDevices.enumerateDevices()
.then((devices) => {
// 要求する際のID devices[].deviceId
// デバイスの名称/ない場合も有り devices[].label
このデバイスIDを利用して、ストリームが要求できます。
navigator.mediaDevices.getUserMedia(
{
video: {
deviceId: ”ID-XXX”
width: 1280,
height: 720
}
}
).then((stream) => {
ちなみに複数の音声ソースがある場合、同様にaudioのみ取得も可能です。
navigator.mediaDevices.getUserMedia(
{
audio: {
deviceId: ”ID-XXX”
}
}
).then((stream) => {
あとはこのストリームをメイン用とワイプ用のvideo tagに流し込めば再生ができます。

メイン画面はクロームキャストからタブ転送、サブ画面は人物移すためのカメラの状態となっています。
ここまでで、ブラウザでワイプ状態の完成です。
ふたつめ クロマキー
このままでは、ワイプ部分が背景残ってしまっているのでこれをなんとかしようかと思います。
人物の背景は、別のシステムでグリーンバックにするか、物理のグリーンバックの想定です。
ここでの方針はcanvas転写+アルファチャンネル操作になります。
単純にRGBで閾値決めて、やってもいいですがせっかくなら色差を利用します
今回はCIEDE2000を採用 実装するのも面倒なので、こちらを利用させていただきました。
https://github.com/markusn/color-diff
ちなみに自力で実装する場合、RGB=>XYZ=>labと変換していく形になるかと思います。
擬似コード
// Diff color-diffライブラリ (import Diff from 'color-diff')
// video 画面上では非表示のビデオ
// canvas 転写および透過処理後の表示用キャンバス
let context = canvas.getContext('2d');
let green_lab = Diff.rgb_to_lab({ R: 0, G: 255, B: 0 })
let threshold = 25
let loop = function () {
context.drawImage(video, 0, 0, canvas.width, canvas.height)
let image = context.getImageData(0, 0, canvas.width, canvas.height)
let data = image.data;
for (let i = 0, l = data.length; i < l; i += 4) {
let r = data[i]
let g = data[i + 1]
let b = data[i + 2]
let lab = Diff.rgb_to_lab({ R: r, G: g, B: b })
let delta = Diff.diff(green_lab, lab)
//deltaによってアルファチャンネルに値を設定
if (delta < threshold) {
data[i + 3] = 0;
}
context.putImageData(new ImageData(data, canvas.width), 0, 0)
requestAnimationFrame(loop);
}
}
navigator.mediaDevices.getUserMedia(
{
video: {
deviceId: "ID-XXX",
width: 1280,
height: 720
}
}
).then((stream)=>{
video.srcObject = stream;
video.onloadedmetadata = function (e) {
video.play()
loop()
};
})

みっつめ 背景検出
グリーンバックがなくても、人物だけ切り抜きたい。
パッと思いつくところで下記方法があります。
前景/背景の判断をさせる
growcutなる方法や機械学習系の方法があるみたいですね。
しかし、すぐに利用できる形のものが見つからなかったので保留。
人物を推定して、人物以外を透過する
tfjs-modelsの中にbody-pixというものが使えそうでした。
リファレンス通りですが、npmで導入する場合関連のライブラリも忘れずに。
npm install @tensorflow-models/body-pix @tensorflow/tfjs-converter @tensorflow/tfjs-core @tensorflow/tfjs-backend-webgl
検出結果を利用して、アルファチャンネルの値を設定する感じになります。
import * as bodyPix from '@tensorflow-models/body-pix';
import '@tensorflow/tfjs-backend-webgl';
省略
const net = await bodyPix.load();
let image = hiddenContext.getImageData(0, 0, hiddenCanvas.width, hiddenCanvas.height)
net.segmentPerson(image,
{
flipHorizontal: false,
internalResolution: 'medium',
segmentationThreshold: 0.7
}).then((segmentation) => {
// segmentation.dataにて、透過するかどうか決定する
処理負荷が結構高いので実際には、segmentationを作成するバッファする部分と画像転写する部分を非同期で別に動かしています。

ちなみにbody-pixは色々試してみましたがWebGL使わないと、速度的に厳しいものがありました。
キャラクターではなく、ちゃんと人物ならばもう少し精度がよかったです。

おまけ ワンライナー
どんなページにも、Webカメラのワイプを表示させたい。
以下をデベロッパーツールのコンソールか、URLバーに入力にて表示(chromeとsafariのみ動作確認済み)。
javascript:video=document.createElement("video");video.style="z-index:999;bottom:5px;right:5px;position:fixed;width:30vw;height:16.875vw;";document.body.appendChild(video);navigator.mediaDevices.getUserMedia({video:{width:1280,height:720}}).then((stream)=>{video.srcObject=stream;video.onloadedmetadata=(e)=>{video.play()}});
動画位置の移動、拡大縮小、ドラッグアンドドロップなど割と作り込んでいけば
簡易的な共有画面用のスイッチャーが作れそうですよね。
ここで作成したものは下記のページで閲覧が可能です。
サンプル
https://uvc-test.firebaseapp.com/