Repository navigation
Expand file tree
/
Copy pathhand_tracker.js
More file actions
99 lines (88 loc) · 3.76 KB
/
Copy pathhand_tracker.js
File metadata and controls
99 lines (88 loc) · 3.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import {
HandLandmarker,
FilesetResolver
} from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/vision_bundle.mjs";
export class HandTracker {
constructor() {
this.handLandmarker = null;
this.prevLeft = null;
this.prevRight = null;
this.smoothing = 0.4; // 0 = no smoothing, 1 = full smoothing
this.ready = false;
this.error = null;
}
async initialize() {
try {
const vision = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm"
);
this.handLandmarker = await HandLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath: "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task",
delegate: "GPU"
},
runningMode: "VIDEO",
numHands: 2,
minHandDetectionConfidence: 0.5,
minHandPresenceConfidence: 0.5,
minTrackingConfidence: 0.5
});
this.ready = true;
console.log("[HandTracker] Initialized successfully.");
} catch (err) {
this.error = err;
console.error("[HandTracker] Initialization failed:", err);
throw err;
}
}
/**
* Detect hands in the current video frame.
* Returns:
* fingertips – { left: {x,y}|null, right: {x,y}|null } (normalized 0-1)
* fullLandmarks – { left: Array[21]|null, right: Array[21]|null }
* handCount – number of hands detected (0, 1, or 2)
*/
detect(videoElement, timestamp) {
if (!this.handLandmarker) {
return { fingertips: { left: null, right: null }, fullLandmarks: { left: null, right: null }, handCount: 0 };
}
const results = this.handLandmarker.detectForVideo(videoElement, timestamp);
const fingertips = { left: null, right: null };
const fullLandmarks = { left: null, right: null };
const handCount = results.landmarks ? results.landmarks.length : 0;
if (handCount > 0) {
// Build array of { landmarks, tipX, tipY } for each detected hand
const hands = results.landmarks.map(lm => ({
landmarks: lm,
tipX: lm[8].x, // index fingertip (landmark 8), normalized 0-1
tipY: lm[8].y
}));
// Sort by tipX ascending → smaller X = more to the left in camera space
hands.sort((a, b) => a.tipX - b.tipX);
// Assign first (leftmost) to "left", second to "right"
// Apply exponential smoothing to reduce jitter
const applySmooth = (prev, rawX, rawY) => {
if (!prev) return { x: rawX, y: rawY };
return {
x: prev.x * this.smoothing + rawX * (1 - this.smoothing),
y: prev.y * this.smoothing + rawY * (1 - this.smoothing)
};
};
this.prevLeft = applySmooth(this.prevLeft, hands[0].tipX, hands[0].tipY);
fingertips.left = this.prevLeft;
fullLandmarks.left = hands[0].landmarks;
if (hands[1]) {
this.prevRight = applySmooth(this.prevRight, hands[1].tipX, hands[1].tipY);
fingertips.right = this.prevRight;
fullLandmarks.right = hands[1].landmarks;
} else {
this.prevRight = null;
}
} else {
// No hands detected – reset smooth history
this.prevLeft = null;
this.prevRight = null;
}
return { fingertips, fullLandmarks, handCount };
}
}