🤣
【ネタ】笑ってはいけない Web 会議を作ってみる。デモアプリ編
※今回はネタ投稿です。
ガキ使にインスパイアを受けて、「笑ってはいけない Web 会議」ができるアプリを作ろうと思います。
今回はデモアプリ編ということで、『Web カメラの映像から笑顔を検出して、「ババーン!〇〇 OUT !」の演出をする』ところまで作ってみました。
成果物
リポジトリ URL: https://github.com/t-tonyo-maru/pub_web_warattehaikenai-web-mtg
デモ URL: https://t-tonyo-maru.github.io/pub_web_warattehaikenai-web-mtg/
デモ URL を閲覧するうえでの注意点
- ※「ババーン!」の例の音が鳴ります。
音量を低めに設定していますが、ご注意ください。 - ※PC で閲覧してください。
モバイルは未検証です。モバイルでも動作するとは思いますが、かなり処理が重いと思います。
動作確認手順
- デモ URL にアクセスします。
- Web カメラの利用許可が求められますので、許可をしてください。
-
Web カメラで得た映像の収集・保存などはしませんので、ご安心ください。
あくまで表情を解析するために、デモ URL 内で利用するだけです。
-
Web カメラで得た映像の収集・保存などはしませんので、ご安心ください。
- Web カメラに映った人物が笑うと「ババーン!」の効果音と「〇〇 OUT」の文字が展開されます。
- 画面右上のコントロールパネルから、デモアプリの設定を変更できます。
- 笑顔の判定ライン: 本デモアプリは「検出された顔の表情が喜び・幸せ(happy)である確率」が閾値を超えることで「笑っている」とみなします。その閾値を変更できます。
あくまで確率による判定であるため、コントロールパネル上で 1 にすると「笑っている」と判定されなくなります。 - SE のボリューム: 「ババーン!」の効果音のボリュームを変更できます。
- SE を再生する: 「ババーン!」の効果音を再生するかどうかを切り替えられます。
- 罰ゲーム対象者: 「〇〇 OUT」の「〇〇」に展開する文字を変更できます。
(ココリコ田中さんが好きなので、デフォルトは田中です。)
- 笑顔の判定ライン: 本デモアプリは「検出された顔の表情が喜び・幸せ(happy)である確率」が閾値を超えることで「笑っている」とみなします。その閾値を変更できます。
参考サイト・素材サイト
本デモアプリは face-api.js (Github / npm)を利用しています。
face-api.js の使い方は、【うわっ...私の表情、硬すぎ...?】face-api.jsで顔検出して感情と年齢を判定するを参考にしています。素晴らしい解説記事です。
また、「ババーン!」の効果音は DOVA-SYNDROME を利用しました。
解説
本デモアプリの処理はすべて src/main.ts に記述しています。
Web カメラとの許可取得と face-api.js との連携のみ解説します。
src/main.ts
// face-api.js モデルを読み込みます … ①
Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri(`${GITHUB_PAGES_PATH}/weights`),
faceapi.nets.faceLandmark68Net.loadFromUri(`${GITHUB_PAGES_PATH}/weights`),
faceapi.nets.faceRecognitionNet.loadFromUri(`${GITHUB_PAGES_PATH}/weights`),
faceapi.nets.faceExpressionNet.loadFromUri(`${GITHUB_PAGES_PATH}/weights`),
faceapi.nets.ageGenderNet.loadFromUri(`${GITHUB_PAGES_PATH}/weights`)
])
.then(async () => {
// モデルの読み込み後に、Webカメラの許可をユーザーに求めます … ②
return await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: SCREEN.w },
height: { ideal: SCREEN.h }
}
});
})
.then((stream) => {
// video 要素の srcObject に stream をセットします … ③
videoEl.srcObject = stream;
})
.catch((error) => {
console.error(error);
});
// Web カメラが許可され、video に再生されたときに発火する関数 … ④
videoEl.addEventListener('play', () => {
const canvas = faceapi.createCanvasFromMedia(videoEl);
canvas.id = 'canvas';
canvas.width = SCREEN.w;
canvas.height = SCREEN.h;
app.append(canvas);
const context = canvas.getContext('2d');
faceapi.matchDimensions(canvas, {
width: SCREEN.w,
height: SCREEN.h
});
const detectFaces = async () => {
const detections = await faceapi
.detectAllFaces(videoEl, new faceapi.TinyFaceDetectorOptions())
.withFaceLandmarks()
.withFaceExpressions()
.withAgeAndGender();
const resizedDetections = faceapi.resizeResults(detections, {
width: SCREEN.w,
height: SCREEN.h
});
if (context) context.clearRect(0, 0, canvas.width, canvas.height);
faceapi.draw.drawDetections(canvas, resizedDetections);
faceapi.draw.drawFaceLandmarks(canvas, resizedDetections);
faceapi.draw.drawFaceExpressions(canvas, resizedDetections);
if (isOut && context) {
// 「〇〇 OUT」を描画します
const text = `${outUserName} OUT`;
context.font = 'bold 48px Arial';
context.textAlign = 'center';
context.strokeStyle = 'white';
context.lineWidth = 4;
context.strokeText(text, canvas.width / 2, canvas.height - 40);
context.fillStyle = 'red';
context.fillText(text, canvas.width / 2, canvas.height - 40);
// text align をリセット
context.textAlign = 'left';
}
for (const detection of resizedDetections) {
// face-api のボックスの描画します
const drawBox = new faceapi.draw.DrawBox(detection.detection.box, {});
drawBox.draw(canvas);
// detection.expressions.happy から「ユーザーが笑顔か」を判定します … ⑤
// ババーン!は連続発火させずに、アイドリングタイム分だけ処理を間引きます
if (detection.expressions.happy > smileBorderLine) {
if (!(lastOutTime < IDLING_TIME)) {
isOut = true;
lastOutTime = IDLING_TIME;
seEl.play();
}
}
if (isOut) {
lastOutTime -= DECREMENT_TIME;
}
if (lastOutTime <= 0) {
isOut = false;
lastOutTime = IDLING_TIME;
}
}
// setTimeout で再帰的に detectFaces 関数を実行します
setTimeout(() => detectFaces(), DECREMENT_TIME);
};
setTimeout(() => detectFaces(), DECREMENT_TIME);
});
- ①: face-api.js のモデルを読み込みます。
- 今回は Vite プロジェクトで組んでいます。face-api.js リポジトリの weights に格納されたファイルをダウンロードして、public 配下に格納しました。
- ②: モデルの読み込み後に、Webカメラの許可をユーザーに求めます。
-
navigator.mediaDevices.getUserMedia()
で、Webカメラの許可をユーザーに求めます。 -
then()
チェーンになっているので、モデルの読み込みが完了後に発火します。
-
- ③: video 要素の srcObject に stream をセットします。
- これで video 要素に Web カメラの映像が展開されます。
- ④: Web カメラが許可され、video に再生されたときに発火する関数を用意します。
- video 要素にはあらかじめ
autoplay
属性を付与しているので、③まで来ると自動的に発火します。 - 関数の内容は、【うわっ...私の表情、硬すぎ...?】face-api.jsで顔検出して感情と年齢を判定するとほぼ同様です。
- video 要素にはあらかじめ
- ⑤:
detection.expressions.happy
から「ユーザーが笑顔か」を判定します。- Web カメラの映像に登場する顔の表情を
resizedDetections
からdetection
として取り出します。resizedDetections
配列の各要素が、検出された顔の情報を持つオブジェクトです。
そこから、さらにdetection.expressions.happy
= 検出された顔の表情が喜び・幸せ(happy)である確率を取り出します。 - あとは、その値が閾値を超えていれば、「ババーン!」の音が鳴らされます。
- 「ババーン!」の音が連続で鳴らされるのを防ぐために、アイドリングタイム(5秒)を設定しています。
- Web カメラの映像に登場する顔の表情を
まとめ
さて、デモアプリができましたので、次は実際に Web 会議ができるようしていこうと思います。
Wails とか Electron あたりを使って、デスクトップアプリを作れると良さそうですかね?
次回はいつになるか分かりませんが、やる気と実装する時間ができましたら、続きをやってみたいと思います〜。
Discussion