MediaPipe Pose는 이미지/영상에서 33개 인체 키포인트(랜드마크)를 실시간 추정한다.
브라우저(WASM/WebGL)·모바일·서버 등 다양한 환경에서 동작한다.
각 키포인트는 { x, y, z, visibility }를 가진다.
x, y: 0~1 정규화 화면 좌표(좌상단 원점). 픽셀 변환은 x*W, y*H.z: 카메라 축 기준 상대 깊이(절대 거리 아님).visibility: 0~1 신뢰도(가려짐·불확실성 반영).results.landmarks[0]
{ x, y, z, visibility }.results.worldLandmarks[0]
results.timestampMs
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 // 프레임간 추적 확신도
});
lite → full → heavy (정확도↑/속도↓).minTrackingConfidence를 약간 올려 안정화.numPoses: 1로 불필요한 오검출·부하를 줄임.// 픽셀 변환
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
};
<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으로 낮춘다.
LHIP=23, RHIP=24, LKNEE=25, RKNEE=26, LANKLE=27, RANKLE=28LSHOULDER=11, RSHOULDER=12, LEAR=7, REAR=8LFOOT_INDEX=31, RFOOT_INDEX=32, LHEEL=29, RHEEL=30const 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 이상만 사용.dist3(hip,knee)로 정규화해(yHip - yKnee) / femurLenPx > 0.05~0.08 같은 상대 임계도 함께 적용.requestAnimationFrame 사용(추가 타이머 남발 금지).lite로 시작 → 필요 시 full/heavy.visibility >= 임계 포인트가 일정 수 이상인 프레임 비율.