MediaPipe Pose 정리

a·2025년 10월 20일
0

사이드 프로젝트

목록 보기
3/3

MediaPipe Pose 정리: 사람 자세추적

1) 개요

  • MediaPipe Pose는 이미지/영상에서 33개 인체 키포인트(랜드마크)를 실시간 추정한다.

  • 브라우저(WASM/WebGL)·모바일·서버 등 다양한 환경에서 동작한다.

  • 각 키포인트는 { x, y, z, visibility }를 가진다.

    • x, y: 0~1 정규화 화면 좌표(좌상단 원점). 픽셀 변환은 x*W, y*H.
    • z: 카메라 축 기준 상대 깊이(절대 거리 아님).
    • visibility: 0~1 신뢰도(가려짐·불확실성 반영).

2) 장점 요약

  • 셋업 간단: CDN만으로 구동 가능.
  • 경량/빠름: 저사양 기기에서도 30FPS 내외 가능.
  • 전신 33포인트: 어깨·엉덩이·무릎·발목 등 하체 분석에 유리.
  • 배포 용이: 연구/프로토타입부터 상용 초입까지 활용하기 좋음.

3) 결과/옵션에서 알아야 할 변수(치트시트)

(A) 출력 결과

  • results.landmarks[0]

    • 33개 포인트 배열. 각 원소: { x, y, z, visibility }.
  • results.worldLandmarks[0]

    • 카메라 독립 월드 좌표(단위≈미터). 거리/각도 계산에 안정적.
  • results.timestampMs

    • 비디오 추론 시각(ms). 홀드 판정, 드롭 프레임 분석 등에 사용.

(B) 생성 옵션

const landmarker = await PoseLandmarker.createFromOptions(fileset, {
  baseOptions: {
    modelAssetPath: "…/pose_landmarker_lite.task",
    // delegate: "GPU" | "CPU"  // 웹 기본은 GPU 백엔드
  },
  runningMode: "VIDEO",            // "IMAGE" | "VIDEO"
  numPoses: 1,                     // 추적 인원 수
  minPoseDetectionConfidence: 0.5, // 사람 감지 확신도
  minPosePresenceConfidence: 0.5,  // 포즈 존재 확신도
  minTrackingConfidence: 0.5       // 프레임간 추적 확신도
});
  • 모델 크기: litefullheavy (정확도↑/속도↓).
  • 흔들림이 잦으면 minTrackingConfidence를 약간 올려 안정화.
  • 다인 추적이 아니면 numPoses: 1로 불필요한 오검출·부하를 줄임.

(C) 좌표계/파생 변수 유틸

// 픽셀 변환
const toPx = (p, W, H) => ({ x: p.x * W, y: p.y * H, z: p.z });

// 가시성 필터
const VIS_TH = 0.6;
const pick = (lm, i) => (lm?.[i] && (lm[i].visibility ?? 0) >= VIS_TH) ? lm[i] : null;

// 거리/각도 (월드 좌표 권장)
const dist3 = (a,b)=>Math.hypot(a.x-b.x, a.y-b.y, a.z-b.z);
const angleABC = (a,b,c)=>{ // ∠ABC
  const u={x:a.x-b.x,y:a.y-b.y,z:a.z-b.z};
  const v={x:c.x-b.x,y:c.y-b.y,z:c.z-b.z};
  const dot=u.x*v.x+u.y*v.y+u.z*v.z;
  const nu=Math.hypot(u.x,u.y,u.z), nv=Math.hypot(v.x,v.y,v.z);
  return Math.acos(Math.min(1, Math.max(-1, dot/(nu*nv)))); // rad
};

4) 웹에서 빠르게 시작하기 (최소 예제)

<video id="cam" playsinline autoplay></video>
<canvas id="view"></canvas>
<script type="module">
  import {
    FilesetResolver, PoseLandmarker, DrawingUtils
  } from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest";

  const video = document.getElementById("cam");
  const canvas = document.getElementById("view");
  const ctx = canvas.getContext("2d");
  const drawer = new DrawingUtils(ctx);

  // 카메라
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  video.srcObject = stream;
  await video.play();
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;

  // 모델
  const fileset = await FilesetResolver.forVisionTasks(
    "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
  );
  const landmarker = await PoseLandmarker.createFromOptions(fileset, {
    baseOptions: {
      modelAssetPath:
        "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task",
    },
    runningMode: "VIDEO",
    numPoses: 1,
    minPoseDetectionConfidence: 0.5,
    minPosePresenceConfidence: 0.5,
    minTrackingConfidence: 0.5,
  });

  // 루프
  async function loop() {
    const now = performance.now();
    const res = await landmarker.detectForVideo(video, now);

    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
    if (res?.landmarks?.[0]) {
      drawer.drawLandmarks(res.landmarks[0]);
      drawer.drawConnectors(res.landmarks[0], PoseLandmarker.POSE_CONNECTIONS);
    }
    requestAnimationFrame(loop);
  }
  loop();
</script>

팁: 성능이 낮으면 입력 해상도를 960×540 또는 640×360으로 낮춘다.

5) 자주 쓰는 인덱스(하체·코어 위주)

  • 골반/무릎/발목: LHIP=23, RHIP=24, LKNEE=25, RKNEE=26, LANKLE=27, RANKLE=28
  • 어깨/귀(상체 기준선): LSHOULDER=11, RSHOULDER=12, LEAR=7, REAR=8
  • 발끝/뒤꿈치: LFOOT_INDEX=31, RFOOT_INDEX=32, LHEEL=29, RHEEL=30
    ※ 화면이 미러링되어 보여도 인덱스는 인체 기준(L/R) 그대로다.

6) 예: 스쿼트 깊이(힙이 무릎보다 낮은가)

const LHIP=23, RHIP=24, LKNEE=25, RKNEE=26;
const VIS_TH = 0.6;

function pickYPx(lm, idx, H) {
  const p = lm?.[idx];
  return (p && (p.visibility ?? 0) >= VIS_TH) ? p.y * H : null;
}

function squatDepthPass(lm, H) {
  const yHip  = pickYPx(lm, RHIP, H) ?? pickYPx(lm, LHIP, H);
  const yKnee = pickYPx(lm, RKNEE, H) ?? pickYPx(lm, LKNEE, H);
  if (yHip == null || yKnee == null) return { decided: false };
  // 캔버스 y는 아래로 갈수록 커진다 → yHip - yKnee > 0 이면 힙이 더 낮음
  const pass = (yHip - yKnee) > 0;
  return { decided: true, pass, diffPx: (yHip - yKnee) };
}

권장 보정/완화

  • 가시성 임계: visibility >= 0.6 이상만 사용.
  • 홀드 시간: 조건을 0.3~0.5초 연속 만족 시 최종 PASS.
  • 스케일 보정: 대퇴 길이 dist3(hip,knee)로 정규화해
    (yHip - yKnee) / femurLenPx > 0.05~0.08 같은 상대 임계도 함께 적용.

7) 성능 최적화 체크리스트

  • 입력 해상도 낮추기(예: 960×540/640×360).
  • requestAnimationFrame 사용(추가 타이머 남발 금지).
  • 디버그 드로잉 토글로 불필요한 렌더링 최소화.
  • 모델 변형: lite로 시작 → 필요 시 full/heavy.

8) 흔한 문제와 대처

  • landmarks가 null/빈 배열: 인물 크기·조명 부족 → 카메라 거리/조명 개선.
  • 좌우 혼동: 미러 화면이라도 인덱스는 L/R 고정.
  • z(깊이) 오해: 절대 거리 아님. 기기/장면 비교엔 정규화 또는 상대 비교만.

9) 품질 확인 간단 지표

  • 유효 프레임 비율: visibility >= 임계 포인트가 일정 수 이상인 프레임 비율.
  • 지연/프레임레이트: end-to-end ms, FPS.
  • 라벨 비교: 샘플 영상(20~50개) 수동 라벨과 일치율/정밀도/재현율.

0개의 댓글