はじめに
こんにちは。
MYJLab Advent Calendar 2024の5日目を担当する3年小笠原です。
今回は10月の相模原祭で宮治研究室として出展した、表情をリアルタイムで認識して爆発させる"花火げゑむ"を紹介します!
本当は自分で作ったやつを投稿しようと思ってたけど、時間がありませんでした
同じチームmのジェイ、めぐ、りほ、ごめんなさい!お菓子あげるから許して😢
どういうゲーム?
花火げゑむ
※カメラ起動・音楽あり(重すぎてスマホ非対応です)
- 画面上に顔文字の花火の玉がランダムで打ち上がる( 😁 😭 😐 😨 😳 😡 : 全6種類)
- リアルタイムでプレイヤーの表情の認識して、各顔文字に合った表情をすると花火が 散る
- 制限時間内に 散った 花火の数を競う
技術構成
今回使用した言語はJavaScriptのみで、2つのライブラリを使いました。
機械学習といえばPythonの方が主流ですが、花火を描画できるようなPythonライブラリがいまいち見つからなかったので、全てJavaScriptで書きました
p5.jsはビジュアルアートを作成できるProcessingという言語をJavaScriptに移植したもので、ユーザーの操作に応じたインタラクティブな要素を描画できます。
ml5.jsは静止画・動画から写っているものを判別したり、人間のポーズを取得したりできる機械学習のライブラリです。
今回の表情認識はml5.js内のFace-Api機能を利用して、リアルタイムで表情を取得しています。
また、文化祭当日にランキングを表示させたかったため、ブラウザで動くクライアントサイドのインメモリデータベースであるAlaSQLを使用し、ブラウザのLocalStrage上でスコアの上位5つを保存しています。
主要なコード達
p5.js, ml5.jsの導入方法・UIの記述などは省きます!
キャンバスの作成、Face-Apiの読み込み
function setup() {
canvas = createCanvas(windowWidth, windowHeight);
colorMode(HSB);
stroke(255);
strokeWeight(4);
canvas.id("canvas");
video = createCapture(VIDEO);
video.id("video");
video.size(width, height);
const faceOptions = {
withLandmarks: true,
withExpressions: true,
withDescriptors: true,
minConfidence: 0.5
};
faceapi = ml5.faceApi(video, faceOptions, faceReady);
~~~~~~~~~~~~~~~
};
表情認識の開始・ユーザーの顔に特徴点を表示・各表情のパーセンテージを変数に格納
function faceReady() {
faceapi.detect(gotFaces);
}
function gotFaces(error, result) {
if (error) {
console.log(error);
return;
}
detections = result;
clear();
drawBoxs(detections);
drawExpressions(detections, 80, 250, 28);
faceapi.detect(gotFaces);
}
function drawBoxs(detections){
if (detections.length > 0) {
for (f=0; f < detections.length; f++){
let {_x, _y, _width, _height} = detections[0].alignedRect._box;
stroke(44, 225, 225);
strokeWeight(5);
noFill();
rect(_x, _y -100, _width, _height + 100);
}
}
}
function drawLandmarks(detections){
if (detections.length > 0) {
for (f=0; f < detections.length; f++){
let points = detections[f].landmarks.positions;
for (let i = 0; i < points.length; i++) {
stroke(44, 169, 225);
strokeWeight(3);
point(points[i]._x , points[i]._y * 1.6 - 280);
}
}
}
}
function drawExpressions(detections, x, y, textYSpace){
if (detections.length > 0) {
let {neutral, happy, angry, sad, disgusted, surprised, fearful} = detections[0].expressions;
happyG = happy;
neutralG = neutral;
angerG = angry;
sadG = sad;
disgustedG = disgusted;
surprisedG = surprised;
fearfulG = fearful;
x = x - 50;
y = 70;
if(!gameStarted && titleVisible){
textFont('Helvetica Neue');
textSize(50);
noStroke();
fill(0);
text("😐 : " + nf(neutral * 100, 2, 1) + "%", x, y);
text("😄 : " + nf(happy * 100, 2, 1) + "%", x, y + textYSpace * 2);
text("😡 : " + nf(angry * 100, 2, 1) + "%", x, y + textYSpace * 4);
text("😭 : " + nf(sad * 100, 2, 1) + "%", x, y + textYSpace * 6);
text("😳 : " + nf(surprised * 100, 2, 1) + "%", x, y + textYSpace * 8);
text("😨 : " + nf(fearful * 100, 2, 1) + "%", x, y + textYSpace * 10);
}
}
}
顔文字花火玉の発火、打ち上げ
function draw() {
~~~~~~~~~
if (gameStarted) {
if (titleDiv) {
titleDiv.remove();
titleDiv = null;
}
//各表情ごとの花火のクラスを生成し、全て allFireworks 配列に格納
if (random(1) < shootingRate) {
allFireworks.push(new HappyFirework());
}
if (random(1) < shootingRate) {
allFireworks.push(new SadFirework());
}
if (random(1) < shootingRate) {
allFireworks.push(new AngryFirework());
}
if (random(1) < shootingRate) {
allFireworks.push(new FearfulFirework());
}
if (random(1) < shootingRate) {
allFireworks.push(new SurprisedFirework());
}
if (random(1) < shootingRate) {
allFireworks.push(new NeutralFirework());
}
for (let i = allFireworks.length - 1; i >= 0; i--) {
allFireworks[i].update();
allFireworks[i].show();
if (allFireworks[i].done()) {
allFireworks.splice(i, 1);
}
}
~~~~~~~~~
顔文字花火玉の爆発処理
class Firework {
constructor(emoji) {
this.firework = new Particle(random(width), height, true, emoji);
this.exploded = false;
this.particles = [];
this.emoji = emoji;
}
done() {
return this.exploded && this.particles.length === 0;
}
explode() {
for (let i = 0; i < 100; i++) {
let p = new Particle(this.firework.pos.x, this.firework.pos.y, false, this.emoji);
this.particles.push(p);
}
playSfx2sec(sfx1);
explosionCount++;
}
show() {
if (!this.exploded) {
this.firework.show();
}
for (let i = 0; i < this.particles.length; i++) {
this.particles[i].show();
}
}
}
各表情ごとの花火爆発閾値の設定
//(´ε` )♥////(´ε` )♥///////(´ε` )♥/////////
/////////////////////////////////////////////
// ( ✹‿✹ ) 以下、表情ごとの花火のクラス ( ✹‿✹ )//
/////////////////////////////////////////////
// updateメソッドをいじって条件やエフェクトを追加・変更
// 打ち上がる花火の高さ(速さ)はParticleクラスで調整
class HappyFirework extends Firework {
constructor(){
let emoji = happyImage;
super(emoji);
}
update() {
if (!this.exploded) {
this.firework.applyForce(gravity);
this.firework.update();
if (this.firework.vel.y >= 0) {
if (happyG * 100 >= 0.98) {
this.exploded = true;
this.explode();
}
}
}
for (let i = this.particles.length - 1; i >= 0; i--) {
this.particles[i].applyForce(gravity);
this.particles[i].update();
if (this.particles[i].done()) {
this.particles.splice(i, 1);
}
}
}
show() {
if (!this.exploded) {
this.firework.show();
}
for (let i = 0; i < this.particles.length; i++) {
this.particles[i].show();
}
}
}
~~~~~~~~~~~~
// 以下、5種類の表情ごとに個別に設定(各表情によって表現の難易度が異なるため)
おわりに
ぜひやってみてください!
きっと表情筋がバキバキに鍛えられることでしょう