fruitlearn / js /handTracking.js
Nguyen Van Manh
a
4cfb14f
Raw
History Blame Contribute Delete
2.4 kB
// MediaPipe Tasks Vision — HandLandmarker. Loaded from CDN (online delivery, per plan).
// The heavy library is imported LAZILY (dynamic import inside initHandTracking) so page load and
// the Start button never depend on the CDN; a CDN failure becomes a catchable startup error.
const LIB_URL = "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/vision_bundle.mjs";
const WASM_PATH = "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm";
const MODEL_URL =
"https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task";
// Landmarks that sit around the palm; averaging them gives a stable anchor for the hand icon
// (steadier than a single fingertip, and matches a big palm-shaped cursor).
const PALM_LANDMARKS = [0, 5, 9, 13, 17];
let handLandmarker = null;
export async function initHandTracking() {
const { HandLandmarker, FilesetResolver } = await import(LIB_URL);
const vision = await FilesetResolver.forVisionTasks(WASM_PATH);
const options = {
baseOptions: { modelAssetPath: MODEL_URL, delegate: "GPU" },
runningMode: "video",
numHands: 1,
};
try {
handLandmarker = await HandLandmarker.createFromOptions(vision, options);
} catch (err) {
// Some machines/browsers lack a usable GPU delegate — fall back to CPU.
console.warn("HandLandmarker GPU init failed, falling back to CPU:", err);
options.baseOptions.delegate = "CPU";
handLandmarker = await HandLandmarker.createFromOptions(vision, options);
}
return handLandmarker;
}
// Returns { x, y } normalized [0,1] palm anchor in the UN-flipped video frame, or null if no hand.
// `timestampMs` must be monotonically increasing (the requestAnimationFrame timestamp works).
export function detectHand(video, timestampMs) {
if (!handLandmarker || !video.videoWidth) return null;
let result;
try {
result = handLandmarker.detectForVideo(video, timestampMs);
} catch (err) {
// A rare frame can throw (e.g. timestamp hiccup) — skip it rather than crash the loop.
return null;
}
if (!result || !result.landmarks || result.landmarks.length === 0) return null;
const lm = result.landmarks[0];
let x = 0;
let y = 0;
for (const i of PALM_LANDMARKS) {
x += lm[i].x;
y += lm[i].y;
}
x /= PALM_LANDMARKS.length;
y /= PALM_LANDMARKS.length;
return { x, y };
}