👙

[TensorFlow.js+MediaPipe Hands]気軽にロマンを探せるアプリを作った

に公開2

はじめに

人はロマン(おっぱい)を求めてしまう生き物です。特にストレスが溜まったらおっぱいに癒されたい、そう思うことは少なくないかと思います。
けど手軽な場所におっぱいがない、そんな人のためにどこでもおっぱいを探すことができるアプリを作りました。

https://x.com/toffy_dev/status/1884010569826721952

※音が鳴るので注意してください

中身はMediaPipe Handsを使って結構真面目に手の位置を取っているので、今回はMediaPipe Handsモデルを使った手の検出方法などに視点を当てて書いていきたいと思います。

アプリ: https://oppai.toffy.dev/
GitHub: https://github.com/toffyui/oppai

アプリの内容

使い方はとても簡単です。
おっぱいを探したいときにアプリを開いて、おっぱいを探しに行くをクリックします。

あとはひたすらおっぱいを探すだけです。
おっぱいに触れた場合はおっぱいを触っている音がでます。あとおっぱいを触って嬉しくなったことを表現するために背景が少し赤くなるようにしています。

手のひらの中心がおっぱいにあたれば、画面が切り替わって、何秒でおっぱいを探すことができたのかが確認できます。大当たり〜

技術スタック

  • React
  • TypeScript
  • TensorFlow.js(Hand Pose Detection)
  • Vercel

TensorFlow.jsライブラリの一つに丁度HandPoseという手のランドマーク検出に特化したモデルがあったので、それを最初に使っていたんですが、両手での探知ができないことに気がついたのでHand Pose Detectionを使いました。

手のランドマークの検出方法

必要なライブラリのインストール

yarn add @tensorflow-models/hand-pose-detection @tensorflow/tfjs-core @tensorflow/tfjs-backend-webgl @tensorflow/tfjs-converter @mediapipe/hands

手の検出方法

流れとしては以下です

1. カメラを用意する
2. モデルを読み込む
3. 手の検出をリアルタイムで実行する
  1. カメラの用意
  const videoRef = useRef<HTMLVideoElement>(null);
  const [isVideoReady, setIsVideoReady] = useState(false);
  useEffect(() => {
    const setupCamera = async () => {
      const stream = await navigator.mediaDevices.getUserMedia({ video: true });
      if (videoRef.current) {
        videoRef.current.srcObject = stream;

        videoRef.current.onloadedmetadata = () => {
          videoRef.current!.width = videoRef.current!.videoWidth;
          videoRef.current!.height = videoRef.current!.videoHeight;
          setIsVideoReady(true);
        };

        await new Promise((resolve) => {
          videoRef.current!.onloadeddata = resolve;
        });
      }
    };

    setupCamera();
  }, []);

カメラが用意完了してからモデルを読み込みたいので、カメラの読み込みが完了したらフラグをTrueにしておきます。

  1. モデルを読み込む
import * as handPoseDetection from "@tensorflow-models/hand-pose-detection";

  useEffect(() => {
    if (!isVideoReady) return;
    const loadHandposeModel = async () => {
      await tf.setBackend("webgl");
      await tf.ready();

      const model = await handPoseDetection.createDetector(
        handPoseDetection.SupportedModels.MediaPipeHands,
        { runtime: "tfjs", modelType: "full" }
      );
      detectHands(model);
    };
    loadHandposeModel();
  }, [isVideoReady]);

あとはここで読み込んだモデルを使って、手の検出をリアルタイムで行う関数を作ります。

  1. 手の検出をリアルタイムで実行する
  const detectHands = async (model: handPoseDetection.HandDetector) => {
    if (!videoRef.current) return;

    const video = videoRef.current;
    const videoWidth = video.videoWidth;
    const videoHeight = video.videoHeight;

    const detect = async () => {
      const hands = await model.estimateHands(video);
      if (hands.length > 0 && randomPoint) {
        let isNear = false;
        let isHit = false;

        hands.forEach((hand) => {
          const landmarks = hand.keypoints; // もし3Dの座標を取得したい場合はkeypoints3Dを使う

          // 手の中心の位置を計算する
          const palmLandmarksIndices = [0, 1, 5, 9, 13, 17];
          const palmLandmarks = palmLandmarksIndices.map(
            (index) => landmarks[index]
          );
          const palmCenter = palmLandmarks.reduce(
            (acc, { x, y }) => {
              acc.x += x;
              acc.y += y;
              return acc;
            },
            { x: 0, y: 0 }
          );
          palmCenter.x /= palmLandmarks.length;
          palmCenter.y /= palmLandmarks.length;

          // ビデオサイズに合わせて微調整
          const scaledX =
            window.innerWidth -
            (palmCenter.x / videoWidth) * window.innerWidth;
          const scaledY = (palmCenter.y / videoHeight) * window.innerHeight;

          const scaledLandmarks = landmarks.map(({ x, y }) => ({
            x: window.innerWidth - (x / videoWidth) * window.innerWidth,
            y: (y / videoHeight) * window.innerHeight,
          }));

          // 省略(ここでおっぱいの位置に手が触れているかどうかを計算して状態を更新)
          // 省略(どちらかの手の中心がおっぱいの位置の中心にあるかを計算して状態を更新)
        });
      }
      requestAnimationFrame(detect);
    };

    detect();
  };

おっぱいの位置に手が当たっているかどうか部分等のコードに関しては他にあまり使い道が思いつかないので省略してますが、一応全体のコードが入ったリポジトリを載せておきます。

https://github.com/toffyui/oppai

あとがき

手の検知は3Dにしようかとも考えましたが、3Dにすると本当におっぱいを見つける難易度が上がりすぎてしまうのでz軸に関しては考えないことにしました。
ただ手の3D検知は可能性を感じたので今度何かの機会に使いたいなと思っています。

何も役に立たないアプリを作ってしまいましたが、やはりクソアプリを作っている時が一番生を感じますね。今後も定期的に作り続けようと思います。

それでは最後まで読んでくださりありがとうございました。
少しでも笑ってもらえたら/参考になれば嬉しいです。

参照

Discussion

KAKA

爆笑しました。ありがとうございます。