hand-guest-control 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -64
- package/dist/index.js +819 -0
- package/dist/index.js.map +1 -0
- package/dist/mediapipe.js +49 -0
- package/dist/mediapipe.js.map +1 -0
- package/package.json +16 -5
- package/src/HandGestureDetector.js +0 -192
- package/src/HandStateManager.js +0 -276
- package/src/constants.js +0 -52
- package/src/index.js +0 -9
- package/src/mediapipe.js +0 -59
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/HandGestureDetector.ts","../src/ActionMapper.ts","../src/validate.ts","../src/HandStateManager.ts"],"sourcesContent":["import type { ActionMap, GestureAction, HandState, ResolvedHandConfig } from './types.js';\n\nexport const HAND_STATES = {\n NOT_DETECTED: 'not_detected',\n WAITING: 'waiting',\n STARTING: 'starting',\n OPEN: 'open',\n CLOSED: 'closed',\n POINTING: 'pointing',\n FLIPPING: 'flipping',\n THUMB_UP: 'thumb_up',\n SWIPE_NEXT: 'swipe_next',\n SWIPE_PREVIOUS: 'swipe_previous',\n PALM_LEFT: 'palm_left',\n PALM_RIGHT: 'palm_right',\n PALM_UP: 'palm_up',\n PALM_DOWN: 'palm_down'\n} as const satisfies Record<string, HandState>;\n\nexport const GESTURE_ACTIONS = {\n NONE: 'none',\n SWIPE_LEFT: 'swipe_left',\n SWIPE_RIGHT: 'swipe_right',\n ZOOM_IN: 'zoom_in',\n ZOOM_OUT: 'zoom_out',\n ROTATE_LEFT: 'rotate_left',\n ROTATE_RIGHT: 'rotate_right',\n ROTATE_UP: 'rotate_up',\n ROTATE_DOWN: 'rotate_down',\n OK: 'ok',\n CUSTOM: 'custom'\n} as const satisfies Record<string, GestureAction>;\n\nexport const LANDMARKS = {\n WRIST: 0,\n THUMB_TIP: 4,\n INDEX_TIP: 8,\n MIDDLE_TIP: 12,\n RING_TIP: 16,\n PINKY_TIP: 20\n} as const;\n\nexport const HAND_CONNECTIONS: ReadonlyArray<readonly [number, number]> = [\n [0, 1], [1, 2], [2, 3], [3, 4],\n [0, 5], [5, 6], [6, 7], [7, 8],\n [0, 9], [9, 10], [10, 11], [11, 12],\n [0, 13], [13, 14], [14, 15], [15, 16],\n [0, 17], [17, 18], [18, 19], [19, 20],\n [5, 9], [9, 13], [13, 17]\n];\n\nexport const DEFAULT_ACTION_MAP: ActionMap = {\n [HAND_STATES.SWIPE_PREVIOUS]: GESTURE_ACTIONS.SWIPE_LEFT,\n [HAND_STATES.SWIPE_NEXT]: GESTURE_ACTIONS.SWIPE_RIGHT,\n [HAND_STATES.OPEN]: GESTURE_ACTIONS.ZOOM_OUT,\n [HAND_STATES.CLOSED]: GESTURE_ACTIONS.ZOOM_IN,\n [HAND_STATES.THUMB_UP]: GESTURE_ACTIONS.OK,\n [HAND_STATES.PALM_LEFT]: GESTURE_ACTIONS.ROTATE_LEFT,\n [HAND_STATES.PALM_RIGHT]: GESTURE_ACTIONS.ROTATE_RIGHT,\n [HAND_STATES.PALM_UP]: GESTURE_ACTIONS.ROTATE_UP,\n [HAND_STATES.PALM_DOWN]: GESTURE_ACTIONS.ROTATE_DOWN,\n [HAND_STATES.FLIPPING]: GESTURE_ACTIONS.CUSTOM\n};\n\nexport const DEFAULT_CONFIG: ResolvedHandConfig = {\n maxHistorySize: 10,\n maxCentroidHistorySize: 10,\n rotationSensitivity: 4,\n detectionTolerance: 2000,\n startingDelay: 1000,\n waitingTimeout: 3000,\n minOpenConfidence: 5,\n minPointingConfidence: 25,\n minFlippingConfidence: 25,\n minClosedConfidence: 5,\n minThumbUpConfidence: 15,\n minPointingCount: 5,\n pointingThreshold: 0.3,\n swipeSource: 'index',\n swipeMinRange: 0.03,\n swipeCooldownMs: 2000,\n swipeMinHistory: 6,\n palmDirectionThreshold: 0.025,\n palmDirectionMinFrames: 6,\n rotationActionThreshold: 0.5,\n enableContinuousRotation: true,\n mapActions: true,\n actionMap: null,\n openHandThresholds: {\n fingerSpread: 0.2,\n indexToMiddle: 0.15,\n minExtendedFingers: 4,\n maxExtendedFingers: 6,\n middleAngle: 50\n },\n thumbUpThresholds: {\n minThumbExtension: 0.9,\n maxOtherFingersRatio: 0.8\n },\n debug: false\n};\n","import { HAND_STATES, LANDMARKS } from './constants.js';\nimport type {\n FingerDistances,\n GestureHistories,\n GestureIndices,\n HandState,\n Landmark,\n Landmarks,\n PalmDirection,\n ResolvedHandConfig,\n TimedPoint\n} from './types.js';\n\ntype NormalizedFingers = Record<'thumb' | 'index' | 'middle' | 'ring' | 'pinky' | 'thumbToIndex', number>;\n\nexport class HandGestureDetector {\n private readonly config: ResolvedHandConfig;\n\n constructor(config: ResolvedHandConfig) {\n this.config = config;\n }\n\n static getDistance(p1: Landmark, p2: Landmark): number {\n const dx = p1.x - p2.x;\n const dy = p1.y - p2.y;\n return Math.sqrt(dx * dx + dy * dy);\n }\n\n static getAngle(p1: Landmark, p2: Landmark, p3: Landmark): number {\n const v1x = p2.x - p1.x;\n const v1y = p2.y - p1.y;\n const v2x = p3.x - p2.x;\n const v2y = p3.y - p2.y;\n const dot = v1x * v2x + v1y * v2y;\n const det = v1x * v2y - v1y * v2x;\n let angle = (Math.atan2(det, dot) * 180) / Math.PI;\n return angle < 0 ? angle + 360 : angle;\n }\n\n computeDistances(landmarks: Landmarks): FingerDistances {\n const wrist = landmarks[LANDMARKS.WRIST];\n const tips = [\n LANDMARKS.THUMB_TIP,\n LANDMARKS.INDEX_TIP,\n LANDMARKS.MIDDLE_TIP,\n LANDMARKS.RING_TIP,\n LANDMARKS.PINKY_TIP\n ] as const;\n\n const fingerNames = ['Thumb', 'Index', 'Middle', 'Ring', 'Pinky'] as const;\n const adjacentNames = ['thumbToIndex', 'indexToMiddle', 'middleToRing', 'ringToPinky'] as const;\n\n const distances = {} as FingerDistances;\n\n tips.forEach((tip, i) => {\n const palmKey = `palmTo${fingerNames[i]}` as keyof FingerDistances;\n distances[palmKey] = HandGestureDetector.getDistance(wrist, landmarks[tip]);\n\n if (i > 0) {\n distances[adjacentNames[i - 1]] = HandGestureDetector.getDistance(\n landmarks[tips[i - 1]],\n landmarks[tip]\n );\n }\n });\n\n return distances;\n }\n\n private getNormalizedFingerLengths(distances: FingerDistances): NormalizedFingers {\n const handSize = (distances.palmToThumb + distances.palmToMiddle) / 2 || 0.15;\n return {\n thumb: distances.palmToThumb / handSize,\n index: distances.palmToIndex / handSize,\n middle: distances.palmToMiddle / handSize,\n ring: distances.palmToRing / handSize,\n pinky: distances.palmToPinky / handSize,\n thumbToIndex: distances.thumbToIndex / handSize\n };\n }\n\n getTimedEntries(history: Array<TimedPoint | null>): TimedPoint[] {\n return history\n .filter((entry): entry is TimedPoint => entry !== null)\n .sort((a, b) => (a.time - b.time) || ((a.seq ?? 0) - (b.seq ?? 0)));\n }\n\n private getDirectionDelta(first: TimedPoint, last: TimedPoint) {\n return {\n dx: last.x - first.x,\n dy: last.y - first.y\n };\n }\n\n checkOpenHand(distances: FingerDistances, histories: GestureHistories, indices: GestureIndices): boolean {\n const normalized = this.getNormalizedFingerLengths(distances);\n\n if (this.config.debug) {\n console.log(`Hand state check (open): thumb=${normalized.thumb.toFixed(2)}, ` +\n `index=${normalized.index.toFixed(2)}, middle=${normalized.middle.toFixed(2)}, ` +\n `ring=${normalized.ring.toFixed(2)}, pinky=${normalized.pinky.toFixed(2)}, ` +\n `thumbToIndex=${normalized.thumbToIndex.toFixed(2)}`);\n }\n\n const fingers = ['index', 'middle', 'ring', 'pinky'] as const;\n const extended = fingers.filter((finger) => normalized[finger] > normalized.thumb).length;\n const isCandidate = extended >= this.config.openHandThresholds.minExtendedFingers;\n\n histories.open[indices.open] = isCandidate;\n indices.open = (indices.open + 1) % this.config.minOpenConfidence;\n\n return histories.open.filter(Boolean).length >= Math.ceil(this.config.minOpenConfidence * 0.7);\n }\n\n checkClosedHand(distances: FingerDistances, histories: GestureHistories, indices: GestureIndices): boolean {\n const normalized = this.getNormalizedFingerLengths(distances);\n\n if (this.config.debug) {\n console.log(`Hand state check (closed): thumb=${normalized.thumb.toFixed(2)}, ` +\n `index=${normalized.index.toFixed(2)}, middle=${normalized.middle.toFixed(2)}, ` +\n `ring=${normalized.ring.toFixed(2)}, pinky=${normalized.pinky.toFixed(2)}, ` +\n `thumbToIndex=${normalized.thumbToIndex.toFixed(2)}`);\n }\n\n const fingers = ['index', 'middle', 'ring', 'pinky'] as const;\n const closedFingers = fingers.filter((finger) => normalized[finger] < normalized.thumb).length;\n const isCandidate = closedFingers >= this.config.openHandThresholds.minExtendedFingers;\n\n histories.closed[indices.closed] = isCandidate;\n indices.closed = (indices.closed + 1) % this.config.minClosedConfidence;\n\n return histories.closed.filter(Boolean).length >= Math.ceil(this.config.minClosedConfidence * 0.7);\n }\n\n isPointing(distances: FingerDistances, histories: GestureHistories, indices: GestureIndices): boolean {\n const handSize = distances.palmToIndex || 0.15;\n const normalized = {\n index: distances.palmToIndex / handSize,\n middle: distances.palmToMiddle / handSize,\n ring: distances.palmToRing / handSize,\n pinky: distances.palmToPinky / handSize,\n thumb: distances.palmToThumb / handSize\n };\n\n const otherFingers = [normalized.middle, normalized.ring, normalized.pinky, normalized.thumb];\n const isIndexExtended = 0.5 > Math.max(...otherFingers) && normalized.thumb < 0.7;\n\n histories.pointing[indices.pointing] = isIndexExtended;\n indices.pointing = (indices.pointing + 1) % this.config.minPointingConfidence;\n\n return histories.pointing.filter(Boolean).length >= Math.ceil(this.config.minPointingConfidence * 0.9);\n }\n\n isThumbUp(\n distances: FingerDistances,\n landmarks: Landmarks,\n histories: GestureHistories,\n indices: GestureIndices\n ): boolean {\n const normalized = this.getNormalizedFingerLengths(distances);\n const { minThumbExtension, maxOtherFingersRatio } = this.config.thumbUpThresholds;\n const thumbTip = landmarks[LANDMARKS.THUMB_TIP];\n const wrist = landmarks[LANDMARKS.WRIST];\n\n const othersClosed = (['index', 'middle', 'ring', 'pinky'] as const)\n .every((finger) => normalized[finger] < normalized.thumb * maxOtherFingersRatio);\n const thumbExtended = normalized.thumb >= minThumbExtension;\n const thumbAboveWrist = thumbTip.y < wrist.y;\n const isCandidate = thumbExtended && othersClosed && thumbAboveWrist;\n\n histories.thumbUp[indices.thumbUp] = isCandidate;\n indices.thumbUp = (indices.thumbUp + 1) % this.config.minThumbUpConfidence;\n\n return histories.thumbUp.filter(Boolean).length >= Math.ceil(this.config.minThumbUpConfidence * 0.8);\n }\n\n isFlipping(distances: FingerDistances, histories: GestureHistories, indices: GestureIndices): boolean {\n const handSize = distances.palmToIndex || 0.15;\n const normalized = {\n index: distances.palmToIndex / handSize,\n middle: distances.palmToMiddle / handSize,\n ring: distances.palmToRing / handSize,\n pinky: distances.palmToPinky / handSize,\n thumb: distances.palmToThumb / handSize\n };\n\n if (this.config.debug) {\n console.log(`Hand state check (flip): index=${normalized.index.toFixed(2)}, ` +\n `middle=${normalized.middle.toFixed(2)}, ring=${normalized.ring.toFixed(2)}, ` +\n `pinky=${normalized.pinky.toFixed(2)}, thumb=${normalized.thumb.toFixed(2)}`);\n }\n\n const isFlippingGesture = normalized.middle > 0.8 &&\n Math.max(normalized.ring, normalized.pinky, normalized.thumb) < 0.5;\n\n histories.flipping[indices.flipping] = isFlippingGesture;\n indices.flipping = (indices.flipping + 1) % this.config.minFlippingConfidence;\n\n return histories.flipping.filter(Boolean).length >= Math.ceil(this.config.minFlippingConfidence * 0.9);\n }\n\n getSwipeHistory(histories: GestureHistories): Array<TimedPoint | null> {\n return this.config.swipeSource === 'index' ? histories.indexTip : histories.landmark0;\n }\n\n checkSwipeNext(\n swipeHistory: Array<TimedPoint | null>,\n distances: FingerDistances,\n histories: GestureHistories,\n indices: GestureIndices,\n pointingCount: number,\n lastActionTime: number\n ): boolean {\n if (\n !this.isPointing(distances, histories, indices) ||\n this.getTimedEntries(swipeHistory).length < this.config.swipeMinHistory ||\n pointingCount < this.config.minPointingCount ||\n Date.now() - lastActionTime < this.config.swipeCooldownMs\n ) {\n return false;\n }\n\n const entries = this.getTimedEntries(swipeHistory);\n const xPositions = entries.map((e) => e.x);\n const xRange = Math.max(...xPositions) - Math.min(...xPositions);\n const firstX = entries[0].x;\n const lastX = entries[entries.length - 1].x;\n\n return xRange > this.config.swipeMinRange && lastX - firstX > this.config.swipeMinRange;\n }\n\n checkSwipePrevious(\n swipeHistory: Array<TimedPoint | null>,\n distances: FingerDistances,\n histories: GestureHistories,\n indices: GestureIndices,\n pointingCount: number,\n lastActionTime: number\n ): boolean {\n if (\n !this.isPointing(distances, histories, indices) ||\n this.getTimedEntries(swipeHistory).length < this.config.swipeMinHistory ||\n pointingCount < this.config.minPointingCount ||\n Date.now() - lastActionTime < this.config.swipeCooldownMs\n ) {\n return false;\n }\n\n const entries = this.getTimedEntries(swipeHistory);\n const xPositions = entries.map((e) => e.x);\n const xRange = Math.max(...xPositions) - Math.min(...xPositions);\n const firstX = entries[0].x;\n const lastX = entries[entries.length - 1].x;\n\n return xRange > this.config.swipeMinRange && firstX - lastX > this.config.swipeMinRange;\n }\n\n detectPalmDirection(centroidHistory: Array<TimedPoint | null>): PalmDirection | null {\n const entries = this.getTimedEntries(centroidHistory);\n if (entries.length < this.config.palmDirectionMinFrames) {\n return null;\n }\n\n const { dx, dy } = this.getDirectionDelta(entries[0], entries[entries.length - 1]);\n const threshold = this.config.palmDirectionThreshold;\n\n if (Math.abs(dx) >= Math.abs(dy) && Math.abs(dx) > threshold) {\n return dx > 0 ? HAND_STATES.PALM_RIGHT : HAND_STATES.PALM_LEFT;\n }\n\n if (Math.abs(dy) > threshold) {\n return dy > 0 ? HAND_STATES.PALM_DOWN : HAND_STATES.PALM_UP;\n }\n\n return null;\n }\n}\n","import {\n DEFAULT_ACTION_MAP,\n GESTURE_ACTIONS,\n HAND_STATES\n} from './constants.js';\nimport type {\n ActionMap,\n ActionMapperOptions,\n GestureAction,\n GestureBaseResult,\n GestureResult,\n RotationVector\n} from './types.js';\n\nexport type GestureMapInput = Omit<GestureBaseResult, 'gesture'> & {\n gesture?: GestureBaseResult['gesture'];\n};\n\nexport function resolveRotationAction(\n rotation: RotationVector,\n threshold = 0.5\n): GestureAction | null {\n const [vertical, horizontal] = rotation;\n const absVertical = Math.abs(vertical);\n const absHorizontal = Math.abs(horizontal);\n\n if (absVertical < threshold && absHorizontal < threshold) {\n return null;\n }\n\n if (absHorizontal >= absVertical) {\n return horizontal > 0 ? GESTURE_ACTIONS.ROTATE_RIGHT : GESTURE_ACTIONS.ROTATE_LEFT;\n }\n\n return vertical > 0 ? GESTURE_ACTIONS.ROTATE_DOWN : GESTURE_ACTIONS.ROTATE_UP;\n}\n\nexport class ActionMapper {\n private readonly actionMap: ActionMap & typeof DEFAULT_ACTION_MAP;\n private readonly rotationThreshold: number;\n private readonly enableContinuousRotation: boolean;\n\n constructor(config: ActionMapperOptions = {}) {\n this.actionMap = {\n ...DEFAULT_ACTION_MAP,\n ...(config.actionMap ?? {})\n };\n this.rotationThreshold = config.rotationActionThreshold ?? 0.5;\n this.enableContinuousRotation = config.enableContinuousRotation ?? true;\n }\n\n map(result: GestureMapInput): GestureResult {\n const base: GestureBaseResult = {\n ...result,\n gesture: result.gesture ?? result.state\n };\n const actions: GestureAction[] = [];\n const add = (action: GestureAction | null | undefined) => {\n if (action && action !== GESTURE_ACTIONS.NONE && !actions.includes(action)) {\n actions.push(action);\n }\n };\n\n add(this.actionMap[base.state]);\n\n if (base.palmDirection) {\n add(this.actionMap[base.palmDirection]);\n }\n\n const rotationStates = [HAND_STATES.OPEN, HAND_STATES.CLOSED] as const;\n if (\n this.enableContinuousRotation &&\n (rotationStates as readonly string[]).includes(base.state) &&\n !base.palmDirection\n ) {\n add(resolveRotationAction(base.rotation, this.rotationThreshold));\n }\n\n return {\n ...base,\n actions,\n primaryAction: actions[0] ?? GESTURE_ACTIONS.NONE\n };\n }\n}\n\nexport function mapGestureToAction(\n result: GestureMapInput,\n actionMap: ActionMapperOptions['actionMap'] = DEFAULT_ACTION_MAP,\n options: ActionMapperOptions = {}\n): GestureResult {\n const mapper = new ActionMapper({\n actionMap,\n rotationActionThreshold: options.rotationActionThreshold,\n enableContinuousRotation: options.enableContinuousRotation\n });\n return mapper.map(result);\n}\n","import { DEFAULT_CONFIG } from './constants.js';\nimport type { HandConfig, ResolvedHandConfig, SwipeSource } from './types.js';\n\nexport class ConfigValidationError extends Error {\n readonly issues: string[];\n\n constructor(issues: string[]) {\n super(`Invalid hand-guest-control config: ${issues.join('; ')}`);\n this.name = 'ConfigValidationError';\n this.issues = issues;\n }\n}\n\nfunction isPositiveInt(value: unknown, field: string, issues: string[]): void {\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n issues.push(`${field} must be a positive integer`);\n }\n}\n\nfunction isPositiveNumber(value: unknown, field: string, issues: string[]): void {\n if (typeof value !== 'number' || Number.isNaN(value) || value <= 0) {\n issues.push(`${field} must be a positive number`);\n }\n}\n\nfunction isNonNegativeInt(value: unknown, field: string, issues: string[]): void {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n issues.push(`${field} must be a non-negative integer`);\n }\n}\n\nfunction isRatio(value: unknown, field: string, issues: string[]): void {\n if (typeof value !== 'number' || Number.isNaN(value) || value < 0 || value > 1) {\n issues.push(`${field} must be between 0 and 1`);\n }\n}\n\nfunction isSwipeSource(value: unknown, issues: string[]): asserts value is SwipeSource {\n if (value !== 'index' && value !== 'wrist') {\n issues.push(`swipeSource must be \"index\" or \"wrist\"`);\n }\n}\n\nexport function validateHandConfig(config: HandConfig): void {\n const issues: string[] = [];\n\n if (config.maxHistorySize !== undefined) isPositiveInt(config.maxHistorySize, 'maxHistorySize', issues);\n if (config.maxCentroidHistorySize !== undefined) {\n isPositiveInt(config.maxCentroidHistorySize, 'maxCentroidHistorySize', issues);\n }\n if (config.rotationSensitivity !== undefined) {\n isPositiveNumber(config.rotationSensitivity, 'rotationSensitivity', issues);\n }\n if (config.detectionTolerance !== undefined) {\n isPositiveInt(config.detectionTolerance, 'detectionTolerance', issues);\n }\n if (config.startingDelay !== undefined) isPositiveInt(config.startingDelay, 'startingDelay', issues);\n if (config.waitingTimeout !== undefined) isPositiveInt(config.waitingTimeout, 'waitingTimeout', issues);\n if (config.minOpenConfidence !== undefined) isPositiveInt(config.minOpenConfidence, 'minOpenConfidence', issues);\n if (config.minPointingConfidence !== undefined) {\n isPositiveInt(config.minPointingConfidence, 'minPointingConfidence', issues);\n }\n if (config.minFlippingConfidence !== undefined) {\n isPositiveInt(config.minFlippingConfidence, 'minFlippingConfidence', issues);\n }\n if (config.minClosedConfidence !== undefined) {\n isPositiveInt(config.minClosedConfidence, 'minClosedConfidence', issues);\n }\n if (config.minThumbUpConfidence !== undefined) {\n isPositiveInt(config.minThumbUpConfidence, 'minThumbUpConfidence', issues);\n }\n if (config.minPointingCount !== undefined) isPositiveInt(config.minPointingCount, 'minPointingCount', issues);\n if (config.pointingThreshold !== undefined) isRatio(config.pointingThreshold, 'pointingThreshold', issues);\n if (config.swipeSource !== undefined) isSwipeSource(config.swipeSource, issues);\n if (config.swipeMinRange !== undefined) isPositiveNumber(config.swipeMinRange, 'swipeMinRange', issues);\n if (config.swipeCooldownMs !== undefined) isNonNegativeInt(config.swipeCooldownMs, 'swipeCooldownMs', issues);\n if (config.swipeMinHistory !== undefined) isPositiveInt(config.swipeMinHistory, 'swipeMinHistory', issues);\n if (config.palmDirectionThreshold !== undefined) {\n isPositiveNumber(config.palmDirectionThreshold, 'palmDirectionThreshold', issues);\n }\n if (config.palmDirectionMinFrames !== undefined) {\n isPositiveInt(config.palmDirectionMinFrames, 'palmDirectionMinFrames', issues);\n }\n if (config.rotationActionThreshold !== undefined) {\n isPositiveNumber(config.rotationActionThreshold, 'rotationActionThreshold', issues);\n }\n if (config.mapActions !== undefined && typeof config.mapActions !== 'boolean') {\n issues.push('mapActions must be a boolean');\n }\n if (config.debug !== undefined && typeof config.debug !== 'boolean') {\n issues.push('debug must be a boolean');\n }\n if (config.enableContinuousRotation !== undefined && typeof config.enableContinuousRotation !== 'boolean') {\n issues.push('enableContinuousRotation must be a boolean');\n }\n\n if (config.openHandThresholds) {\n const t = config.openHandThresholds;\n if (t.minExtendedFingers !== undefined && (!Number.isInteger(t.minExtendedFingers) || t.minExtendedFingers < 1)) {\n issues.push('openHandThresholds.minExtendedFingers must be >= 1');\n }\n }\n\n if (config.thumbUpThresholds) {\n const t = config.thumbUpThresholds;\n if (t.minThumbExtension !== undefined) {\n isPositiveNumber(t.minThumbExtension, 'thumbUpThresholds.minThumbExtension', issues);\n }\n if (t.maxOtherFingersRatio !== undefined) {\n isPositiveNumber(t.maxOtherFingersRatio, 'thumbUpThresholds.maxOtherFingersRatio', issues);\n }\n }\n\n if (issues.length > 0) {\n throw new ConfigValidationError(issues);\n }\n}\n\nexport function mergeHandConfig(config: HandConfig = {}): ResolvedHandConfig {\n validateHandConfig(config);\n\n return {\n ...DEFAULT_CONFIG,\n ...config,\n openHandThresholds: {\n ...DEFAULT_CONFIG.openHandThresholds,\n ...(config.openHandThresholds ?? {})\n },\n thumbUpThresholds: {\n ...DEFAULT_CONFIG.thumbUpThresholds,\n ...(config.thumbUpThresholds ?? {})\n },\n actionMap: config.actionMap ?? DEFAULT_CONFIG.actionMap\n };\n}\n","import { GESTURE_ACTIONS, HAND_STATES, LANDMARKS } from './constants.js';\nimport { ActionMapper } from './ActionMapper.js';\nimport { HandGestureDetector } from './HandGestureDetector.js';\nimport { mergeHandConfig } from './validate.js';\nimport type {\n GestureBaseResult,\n GestureHistories,\n GestureIndices,\n GestureResult,\n HandConfig,\n HandState,\n Landmarks,\n PalmDirection,\n ResolvedHandConfig,\n RotationVector\n} from './types.js';\n\nconst CONFIDENCE_BY_HISTORY: Record<string, keyof ResolvedHandConfig> = {\n open: 'minOpenConfidence',\n pointing: 'minPointingConfidence',\n closed: 'minClosedConfidence',\n flipping: 'minFlippingConfidence',\n thumbUp: 'minThumbUpConfidence'\n};\n\nconst HISTORY_KEY_BY_STATE: Partial<Record<HandState, keyof GestureHistories>> = {\n [HAND_STATES.OPEN]: 'open',\n [HAND_STATES.POINTING]: 'pointing',\n [HAND_STATES.CLOSED]: 'closed',\n [HAND_STATES.FLIPPING]: 'flipping',\n [HAND_STATES.THUMB_UP]: 'thumbUp'\n};\n\nconst ROTATION_DISABLED_STATES: readonly HandState[] = [\n HAND_STATES.POINTING,\n HAND_STATES.SWIPE_NEXT,\n HAND_STATES.SWIPE_PREVIOUS,\n HAND_STATES.FLIPPING,\n HAND_STATES.THUMB_UP\n];\n\nconst PALM_DIRECTION_STATES: readonly HandState[] = [\n HAND_STATES.OPEN,\n HAND_STATES.CLOSED\n];\n\nexport class HandStateManager {\n readonly config: ResolvedHandConfig;\n private readonly gestureDetector: HandGestureDetector;\n private readonly actionMapper: ActionMapper;\n\n histories!: GestureHistories;\n indices!: GestureIndices;\n\n private prevLandmark0: { x: number; y: number } | null = null;\n private lastActionTime = 0;\n private pointingCount = 0;\n private flippingCount = 0;\n private detectionPausedUntil = 0;\n private noDetectionStartTime = 0;\n private lastDetectionTime = 0;\n lastState: HandState | null = null;\n lastPalmDirection: PalmDirection | null = null;\n private frameSeq = 0;\n\n constructor(config: HandConfig = {}) {\n this.config = mergeHandConfig(config);\n this.gestureDetector = new HandGestureDetector(this.config);\n this.actionMapper = new ActionMapper({\n actionMap: this.config.actionMap ?? undefined,\n rotationActionThreshold: this.config.rotationActionThreshold,\n enableContinuousRotation: this.config.enableContinuousRotation\n });\n this.initializeState();\n }\n\n initializeState(): void {\n this.histories = {\n centroid: new Array(this.config.maxCentroidHistorySize).fill(null),\n landmark0: new Array(this.config.maxHistorySize).fill(null),\n indexTip: new Array(this.config.maxHistorySize).fill(null),\n open: new Array(this.config.minOpenConfidence).fill(false),\n pointing: new Array(this.config.minPointingConfidence).fill(false),\n flipping: new Array(this.config.minFlippingConfidence).fill(false),\n closed: new Array(this.config.minClosedConfidence).fill(false),\n thumbUp: new Array(this.config.minThumbUpConfidence).fill(false)\n };\n\n this.indices = {\n history: 0,\n indexTip: 0,\n centroid: 0,\n open: 0,\n pointing: 0,\n flipping: 0,\n closed: 0,\n thumbUp: 0\n };\n\n this.resetTrackingState();\n }\n\n resetTrackingState(): void {\n this.prevLandmark0 = null;\n this.lastActionTime = 0;\n this.pointingCount = 0;\n this.flippingCount = 0;\n this.detectionPausedUntil = 0;\n this.noDetectionStartTime = 0;\n this.lastDetectionTime = 0;\n this.lastState = null;\n this.lastPalmDirection = null;\n this.frameSeq = 0;\n }\n\n buildResult(\n state: HandState,\n rotation: RotationVector = [0, 0, 0],\n palmDirection: PalmDirection | null = null\n ): GestureResult {\n const result: GestureBaseResult = {\n state,\n gesture: state,\n rotation,\n palmDirection\n };\n\n if (!this.config.mapActions) {\n return {\n ...result,\n actions: [],\n primaryAction: GESTURE_ACTIONS.NONE\n };\n }\n\n return this.actionMapper.map(result);\n }\n\n calculateCentroid(landmarks: Landmarks, now: number): { x: number; y: number } {\n if (!landmarks?.length) {\n return { x: 0, y: 0 };\n }\n\n const wrist = landmarks[LANDMARKS.WRIST] ?? landmarks[0];\n const indexTip = landmarks[LANDMARKS.INDEX_TIP];\n this.frameSeq += 1;\n const pointMeta = { time: now, seq: this.frameSeq };\n\n this.histories.landmark0[this.indices.history] = {\n x: wrist.x,\n y: wrist.y,\n ...pointMeta\n };\n this.indices.history = (this.indices.history + 1) % this.config.maxHistorySize;\n\n if (indexTip) {\n this.histories.indexTip[this.indices.indexTip] = {\n x: indexTip.x,\n y: indexTip.y,\n ...pointMeta\n };\n this.indices.indexTip = (this.indices.indexTip + 1) % this.config.maxHistorySize;\n }\n\n const validPoints = this.histories.landmark0.filter((p): p is NonNullable<typeof p> => p !== null);\n if (!validPoints.length) {\n return { x: wrist.x, y: wrist.y };\n }\n\n const sum = validPoints.reduce(\n (acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }),\n { x: 0, y: 0 }\n );\n\n return {\n x: sum.x / validPoints.length,\n y: sum.y / validPoints.length\n };\n }\n\n getRotation(landmarks: Landmarks, centroid: { x: number; y: number }): RotationVector {\n if (!landmarks[LANDMARKS.MIDDLE_TIP]) {\n return [0, 0, 0];\n }\n\n const handSize = HandGestureDetector.getDistance(\n landmarks[LANDMARKS.WRIST],\n landmarks[LANDMARKS.THUMB_TIP]\n ) || 0.15;\n\n const scaleFactor = 0.15 / handSize;\n const delta = this.prevLandmark0\n ? {\n x: (centroid.x - this.prevLandmark0.x) * scaleFactor,\n y: (centroid.y - this.prevLandmark0.y) * scaleFactor\n }\n : { x: 0, y: 0 };\n\n this.prevLandmark0 = { x: centroid.x, y: centroid.y };\n\n return [\n delta.y * this.config.rotationSensitivity,\n delta.x * this.config.rotationSensitivity,\n 0\n ];\n }\n\n handleNoDetection(now: number): GestureResult {\n this.pointingCount = 0;\n this.flippingCount = 0;\n this.lastPalmDirection = null;\n\n if (!this.noDetectionStartTime) {\n this.noDetectionStartTime = now;\n }\n\n const noDetectionDuration = now - this.noDetectionStartTime;\n\n if (noDetectionDuration < this.config.detectionTolerance) {\n return this.buildResult(this.lastState ?? HAND_STATES.NOT_DETECTED);\n }\n\n if (noDetectionDuration >= this.config.waitingTimeout) {\n this.lastState = HAND_STATES.WAITING;\n if (this.config.debug) {\n console.log(`Transitioned to WAITING after ${this.config.waitingTimeout / 1000}s`);\n }\n return this.buildResult(HAND_STATES.WAITING);\n }\n\n this.lastState = HAND_STATES.NOT_DETECTED;\n if (this.config.debug) {\n console.log(`Transitioned to NOT_DETECTED after ${this.config.detectionTolerance / 1000}s`);\n }\n return this.buildResult(HAND_STATES.NOT_DETECTED);\n }\n\n handleDetectionReconnection(now: number): GestureResult | null {\n const isWaitingOrNotDetected =\n this.lastState === HAND_STATES.WAITING || this.lastState === HAND_STATES.NOT_DETECTED;\n const isReconnection =\n this.lastState !== HAND_STATES.WAITING &&\n this.lastState !== HAND_STATES.NOT_DETECTED &&\n this.lastState !== HAND_STATES.STARTING &&\n now - this.lastDetectionTime <= this.config.detectionTolerance;\n\n if (isReconnection) {\n if (this.config.debug) {\n console.log(\n `Short reconnection (${(now - this.lastDetectionTime) / 1000}s), continuing with last state: ${this.lastState}`\n );\n }\n } else if (isWaitingOrNotDetected) {\n this.detectionPausedUntil = now + this.config.startingDelay;\n this.noDetectionStartTime = 0;\n this.lastState = HAND_STATES.STARTING;\n return this.buildResult(HAND_STATES.STARTING);\n }\n\n return null;\n }\n\n determineHandState(distances: ReturnType<HandGestureDetector['computeDistances']>, landmarks: Landmarks): HandState {\n const swipeHistory = this.gestureDetector.getSwipeHistory(this.histories);\n\n if (this.gestureDetector.checkSwipeNext(\n swipeHistory, distances, this.histories, this.indices,\n this.pointingCount, this.lastActionTime\n )) {\n this.resetSwipeState();\n return HAND_STATES.SWIPE_NEXT;\n }\n\n if (this.gestureDetector.checkSwipePrevious(\n swipeHistory, distances, this.histories, this.indices,\n this.pointingCount, this.lastActionTime\n )) {\n this.resetSwipeState();\n return HAND_STATES.SWIPE_PREVIOUS;\n }\n\n if (this.gestureDetector.isThumbUp(distances, landmarks, this.histories, this.indices)) {\n return HAND_STATES.THUMB_UP;\n }\n\n if (this.gestureDetector.isFlipping(distances, this.histories, this.indices)) {\n return HAND_STATES.FLIPPING;\n }\n\n if (this.gestureDetector.checkClosedHand(distances, this.histories, this.indices)) {\n return HAND_STATES.CLOSED;\n }\n\n if (this.gestureDetector.checkOpenHand(distances, this.histories, this.indices)) {\n return HAND_STATES.OPEN;\n }\n\n if (this.gestureDetector.isPointing(distances, this.histories, this.indices)) {\n return HAND_STATES.POINTING;\n }\n\n return HAND_STATES.CLOSED;\n }\n\n resetSwipeState(): void {\n this.histories.landmark0.fill(null);\n this.histories.indexTip.fill(null);\n this.indices.history = 0;\n this.indices.indexTip = 0;\n this.lastActionTime = Date.now();\n this.pointingCount = 0;\n this.flippingCount = 0;\n }\n\n validateStateTransition(newState: HandState): HandState {\n const validTransitionStates: HandState[] = [\n HAND_STATES.OPEN,\n HAND_STATES.POINTING,\n HAND_STATES.CLOSED,\n HAND_STATES.FLIPPING,\n HAND_STATES.THUMB_UP\n ];\n\n if (newState === this.lastState || !validTransitionStates.includes(newState)) {\n return newState;\n }\n\n const historyKey = HISTORY_KEY_BY_STATE[newState];\n if (!historyKey) {\n return newState;\n }\n\n const confidenceKey = CONFIDENCE_BY_HISTORY[historyKey];\n const requiredConfidence = this.config[confidenceKey] as number;\n const actualConfidence = this.histories[historyKey].filter(Boolean).length;\n\n if (actualConfidence < requiredConfidence * 0.9) {\n return this.lastState ?? newState;\n }\n\n return newState;\n }\n\n clearAllHistory(): void {\n this.histories.centroid.fill(null);\n this.histories.landmark0.fill(null);\n this.histories.indexTip.fill(null);\n this.histories.open.fill(false);\n this.histories.pointing.fill(false);\n this.histories.flipping.fill(false);\n this.histories.closed.fill(false);\n this.histories.thumbUp.fill(false);\n\n Object.keys(this.indices).forEach((key) => {\n this.indices[key as keyof GestureIndices] = 0;\n });\n this.resetTrackingState();\n }\n\n processLandmarks(landmarks: Landmarks | null | undefined): GestureResult {\n const now = Date.now();\n\n if (this.detectionPausedUntil > now) {\n if (this.config.debug) {\n console.log(`In STARTING state until ${new Date(this.detectionPausedUntil).toLocaleTimeString()}`);\n }\n return this.buildResult(HAND_STATES.STARTING);\n }\n\n if (!landmarks?.[LANDMARKS.PINKY_TIP]) {\n return this.handleNoDetection(now);\n }\n\n this.lastDetectionTime = now;\n const reconnectionResult = this.handleDetectionReconnection(now);\n if (reconnectionResult) {\n return reconnectionResult;\n }\n\n this.noDetectionStartTime = 0;\n this.detectionPausedUntil = 0;\n\n const distances = this.gestureDetector.computeDistances(landmarks);\n const centroid = this.calculateCentroid(landmarks, now);\n\n this.histories.centroid[this.indices.centroid] = {\n x: centroid.x,\n y: centroid.y,\n time: now,\n seq: this.frameSeq\n };\n this.indices.centroid = (this.indices.centroid + 1) % this.config.maxCentroidHistorySize;\n\n const rotation = this.getRotation(landmarks, centroid);\n\n this.pointingCount += this.gestureDetector.isPointing(distances, this.histories, this.indices) ? 1 : 0;\n this.flippingCount += this.gestureDetector.isFlipping(distances, this.histories, this.indices) ? 1 : 0;\n\n let newState = this.determineHandState(distances, landmarks);\n newState = this.validateStateTransition(newState);\n\n let palmDirection: PalmDirection | null = null;\n if ((PALM_DIRECTION_STATES as readonly string[]).includes(newState)) {\n palmDirection = this.gestureDetector.detectPalmDirection(this.histories.centroid);\n this.lastPalmDirection = palmDirection;\n } else {\n this.lastPalmDirection = null;\n }\n\n this.lastState = newState;\n\n if ((ROTATION_DISABLED_STATES as readonly string[]).includes(newState)) {\n rotation.fill(0);\n }\n\n return this.buildResult(newState, rotation, palmDirection);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/mediapipe.ts
|
|
2
|
+
import { HandLandmarker, FilesetResolver } from "@mediapipe/tasks-vision";
|
|
3
|
+
var DEFAULT_MEDIAPIPE_CONFIG = {
|
|
4
|
+
wasmPath: "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm",
|
|
5
|
+
modelPath: "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task"
|
|
6
|
+
};
|
|
7
|
+
var DEFAULT_DETECTION_CONFIG = {
|
|
8
|
+
runningMode: "VIDEO",
|
|
9
|
+
numHands: 1,
|
|
10
|
+
minHandDetectionConfidence: 0.6,
|
|
11
|
+
minHandPresenceConfidence: 0.6,
|
|
12
|
+
minTrackingConfidence: 0.5
|
|
13
|
+
};
|
|
14
|
+
async function createHandLandmarker(options = {}) {
|
|
15
|
+
const config = {
|
|
16
|
+
...DEFAULT_MEDIAPIPE_CONFIG,
|
|
17
|
+
...DEFAULT_DETECTION_CONFIG,
|
|
18
|
+
...options
|
|
19
|
+
};
|
|
20
|
+
const vision = await FilesetResolver.forVisionTasks(config.wasmPath);
|
|
21
|
+
const landmarkerOptions = {
|
|
22
|
+
baseOptions: {
|
|
23
|
+
modelAssetPath: config.modelPath,
|
|
24
|
+
delegate: "GPU"
|
|
25
|
+
},
|
|
26
|
+
runningMode: config.runningMode,
|
|
27
|
+
numHands: config.numHands,
|
|
28
|
+
minHandDetectionConfidence: config.minHandDetectionConfidence,
|
|
29
|
+
minHandPresenceConfidence: config.minHandPresenceConfidence,
|
|
30
|
+
minTrackingConfidence: config.minTrackingConfidence
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
return await HandLandmarker.createFromOptions(vision, landmarkerOptions);
|
|
34
|
+
} catch (gpuError) {
|
|
35
|
+
console.warn("GPU delegate failed, falling back to CPU:", gpuError);
|
|
36
|
+
return HandLandmarker.createFromOptions(vision, {
|
|
37
|
+
...landmarkerOptions,
|
|
38
|
+
baseOptions: {
|
|
39
|
+
modelAssetPath: config.modelPath
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
DEFAULT_DETECTION_CONFIG,
|
|
46
|
+
DEFAULT_MEDIAPIPE_CONFIG,
|
|
47
|
+
createHandLandmarker
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=mediapipe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mediapipe.ts"],"sourcesContent":["import { HandLandmarker, FilesetResolver } from '@mediapipe/tasks-vision';\nimport type { HandLandmarker as HandLandmarkerType } from '@mediapipe/tasks-vision';\nimport type { MediapipeConfig } from './types.js';\n\nexport const DEFAULT_MEDIAPIPE_CONFIG = {\n wasmPath: 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm',\n modelPath: 'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task'\n} as const satisfies Pick<MediapipeConfig, 'wasmPath' | 'modelPath'>;\n\nexport const DEFAULT_DETECTION_CONFIG = {\n runningMode: 'VIDEO',\n numHands: 1,\n minHandDetectionConfidence: 0.6,\n minHandPresenceConfidence: 0.6,\n minTrackingConfidence: 0.5\n} as const satisfies Omit<MediapipeConfig, 'wasmPath' | 'modelPath'>;\n\nexport type CreateHandLandmarkerOptions = Partial<MediapipeConfig>;\n\n/**\n * Create a MediaPipe HandLandmarker with GPU fallback to CPU.\n */\nexport async function createHandLandmarker(\n options: CreateHandLandmarkerOptions = {}\n): Promise<HandLandmarkerType> {\n const config = {\n ...DEFAULT_MEDIAPIPE_CONFIG,\n ...DEFAULT_DETECTION_CONFIG,\n ...options\n };\n\n const vision = await FilesetResolver.forVisionTasks(config.wasmPath);\n\n const landmarkerOptions = {\n baseOptions: {\n modelAssetPath: config.modelPath,\n delegate: 'GPU' as const\n },\n runningMode: config.runningMode as 'VIDEO' | 'IMAGE',\n numHands: config.numHands,\n minHandDetectionConfidence: config.minHandDetectionConfidence,\n minHandPresenceConfidence: config.minHandPresenceConfidence,\n minTrackingConfidence: config.minTrackingConfidence\n };\n\n try {\n return await HandLandmarker.createFromOptions(vision, landmarkerOptions);\n } catch (gpuError) {\n console.warn('GPU delegate failed, falling back to CPU:', gpuError);\n return HandLandmarker.createFromOptions(vision, {\n ...landmarkerOptions,\n baseOptions: {\n modelAssetPath: config.modelPath\n }\n });\n }\n}\n"],"mappings":";AAAA,SAAS,gBAAgB,uBAAuB;AAIzC,IAAM,2BAA2B;AAAA,EACtC,UAAU;AAAA,EACV,WAAW;AACb;AAEO,IAAM,2BAA2B;AAAA,EACtC,aAAa;AAAA,EACb,UAAU;AAAA,EACV,4BAA4B;AAAA,EAC5B,2BAA2B;AAAA,EAC3B,uBAAuB;AACzB;AAOA,eAAsB,qBACpB,UAAuC,CAAC,GACX;AAC7B,QAAM,SAAS;AAAA,IACb,GAAG;AAAA,IACH,GAAG;AAAA,IACH,GAAG;AAAA,EACL;AAEA,QAAM,SAAS,MAAM,gBAAgB,eAAe,OAAO,QAAQ;AAEnE,QAAM,oBAAoB;AAAA,IACxB,aAAa;AAAA,MACX,gBAAgB,OAAO;AAAA,MACvB,UAAU;AAAA,IACZ;AAAA,IACA,aAAa,OAAO;AAAA,IACpB,UAAU,OAAO;AAAA,IACjB,4BAA4B,OAAO;AAAA,IACnC,2BAA2B,OAAO;AAAA,IAClC,uBAAuB,OAAO;AAAA,EAChC;AAEA,MAAI;AACF,WAAO,MAAM,eAAe,kBAAkB,QAAQ,iBAAiB;AAAA,EACzE,SAAS,UAAU;AACjB,YAAQ,KAAK,6CAA6C,QAAQ;AAClE,WAAO,eAAe,kBAAkB,QAAQ;AAAA,MAC9C,GAAG;AAAA,MACH,aAAa;AAAA,QACX,gBAAgB,OAAO;AAAA,MACzB;AAAA,IACF,CAAC;AAAA,EACH;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hand-guest-control",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Hand gesture detection and state management using MediaPipe landmarks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "HoloLab",
|
|
@@ -13,23 +13,34 @@
|
|
|
13
13
|
"hand-landmarks"
|
|
14
14
|
],
|
|
15
15
|
"sideEffects": false,
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
16
18
|
"exports": {
|
|
17
|
-
".":
|
|
18
|
-
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./mediapipe": {
|
|
24
|
+
"types": "./dist/mediapipe.d.ts",
|
|
25
|
+
"import": "./dist/mediapipe.js"
|
|
26
|
+
}
|
|
19
27
|
},
|
|
20
28
|
"files": [
|
|
21
|
-
"
|
|
29
|
+
"dist",
|
|
22
30
|
"LICENSE",
|
|
23
31
|
"README.md"
|
|
24
32
|
],
|
|
25
33
|
"scripts": {
|
|
26
|
-
"
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"test": "npm run build && node --test test/*.test.js",
|
|
27
36
|
"prepublishOnly": "npm test",
|
|
28
37
|
"demo": "vite",
|
|
29
38
|
"demo:build": "vite build"
|
|
30
39
|
},
|
|
31
40
|
"devDependencies": {
|
|
32
41
|
"@mediapipe/tasks-vision": "^0.10.0",
|
|
42
|
+
"tsup": "^8.5.0",
|
|
43
|
+
"typescript": "^5.9.0",
|
|
33
44
|
"vite": "^7.0.0"
|
|
34
45
|
},
|
|
35
46
|
"peerDependencies": {
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { LANDMARKS } from './constants.js';
|
|
2
|
-
|
|
3
|
-
export class HandGestureDetector {
|
|
4
|
-
constructor(config) {
|
|
5
|
-
this.config = config;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
static getDistance(p1, p2) {
|
|
9
|
-
const dx = p1.x - p2.x;
|
|
10
|
-
const dy = p1.y - p2.y;
|
|
11
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
static getAngle(p1, p2, p3) {
|
|
15
|
-
const v1x = p2.x - p1.x;
|
|
16
|
-
const v1y = p2.y - p1.y;
|
|
17
|
-
const v2x = p3.x - p2.x;
|
|
18
|
-
const v2y = p3.y - p2.y;
|
|
19
|
-
const dot = v1x * v2x + v1y * v2y;
|
|
20
|
-
const det = v1x * v2y - v1y * v2x;
|
|
21
|
-
let angle = (Math.atan2(det, dot) * 180) / Math.PI;
|
|
22
|
-
return angle < 0 ? angle + 360 : angle;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
computeDistances(landmarks) {
|
|
26
|
-
const wrist = landmarks[LANDMARKS.WRIST];
|
|
27
|
-
const tips = [
|
|
28
|
-
LANDMARKS.THUMB_TIP,
|
|
29
|
-
LANDMARKS.INDEX_TIP,
|
|
30
|
-
LANDMARKS.MIDDLE_TIP,
|
|
31
|
-
LANDMARKS.RING_TIP,
|
|
32
|
-
LANDMARKS.PINKY_TIP
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
const distances = {};
|
|
36
|
-
const fingerNames = ['Thumb', 'Index', 'Middle', 'Ring', 'Pinky'];
|
|
37
|
-
const adjacentNames = ['thumbToIndex', 'indexToMiddle', 'middleToRing', 'ringToPinky'];
|
|
38
|
-
|
|
39
|
-
tips.forEach((tip, i) => {
|
|
40
|
-
distances[`palmTo${fingerNames[i]}`] = HandGestureDetector.getDistance(wrist, landmarks[tip]);
|
|
41
|
-
|
|
42
|
-
if (i > 0) {
|
|
43
|
-
distances[adjacentNames[i - 1]] = HandGestureDetector.getDistance(
|
|
44
|
-
landmarks[tips[i - 1]],
|
|
45
|
-
landmarks[tip]
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
return distances;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
checkOpenHand(distances, histories, indices) {
|
|
54
|
-
const handSize = (distances.palmToThumb + distances.palmToMiddle) / 2 || 0.15;
|
|
55
|
-
const normalized = {
|
|
56
|
-
thumb: distances.palmToThumb / handSize,
|
|
57
|
-
index: distances.palmToIndex / handSize,
|
|
58
|
-
middle: distances.palmToMiddle / handSize,
|
|
59
|
-
ring: distances.palmToRing / handSize,
|
|
60
|
-
pinky: distances.palmToPinky / handSize,
|
|
61
|
-
thumbToIndex: distances.thumbToIndex / handSize
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
if (this.config.debug) {
|
|
65
|
-
console.log(`Hand state check (open): thumb=${normalized.thumb.toFixed(2)}, ` +
|
|
66
|
-
`index=${normalized.index.toFixed(2)}, middle=${normalized.middle.toFixed(2)}, ` +
|
|
67
|
-
`ring=${normalized.ring.toFixed(2)}, pinky=${normalized.pinky.toFixed(2)}, ` +
|
|
68
|
-
`thumbToIndex=${normalized.thumbToIndex.toFixed(2)}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const fingers = ['index', 'middle', 'ring', 'pinky'];
|
|
72
|
-
const extended = fingers.filter((finger) => normalized[finger] > normalized.thumb).length;
|
|
73
|
-
|
|
74
|
-
const isCandidate = extended >= this.config.openHandThresholds.minExtendedFingers;
|
|
75
|
-
histories.open[indices.open] = isCandidate;
|
|
76
|
-
indices.open = (indices.open + 1) % this.config.minOpenConfidence;
|
|
77
|
-
|
|
78
|
-
return histories.open.filter(Boolean).length >= Math.ceil(this.config.minOpenConfidence * 0.7);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
checkClosedHand(distances, histories, indices) {
|
|
82
|
-
const handSize = (distances.palmToThumb + distances.palmToMiddle) / 2 || 0.15;
|
|
83
|
-
const normalized = {
|
|
84
|
-
thumb: distances.palmToThumb / handSize,
|
|
85
|
-
index: distances.palmToIndex / handSize,
|
|
86
|
-
middle: distances.palmToMiddle / handSize,
|
|
87
|
-
ring: distances.palmToRing / handSize,
|
|
88
|
-
pinky: distances.palmToPinky / handSize,
|
|
89
|
-
thumbToIndex: distances.thumbToIndex / handSize
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
if (this.config.debug) {
|
|
93
|
-
console.log(`Hand state check (closed): thumb=${normalized.thumb.toFixed(2)}, ` +
|
|
94
|
-
`index=${normalized.index.toFixed(2)}, middle=${normalized.middle.toFixed(2)}, ` +
|
|
95
|
-
`ring=${normalized.ring.toFixed(2)}, pinky=${normalized.pinky.toFixed(2)}, ` +
|
|
96
|
-
`thumbToIndex=${normalized.thumbToIndex.toFixed(2)}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const fingers = ['index', 'middle', 'ring', 'pinky'];
|
|
100
|
-
const closedFingers = fingers.filter((finger) => normalized[finger] < normalized.thumb).length;
|
|
101
|
-
|
|
102
|
-
const isCandidate = closedFingers >= this.config.openHandThresholds.minExtendedFingers;
|
|
103
|
-
histories.closed[indices.closed] = isCandidate;
|
|
104
|
-
indices.closed = (indices.closed + 1) % this.config.minClosedConfidence;
|
|
105
|
-
|
|
106
|
-
return histories.closed.filter(Boolean).length >= Math.ceil(this.config.minClosedConfidence * 0.7);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
isPointing(distances, histories, indices) {
|
|
110
|
-
const handSize = distances.palmToIndex || 0.15;
|
|
111
|
-
const normalized = {
|
|
112
|
-
index: distances.palmToIndex / handSize,
|
|
113
|
-
middle: distances.palmToMiddle / handSize,
|
|
114
|
-
ring: distances.palmToRing / handSize,
|
|
115
|
-
pinky: distances.palmToPinky / handSize,
|
|
116
|
-
thumb: distances.palmToThumb / handSize
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const otherFingers = [normalized.middle, normalized.ring, normalized.pinky, normalized.thumb];
|
|
120
|
-
const isIndexExtended = 0.5 > Math.max(...otherFingers) && normalized.thumb < 0.7;
|
|
121
|
-
|
|
122
|
-
histories.pointing[indices.pointing] = isIndexExtended;
|
|
123
|
-
indices.pointing = (indices.pointing + 1) % this.config.minPointingConfidence;
|
|
124
|
-
|
|
125
|
-
return histories.pointing.filter(Boolean).length >= Math.ceil(this.config.minPointingConfidence * 0.9);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
isFlipping(distances, histories, indices) {
|
|
129
|
-
const handSize = distances.palmToIndex || 0.15;
|
|
130
|
-
const normalized = {
|
|
131
|
-
index: distances.palmToIndex / handSize,
|
|
132
|
-
middle: distances.palmToMiddle / handSize,
|
|
133
|
-
ring: distances.palmToRing / handSize,
|
|
134
|
-
pinky: distances.palmToPinky / handSize,
|
|
135
|
-
thumb: distances.palmToThumb / handSize
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
if (this.config.debug) {
|
|
139
|
-
console.log(`Hand state check (flip): index=${normalized.index.toFixed(2)}, ` +
|
|
140
|
-
`middle=${normalized.middle.toFixed(2)}, ring=${normalized.ring.toFixed(2)}, ` +
|
|
141
|
-
`pinky=${normalized.pinky.toFixed(2)}, thumb=${normalized.thumb.toFixed(2)}`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const isFlippingGesture = normalized.middle > 0.8 &&
|
|
145
|
-
Math.max(normalized.ring, normalized.pinky, normalized.thumb) < 0.5;
|
|
146
|
-
|
|
147
|
-
histories.flipping[indices.flipping] = isFlippingGesture;
|
|
148
|
-
indices.flipping = (indices.flipping + 1) % this.config.minFlippingConfidence;
|
|
149
|
-
|
|
150
|
-
return histories.flipping.filter(Boolean).length >= Math.ceil(this.config.minFlippingConfidence * 0.9);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
checkSwipeNext(landmarks, distances, histories, indices, pointingCount, lastActionTime) {
|
|
154
|
-
if (
|
|
155
|
-
!this.isPointing(distances, histories, indices) ||
|
|
156
|
-
!landmarks ||
|
|
157
|
-
histories.landmark0.filter(Boolean).length < 6 ||
|
|
158
|
-
pointingCount < this.config.minPointingCount ||
|
|
159
|
-
Date.now() - lastActionTime < 2000
|
|
160
|
-
) {
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const entries = histories.landmark0.filter(Boolean);
|
|
165
|
-
const xPositions = entries.map((e) => e.x);
|
|
166
|
-
const xRange = Math.max(...xPositions) - Math.min(...xPositions);
|
|
167
|
-
const firstX = entries[0].x;
|
|
168
|
-
const lastX = entries[entries.length - 1].x;
|
|
169
|
-
|
|
170
|
-
return xRange > 0.03 && lastX - firstX > 0.03;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
checkSwipePrevious(landmarks, distances, histories, indices, pointingCount, lastActionTime) {
|
|
174
|
-
if (
|
|
175
|
-
!this.isPointing(distances, histories, indices) ||
|
|
176
|
-
!landmarks ||
|
|
177
|
-
histories.landmark0.filter(Boolean).length < 6 ||
|
|
178
|
-
pointingCount < this.config.minPointingCount ||
|
|
179
|
-
Date.now() - lastActionTime < 2000
|
|
180
|
-
) {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const entries = histories.landmark0.filter(Boolean);
|
|
185
|
-
const xPositions = entries.map((e) => e.x);
|
|
186
|
-
const xRange = Math.max(...xPositions) - Math.min(...xPositions);
|
|
187
|
-
const firstX = entries[0].x;
|
|
188
|
-
const lastX = entries[entries.length - 1].x;
|
|
189
|
-
|
|
190
|
-
return xRange > 0.03 && firstX - lastX > 0.03;
|
|
191
|
-
}
|
|
192
|
-
}
|