【JavaScript】表情認識で操作する花火げゑむ作った(みんなで)

## はじめに

こんにちは。
MYJLab Advent Calendar 2024の5日目を担当する3年小笠原です。

今回は10月の相模原祭で宮治研究室として出展した、表情をリアルタイムで認識して爆発させる"花火げゑむ"を紹介します！

> 本当は自分で作ったやつを投稿しようと思ってたけど、時間がありませんでした
> 同じチームmのジェイ、めぐ、りほ、ごめんなさい！お菓子あげるから許して😢

## どういうゲーム?
[花火げゑむ](https://ogaoga08.github.io/sagaFes_team-m/)
※カメラ起動・音楽あり(重すぎてスマホ非対応です)

1. 画面上に顔文字の花火の玉がランダムで打ち上がる( 😁 😭 😐 😨 😳 😡 : 全6種類)
1. リアルタイムでプレイヤーの表情の認識して、各顔文字に合った表情をすると花火が _散る_
1. 制限時間内に _散った_ 花火の数を競う


![IMG_9316.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3774078/b9a6a9ab-98e0-335d-55d1-88b7a0172c2d.jpeg)
相模原祭での様子


![スクリーンショット 2024-10-11 12.17.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3774078/deb9fb70-8e03-cfb8-5ae6-58bf7b8d7637.png)
実際のゲーム画面

## 技術構成
#### 今回使用した言語はJavaScriptのみで、2つのライブラリを使いました。

- [p5.js](https://p5js.org/) : インタラクティブアートを描画できるライブラリ
- [ml5.js](https://ml5js.org/) : 簡易的な機械学習ができるライブラリ

> 機械学習といえばPythonの方が主流ですが、花火を描画できるようなPythonライブラリがいまいち見つからなかったので、全てJavaScriptで書きました

p5.jsはビジュアルアートを作成できるProcessingという言語をJavaScriptに移植したもので、ユーザーの操作に応じたインタラクティブな要素を描画できます。
ml5.jsは静止画・動画から写っているものを判別したり、人間のポーズを取得したりできる機械学習のライブラリです。
今回の表情認識はml5.js内のFace-Api機能を利用して、リアルタイムで表情を取得しています。

また、文化祭当日にランキングを表示させたかったため、ブラウザで動くクライアントサイドのインメモリデータベースである[AlaSQL](https://alasql.org)を使用し、ブラウザのLocalStrage上でスコアの上位5つを保存しています。

## 主要なコード達
p5.js, ml5.jsの導入方法・UIの記述などは省きます！

#### キャンバスの作成、Face-Apiの読み込み
```javascript
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);
  
  ~~~~~~~~~~~~~~~
  
};
```

#### 表情認識の開始・ユーザーの顔に特徴点を表示・各表情のパーセンテージを変数に格納
```javascript
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);
    }
    
  }
}
```

#### 顔文字花火玉の発火、打ち上げ
```javascript
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);
      }
    }
    
    ~~~~~~~~~

```

#### 顔文字花火玉の爆発処理

```javascript
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();
    }
  }
}

```

#### 各表情ごとの花火爆発閾値の設定
```javascript
//(´ε｀ )♥////(´ε｀ )♥///////(´ε｀ )♥/////////
/////////////////////////////////////////////
// ( ✹‿✹ ) 以下、表情ごとの花火のクラス ( ✹‿✹ )//
/////////////////////////////////////////////

// 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種類の表情ごとに個別に設定(各表情によって表現の難易度が異なるため)

```

## おわりに

ぜひやってみてください！
きっと表情筋がバキバキに鍛えられることでしょう