3️⃣ 웹 AI 모델로 피코 제어하기

image.png

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>손 제스처 + 블루투스</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      background: #000;
    }
    #video, #canvas {
      position: absolute;
      top: 0; left: 0;
      width: 100vw;
      height: 100vh;
      object-fit: cover;
      transform: scaleX(-1);
    }
    #gestureNumber {
      position: absolute;
      top: 40px;
      left: 50%;
      transform: translateX(-50%);
      color: white;
      font-size: 6rem;
      font-weight: bold;
      text-shadow: 2px 2px 10px #000;
      z-index: 10;
    }
    #connectButton {
      position: absolute;
      top: 10px;
      left: 10px;
      z-index: 20;
      padding: 10px 20px;
      font-size: 16px;
    }
  </style>
</head>
<body>
  <video id="video" autoplay playsinline muted></video>
  <canvas id="canvas"></canvas>
  <div id="gestureNumber">-</div>
  <button id="connectButton">블루투스 연결</button>

  <!-- MediaPipe Hands -->
  <script src="<https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.min.js>"></script>
  <script src="<https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.min.js>"></script>
  <script src="<https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.min.js>"></script>

  <script>
    const video = document.getElementById('video');
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const gestureDisplay = document.getElementById('gestureNumber');
    const connectButton = document.getElementById('connectButton');

    let lastGesture = null;
    let device, server, writeCharacteristic;
    let isConnected = false;

    const NUS_SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
    const NUS_TX_CHAR_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';

    connectButton.addEventListener('click', async () => {
      try {
        device = await navigator.bluetooth.requestDevice({
          filters: [{ services: [NUS_SERVICE_UUID] }]
        });
        server = await device.gatt.connect();
        const service = await server.getPrimaryService(NUS_SERVICE_UUID);
        writeCharacteristic = await service.getCharacteristic(NUS_TX_CHAR_UUID);
        isConnected = true;
        alert('블루투스 연결 완료');
      } catch (error) {
        alert('연결 실패: ' + error);
      }
    });

    function countFingers(landmarks) {
      let count = 0;
      const tips = [8, 12, 16, 20];
      for (let tip of tips) {
        if (landmarks[tip].y < landmarks[tip - 2].y) count++;
      }
      if (landmarks[4].x > landmarks[2].x) count++;
      return count;
    }

    function isHandOpen(fingerCount) {
      return fingerCount >= 4;
    }

    function detectGesture(results) {
      const hands = results.multiHandLandmarks;
      if (!hands || hands.length === 0) {
        updateGesture("-");
        return;
      }

      const states = hands.map(lm => isHandOpen(countFingers(lm)));

      if (states.length === 1) {
        if (states[0]) updateGesture("1");
        else updateGesture("2");
      } else if (states.length === 2) {
        if (states[0] && states[1]) updateGesture("3");
        else if (!states[0] && !states[1]) updateGesture("4");
        else updateGesture("-");
      }
    }

    function updateGesture(gesture) {
      if (gesture !== lastGesture) {
        gestureDisplay.textContent = gesture;
        lastGesture = gesture;
        if (isConnected && gesture !== "-") {
          sendData(gesture);
        }
      }
    }

    async function sendData(data) {
      const encoded = new TextEncoder().encode(data);
      try {
        await writeCharacteristic.writeValue(encoded);
        console.log("전송됨:", data);
      } catch (err) {
        console.error("전송 실패:", err);
      }
    }

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

    hands.setOptions({
      maxNumHands: 2,
      modelComplexity: 1,
      minDetectionConfidence: 0.7,
      minTrackingConfidence: 0.7
    });

    hands.onResults(results => {
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.drawImage(results.image, 0, 0, canvas.width, canvas.height);

      if (results.multiHandLandmarks) {
        for (let lm of results.multiHandLandmarks) {
          drawConnectors(ctx, lm, HAND_CONNECTIONS, { color: '#00FF00', lineWidth: 2 });
          drawLandmarks(ctx, lm, { color: '#FF0000', lineWidth: 2 });
        }
      }

      detectGesture(results);
    });

    async function startCamera() {
      const stream = await navigator.mediaDevices.getUserMedia({ video: true });
      video.srcObject = stream;
      video.onloadedmetadata = () => {
        const camera = new Camera(video, {
          onFrame: async () => await hands.send({ image: video }),
          width: 640,
          height: 480
        });
        camera.start();
      };
    }

    startCamera().catch(err => alert("카메라 접근 실패: " + err.message));
  </script>
</body>
</html>

2️⃣ 미디어파이프로 웹에서 불러오기

image.png

1️⃣ 코드펜으로 나만의 웹사이트 제작하기

<https://codepen.io/leeun/full/XJbqNgZ>

image.png

image.png

image.png

image.png

image.png

image.png

<aside> 📌

[과제명][학교][이름] 바꿔주세요, 과제태그를 선생님 설명 듣고 넣어주세요.

</aside>