真机日志定位的问题: - 画面外的关键点坐标是 NaN 或字符串,平滑后永久污染膝、踝坐标,深蹲指标恒为 0、站位却被误判通过。 入口处把非数字坐标当作看不见,平滑时上一帧无效就用新坐标重新开始 - 识别引擎单帧跳动越过计数线也会计一次:连续 2 帧达到计数线才算开始;深蹲连续 2 帧回到站立才算完成 - 离太近时腿部点置信度只有 0.1~0.2、位置靠猜:计数的可见阈值保持 0.3,不用膝盖替补脚踝 按真机跟练的实际幅度放宽:深蹲 0.18→0.13、高抬腿 0.22→0.16、开合跳 120°→95°、侧平举 68°→58°, 左右只看清一侧也能算;开合跳 140° 满分,没迈步只提示不压分。
61 lines
2.6 KiB
TypeScript
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)
|
|
})
|