チュートリアル アブラカダブラ: p5.jsとml5.jsで手を使って話そう

アブラカダブラ: p5.jsとml5.jsで手を使って話そう

By Akanksha Vyas, Portia Morrell

このチュートリアルでは、p5.jsのスケッチを手で制御する方法を学びます。最後には、手を動かすだけでコンピュータの画面上で花が開いたり閉じたりするような、クールなものを作ることができるようになります!

想像してみてください:p5.jsを使ってコンピュータで花を作っています。手を開くと花が咲き、拳を握ると花も閉じます。

ラップトップ画面上で円で作られた花をp5.jsで制御する手。手を開くと花が開き、拳を握ると花が閉じる。

ラップトップ画面上で円で作られた花をp5.jsで制御する手。手を開くと花が開き、拳を握ると花が閉じる。

前提条件

始める前に、以下のような基本的なコーディングの概念を知っておく必要があります:

  • 変数
  • 配列
  • ループ
  • オブジェクト
  • マウスによるインタラクティビティ

必要なもの:

  • コンピュータ
  • ウェブカメラ
  • インターネット接続
  • p5.jsコードを書いて実行する場所。詳しくは環境のセットアップをご覧ください。

インタラクティビティに関する過去のチュートリアルでは、マウスやキーボード(物理的なオブジェクト)を使ってコンピュータと対話する方法を学びました。これらのツールは長年にわたってコンピュータとの対話を助けてきました。しかし今では、必ずしもそれらは必要ありません。このチュートリアルでは、魔法使いが杖を振るように、手を直接使ってp5.jsの描画を制御する方法を学びます!

これを実現するために、ml5.jsライブラリHandPoseという機械学習モデルを使用します。ml5.js HandPoseモデルは、コンピュータ画面上での手の位置を特定します。

機械学習とは、たくさんの例を見せることでコンピュータに学習させ、選択を行わせる方法です。コードは例を見て、私たちが学ぶのと同じように、つながりを作り出します。猫と犬の違いを教えたい場合、たくさんの写真を見せて、どれが猫でどれが犬かを教えます。機械学習モデルに見せる例が多ければ多いほど、より良くなります。その後、新しい写真を見せると、それが猫なのか犬なのかを判断できるようになります。これが機械学習の実践です。機械学習の例については、YouTubeのこのビデオをご覧ください。

ml5.jsHandPose機械学習モデルは、画像内の手を認識し、各指のポイントを特定することができます。マウスを使ってインタラクティブなスケッチを作る場合、マウスカーソルの位置を取得し、mouseXmouseYのような組み込み変数を使って画面上のものを制御します。ml5.jsを使うと、ウェブカメラを通じて手の動きで同様のことができます。

ステップ1 - ml5.jsのセットアップ

  • p5.js Web エディタでml5.js HandPose Imageの例を開きます。コピーを作成し、“Handpose Sketch”のような名前を付けてください。

    p5.js Web エディタで実行されているml5.js Handpose_Imageコード。手の画像がキャンバス上に表示され、各指と手のひらに沿って緑色の点が表示されています。キャンバスの上に「Handpose with single image」というテキストが表示されています。

    p5.js Web エディタで実行されているml5.js Handpose_Imageコード。手の画像がキャンバス上に表示され、各指と手のひらに沿って緑色の点が表示されています。キャンバスの上に「Handpose with single image」というテキストが表示されています。

  • このリンクから手の画像をダウンロードし、hand.jpgという名前を付けます。データフォルダの中にアップロードしてください。画像が既にフォルダ内に存在する場合は、置き換えてください。

let handpose;
let predictions = [];
let img;
// メインプログラムが開始する前に画像を読み込む
function preload(){
  img = loadImage("data/hand.jpg");
}
function setup() {
  // 画像のサイズ以上のキャンバスを作成する
  createCanvas(400, 350);
  // 読み込まれたらmodelReady()を呼び出す
  handpose = ml5.handpose(modelReady);
  frameRate(1); // この場合、速く実行する必要がないのでframeRateを1に設定
}
// poseNetの準備ができたら、検出を行う
function modelReady() {
  console.log("Model ready!");
 
  // predict関数が呼び出されたとき、
  // handposeに結果の処理方法を指示する。
  // この場合、結果をグローバル変数の
  // predictionsに代入する
  handpose.on("predict", results => {
    predictions = results;
  });
  handpose.predict(img);
}
// ポーズが見つかるまで、draw()は何も表示しない
function draw() {
  if (predictions.length > 0) {
    image(img, 0, 0, width, height);
    drawKeypoints();
    noLoop(); // ポーズの推定が完了したらループを停止
  }
}
// 検出されたキーポイント上に楕円を描画する関数
function drawKeypoints() {
  for (let i = 0; i < predictions.length; i += 1) {
    const prediction = predictions[i];
    for (let j = 0; j < prediction.landmarks.length; j += 1) {
      const keypoint = prediction.landmarks[j];
      fill(0, 255, 0);
      noStroke();
      ellipse(keypoint[0], keypoint[1], 10, 10);
    }
  }
}

緑色の4つの点で各指を識別し、5つの緑色の点で親指と手のひらの下部を識別した手のひら。

緑色の4つの点で各指を識別し、5つの緑色の点で親指と手のひらの下部を識別した手のひら。

ライブデモ

ml5.jsサンプルコードの詳細

ml5.jsの仕組み、特にHandPoseモデルについて詳しく見ていきましょう。ml5.jsは、私たち全員が機械学習を簡単に使えるようにするために作られたツールです。p5.jsとの相性が抜群で、私たちが行うように、カメラを使ってコンピュータが「見て、聞いて、理解する」ことができます!

HandPoseモデルは画像認識アルゴリズムを使用して手を認識します。手のひらと指を検出し、カメラの前で手を動かすとそれらを追跡し続けることができます。一度に1つの手しか検出できませんが、手の21の異なるキーポイントを3D空間で識別することができます。これは、各ポイントのx-y-z座標が得られることを意味します。これらのキーポイントは、手のひらと指の重要な部分を示しています。

では、これを実際に見るために静止画像から始めましょう。

//handPose mlモデルの変数
let handpose;
//手のポイント予測用の配列
let predictions = [];
//画像用の変数
let img;
// メインプログラムが開始する前にデータフォルダから画像を読み込む
function preload(){
  img = loadImage("data/hand.jpg");
}

function setup() {
  // 画像のサイズ以上のキャンバスを作成する
  createCanvas(400, 350);
  // handposeモデル(次の行で初期化される)の読み込みが開始したことを知らせる
  print("loading")
 
  /* handposeモデルを初期化し、
  modelReady関数をコールバックとして渡し、
  handposeモデルが読み込まれて使用可能になったら
  modelReadyを呼び出す */
  handpose = ml5.handpose(modelReady);
  /* 画像は変化しないので、モデルの予測も
  変化しないはずなのでframeRateを1に設定 */
  frameRate(1);
}
// handPose mlモデルの準備ができたらこの関数が実行される
function modelReady() {
  // モデルが読み込まれ、予測開始の準備ができたことを知らせる
  console.log("Model ready!");
 
  /* predict関数が呼び出されたとき、結果の
     処理方法をhandposeに指示する関数を渡す */
   handpose.on("predict", function(results) {
    // 予測結果をpredictions変数に格納
    predictions = results;
  });
  /* 手のランドマークと指を識別するために
    画像に対してhandpose.predictメソッドを呼び出す */
   handpose.predict(img);
}
// ポーズが見つかるまで、draw()は何も表示しない
function draw() {
  if (predictions.length > 0) {
    //キャンバスに画像を表示
    image(img, 0, 0, width, height);
   
    //全ての予測に対してポイントを描画する関数
    drawKeypoints();
    // ポーズの推定が完了したら描画を停止
    noLoop();  
  }
}
// 検出されたキーポイント上に楕円を描画する関数
function drawKeypoints() {
 // predictions配列に予測が存在することを確認
 if (predictions.length > 0) {
    // Handposeモデルは予測を続けます。シンプルにするため、この場合は最初の予測のみを使用
    let prediction = predictions[0];
           /* 各予測ポイントの配列
              インデックス 0 - 4 : 親指
              インデックス 5 - 8 : 人差し指
              インデックス 9 - 12 : 中指
              インデックス 13 - 16 : 薬指
              インデックス 17 - 20 : 小指
           */
     
     let keypoints = prediction.landmarks
   
    //コンソールに予測オブジェクトを表示   
    console.log(prediction)
   
    /* 予測された全てのキーポイントをループ処理
              インデックス 0 : x
              インデックス 1 : y
              インデックス 2 : z  */
         
    for (let keypoint of keypoints) {
        //各キーポイントに楕円を描画
        fill(0, 255, 0);
        noStroke();
        ellipse(keypoint[0], keypoint[1], 10, 10);
    }
  }
}

緑色の4つの点で各指を識別し、5つの緑色の点で親指と手のひらの下部を識別した手のひら。

緑色の4つの点で各指を識別し、5つの緑色の点で親指と手のひらの下部を識別した手のひら。

ライブデモ

コードを実行してみましょう。

  1. まず、HandPoseモデルが読み込み中であることを知らせるため、コンソールに”loading”が表示されます。
  2. 準備ができると、コンソールに”Model ready!”が表示されます。これは、HandPoseモデルが読み込まれ、画像内の手の位置を特定する準備ができたことを意味します。
  3. モデルが読み込まれて使用可能になると、modelReady関数が呼び出されます。HandPoseモデルはml5.jsのウェブサイトでホストされています。読み込みには通常数秒かかります。modelReady関数は画像を見て、手がどこにあるかを理解しようとします。
  4. HandPoseのpredict()関数(modelReady関数内)は、手の形をマッピングするJavaScriptオブジェクトを返します。このオブジェクトはpredictionsという変数に格納されます。このデータを表示するには、draw()関数内にconsole.log(predictions[0])というprint文を追加できます。これは条件文の中でnoLoop()の直後に見つけることができます。モデルが予測した手のポイントが次の行に表示されます。

ステップ2 - データを理解しよう

console.log(predictions[0])の出力は以下のようになります:

{
  handInViewConfidence: 0.9990172386169434, 
  boundingBox: {
    topLeft: Array(2),
    bottomRight: Array(2),
  },
  landmarks: Array(21),
  annotations: {
    thumb: Array(4),
    indexFinger: Array(4),
    middleFinger: Array(4),
    ringFinger: Array(4)
    pinky: Array(4),
    palmBase: Array(1)
  }
}

プログラムを実行すると、手の異なる部分の位置を理解するのに役立つオブジェクトの配列が得られます:

  • handInViewConfidence:フレーム内に手が存在する確率を示すスコアです(プログラムが手を見ていると確信している度合い)。スコアが高いほど、手を見ていることをより確信しています。
  • boundingBox:手の端がどこにあるかを示します。手を囲むボックスの右上隅と左下隅のx-y座標を提供します。このボックスは下の画像で緑色のボックスとして表示されています。
  • landmarks:モデルが推測する21の予測ポイントの配列で、手の異なる領域を示します。これらのポイントは手のひらの下部から始まり、各指に4つのポイントを含めて上に向かっています。手の上の異なる場所を示すマーカーのようなものです。
  • annotations:各指がどこにあるかの予測です。各指について、下から先端まで4つのポイントがあり、どの指がどれかを簡単に判別できます。画像では、これらのポイントは小さな緑の円として見ることができます。

landmarksとannotationsの各ポイントには、3D空間での位置を示す3つの数字があります。これらはそのポイントのx-y-z座標です。インデックス0 = x、インデックス1 = y、インデックス2 = zです。

緑色の4つの点で各指を識別し、5つの点で親指から手の下部を識別した手に、緑色の境界ボックスが手を囲んでいる

緑色の4つの点で各指を識別し、5つの点で親指から手の下部を識別した手に、緑色の境界ボックスが手を囲んでいる

例:アノテーション(注釈)

以下の例では、各指先を異なる色で描画します。annotationsを使用して、各指のポイントを含む配列を取得します。すでに各指の先端は配列の最後のポイントであり、このポイントにはx-y-z座標があることを知っています。その点の最初の2つの数値(x-y座標)を使用して、指先の位置に正確に楕円を描画します。

// preload(), setup(), modelReady(), draw() は前述の通り

// 指のオブジェクトを作成する関数
function createFinger(name, points, color) {
  return {
    name: name,
    points: points,
    color: color
  };
}
// 検出されたアノテーション上に楕円を描画する関数
function drawKeypoints() {

  if (predictions.length > 0) {
    let prediction = predictions[0];
   
    // ラベル、ポイント、色を持つ指オブジェクトを作成
    let fingers = [
        createFinger("thumb", prediction.annotations.thumb, 'red'),
        createFinger("indexFinger", prediction.annotations.indexFinger, 'green'),
        createFinger("middleFinger", prediction.annotations.middleFinger, 'blue'),
        createFinger("ringFinger", prediction.annotations.ringFinger, 'yellow'),
        createFinger("pinky", prediction.annotations.pinky, 'purple')
      ];
    
    //各指の色とポイントにアクセスするためにイテレート
    for (let i = 0; i < fingers.length; i += 1) {
      let finger = fingers[i];
      fill(finger.color);
      noStroke();
     
      // finger.pointsリストの最後の要素(インデックス3の要素)にある指先にアクセスし、そのx-y座標を使用して楕円を描画
      ellipse(finger.points[3][0], finger.points[3][1], 10, 10);
    }
  }
}

各指と親指の先端が異なる色の点で識別された手

各指と親指の先端が異なる色の点で識別された手

ライブデモ

上記の例のコードで何が起きているか見てみましょう:

  • まず、createFinger()という関数を追加します。この関数は以下を持つ指オブジェクトを作成します:
    • predictions[0].annotationsオブジェクトのキーに対応する指の名前
    • 各指に対応する配列のポイント
    • 描画したい指の点の色

次にdrawKeypoints()関数に移ります。この関数で起こることは:

  • まず、手のポイントに対する予測が少なくとも1つあるかチェックする条件を追加します。変数predictionをそのような最初の予測に割り当てます
  • 次に、各指に対してcreateFinger()関数をループで呼び出し、指オブジェクトのリストを作成します
    • 各指オブジェクトは3つのプロパティを含みます:名前、そのポイント、色(createFinger()関数の定義で見たように)
  • 次に、fingersリストをループして各指の色を取得し、fill()に設定するループがあります。これにより、その行以降に描画されるものすべてがその色で描画されます
  • 最後に、finger.points[3]にある指先のx座標とy座標を使用して楕円を描画します
    • finger.points[3][0]はポイントのx座標に対応します
    • finger.points[3][1]はポイントのy座標に対応します

例:帽子

この例では、指先にジェスター帽子をかぶせます。これを実現するには、各指先の楕円を帽子の画像に置き換えるだけです。

//handpose mlモデルの変数
let handpose;

//手のポイント予測用の配列
let predictions = [];

//画像用の変数
let img;

// 帽子の画像を保存する新しい変数
let hatImage;

// メインプログラムが開始する前に画像を読み込む
function preload() {
  img = loadImage("data/hand.jpg");

  // 帽子の画像を読み込む
  hatImage = loadImage(""data/hat.png");
}
function setup() {
  // 少なくとも画像サイズのキャンバスを作成
  createCanvas(400, 350);
  print("loading")
  // 読み込まれたらmodelReady()を呼び出す
  handpose = ml5.handpose(modelReady);
  frameRate(1); // この場合は速く実行する必要がないのでフレームレートを1に設定
}

// ポーズが見つかるまでdraw()は何も表示しない
function draw() {
  if (predictions.length > 0) {
    image(img, 0, 0, width, height);
    drawKeypoints();
    noLoop(); // ポーズが推定されたらループを停止
  }
}

// モデルの準備ができたら、メッセージがコンソールに表示され、各ランドマークを配置する場所を予測する
function modelReady() {
  console.log("Model ready!");
    // handposeの準備ができたら、検出を実行
    handpose.on("predict", function(results) {
    predictions = results;
  });
  //handpose.predictメソッドをimgに対して呼び出し、ランドマークと指を予測
  handpose.predict(img);
}

// 指を作成する関数
function createFinger(name, points, color) {
  return {
    name: name,
    points: points,
    color: color
  };
}
// 検出されたアノテーション上に楕円を描画する関数
function drawKeypoints() {
   if (predictions.length > 0) {
    let prediction = predictions[0];
   
    // ラベル、ポイント、色を持つ指オブジェクトを作成
    let fingers = [
        createFinger("thumb", prediction.annotations.thumb, 'red'),
        createFinger("indexFinger", prediction.annotations.indexFinger, 'green'),
        createFinger("middleFinger", prediction.annotations.middleFinger, 'blue'),
        createFinger("ringFinger", prediction.annotations.ringFinger, 'yellow'),
        createFinger("pinky", prediction.annotations.pinky, 'purple')
      ];
    for (let i = 0; i < fingers.length; i += 1) {
      let finger = fingers[i];
      fill(finger.color);
      noStroke();
     
      // finger.pointsリストの最後の要素(インデックス3の要素)にある指先にアクセスし、そのx-y座標を使用して楕円を描画
      ellipse(finger.points[3][0], finger.points[3][1], 10, 10);
      
      // 前の例の点の代わりに帽子画像をレンダリング
      image(hatImage, finger.points[3][0] - hatImage.width / 2, finger.points[3][1] - hatImage.height / 2);
    }
  }
}

各指と親指の先端にジョーカーの帽子がついた手

各指と親指の先端にジョーカーの帽子がついた手

ライブデモ

この例のコードを理解しましょう:

  • まず、透明な背景を持つ小さな.pngファイル(最大100x100ピクセル)をアップロードします。この画像を保存する新しい変数を作成し、preload関数で読み込みます。
  • 次に、前のコード例の各楕円と同じ位置に画像を配置します。
  • 次に、各指先ポイントで画像を中央に配置するようにポイントを調整します。画像は指先ポイントが帽子画像の左下隅、つまり(0,0)ポイントとなるようにレンダリングされます。帽子画像をそのポイントで中央に配置するために、指先ポイントのx座標から画像の幅の半分を引き、y座標から画像の高さの半分を引きます。
  • 最後に、drawKeypoints()関数の最後で楕円を描画する代わりにhatImageをレンダリングします。

例:ライブWebcamフィードの使用

この例では、静止画像をライブwebcamフィードに置き換えます。

ml5.js Handposeに戻り、p5.js web editorのWebcamリファレンスを開きます。コピーを作成し、“Handpose Webcam Sketch”のような名前を付けてください。

let handpose;
let video;
let predictions = [];
function setup() {
  createCanvas(640, 480);
  video = createCapture(VIDEO);
  video.size(width, height);
  handpose = ml5.handpose(video, modelReady);
  // これは新しい手のポーズが検出されるたびにグローバル変数"predictions"を
  // 配列で満たすイベントを設定します
  handpose.on("predict", results => {
    predictions = results;
  });
  // ビデオ要素を非表示にし、キャンバスのみを表示
  video.hide();
}
function modelReady() {
  console.log("Model ready!");
}
function draw() {
  image(video, 0, 0, width, height);
  // すべてのキーポイントとスケルトンを描画するために両方の関数を呼び出すことができます
  drawKeypoints();
}
// 検出されたキーポイント上に楕円を描画する関数
function drawKeypoints() {
  for (let i = 0; i < predictions.length; i += 1) {
    const prediction = predictions[i];
    for (let j = 0; j < prediction.landmarks.length; j += 1) {
      const keypoint = prediction.landmarks[j];
      fill(0, 255, 0);
      noStroke();
      ellipse(keypoint[0], keypoint[1], 10, 10);
    }
  }
}

空中に掲げられた手のビデオフィードで、各指に緑の点がついている

空中に掲げられた手のビデオフィードで、各指に緑の点がついている

ライブデモ

例:ライブWebcamフィードを使用した帽子

このコードを変更して、各指先に帽子を追加してみましょう。以下が必要な手順です:

  1. 帽子画像を保存する変数を追加
  2. preload関数で帽子画像を読み込む
  3. setup()関数内で、HandPoseモデルからの予測結果をpredictions配列に保存する行があります。このコードをmodelReady()関数に移動します。これは前の例のようにコードを動作させるためです。ただし、この変更をしなくてもスケッチは動作します。両方の方法が正しいためです。
  4. 前の例からcreateFinger関数を追加
  5. 前の「帽子」の例と同じdrawKeypoints関数に置き換えます。このコードはcreateFinger関数を使用して指を作成し、予測された指先のポイントに帽子を描画します。

今回はライブビデオフィードを使用しているので、指先は動いています。

//handpose mlモデルの変数
let handpose;
//ビデオフィードを保存する変数
let video;
//手のポイント予測用の配列
let predictions = [];
//帽子の画像を保存する変数
let hatImage;
// メインプログラムが開始する前に帽子の画像を読み込む
function preload() {
  hatImage = loadImage("data/hat.png");
}
function setup() {
  // キャンバスを作成
 createCanvas(640, 480);
  /* ビデオフィードをキャプチャし、現在のキャンバスの幅と高さに設定 */
 video = createCapture(VIDEO);
 video.size(width, height);
 /* 読み込まれたらmodelReady()を呼び出し、ビデオフィードを渡す */
 handpose = ml5.handpose(video, modelReady);
 /* これは新しい手のポーズが検出されるたびにグローバル変数"predictions"を
  配列で満たすイベントを設定します */
 handpose.on("predict", results => {
   predictions = results;
 });
 // ビデオ要素を非表示にし、キャンバスのみを表示
 video.hide();
}
// モデルの準備ができたら、メッセージがコンソールに表示され、各ランドマークを配置する場所を予測する
function modelReady() {
  console.log("Model ready!");
    // handposeの準備ができたら、検出を実行
    handpose.on("predict", function(results) {
    predictions = results;
  });
   //handpose.predictメソッドをvideoに対して呼び出し、ランドマークと指を予測
  handpose.predict(video);
}
// 指を作成する関数
function createFinger(name, points, color) {
  return {
    name: name,
    points: points,
    color: color
  };
}
function draw() {
  // ビデオフィードをレンダリング
  image(video, 0, 0, width, height);
  // キーポイントを使用して描画する関数を呼び出す
  drawKeypoints();
}
// 検出されたキーポイント上に帽子を描画する関数
function drawKeypoints() {
   if (predictions.length > 0) {
    let prediction = predictions[0];
   
    // 各指のポイントを構造体に追加
    let fingers = [
        createFinger("thumb", prediction.annotations.thumb, 'red'),
        createFinger("indexFinger", prediction.annotations.indexFinger, 'green'),
        createFinger("middleFinger", prediction.annotations.middleFinger, 'blue'),
        createFinger("ringFinger", prediction.annotations.ringFinger, 'yellow'),
        createFinger("pinky", prediction.annotations.pinky, 'purple')
      ];
    for (let i = 0; i < fingers.length; i += 1) {
      let finger = fingers[i];
      fill(finger.color);
      noStroke();
     
      // finger.pointsリストの最後の要素(インデックス3の要素)にある指先にアクセスし、そのx-y座標を使用して楕円を描画
      ellipse(finger.points[3][0], finger.points[3][1], 10, 10);
     
      image(hatImage, finger.points[3][0] - hatImage.width / 2, finger.points[3][1] - hatImage.height / 2);
    }
  }
}

手が動き回る画面で、各指と親指の先端にジョーカーの帽子がついており、帽子は手と一緒に動く

手が動き回る画面で、各指と親指の先端にジョーカーの帽子がついており、帽子は手と一緒に動く

ライブデモ

試してみよう!

各指に異なる帽子をつけてみましょう!

ステップ3 - インタラクティビティの追加

これでml5.js HandPoseモデルが提供するデータを理解できたので、それを使用してスケッチとインタラクトすることができます。条件分岐とインタラクティビティチュートリアルには、スケッチとインタラクトする楽しい例がいくつかあります。これらの例のいくつかから始めましょう - ただし、マウスの代わりに手を使用します。

マウスを使用してスケッチとインタラクトする場合、カーソルのx-y座標を追跡するためにmouseXmouseY変数を使用しました。今度は、指先のx-y座標を使用します。

例:人差し指でボールを動かす

この例では、人差し指で画面上の円を制御します。

「ライブWebcamフィードの使用」の例から始めましょう。

指でボールを動かすために以下の手順を実行します:

  • HandPoseモデルからの予測結果をpredictions配列に保存する行をsetup()関数に移動します(前の例と同様)。
  • drawKeypoints関数をdrawObject関数に置き換えます。
    • drawObject関数では、人差し指の4番目のポイント(配列のインデックス3)のx-y座標を見つけます。すでに知っているように、これは指先です。高さ(h)と幅(w)のパラメータを33に増やして、ここに大きな楕円を描画します。
    • draw関数内のdrawKeypointsの呼び出しをdrawObjectに置き換えます。

これで人差し指で画面上の白い円を制御できるようになりました。

//handpose mlモデルの変数
let handpose;
//ビデオフィードを保存する変数
let video;
//手のポイント予測用の配列
let predictions = [];
function setup() {
  // キャンバスを作成
 createCanvas(640, 480);
  /* ビデオフィードをキャプチャし、現在のキャンバスの幅と高さに設定 */
 video = createCapture(VIDEO);
 video.size(width, height);

  /* handposeモデル(次の行で初期化される)が読み込み中であることを知らせるためにプリント */
  print("loading")
  // 読み込まれたらmodelReady()を呼び出す
  handpose = ml5.handpose(video, modelReady);
  // ビデオ要素を非表示にし、キャンバスのみを表示
  video.hide();
}
// モデルの準備ができたら、メッセージがコンソールに表示され、各ランドマークを配置する場所を予測する
function modelReady() {
  console.log("Model ready!");
    // handposeの準備ができたら、検出を実行
    handpose.on("predict", function(results) {
    predictions = results;
  });
  //handpose.predictメソッドをvideoに対して呼び出し、ランドマークと指を予測
  handpose.predict(video);
}
function draw() {
  // ビデオフィードをレンダリング
  image(video, 0, 0, width, height);
  // キーポイントを使用して描画する関数を呼び出す
  drawObject();
}
// 指先にボールを描画する関数
function drawObject() {
  if (predictions.length > 0) {
    let prediction = predictions[0];
    let x = prediction.annotations.indexFinger[3][0]
    let y = prediction.annotations.indexFinger[3][1]
    print(prediction, x, y)
    noStroke();   
    // finger.pointsリストの最後の要素(インデックス3の要素)にある指先にアクセスし、そのx-y座標を使用して楕円を描画し、最も近い整数に丸める
    ellipse(round(x), round(y), 33, 33);
  }
}

指(1本または複数)が画面上を動き回り、人差し指の先端にのみ白い点がついている動画

指(1本または複数)が画面上を動き回り、人差し指の先端にのみ白い点がついている動画

ライブデモ

例:3本の異なる指で3つのボールを動かす

この例では、人差し指、中指、薬指の3本の指で画面上の3つの異なる円を制御します。

前回のスケッチを続けて修正しましょう。drawObject関数で変更する必要があるのは:

  • 前の例では人差し指の4番目のポイントのx-y座標を見つけました。これに加えて、中指と薬指のx-y座標も見つけます。それぞれを異なる変数に保存します。
  • そして、各ポイントに楕円を描画します。

これで3本の指にそれぞれ円が表示されるようになりました。

// setup(), modelReady(), draw() は前述の通り

// 指先にボールを描画する関数
function drawObject() {
  if (predictions.length > 0) {
    let prediction = predictions[0];
    //人差し指
    let indexX = prediction.annotations.indexFinger[3][0]
    let indexY = prediction.annotations.indexFinger[3][1]
    //中指    

    let middleX = prediction.annotations.middleFinger[3][0]
    let middleY = prediction.annotations.middleFinger[3][1]
    //薬指
    let ringX = prediction.annotations.ringFinger[3][0]
    let ringY = prediction.annotations.ringFinger[3][1]
    noStroke();


    // 下の円
    ellipse(round(indexX), round(indexY), 33, 33);    // 上の円
    ellipse(round(middleX), round(middleY), 33, 33); // 中央の円
    ellipse(round(ringX), round(ringY), 33, 33); // 下の円
  }
}

空中の手の3本の中央の指に白い円が3つついているwebcamフィード

空中の手の3本の中央の指に白い円が3つついているwebcamフィード

ライブデモ

試してみよう!

指を指している指に基づいて楕円のサイズを変更してください。小指は小さな円を、親指は最大の円を作ることができます。

例:指を指している場所にオブジェクトを移動

この例では、人差し指が指している画面の側に応じて、右または左に長方形を描画します。

「人差し指でボールを動かす」の例に戻って修正しましょう。drawObject関数で変更する必要があるのは:

  • まず、指先に描画する楕円のサイズを変更します。楕円の高さ(h)と幅(w)を非常に小さく(20)します。これを使って指の位置を追跡します。
  • 次に、人差し指の先端のx座標が300未満かどうかをチェックする条件を追加します。画面の幅が600なので、300は中間点を示します。これは指が画面の左半分を指していることを示します。ここで画面の左半分を覆う長方形を描画できます。コードがelse句に入った場合、指のx座標が300より大きいことがわかります。これは指が画面の右半分を指していることを示します。ここで画面の左半分を覆う長方形を描画します。

コードを実行してみましょう。指を左右に動かし、小さな灰色の点を使って指の位置を追跡します。人差し指を右から左に動かすと、キャンバスのその半分に長方形が表示されます。

毎回新しい長方形を描画していますが、指で同じ長方形を動かしているように見えます!

// … setup(), modelReady(), draw() は前述の通り

// 指が指している画面の半分に長方形を描画する関数
function drawObject() {
  if (predictions.length > 0) {
    let prediction = predictions[0];
    let x = prediction.annotations.indexFinger[3][0]
    let y = prediction.annotations.indexFinger[3][1]
    print(prediction, x, y)
    fill(51);
    noStroke();

    // 指を追跡する小さな楕円
    ellipse(round(x), round(y), 20, 20) 
    
    // 指のポイントが画面の左側にある場合は最初の長方形を描画し、そうでない場合は2番目の長方形を描画
    if (x < (300)) {
      rect(0, 0, 300, 480);  // 左
    }
    else {
      rect(300, 0, 300, 480); // 右
    }
  }
}

右または左を指している指と、指している側を覆う灰色の長方形

右または左を指している指と、指している側を覆う灰色の長方形

ライブデモ

試してみよう!

小さなゲームを作成してください。キャンバスを2つに分割し、画面の上からボールを落とします。各ボールには色があり、その色に基づいて、その側を指すことで右または左に送る必要があります。スコアを記録してください。

例:指を指すことでオブジェクトの色を変更

この例では、オブジェクトを指すことでその色を変更します。また、カーソルが画面上に描画されないように、drawループで背景も描画します。

前回のスケッチを続けて修正しましょう。

  • まず、スケッチに背景を追加して、オブジェクトの周りのビデオ背景が見えないようにします。setup関数にfill(0, 255, 0);の行を追加して、グレーの背景を追加します。
  • 次に、drawObject関数で変更を加えます:
    • まず、x座標150、y座標150に幅100、高さ200の長方形を作成します。
    • 次に、長方形を描画する前に、人差し指のx座標が長方形の中にあるかどうかをチェックする条件を追加します。これは長方形を指していることを示します。この場合、fill(255, 204, 0)を設定し、これ以降に描画されるものすべてを黄色で描画します。

コードを実行してみましょう。カメラの前で指を動かし、小さな灰色の点を追って指の位置を確認します。長方形の中に指を入れてみると、色が変わるのが見えます!

//handpose mlモデルの変数
let handpose;
//ビデオフィードを保存する変数
let video;
//手のポイント予測用の配列
let predictions = [];

function setup() {
  createCanvas(600, 480);
  video = createCapture(VIDEO);
  video.size(width, height);
  print("loading");
  fill(0, 255, 0);
  handpose = ml5.handpose(video, modelReady);
  // これは新しい手のポーズが検出されるたびにグローバル変数"predictions"を
  // 配列で満たすイベントを設定します
  handpose.on("predict", results => {
    predictions = results;
  });
  // ビデオ要素を非表示にし、キャンバスのみを表示
  video.hide();
}
// … modelReady(), draw() は前述の通り
// 指を追跡し、適切な色で長方形を描画する関数
function drawObject() {
   if (predictions.length > 0) {
      let prediction = predictions[0];
      let x = prediction.annotations.indexFinger[3][0]
      let y = prediction.annotations.indexFinger[3][1]
      print(prediction, x, y)
      fill(51);
      noStroke();
      // 指を追跡する小さな楕円
      ellipse(round(x), round(y), 20, 20)
      // 指のポイントが長方形の領域内にある場合は黄色で描画し、そうでない場合はグレーで描画
      if ((x > 150) && (x < 250) && (y > 150) && (y < 350))
    {
      fill(255, 204, 0);
    } else {
      fill(51);
    }
  }
  rect(150, 150, 100, 200);
 
}

画面中央の灰色のボックスと、ボックスの近くを動く小さな灰色の点。点がボックス内にあるとボックスが黄色に変わる

画面中央の灰色のボックスと、ボックスの近くを動く小さな灰色の点。点がボックス内にあるとボックスが黄色に変わる

ライブデモ

試してみよう!

例:花を咲かせる

この例では、楕円を使って花が咲くのをシミュレートし、指で咲き具合を制御する簡単なスケッチを作ります。

前回のスケッチを続けて修正しましょう。

  • 花びらの数、花びらのサイズ、咲き具合のサイズ、閾値、角度を保存する新しい変数を追加します。これらの変数については以下で詳しく説明します。

drawObject()関数では:

  • まず、花をどれだけ咲かせたいかを決めるため、targetSizeを0に設定して開始点とします。
  • prediction.annotationsを使って親指と小指の先端を見つけます。
  • 次に、これら2点がどれだけ離れているかを測定します。この距離は、手が開いているか閉じているかを判断するのに役立ちます。
    • 手が大きく開いている場合(距離が設定した閾値より大きい場合)、花を完全に咲かせることにします。
    • すでに閾値を100に設定しています。花を咲かせることに決めた場合、targetSizeを200ピクセルに設定します。
  • 次に、描画エリアの中央に移動し(translate(width / 2, height / 2))、花を中央に描画できるようにします。noStroke()(輪郭線なし)とfill(220, 20, 60, 50)を設定して、花に美しいピンク色を与えます。
  • lerpという関数を使って、花の咲き具合を先ほど決めたtargetSizeにスムーズに変化させます。これにより、変化が自然でスムーズに見えます。
  • 最後に、花を描画します。円を描いて花全体を作るために、一つずつ花びらを描画します。各花びらについて、ellipse関数を使って描画し、次の花びらを描く前に少し回転させて、きれいに広がるようにします。

コードを実行してみましょう!拳を作って(カメラに向かってパンチするように)、それから手を開いて花が咲くのを見てください。

//handpose mlモデルの変数
let handpose;
//ビデオフィードを保存する変数
let video;
//手のポイント予測用の配列
let predictions = [];
// 花の花びらの数を示す変数
let numPetals = 12;
// 各花びらの間の角度を計算する変数。円全体(360度)を花びらの数で割ります
let angle = 360 / numPetals;
// 各花びらの個々のサイズを設定する変数。数が大きいほど、各花びらが大きくなります
let petalSize = 100;
// 花の咲き具合の現在のサイズを追跡する変数。0から始まりますが、手の動きに基づいて変化します
let bloomSize = 0;
// 距離の閾値を設定する変数。手が開いているか閉じているかを判断するために使用されます。親指と小指の間の距離がこれより大きい場合、手は開いていると判断されます
let threshold = 100;
function setup() {
  // キャンバスを作成
  createCanvas(1000, 800);
 
  // ビデオをキャプチャ
  video = createCapture(VIDEO);
  video.size(width, height);
   
  // 角度を度単位で測定
  angleMode(DEGREES);
  // HandPoseモデルがまだ読み込み中であることを知らせる
  print("loading");
  // 読み込まれたらmodelReady()を呼び出す
  handpose = ml5.handpose(video, modelReady);
  // ビデオ要素を非表示にし、キャンバスのみを表示
  video.hide();
}
// モデルの準備ができたら、メッセージがコンソールに表示され、各ランドマークを配置する場所を予測する
function modelReady() {
  console.log("Model ready!");
  // handposeの準備ができたら、検出を実行
  handpose.on("predict", function(results) {
    predictions = results;
  });
  // 読み込まれたらmodelReady()を呼び出す
  handpose = ml5.handpose(video, modelReady);
}
function draw() {
  background(255);
  drawObject();
}
// この関数では、検出された手が開いているときは花を完全に咲かせ、手が拳のときは花を閉じた状態で描画します
function drawObject() {
  // 咲き具合の目標サイズを設定
  let targetSize = 0;
 
  if (predictions.length > 0) {
    let prediction = predictions[0];
   
    // 親指と小指の先端を見つける
    let thumbTip = prediction.annotations.thumb[3];
    let pinkyTip = prediction.annotations.pinky[3];
   
    // 親指と小指の間の距離を計算
    let distance = dist(thumbTip[0], thumbTip[1], pinkyTip[0], pinkyTip[1]);
   
    // 手が開いている場合、花の目標サイズを
    // 200ピクセルに設定
    if (distance > threshold) {
      targetSize = 200;
    }
  }
 
  // キャンバスの中心に原点(0, 0)を移動
  translate(width / 2, height / 2);
 
  // 描画スタイルを設定
  noStroke();
  fill(220, 20, 60, 50);
  // 咲き具合のサイズをスムーズに遷移
  bloomSize = lerp(bloomSize, targetSize, 0.1);
  // 花を描画
  for (let i = 0; i < numPetals; i += 1) {
    // 花びらを描画
    ellipse(bloomSize, 0, petalSize, petalSize);
   
    // 次の花びらのために座標系を回転
    rotate(angle);
  }
}

円が集まって1つの大きな円になり、多くの円として開く花

円が集まって1つの大きな円になり、多くの円として開く花

スケッチ作成:Akif Kazi、KJ Somaiya University, Mumbai の学生。

ライブデモ

試してみよう!
  • たくさんの種(楕円で表現できます)があるスケッチを作成してください。1つを指さし、指を使って咲かせてください。
  • あなたのp5.jsスケッチコレクションの中から、マウスを使ってスケッチとインタラクトするものを見つけてください。代わりに手を使ってみましょう。

次のステップ

ml5.jsライブラリのPoseNetFacemeshモデルを探索してみましょう。PoseNetモデルは、HandPoseが手に対して行うのと全く同じ方法で、全身の点を検出します。Facemeshモデルは顔の点を返します。これらのモデルが返すデータを理解し、スケッチで使用してみましょう。

試してみよう!

Facemeshモデルを使用して、以下のようなものを作成してください:

横から横へ動く人の顔で、鼻に紫の点、右目に黄色の点、左目に黄色い花がついている

横から横へ動く人の顔で、鼻に紫の点、右目に黄色の点、左目に黄色い花がついている

リソース