用前置摄像头 + VisionKit/ROI 管线驱动开合计数,复用跟练舞台与连击粒子,并加固结束收尾避免末段卡死无响应。 Co-authored-by: Cursor <cursoragent@cursor.com>
69 lines
2.0 KiB
TypeScript
69 lines
2.0 KiB
TypeScript
import type { CameraFrame, HandSample } from './types'
|
|
|
|
function clamp01(v: number) {
|
|
return Math.max(0, Math.min(1, v))
|
|
}
|
|
|
|
/**
|
|
* 开发者工具 / 不支持 VisionKit 时的粗开合估计:
|
|
* 取画面中心 ROI 的亮度方差与边缘能量,映射到 openness。
|
|
* 仅用于打通链路与 UI 联调,不能作为产品识别。
|
|
*/
|
|
export function estimateOpennessFromRoi(frame: CameraFrame): HandSample {
|
|
const { width, height, data } = frame
|
|
const u8 = new Uint8Array(data)
|
|
const cx0 = Math.floor(width * 0.28)
|
|
const cy0 = Math.floor(height * 0.22)
|
|
const cx1 = Math.floor(width * 0.72)
|
|
const cy1 = Math.floor(height * 0.78)
|
|
const step = Math.max(4, Math.floor(Math.min(width, height) / 40))
|
|
|
|
let count = 0
|
|
let sum = 0
|
|
let sumSq = 0
|
|
let edge = 0
|
|
let prev = 0
|
|
|
|
for (let y = cy0; y < cy1; y += step) {
|
|
for (let x = cx0; x < cx1; x += step) {
|
|
const i = (y * width + x) * 4
|
|
const r = u8[i]
|
|
const g = u8[i + 1]
|
|
const b = u8[i + 2]
|
|
// 粗略肤色门控 + 亮度
|
|
const luma = 0.299 * r + 0.587 * g + 0.114 * b
|
|
const skinish = r > 60 && g > 30 && b > 20 && r >= g && Math.abs(r - g) > 8
|
|
if (!skinish && luma < 40) continue
|
|
sum += luma
|
|
sumSq += luma * luma
|
|
edge += Math.abs(luma - prev)
|
|
prev = luma
|
|
count += 1
|
|
}
|
|
}
|
|
|
|
if (count < 20) {
|
|
return {
|
|
ts: Date.now(),
|
|
openness: 0.5,
|
|
score: 0.05,
|
|
source: 'roi',
|
|
}
|
|
}
|
|
|
|
const mean = sum / count
|
|
const variance = Math.max(0, sumSq / count - mean * mean)
|
|
const edgeNorm = edge / count
|
|
|
|
// 张开手掌通常带来更大轮廓/边缘;握拳更紧凑。经验映射,仅联调。
|
|
const openness = clamp01((Math.sqrt(variance) / 55 + edgeNorm / 40) * 0.85)
|
|
const score = clamp01(0.25 + count / 800)
|
|
|
|
return {
|
|
ts: Date.now(),
|
|
openness,
|
|
score,
|
|
source: 'roi',
|
|
}
|
|
}
|