facemeshを使ってGoogle Meetで自分の顔に画像を上書きするchrome extensionsを作る

2020.07.22

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

はじめに

COVID-19の影響により、リモート勤務が常識となって、それに伴いWeb会議も増えました。
弊社ではWeb会議のツールの1つとしてGoogle meetを使用しているのですが、
meetだと、カメラはONにして顔だけかくすことってできないのかなあ・・とか思ってました。

以前はmeetでどうやって映像をフックして加工するのかよくわからなかったので挫折。
先日こことかここを見かけて、なんとなくできそうな気がしたのでリトライ。

extentionを使った実際のmeet画面は↓みたいなかんじ。
meet画面の左上にトグルスイッチを置いて、ONのときにカメラ表示すると顔が隠れる。

すでにこういうextentionがどこかにあるかもしれませんが、
「顔を隠す」という用途のものがみつからなかったので、
いろいろ試行錯誤しながらやってみました。

どうやって実装するか

chrome extensionsのcontent scriptとしてとして実装します。
Google meet画面でカメラをONにしたタイミングで、facemeshをつかって顔を検出し、
任意の画像を顔に上書きします。
※2人以上カメラにうつった場合は考慮せず

実装のポイント

extentionのソースはここです。
こことかここを参考にして実装。
かなりやっつけなので適当な部分多し。

実装ポイントをいくつか。
まずはGoogle meetを開いたときに実行されるjsファイルのloader.jsです。
ここではcontent script本体のcs.jsファイル、tensorflow.js、facemesh.jsをloadしてます。
参考にしたソースではscriptタグ作ってbodyにつっこんでたんで真似してますが、
こんな方法あったんですね。

// js/loader.js

async function load() {

  const res = await fetch(chrome.runtime.getURL('js/cs.js'), { method: 'GET' });
  const js = await res.text();
  const script = document.createElement('script');
  script.textContent = js;
  document.body.insertBefore(script, document.body.firstChild);

  // --- tensorflow ---
  const res_tf = await fetch('https://cdn.jsdelivr.net/npm/@tensorflow/tfjs', { method: 'GET' });
  const js_tf = await res_tf.text();
  const script_tf = document.createElement('script');
  script_tf.textContent = js_tf;

  // --- facemesh ---
  const script_face = document.createElement('script');
  script_face.src = 'https://cdn.jsdelivr.net/npm/@tensorflow-models/facemesh';

  document.body.insertBefore(script_face, document.body.firstChild);
  document.body.insertBefore(script_tf, document.body.firstChild);
}

window.addEventListener('load', async (evt) => {
  await load()
}, true);

GUI部分

_insertGUI()でhtmlベタ書きしてトグルスイッチをGoogle meet画面の左上に配置。
スイッチはここの書き方を使ってます。
このスイッチをONにした状態でカメラを起動すると、顔に画像が上書きされます。

video情報の上書き

MediaDevices.getUserMediaでは
カメラから映像情報を取得して画像を上書きします。
navigator.mediaDevices.getUserMediaを自作の関数(_modifiedGetUserMedia)に置き換え、
メディアデバイス(カメラとかマイク)にアクセスして、後述する顔判定や画像上書き処理を行います。
※ ソースだと_startStream()

facemeshで顔判定

load.jsで読み込んだfacemeshを使ってモデルの準備をします。  

let _face_model = null;
async function _face_loadModel() {
const model = await facemesh.load();
_face_model = model;
}

estimateFaces関数が映像から顔を検出するAPIです。
requestAnimationFrameで映像が更新されるタイミングで顔検出を行います。

_face_model.estimateFaces(video)
  .then(predictions => {
    const ctx = canvasCtx;
    const width = canvas.width;
    const height = canvas.height;
    //元の映像をdraw
    ctx.drawImage(video, 0, 0);

    //顔が検出されていればマスク用画像合成
    if(predictions.length !== 0) {
      //predictionsから画像を上書きする箇所を計算
      ・・・・・・・・・・
      ctx.drawImage(<上書きする画像(base64)>, x, y, width, height);
    }
  }).catch(err => {
    console.error('estimateFaces ERROR:', err);
});

ちなみにソースでは、上書きする画像は、base64形式の文字列を直接設定したImageをつかってます。
(chromeで画像表示してデベロッパーツールでbase64文字列を表示してそのままセット)

まとめ

今回はfasemeshで顔検出してmeet画像を上書きすることをやってみました。
いろいろ試行錯誤してようやく動いたのですが、最終的なソースをみてみると
けっこうシンプルにできてます。

これを利用すれば背景画像を変えたり、
カメラにうつった人全員の顔情報を元にデータをとってきたりとか
いろいろできそうです。

参考にしたサイト