3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MYJLabAdvent Calendar 2024

Day 5

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

Last updated at Posted at 2024-12-05

はじめに

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

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

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

どういうゲーム?

花火げゑむ
※カメラ起動・音楽あり(重すぎてスマホ非対応です)

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

IMG_9316.JPG
相模原祭での様子

スクリーンショット 2024-10-11 12.17.47.png
実際のゲーム画面

技術構成

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

  • p5.js : インタラクティブアートを描画できるライブラリ
  • ml5.js : 簡易的な機械学習ができるライブラリ

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

おわりに

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

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?