Files
zyt/TUICallKit-Vue3/training/motion/openness.ts
T
longandCursor 729d2a084d feat(training): 新增握力环 2.0 摄像头手势识别页
用前置摄像头 + VisionKit/ROI 管线驱动开合计数,复用跟练舞台与连击粒子,并加固结束收尾避免末段卡死无响应。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 18:31:31 +08:00

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
}