p5.jsとMediaPipeで親指（サムズアップ）検出👍してLINE通知する仕組みをChatGPTと一緒に作る


サムズアップを検出してLINEに通知する仕組みを作ってみます。 **自分でコードは一行も書いてません。**

> ![](https://i.gyazo.com/95aec6fca4b96db1fa97890647ab732b.gif)
> zennで書籍的に作っても良さそうだと思いつつとりあえずメモ的に置いておきます。

## 1 サムズアップ検出アプリを作る

ChatGPTを使ったり、サンプルを改変したりして作ってみます。

### 1-1. 良きサンプルを見つける

> ![スクリーンショット 2024-06-20 16.30.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35387/b6b30293-0823-726d-99e1-5a0c075d885f.png "スクリーンショット 2024-06-20 16.30.28.png")
> 検索結果はタイミングによって変わりますが... https://www.google.com/search?q=p5.js+%E6%8C%87%E6%A4%9C%E5%87%BA


huchinhsiangさんという方の良きサンプルを見つけました。

https://editor.p5js.org/huchinhsiang/sketches/abcFSMGoO

コードの良い悪いはよくわからないので動くかどうかで判断するで良いと思います。

**自分が試した環境で動いているので（現在の自分にとって）良いサンプルということにしましょう。**

> ![](https://i.gyazo.com/440004d9081bd6f59b37ea1899fe5d5b.gif)

### 1-2. サンプルをもとに修正

サンプルをもとにサムズアップを判定する仕組みにしてもらいます。

> ![スクリーンショット 2024-06-20 17.05.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35387/d63f28ad-9aa6-b287-2fc1-095065e90f88.png "スクリーンショット 2024-06-20 17.05.07.png")

### 1-3. 試すと無事にサムズアップ検出してくれた。

GPT-4oに頼んでみましたが、人によって環境が違うと思うので、念の為sketch.jsの中身は[こんな感じ](https://gist.github.com/n0bisuke/a0d406713e279721e7f01ddddee57ae1)です。（一行も自分で書いてない）

これで無事にサムズアップを検出してくれました。

> ![](https://i.gyazo.com/e28d776dfb99b8357cf13a4a868f3dfb.jpg)

## 2. LINE NotifyにアクセスするためのAPIを作る

次にLINEに通知したいですが、CORSという仕組みに阻まれてこのままだとうまく通知できません。

こちらの内容をみてLINE Notifyに送信するAPIを作ってみましょう。

https://zenn.dev/protoout/books/line-notify-cors

（ちょっと複雑かも）

## 3. サムズアップ検出と自作したAPIサーバーへのリクエストを組み合わせる

1-3で作った[コード](https://gist.github.com/n0bisuke/a0d406713e279721e7f01ddddee57ae1)の中の以下の部分に変更を加えて、親指が上がった時に2で作成したMakeのAPIにリクエストをします。

```js
// 親指が上がったときの関数
function onThumbUp() {
  console.log("サムズアップ検出!");
  alert("サムズアップ検出!");
}
```

ChatGPTにこんな感じで聞いてコードを吐き出してもらいました。

https://chatgpt.com/share/e/483bf1d3-287e-4bc9-ab5f-60899007dba0


作ってもらったコードはこちらに貼っておきます。

https://editor.p5js.org/n0bisuke/sketches/FC-VwWQbw

105行目のWebhookURLを自身のURLにすれば動くと思います。

## 4. LINE Notifyと繋げて試す

最後です。うまく動くでしょうか。

・
・
・
・

> ![](https://i.gyazo.com/95aec6fca4b96db1fa97890647ab732b.gif)

動きました！コードの中身全く見てないので不安でしたが動きますね。

（ちなみにシャボン玉みたいな表示が出てくるのはmacOSのデフォルト機能なのでp5.jsやMediaPipeなどは関係なくZoomやGoogle Meetなどでも出てきます、まぎらわしい。。苦笑）

## まとめ

p5.js / MediaPipe / LINE Notifyを繋げてみました。

LINE NotifyはCORS問題でMakeを経由させましたがそれ以外のAPIリクエストならもっとシンプルだったと思います。

コード一行も自分で書かなくてもこれくらいはできちゃうんですねぇ。ChatGPTありがたや。。。

中身は見てないので変なところあってもご愛嬌です。

```js
const isFlipped = true; // 左右反転をさせるかどうかのフラグ、反転をさせるように設定

let keypointsHand = []; // 手のキーポイントを保持する配列
let thumbUpAction = false; // 親指が上がった時のアクションフラグ

// MediaPipe Hands 関連の処理 【ここから↓】
const videoElement = document.getElementsByClassName("input_video")[0];
videoElement.style.display = "none";

function onHandsResults(results) {
  keypointsHand = results.multiHandLandmarks;
}

const hands = new Hands({
  locateFile: (file) => {
    return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
  },
});

hands.setOptions({
  selfieMode: isFlipped, // 表示反転のオプション
  maxNumHands: 2, // 今回、簡単化のため検出数の最大1つまでに制限
  modelComplexity: 1,
  minDetectionConfidence: 0.5,
  minTrackingConfidence: 0.5,
});
hands.onResults(onHandsResults);

const camera = new Camera(videoElement, {
  onFrame: async () => {
    await hands.send({ image: videoElement });
  },
  width: 1280,
  height: 720,
});
camera.start();

// MediaPipe Hands 関連の処理 【ここまで↑】
// p5.js 関連の処理 【ここから↓】

let videoImage;

function setup() {
  createCanvas(720, 500); // アスペクト比を4:3や16:9でないものにしてみる
  videoImage = createGraphics(640, 360); // カメラ映像はいったんここで受けとる
}

function draw() {
  background(80, 80, 150);

  // 「drawingContext」を利用する
  videoImage.drawingContext.drawImage(
    videoElement,
    0,
    0,
    videoImage.width,
    videoImage.height
  );

  push();
  if (isFlipped) {
    // フラグによって表示側の左右反転を切り替え
    translate(width, 0);
    scale(-1, 1);
  }
  // 表示領域とビデオのアスペクト比が異なるようにしたので、表示サイズを補正
  displayWidth = width;
  displayHeight = (width * videoImage.height) / videoImage.width;
  image(videoImage, 0, 0, displayWidth, displayHeight);
  pop();

  if (keypointsHand.length > 0) {
    for (let i = 0; i < keypointsHand.length; i++) {
      const handLandmarks = keypointsHand[i];

      // 親指のランドマーク
      const thumbTip = handLandmarks[4];
      const thumbBase = handLandmarks[2];
      const indexTip = handLandmarks[8];

      // 親指の先に円を表示
      fill(150, 150, 255);
      ellipse(thumbTip.x * displayWidth, thumbTip.y * displayHeight, 30);

      // サムズアップの検出条件：
      // 親指の先端が親指の基部より上にあり、かつ親指が他の指よりも前に突き出している
      if (thumbTip.y < thumbBase.y && thumbTip.z < indexTip.z) {
        if (!thumbUpAction) {
          thumbUpAction = true;
          onThumbUp();
        }
      } else {
        thumbUpAction = false; // 親指が上がっていない時にフラグをリセット
      }
    }
  }
}

// 親指が上がったときの関数
async function onThumbUp() {
  console.log("サムズアップ検出!");
  // alert("サムズアップ検出!");

  // APIサーバーにHTTPリクエストを送信
  const url = 'https://hook.us1.make.com/xxxxxxx'; // ここにAPIのURLを設定してください
  const data = { message: "サムズアップを検出!!!!!!!" };

  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const responseData = await response.json();
    console.log('サーバーからの応答:', responseData);
  } catch (error) {
    console.error('HTTPリクエストのエラー:', error);
  }
}

```