はじめに
ABEJA アドベントカレンダー 2021 の 19日目の記事です。
この記事では、顔画像認識した箇所に画像を上書きするという定番の処理を Meet でリアルタイムに実施する方法について記載したいと思います。
既に同じようなことを実施している先人の方々の記事があったのですが、Trusted Types 絡みでエラーとなってしまったり、機能の on/off 用の GUI を用意していたりで少しコードが複雑になっていました。
本記事では、特別な GUI を用意せず、Meet のカメラボタンに合わせて機能の on/off をする形で実装しています。
また、顔画像認識箇所を tenforflow.js のモデルに任せることで、150行程度のコードでタイトル記載の内容を実現することができます。
実行環境
Chrome の拡張機能として実装しているため、ブラウザとして Chrome をご利用ください。
利用手順
実際の仕組みについて解説する前に、まずは利用手順を記載しておきます。
- 完成版のコード を github からローカルに clone する
- Chrome のアドレスバーに
chrome://extensions
と入力し拡張機能の設定画面を表示する - デベロッパーモードを有効にし、パッケージ化されていない拡張機能を読み込めるようにする
-
パッケージ化されていない拡張機能を読み込む
をクリックし、clone したディレクトリを選択する - 以下の拡張機能が拡張機能一覧にリストアップされていることを確認する
- Meet を開く
- カメラが ON の場合は一旦、OFF にする
- カメラを ON にする
- 顔画像認識された場所に画像が上書きされた状態で映像が出力される(初回は画面表示の際に時間がかかります)
うまくいかない場合は、Chrome のデベロッパーツールを表示し、Console
でエラーが出ていないか確認してみてください。
仕組み
それでは、仕組みを解説していきたいと思います。
まず、Meet での映像配信について簡単に図示しておきます。
起動した Meet でカメラマークを ON にすると、配信配信元となる方のカメラからの映像が Meet 内部で処理され、その映像がサーバー経由で Meet に接続している他ユーザに配信されます。
Meet はブラウザ上で動いており、カメラからの映像は JavaScript から MediaDevices.getUserMedia() と呼ばれる API を利用して取得しています。
この getUserMedia() 関数をフックして、元の処理に対して上書き処理を追加してあげることで、映像を加工して配信することができます。
利用している主な技術は以下の3つです。
- Chrome Extension
- 顔画像認識
- WebRTC(getUserMedia)
Chrome Extension
Chrome に拡張機能を追加する仕組みになります。いくつか種類があるのですが、今回は Content Scripts を利用しています。
Content Scripts で Meet を表示している際に、映像処理をフックする処理を実装(拡張)しています。
Chrome Extension については他に詳しい記事がいくつもあるので、知らない方は Chrome Extension の作り方 (その1: 3つの世界) などのページを参考にしていただければと思います。
顔画像認識
顔画像認識には、tensorflow.js の事前トレーニング済みのモデルである face-landmarks-detection を利用します。face-landmarks-detection では入力として映像ストリームを与えることにより顔の特徴点を検知することができます。得られた点群から顔領域となる矩形を算出、その矩形に合わせて画像を上書きすることで Meet で表示される映像を加工しています。
WebRTC(getUserMedia)
APIを利用することで、ブラウザ同士でリアルタイムなコミュニケーションを実現するための仕組みになります。
主要なブラウザに標準で実装されているため特に追加でソフトウェアをインストールすることなくリアルタイムコミュニケーションを行うことができます。
WebRTC の API として提供される getUserMedia 関数を利用することで、ブラウザ上からカメラのメディアストリームを取得、利用することができます。
Meet では映像処理にこの関数を利用しているため、フックして差し替えることで画像で上書きを行った映像の配信を行います。
コード解説
全体で 150 行程度のコードなので直接コードを見たほうが早いかもしれませんが、ポイントだけピックアップし、簡単に解説しておきます。
ロード処理
ページ全体が読み込まれた後に、loadScript 関数で利用するライブラリの読み込みを行います。
face-landmarks-detection がいくつかのライブラリに依存しており、それらのライブラリは先に読み込んでおく必要があります。
そのため、await を使って順番にライブラリを読み込むように実装しています。
async function load() {
// 上書きに利用する画像(gif)を読み込み
loadLocalScript("lmMarkImg.js")
// face-landmarks-detection.js が依存しているライブラリを順番に読み込む
await loadScript("https://unpkg.com/@tensorflow/tfjs-core@2.4.0/dist/tf-core.js")
await loadScript("https://unpkg.com/@tensorflow/tfjs-converter@2.4.0/dist/tf-converter.js")
await loadScript("https://unpkg.com/@tensorflow/tfjs-backend-webgl@2.4.0/dist/tf-backend-webgl.js")
await loadScript("https://unpkg.com/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js")
// 実際の処理を実行
loadLocalScript("main.js")
}
// ページ全体が読み込まれた後にロード処理を開始
window.addEventListener('load', async (evt) => {
await load()
})
getUserMedia 関数のフック
仕組みのセクションで簡単に説明しましたが、getUserMedia 関数をフックすることで Meet からの映像配信に画像を上書きします。
srcStream
がオリジナルの映像データになり、これを video タグに流し込むことで映像を取得します。
実際に合成した映像は canvas に出力しそこから、ストリームを取得し getUserMedia の出力として return することで、加工した映像を配信することができます。
// 元の getUserMedia 関数を退避しておく
const _getUserMedia = navigator.mediaDevices.getUserMedia.bind(
navigator.mediaDevices
);
// getUserMedia 関数をフック
navigator.mediaDevices.getUserMedia = async function (constraints) {
// オリジナルの映像。本来、配信する予定だったメディアストリーム
const srcStream = await _getUserMedia(constraints)
// 画面共有として呼ばれた際には、画像の上書き処理を実施しない
if (isScreenSharing(constraints)) {
return srcStream
}
// オリジナルの映像は加工処理のため video タグで再生
video.srcObject = srcStream
video.onloadedmetadata = function(e) {
video.play();
video.volume = 0.0;
video.width = video.videoWidth;
video.height = video.videoHeight;
canvas.width = video.width;
canvas.height = video.height;
// 加工結果は canvas へ出力
keepAnimation = true
updateCanvas()
};
// canvas から加工した映像を取得
const outStream = canvas.captureStream(10)
const videoTrack = outStream.getVideoTracks()[0];
replaceStopFunction(srcStream, videoTrack)
// 加工した結果を出力として返す
return outStream
};
画像での上書き処理
Meet の映像に対しての顔画像認識、認識箇所への上書き処理は updateCanvas
関数で行います。
estimateFaces
関数に video ストリームを入力することで、検知した顔の特徴点群(predictions)を取得することができます。
得られた特徴点群から、顔領域となる矩形を算出、矩形に合わせて画像を canvas に上書きする処理は drawImage
関数に任せています。
処理後は、requestAnimationFrame
に updateCanvas
関数自身を与えることで、画面の描画毎に同様の処理を繰り返し、canvas の更新を行っています。
async function updateCanvas() {
if (!keepAnimation) return
if (model) {
// 元の映像を canvas へ出力
canvasCtx.drawImage(video, 0, 0, canvas.width, canvas.height);
// Face landmarks detection のモデルを使い顔の特徴点群を取得
const predictions = await model.estimateFaces({input: video});
// 見つかった顔の数毎に点群を処理
for (const prediction of predictions) {
// 点群 -> 矩形算出 -> 画像を canvas へ出力
drawImage(prediction)
}
}
// 描画毎に updateCanvas を繰り返し実行
requestAnimationFrame(updateCanvas);
}
終わりに
本記事では、 Meet で顔画像認識を行い、画像で上書きを行う Chrome Extension の作成を行いました。
顔画像認識部分を Face landmarks detection に任せることで、コード全体で150行程度のシンプルなコードとなっています。
Meet の映像に対して何か処理をしたいなといった場面がありましたら、ぜひ参考にしていただければと思います。
本記事のコードをベースにいろいろと遊んでみてください。
お知らせ
現在ABEJAでは一緒にAIの社会実装を進める仲間を募集しています。
ABEJA Advent Calendar 2021の記事を見て、少しでも興味を持っていただけたなら、まずはカジュアルにお話を聞きに来ていただければなと思います。
募集職種一覧はこちらから確認できます。ご応募お待ちしております!