週末,ブラウザ上でバーチャル背景合成できるものを作った.
これで任意のビデオ通話ソフトウェアで合成ができるようになった.よくつかうのは Google Meet なのだけれど,画面取り込みすれば簡易的にバーチャル背景機能を使えるかもしれないし,使えないかも知れない.
活用
よくこういう背景合成に「グリーンバック」とかいう緑一色(リューイーソーではない)の布を背景にしてくり抜くのが行われている.一昔前はブルーバックだった気がする.なぜ変わったのかは知らない.
幸いにして自分の家のカーテンが緑色だったのでこれで代用することにした.そうしたら思いの外うまくいった.
配布されているバーチャル背景をつかうことで,あの石畳と木組みの街にも行ける:
【バーチャル背景プレゼント①♪】
— TVアニメ『ご注文はうさぎですか?』 (@usagi_anime) April 8, 2020
テレビ電話等で使用可能なごちうさのラビットハウスや甘兎庵、フルール・ド・ラパンの外観背景をご用意しました!ぜひ画像をダウンロードしてご活用くださいね☆ #gochiusa pic.twitter.com/wmplVxh9O4
シュールですね.
ちなみに合成をやめるとこんな感じで悲壮感が漂う:
YouTube も流せるようにしてみたら幅が広がり始めた:
寝間着姿で日本の絶景を訪れることも可能となった.(4k で GPU をいじめてるからかアドレスバーの一部が壊れ始めた).
動画で我が家に天使が舞い降りさせることもできるけど,普通に起こられそうなので個人的に楽しむことにした.
明るい色ですべてのパラメーターを過剰気味にすると,暗い部分が残って一昔前の iPod CM 風の映像が得られたりもする:
くり抜き機能のあるビデオ通話ソフトウェアに比べれば当然品質は良くはないが,この週末に作ったものにしてはそこそこ活用できるものになっただろう.
ここからは技術の話.
しくみ
まず,カメラの映像は Media Streams API
で普通に映像をキャプチャする.カメラの画像をテクスチャにアップロードすれば,WebGL
でレンダーできる.
そうすれば GLSL
でフラグメントシェーダーを使って,特定の色範囲をマスクすれば,うまくいけば背景を切り抜くことができる.
JavaScript 側でゴリゴリピクセル単位の操作なんてしようものならブラウザが即固まって論外だけど,GLSL なら GPU でやってくれるので問題ない.
そして普通にやると OpenGL の API をがそのまんまで面倒な WebGL まわりも Three.js を使えば簡単に扱える.
くふう
まず,背景として単色が求められる (「マスク色
」と呼ぶことにする).
単純にこの 1 色を指定して切り抜くか判断するとしたら到底無理なので,ある程度あいまいに判定する必要がある.とすると,普通にある程度幅をもたせて範囲内だったら背景と判断して切り抜くようなロジックになる.
イメージ的には「マスク色」と「ある任意のピクセルの色」の距離が「しきい値」以下だったら,背景と判定するような単純なロジック.
ところで単純に色の値を比較するにしてもいろいろな要素が出てくる.まず「色空間」の違いがあるので無視できなさそうである.RGB で 2 色の距離を取ったとしても,知覚的に近いとはあまり思えないかもしれない.
なので,シェーダー側で RGB から HSV に色空間を変換する.HSV 色空間になると「色相」「彩度」「明度」の 3 要素が色の構成要素になった.
ここで距離を考えるときに単純にベクトルの距離をとればいいかというと,やってみて思ったのは,ケースバイケースでいい感じになるパラメーターが異なるので,「しきい値」はその 3 要素それぞれで設定できるのがよさそうと思った.
カーテンみたいにのっぺり単色だと「色相」の差は小さく,「彩度」や「明度」の差を中くらいにすればある程度いい感じになる.
さらなるくふう
さて,これでいいかというと微妙だった.細かなノイズがパラパラと散らばり出て煩わしい感じになってしまう.
空間方向のノイズを低減するというと,ガウシアンをかけるとかそういうぼかし系の処理をかけて平滑化すればなんとかなるだろうと,とりあえず周囲の色を適当な個数サンプリングしてみることにしたら,ある程度マシになった.
時間方向のノイズも積み重ねるバッファを設けて判定を行えばよりチラチラするノイズを低減することができるだろう,と思ったけど面倒なのでしなかった.
ところで
Chrome だと document.createElement('video');
でつくった不可視の要素からはテクスチャをつくることができなかった.(サイズが 0 になってしまってエラーになる).
style="width: 0; height: 0;"
にして document.body.appendChild
することで動作したので,この Workaround でヨシ,としたけど,これもいつか通用しなくなりそうな気がする.
Firefox では問題なく動作する.Chrome に比べて WebGL の動作もスムースな気がする.ファンも回りにくいし,プチフリーズ的なのもない.