Files
zyt/TUICallKit-Vue3/training/pose/__tests__/keypoints.test.ts
T
long a8d00597f4 fix(training): 修复 AI 跟练真机不计数和误计数,放宽动作要求
真机日志定位的问题:
- 画面外的关键点坐标是 NaN 或字符串,平滑后永久污染膝、踝坐标,深蹲指标恒为 0、站位却被误判通过。
  入口处把非数字坐标当作看不见,平滑时上一帧无效就用新坐标重新开始
- 识别引擎单帧跳动越过计数线也会计一次:连续 2 帧达到计数线才算开始;深蹲连续 2 帧回到站立才算完成
- 离太近时腿部点置信度只有 0.1~0.2、位置靠猜:计数的可见阈值保持 0.3,不用膝盖替补脚踝

按真机跟练的实际幅度放宽:深蹲 0.18→0.13、高抬腿 0.22→0.16、开合跳 120°→95°、侧平举 68°→58°,
左右只看清一侧也能算;开合跳 140° 满分,没迈步只提示不压分。
2026-10-31 11:56:31 +08:00

61 lines
2.6 KiB
TypeScript

/**
* 关键点转换与平滑:真机上画面外的点坐标是 NaN,不能让它污染后续帧。
* 运行:node --test training/pose/__tests__/
*/
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { anchorToPose, checkFraming, smoothPose, KP, type Pose } from '../keypoints.ts'
import { createCounter } from '../counters.ts'
import { MOTIONS } from '../motions.ts'
import { sampleMotion } from './sampleMotion.ts'
const LEG = [KP.L_KNEE, KP.R_KNEE, KP.L_ANKLE, KP.R_ANKLE]
/** 示范姿态转成 VisionKit 的 anchor,指定的点坐标为 NaN(画面外) */
function anchorOf(pose: Pose, nanIdx: number[] = []) {
return {
points: pose.points.map((p, i) => (nanIdx.includes(i) ? { x: NaN, y: NaN } : { x: p.x, y: p.y })),
confidence: pose.scores.map((s, i) => (nanIdx.includes(i) ? 0.9 : s)),
score: 0.9,
}
}
test('anchorToPose:坐标为 NaN 的点视为看不见', () => {
const [f] = sampleMotion('squat', 0.1)
const p = anchorToPose(anchorOf(f, LEG), 480, 853, 0)!
for (const i of LEG) {
assert.equal(p.scores[i], 0, `score ${i}`)
assert.ok(Number.isFinite(p.points[i].x) && Number.isFinite(p.points[i].y), `point ${i}`)
}
})
test('smoothPose:上一帧是 NaN 时直接采用新坐标,不会一直是 NaN', () => {
const [f] = sampleMotion('squat', 0.1)
const poisoned: Pose = { ...f, points: f.points.map((p, i) => (LEG.includes(i) ? { x: NaN, y: NaN } : p)) }
const next = smoothPose(poisoned, f)
for (const i of LEG) assert.ok(Number.isFinite(next.points[i].y), `point ${i}`)
})
test('站位:脚踝坐标是 NaN 时不能判定为站位良好', () => {
const [f] = sampleMotion('squat', 0.1)
const p: Pose = { ...f, points: f.points.map((q, i) => (i === KP.L_ANKLE || i === KP.R_ANKLE ? { x: NaN, y: NaN } : q)) }
assert.notEqual(checkFraming(p), 'none')
})
test('真机复现:开始时腿在画面外(NaN),退回画面后深蹲照常计数', () => {
const cycles = 4
const frames = sampleMotion('squat', MOTIONS.squat.cycleSeconds * cycles + 0.3)
const counter = createCounter('squat')
let smoothed: Pose | null = null
// 前 1 秒:离得太近,膝和踝在画面外
for (let i = 0; i < 12; i++) {
const raw = anchorToPose(anchorOf(frames[0], LEG), 480, 853, i * 83)!
smoothed = smoothPose(smoothed, raw)
}
for (const f of frames) {
smoothed = smoothPose(smoothed, anchorToPose(anchorOf(f), 480, 853, f.t)!)
counter.update(smoothed)
}
assert.equal(counter.count, cycles)
})