5幕目のソロテントを購入しました。
リサーチデベロップメントのたかはしとし( @doushiman_jp ) です。
あだ名は避難所です。
前回のブログphoneappli.hatenablog.com
では、face-api.js を使用して、顔認識をして顔部分を(文字通りの)マスキングをしました。
こんなの
その中で
一度Canvas 上で画像をビデオとマスクを合成してからStream を生成すれば、WebRTC にも応用できるのかな〜。
と書いてしまったので、今回はそれをやってみたいと思います。
WebRTC 部分は僕の各ブログではおなじみのSkyWay を使います。
SkyWay 部分のコードはほぼこちらに公開されているサンプルコードをそのまま使います。
前回は、顔認識をして得た動画内の顔の位置座標の上にCanvas でマスクを描画して、それをvideoタグの上に乗せているだけのものでした。
この状態だと、カメラから得た映像と、マスク画像は別々のタグに描画されているだけですので、マスキングした状態の画像をWebRTC では送れません。
なので、一度ビデオ画像とマスク画像を1枚の画像に合成し、その画像を相手側に送ることが必要です。
画像合成
まずは普通に、getUserMedia でカメラの画像を取得してvideoタグに紐付けます。
let camera = document.getElementById('js-local-stream'), navigator.mediaDevices.getUserMedia(constraints) .then((stream) => { camera.srcObject = stream; camera.onloadedmetadata = (e) => { playCamera(); checkFace() }; })
ただし、今回はこの画像は直接描画をしません。
合成の材料にするだけなので、CSS ではdisplay:none としています。
今度はvideo に表示している動画をCanvas タグに描画し、その上にマスク画像を合成します。
checkFace = async () => { let faceData = await faceapi.detectAllFaces( camera, new faceapi.TinyFaceDetectorOptions() ).withFaceExpressions(); if(faceData.length){ const setDetection = () => { ctx.drawImage(localVideo, 0, 0, canvasW, canvasH); // ローカルビデオ画像をCanvasに描画 let box = faceData[0].detection.box; x = box.x, y = box.y, w = box.width, h = box.height; ctx.drawImage(chara, x, y, w, h); // マスク画像を顔の位置に描画 }; setDetection(); } requestAnimationFrame(checkFace) };
以下簡単な説明です。
まず最初に顔認識を行います。
ビデオ画像内に顔を検出したら、そのビデオ画像をCanvas のコンテキストを使って描画します。
ctx.drawImage(localVideo, 0, 0, canvasW, canvasH);
これをCanvas内の背景的に使用します。
この時点で、Canvas はカメラから取得した動画が描画されている状態です。
次に、顔認識で取得したX,Y の位置に、マスク画像を描画します。
let box = faceData[0].detection.box; x = box.x, y = box.y, w = box.width, h = box.height; ctx.drawImage(chara, x, y, w, h);
requestAnimationFrame によって、フレームレートに準じて顔認識 → 画像合成が行われます。
ここまでで、ビデオ画像とマスク画像が一枚の画像として、ローカルで描画されていることになります。
合成画像のStreamをセットする
ここからが今回の本題です。
GitHub から入手したP2P でのビデオチャットのサンプルコードに手を入れます。
getUserMedia でビデオ画像を取得するところは、顔認識のところで既に行っているため、サクッと消します。
その後、SkyWay でのコネクションを確立するときに、peer に合成した画像のStream(mixedStream)を渡します。
const mediaConnection = peer.call(remoteId.value, mixedStream);
通常なら、mixedStream のところは、ローカルビデオのStream を渡すのですが、代わりに合成した画像のStream を渡すことで、マスキング済みの画像が先方に届くので、相手側にもマスクを被った状態で映ります。
これだけですと、接続に行った方はマスク画像になるのですが、接続された方も同様に、マスクを合成済みの画像を返す必要があります。
ですので、メディアコネクションがコールされた時に、ローカルのビデオではなく、合成済み画像のStream をセットすることで実現できます。
peer.on('call', mediaConnection => { mediaConnection.answer(mixedStream);
これで、双方がマスクを被った画像を、送ることができるはずです。
では、P2P でのWeb会議を開始してみましょう...
動物たちのWeb会議
結果はこんな感じです。
どうぶつさんたちだいしゅうごうだわいわい (C)岡崎体育
これで、双方が動物の皮を被ったままで、ビデオチャットが可能になりました。
体を動かしても、動物は動物のままでした。ちゃんと追従します。
ただ、難点もいくつかあって、
- video -> Canvasへの出力のせいか、動画に若干ラグがある
- 顔認識など重めの処理のためか、ファンがぶんぶん回る
- ビデオ画像を大きくすると、さらにファンがぶんぶん回る
- requestAnimationFrame はブラウザのタブがアクティブじゃ無いと更新頻度が下がる(内職しているとバレる)
重めの処理をしているからフレームレートが下がって、それにひきづられて顔認識 → 描画がラグっているのかなと思います。
ここは環境に依存するので、解消は難しいかもしれません。
これを流用すれば、VTuberっぽいビデオ会議や、動くピクトグラムなビデオ会議とか作れるかもですね。
ではでは。