用前置摄像头 + VisionKit/ROI 管线驱动开合计数,复用跟练舞台与连击粒子,并加固结束收尾避免末段卡死无响应。 Co-authored-by: Cursor <cursoragent@cursor.com>
64 lines
2.0 KiB
TypeScript
64 lines
2.0 KiB
TypeScript
import type { HandSample, Point2D } from './types'
|
|
|
|
/** VisionKit 文档:1=布/开掌,3=握拳 */
|
|
const GESTURE_OPEN = 1
|
|
const GESTURE_FIST = 3
|
|
|
|
/**
|
|
* OpenPose-21 指尖索引(相对手腕距离用于连续 openness)
|
|
* 0 wrist, 4 thumb tip, 8 index, 12 middle, 16 ring, 20 pinky
|
|
*/
|
|
const TIP_INDEXES = [4, 8, 12, 16, 20]
|
|
|
|
function dist(a: Point2D, b: Point2D) {
|
|
const dx = a.x - b.x
|
|
const dy = a.y - b.y
|
|
return Math.sqrt(dx * dx + dy * dy)
|
|
}
|
|
|
|
function clamp01(v: number) {
|
|
return Math.max(0, Math.min(1, v))
|
|
}
|
|
|
|
/** 离散手势 → 开合度;未知手势返回 null(交给几何或保持) */
|
|
export function opennessFromGesture(gesture?: number): number | null {
|
|
if (gesture === GESTURE_OPEN) return 1
|
|
if (gesture === GESTURE_FIST) return 0
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* 用指尖到手腕的平均距离 / 掌宽 估计 openness。
|
|
* 握力环场景开合幅度偏小,映射区间比裸掌更紧一些。
|
|
*/
|
|
export function opennessFromLandmarks(points?: Point2D[]): number | null {
|
|
if (!points || points.length < 21) return null
|
|
const wrist = points[0]
|
|
const mcpIndex = points[5]
|
|
const mcpPinky = points[17]
|
|
const palmWidth = dist(mcpIndex, mcpPinky)
|
|
if (palmWidth < 1e-6) return null
|
|
|
|
let tipSum = 0
|
|
for (const i of TIP_INDEXES) {
|
|
tipSum += dist(points[i], wrist)
|
|
}
|
|
const tipMean = tipSum / TIP_INDEXES.length
|
|
// 握力环:握紧约 0.9~1.35 掌宽,半开约 1.5~2.2 掌宽
|
|
const ratio = tipMean / palmWidth
|
|
return clamp01((ratio - 0.95) / 1.15)
|
|
}
|
|
|
|
export function mergeOpenness(sample: Pick<HandSample, 'gesture' | 'points'>): number | null {
|
|
const fromGeom = opennessFromLandmarks(sample.points)
|
|
const fromGesture = opennessFromGesture(sample.gesture)
|
|
|
|
if (fromGeom != null && fromGesture != null) {
|
|
// 几何为主:离散手势(布/拳)在捏环时容易误判,只做轻牵引
|
|
return clamp01(fromGeom * 0.85 + fromGesture * 0.15)
|
|
}
|
|
if (fromGeom != null) return fromGeom
|
|
if (fromGesture != null) return fromGesture
|
|
return null
|
|
}
|