namish10 commited on
Commit
f77da70
·
verified ·
1 Parent(s): 841b0a7

Upload frontend/src/MediaPipeProcessor.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. frontend/src/MediaPipeProcessor.js +582 -0
frontend/src/MediaPipeProcessor.js ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * MediaPipeProcessor.js
3
+ *
4
+ * Advanced hand gesture recognition with swipe and pinch detection.
5
+ * Integrates with LLM Flow for gesture-triggered actions.
6
+ *
7
+ * Features:
8
+ * - Hand landmark detection (21 points)
9
+ * - Face mesh detection (468 points) for face blur
10
+ * - Swipe gesture detection (left, right, up, down)
11
+ * - Pinch gesture detection
12
+ * - Finger count detection
13
+ * - Real-time gesture-to-action triggering
14
+ */
15
+
16
+ class MediaPipeProcessor {
17
+ constructor(options = {}) {
18
+ this.options = {
19
+ maxHands: options.maxHands || 1,
20
+ modelComplexity: options.modelComplexity || 1,
21
+ minDetectionConfidence: options.minDetectionConfidence || 0.5,
22
+ minTrackingConfidence: options.minTrackingConfidence || 0.5,
23
+ onGestureDetected: options.onGestureDetected || null,
24
+ onLandmarksUpdate: options.onLandmarksUpdate || null,
25
+ onSwipeDetected: options.onSwipeDetected || null,
26
+ onPinchDetected: options.onPinchDetected || null,
27
+ ...options
28
+ };
29
+
30
+ this.hands = null;
31
+ this.faceMesh = null;
32
+ this.isInitialized = false;
33
+ this.camera = null;
34
+
35
+ this.lastLandmarks = null;
36
+ this.landmarkHistory = [];
37
+ this.maxHistorySize = 30;
38
+
39
+ this.swipeDetector = new SwipeDetector();
40
+ this.pinchDetector = new PinchDetector();
41
+ this.fingerCounter = new FingerCounter();
42
+
43
+ this.gestureCallback = null;
44
+ this.apiUrl = options.apiUrl || 'http://localhost:5001/api';
45
+ }
46
+
47
+ async initialize() {
48
+ if (this.isInitialized) return true;
49
+
50
+ try {
51
+ const { Hands } = await import('https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/hands.js');
52
+ const { FaceMesh } = await import('https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633529614/face_mesh.js');
53
+ const { Camera } = await import('https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils@0.3.1640029074/camera_utils.js');
54
+
55
+ this.hands = new Hands({
56
+ locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/${file}`
57
+ });
58
+
59
+ this.hands.setOptions({
60
+ maxNumHands: this.options.maxHands,
61
+ modelComplexity: this.options.modelComplexity,
62
+ minDetectionConfidence: this.options.minDetectionConfidence,
63
+ minTrackingConfidence: this.options.minTrackingConfidence
64
+ });
65
+
66
+ this.hands.onResults((results) => this.onHandResults(results));
67
+
68
+ this.faceMesh = new FaceMesh({
69
+ locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633529614/${file}`
70
+ });
71
+
72
+ this.faceMesh.setOptions({
73
+ maxNumFaces: 1,
74
+ refineLandmarks: true,
75
+ minDetectionConfidence: 0.5,
76
+ minTrackingConfidence: 0.5
77
+ });
78
+
79
+ this.faceMesh.onResults((results) => this.onFaceResults(results));
80
+
81
+ this.isInitialized = true;
82
+ return true;
83
+ } catch (error) {
84
+ console.error('Failed to initialize MediaPipe:', error);
85
+ return false;
86
+ }
87
+ }
88
+
89
+ async startCamera(videoElement, canvasElement, overlayCanvasElement) {
90
+ if (!this.isInitialized) {
91
+ await this.initialize();
92
+ }
93
+
94
+ this.videoElement = videoElement;
95
+ this.canvasElement = canvasElement;
96
+ this.overlayCanvasElement = overlayCanvasElement;
97
+
98
+ try {
99
+ const stream = await navigator.mediaDevices.getUserMedia({
100
+ video: { facingMode: 'user', width: 640, height: 480 }
101
+ });
102
+
103
+ videoElement.srcObject = stream;
104
+ await videoElement.play();
105
+
106
+ this.camera = new Camera(videoElement, {
107
+ onFrame: async () => {
108
+ if (this.hands && this.faceMesh) {
109
+ await this.hands.send({ image: videoElement });
110
+ await this.faceMesh.send({ image: videoElement });
111
+ }
112
+ },
113
+ width: 640,
114
+ height: 480
115
+ });
116
+
117
+ await this.camera.start();
118
+ return true;
119
+ } catch (error) {
120
+ console.error('Failed to start camera:', error);
121
+ return false;
122
+ }
123
+ }
124
+
125
+ stopCamera() {
126
+ if (this.camera) {
127
+ this.camera.stop();
128
+ this.camera = null;
129
+ }
130
+
131
+ if (this.videoElement && this.videoElement.srcObject) {
132
+ this.videoElement.srcObject.getTracks().forEach(track => track.stop());
133
+ this.videoElement.srcObject = null;
134
+ }
135
+ }
136
+
137
+ onHandResults(results) {
138
+ if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
139
+ const landmarks = results.multiHandLandmarks[0];
140
+ this.lastLandmarks = landmarks;
141
+
142
+ this.landmarkHistory.push({
143
+ landmarks: landmarks.map(lm => [lm.x, lm.y, lm.z]),
144
+ timestamp: Date.now()
145
+ });
146
+
147
+ if (this.landmarkHistory.length > this.maxHistorySize) {
148
+ this.landmarkHistory.shift();
149
+ }
150
+
151
+ if (this.options.onLandmarksUpdate) {
152
+ this.options.onLandmarksUpdate(landmarks);
153
+ }
154
+
155
+ const fingerCount = this.fingerCounter.count(landmarks);
156
+ const swipe = this.swipeDetector.detect(landmarks, fingerCount, this.landmarkHistory);
157
+ const pinch = this.pinchDetector.detect(landmarks);
158
+
159
+ if (swipe) {
160
+ this.onSwipeGesture(swipe, fingerCount);
161
+ }
162
+
163
+ if (pinch) {
164
+ this.onPinchGesture(pinch);
165
+ }
166
+
167
+ const basicGesture = this.detectBasicGesture(landmarks, fingerCount);
168
+ if (basicGesture && this.options.onGestureDetected) {
169
+ this.options.onGestureDetected(basicGesture);
170
+ }
171
+
172
+ this.drawHandLandmarks(landmarks);
173
+ } else {
174
+ this.lastLandmarks = null;
175
+ this.clearOverlay();
176
+ }
177
+ }
178
+
179
+ onFaceResults(results) {
180
+ if (results.multiFaceLandmarks && results.multiFaceLandmarks.length > 0) {
181
+ this.applyFaceBlur(results.multiFaceLandmarks[0]);
182
+ }
183
+ }
184
+
185
+ onSwipeGesture(swipe, fingerCount) {
186
+ const gestureName = `${fingerCount}_finger_swipe_${swipe.direction}`;
187
+
188
+ console.log(`Swipe detected: ${gestureName} (speed: ${swipe.speed.toFixed(3)})`);
189
+
190
+ if (this.options.onSwipeDetected) {
191
+ this.options.onSwipeDetected({
192
+ ...swipe,
193
+ fingerCount,
194
+ gestureName
195
+ });
196
+ }
197
+
198
+ this.sendGestureToBackend(gestureName, {
199
+ direction: swipe.direction,
200
+ fingerCount,
201
+ speed: swipe.speed,
202
+ startPosition: swipe.startPosition,
203
+ endPosition: swipe.endPosition
204
+ });
205
+ }
206
+
207
+ onPinchGesture(pinch) {
208
+ const gestureName = `pinch_${pinch.type}`;
209
+
210
+ console.log(`Pinch detected: ${gestureName} (distance: ${pinch.distance.toFixed(3)})`);
211
+
212
+ if (this.options.onPinchDetected) {
213
+ this.options.onPinchDetected(pinch);
214
+ }
215
+
216
+ this.sendGestureToBackend(gestureName, {
217
+ type: pinch.type,
218
+ distance: pinch.distance,
219
+ thumbTip: pinch.thumbTip,
220
+ indexTip: pinch.indexTip
221
+ });
222
+ }
223
+
224
+ detectBasicGesture(landmarks, fingerCount) {
225
+ if (fingerCount >= 4) {
226
+ return { type: 'open_palm', fingerCount };
227
+ }
228
+
229
+ if (fingerCount === 1) {
230
+ const indexTip = landmarks[8];
231
+ const indexBase = landmarks[5];
232
+ if (indexTip.y < indexBase.y) {
233
+ return { type: 'pointing', fingerCount: 1 };
234
+ }
235
+ }
236
+
237
+ if (this.isThumbsUp(landmarks)) {
238
+ return { type: 'thumbs_up', fingerCount };
239
+ }
240
+
241
+ return null;
242
+ }
243
+
244
+ isThumbsUp(landmarks) {
245
+ const thumbTip = landmarks[4];
246
+ const thumbIP = landmarks[3];
247
+ const indexTip = landmarks[8];
248
+ const middleTip = landmarks[12];
249
+
250
+ const thumbUp = thumbTip.y < thumbIP.y;
251
+ const fingersDown = indexTip.y > landmarks[6].y &&
252
+ middleTip.y > landmarks[10].y;
253
+
254
+ return thumbUp && fingersDown;
255
+ }
256
+
257
+ sendGestureToBackend(gestureName, parameters) {
258
+ if (!this.lastLandmarks) return;
259
+
260
+ const landmarksArray = this.lastLandmarks.map(lm => [lm.x, lm.y, lm.z]);
261
+
262
+ fetch(`${this.apiUrl}/llm/gesture-action`, {
263
+ method: 'POST',
264
+ headers: { 'Content-Type': 'application/json' },
265
+ body: JSON.stringify({
266
+ user_id: 'demo',
267
+ landmarks: landmarksArray,
268
+ gesture_name: gestureName,
269
+ context: {
270
+ topic: document.title || 'learning',
271
+ timestamp: Date.now()
272
+ }
273
+ })
274
+ }).catch(err => console.log('Backend not available for gesture'));
275
+ }
276
+
277
+ applyFaceBlur(landmarks) {
278
+ if (!this.canvasElement || !this.videoElement) return;
279
+
280
+ const ctx = this.canvasElement.getContext('2d');
281
+ const video = this.videoElement;
282
+
283
+ if (video.videoWidth === 0 || video.videoHeight === 0) return;
284
+
285
+ this.canvasElement.width = video.videoWidth;
286
+ this.canvasElement.height = video.videoHeight;
287
+
288
+ ctx.drawImage(video, 0, 0);
289
+
290
+ let minX = 1, maxX = 0, minY = 1, maxY = 0;
291
+ for (const lm of landmarks) {
292
+ minX = Math.min(minX, lm.x);
293
+ maxX = Math.max(maxX, lm.x);
294
+ minY = Math.min(minY, lm.y);
295
+ maxY = Math.max(maxY, lm.y);
296
+ }
297
+
298
+ const padding = 0.15;
299
+ const padX = (maxX - minX) * padding;
300
+ const padY = (maxY - minY) * padding;
301
+
302
+ const x = Math.max(0, Math.floor((minX - padX) * video.videoWidth));
303
+ const y = Math.max(0, Math.floor((minY - padY) * video.videoHeight));
304
+ const w = Math.min(video.videoWidth, Math.floor((maxX + padX - minX + padX) * video.videoWidth));
305
+ const h = Math.min(video.videoHeight, Math.floor((maxY + padY - minY + padY) * video.videoHeight));
306
+
307
+ if (w > 10 && h > 10) {
308
+ const imageData = ctx.getImageData(x, y, w, h);
309
+ const data = imageData.data;
310
+ const pixelSize = 15;
311
+
312
+ for (let py = 0; py < h; py += pixelSize) {
313
+ for (let px = 0; px < w; px += pixelSize) {
314
+ const i = (py * w + px) * 4;
315
+ const r = data[i];
316
+ const g = data[i + 1];
317
+ const b = data[i + 2];
318
+
319
+ for (let dy = 0; dy < pixelSize && py + dy < h; dy++) {
320
+ for (let dx = 0; dx < pixelSize && px + dx < w; dx++) {
321
+ const ni = ((py + dy) * w + (px + dx)) * 4;
322
+ data[ni] = r;
323
+ data[ni + 1] = g;
324
+ data[ni + 2] = b;
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ ctx.putImageData(imageData, x, y);
331
+ }
332
+ }
333
+
334
+ drawHandLandmarks(landmarks) {
335
+ if (!this.overlayCanvasElement || !this.videoElement) return;
336
+
337
+ const canvas = this.overlayCanvasElement;
338
+ const video = this.videoElement;
339
+
340
+ canvas.width = video.videoWidth;
341
+ canvas.height = video.videoHeight;
342
+
343
+ const ctx = canvas.getContext('2d');
344
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
345
+
346
+ const connections = [
347
+ [0, 1], [1, 2], [2, 3], [3, 4],
348
+ [0, 5], [5, 6], [6, 7], [7, 8],
349
+ [0, 9], [9, 10], [10, 11], [11, 12],
350
+ [0, 13], [13, 14], [14, 15], [15, 16],
351
+ [0, 17], [17, 18], [18, 19], [19, 20],
352
+ [5, 9], [9, 13], [13, 17]
353
+ ];
354
+
355
+ ctx.strokeStyle = 'rgba(0, 255, 136, 0.8)';
356
+ ctx.lineWidth = 2;
357
+
358
+ for (const connection of connections) {
359
+ const start = landmarks[connection[0]];
360
+ const end = landmarks[connection[1]];
361
+
362
+ ctx.beginPath();
363
+ ctx.moveTo(start.x * canvas.width, start.y * canvas.height);
364
+ ctx.lineTo(end.x * canvas.width, end.y * canvas.height);
365
+ ctx.stroke();
366
+ }
367
+
368
+ for (let i = 0; i < landmarks.length; i++) {
369
+ const landmark = landmarks[i];
370
+ const x = landmark.x * canvas.width;
371
+ const y = landmark.y * canvas.height;
372
+
373
+ ctx.beginPath();
374
+ ctx.arc(x, y, i % 4 === 0 ? 6 : 4, 0, 2 * Math.PI);
375
+ ctx.fillStyle = i % 4 === 0 ? '#00ff88' : '#ffffff';
376
+ ctx.fill();
377
+ ctx.strokeStyle = '#000000';
378
+ ctx.lineWidth = 1;
379
+ ctx.stroke();
380
+ }
381
+
382
+ const fingerCount = this.fingerCounter.count(landmarks);
383
+ ctx.fillStyle = '#ffffff';
384
+ ctx.font = 'bold 16px sans-serif';
385
+ ctx.fillText(`Fingers: ${fingerCount}`, 10, 30);
386
+ }
387
+
388
+ clearOverlay() {
389
+ if (!this.overlayCanvasElement) return;
390
+
391
+ const ctx = this.overlayCanvasElement.getContext('2d');
392
+ ctx.clearRect(0, 0, this.overlayCanvasElement.width, this.overlayCanvasElement.height);
393
+ }
394
+
395
+ getLandmarks() {
396
+ return this.lastLandmarks;
397
+ }
398
+
399
+ getLandmarkHistory() {
400
+ return this.landmarkHistory;
401
+ }
402
+ }
403
+
404
+ class SwipeDetector {
405
+ constructor() {
406
+ this.swipeThreshold = 0.12;
407
+ this.minSwipeSpeed = 0.003;
408
+ this.minHistoryForSwipe = 10;
409
+ this.swipeStart = null;
410
+ this.isSwipeInProgress = false;
411
+ this.lastSwipeTime = 0;
412
+ this.swipeCooldown = 500;
413
+ }
414
+
415
+ detect(landmarks, fingerCount, history) {
416
+ if (!landmarks || landmarks.length < 21) return null;
417
+ if (history.length < this.minHistoryForSwipe) return null;
418
+
419
+ const now = Date.now();
420
+ if (now - this.lastSwipeTime < this.swipeCooldown) return null;
421
+
422
+ const wrist = landmarks[0];
423
+ const middleFingerMcp = landmarks[9];
424
+
425
+ const currentPos = {
426
+ x: middleFingerMcp.x,
427
+ y: middleFingerMcp.y,
428
+ z: middleFingerMcp.z || 0
429
+ };
430
+
431
+ if (!this.swipeStart && history.length >= 5) {
432
+ const recent = history.slice(-5);
433
+ const movementX = Math.abs(recent[recent.length - 1].landmarks[9][0] - recent[0].landmarks[9][0]);
434
+ const movementY = Math.abs(recent[recent.length - 1].landmarks[9][1] - recent[0].landmarks[9][1]);
435
+
436
+ if (movementX > this.swipeThreshold || movementY > this.swipeThreshold) {
437
+ this.swipeStart = { ...currentPos, time: now };
438
+ this.isSwipeInProgress = true;
439
+ }
440
+ }
441
+
442
+ if (this.isSwipeInProgress && this.swipeStart) {
443
+ const timeDelta = (now - this.swipeStart.time) / 1000;
444
+ const deltaX = currentPos.x - this.swipeStart.x;
445
+ const deltaY = currentPos.y - this.swipeStart.y;
446
+
447
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
448
+ const speed = distance / Math.max(timeDelta, 0.1);
449
+
450
+ const recentHistory = history.slice(-8);
451
+ const recentMovementX = Math.abs(
452
+ recentHistory[recentHistory.length - 1].landmarks[9][0] -
453
+ recentHistory[0].landmarks[9][0]
454
+ );
455
+ const recentMovementY = Math.abs(
456
+ recentHistory[recentHistory.length - 1].landmarks[9][1] -
457
+ recentHistory[0].landmarks[9][1]
458
+ );
459
+
460
+ if (recentMovementX < 0.008 && recentMovementY < 0.008 && distance > this.swipeThreshold) {
461
+ const direction = this.getSwipeDirection(deltaX, deltaY);
462
+
463
+ this.swipeStart = null;
464
+ this.isSwipeInProgress = false;
465
+ this.lastSwipeTime = now;
466
+
467
+ return {
468
+ direction,
469
+ speed,
470
+ fingerCount,
471
+ startPosition: this.swipeStart ? {
472
+ x: this.swipeStart.x,
473
+ y: this.swipeStart.y
474
+ } : null,
475
+ endPosition: {
476
+ x: currentPos.x,
477
+ y: currentPos.y
478
+ }
479
+ };
480
+ }
481
+
482
+ if (timeDelta > 2) {
483
+ this.swipeStart = null;
484
+ this.isSwipeInProgress = false;
485
+ }
486
+ }
487
+
488
+ return null;
489
+ }
490
+
491
+ getSwipeDirection(dx, dy) {
492
+ const absDx = Math.abs(dx);
493
+ const absDy = Math.abs(dy);
494
+
495
+ if (absDx > absDy) {
496
+ return dx > 0 ? 'right' : 'left';
497
+ } else {
498
+ return dy > 0 ? 'down' : 'up';
499
+ }
500
+ }
501
+ }
502
+
503
+ class PinchDetector {
504
+ constructor() {
505
+ this.pinchThreshold = 0.08;
506
+ this.zoomInThreshold = 0.10;
507
+ this.zoomOutThreshold = 0.18;
508
+ this.lastPinchTime = 0;
509
+ this.pinchCooldown = 800;
510
+ this.isPinching = false;
511
+ }
512
+
513
+ detect(landmarks) {
514
+ if (!landmarks || landmarks.length < 21) return null;
515
+
516
+ const now = Date.now();
517
+ if (now - this.lastPinchTime < this.pinchCooldown) return null;
518
+
519
+ const thumbTip = landmarks[4];
520
+ const indexTip = landmarks[8];
521
+
522
+ const distance = Math.sqrt(
523
+ Math.pow(thumbTip.x - indexTip.x, 2) +
524
+ Math.pow(thumbTip.y - indexTip.y, 2) +
525
+ Math.pow((thumbTip.z || 0) - (indexTip.z || 0), 2)
526
+ );
527
+
528
+ let pinchType = null;
529
+
530
+ if (distance < this.pinchThreshold && !this.isPinching) {
531
+ pinchType = 'grab';
532
+ this.isPinching = true;
533
+ this.lastPinchTime = now;
534
+ } else if (distance > this.zoomOutThreshold && this.isPinching) {
535
+ pinchType = 'zoom_out';
536
+ this.isPinching = false;
537
+ this.lastPinchTime = now;
538
+ } else if (distance > this.zoomInThreshold && distance < this.zoomOutThreshold && this.isPinching) {
539
+ pinchType = 'zoom_in';
540
+ this.isPinching = false;
541
+ this.lastPinchTime = now;
542
+ } else if (distance > this.pinchThreshold * 2) {
543
+ this.isPinching = false;
544
+ }
545
+
546
+ if (pinchType) {
547
+ return {
548
+ type: pinchType,
549
+ distance,
550
+ thumbTip: [thumbTip.x, thumbTip.y, thumbTip.z || 0],
551
+ indexTip: [indexTip.x, indexTip.y, indexTip.z || 0]
552
+ };
553
+ }
554
+
555
+ return null;
556
+ }
557
+ }
558
+
559
+ class FingerCounter {
560
+ count(landmarks) {
561
+ if (!landmarks || landmarks.length < 21) return 0;
562
+
563
+ const fingerTips = [4, 8, 12, 16, 20];
564
+ const fingerBases = [3, 6, 10, 14, 18];
565
+
566
+ let extended = 0;
567
+
568
+ for (let i = 0; i < fingerTips.length; i++) {
569
+ const tip = landmarks[fingerTips[i]];
570
+ const base = landmarks[fingerBases[i]];
571
+
572
+ if (tip.y < base.y) {
573
+ extended++;
574
+ }
575
+ }
576
+
577
+ return extended;
578
+ }
579
+ }
580
+
581
+ export default MediaPipeProcessor;
582
+ export { MediaPipeProcessor, SwipeDetector, PinchDetector, FingerCounter };