Added tuning feature and hand palm action to reset the timer
This commit is contained in:
@@ -9,7 +9,14 @@ class LiveStepDetectorTest {
|
||||
private val shoulder = LandmarkPoint(400f, 500f)
|
||||
private val noAngles = PoseAngles(null, null, null, null)
|
||||
|
||||
private fun frame(t: Long, ankleL: Float, ankleR: Float, hipX: Float, hipY: Float) = PoseFrame(
|
||||
private fun frame(
|
||||
t: Long,
|
||||
ankleL: Float,
|
||||
ankleR: Float,
|
||||
hipX: Float,
|
||||
hipY: Float,
|
||||
leftWristY: Float? = null
|
||||
) = PoseFrame(
|
||||
timestampMs = t,
|
||||
leftAnkle = null,
|
||||
rightAnkle = null,
|
||||
@@ -25,7 +32,7 @@ class LiveStepDetectorTest {
|
||||
rightShoulder = null,
|
||||
leftElbow = null,
|
||||
rightElbow = null,
|
||||
leftWrist = null,
|
||||
leftWrist = leftWristY?.let { LandmarkPoint(390f, it) },
|
||||
rightWrist = null,
|
||||
angles = noAngles
|
||||
)
|
||||
@@ -33,9 +40,10 @@ class LiveStepDetectorTest {
|
||||
/**
|
||||
* Reproduces the failure seen on a real device recording: a run of
|
||||
* bit-identical pose frames (a stalled camera/analysis pipeline, not a
|
||||
* stationary bowler) landing right after real steps were counted. Before
|
||||
* the isStalledFrame fix, StillnessTracker read that frozen run as a
|
||||
* held "ready" stance and reset the count back to zero.
|
||||
* stationary bowler) landing right after real steps were counted.
|
||||
* Before the isStalledFrame fix, the (since-replaced) automatic
|
||||
* stillness-based reset read that frozen run as a held "ready" stance
|
||||
* and wiped the count back to zero.
|
||||
*/
|
||||
@Test
|
||||
fun stalledFramesDoNotResetAnInProgressCount() {
|
||||
@@ -72,12 +80,17 @@ class LiveStepDetectorTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Control case: genuinely near-static hip positions -- sub-pixel jitter
|
||||
* every frame, never bit-identical -- should still trigger a reset, so
|
||||
* the stall filter above isn't just disabling resets outright.
|
||||
* The reset gesture: raising a wrist above its shoulder (past
|
||||
* handRaiseMarginRatio's margin -- 15px at this test's torsoScale of
|
||||
* 300) and holding it there for the full handRaiseHoldMs (5000ms
|
||||
* default) should reset the count to zero. Ankle/hip carry the same
|
||||
* small per-frame jitter as stalledFramesDoNotResetAnInProgressCount's
|
||||
* frozen-frame check needs to *not* trigger on, since only the
|
||||
* ankle/hip fields feed isStalledFrame -- the wrist itself can safely
|
||||
* stay perfectly constant.
|
||||
*/
|
||||
@Test
|
||||
fun genuineStillnessStillResets() {
|
||||
fun handRaiseHeldForFullDurationResets() {
|
||||
val detector = LiveStepDetector()
|
||||
|
||||
// Amplitude 60px -- see the comment in stalledFramesDoNotResetAnInProgressCount.
|
||||
@@ -86,20 +99,114 @@ class LiveStepDetectorTest {
|
||||
val afterStep = detector.update(frame(100, ankleL = 1000f, ankleR = 700f, hipX = 405f, hipY = 805f))
|
||||
assertEquals(1, afterStep.stepCount)
|
||||
|
||||
var result = afterStep
|
||||
var sawReset = false
|
||||
var lastStepCount = afterStep.stepCount
|
||||
var y = 805f
|
||||
var t = 150L
|
||||
while (t <= 1200L) {
|
||||
y += if ((t / 50L) % 2L == 0L) 0.2f else -0.2f
|
||||
val result = detector.update(frame(t, ankleL = 1000f, ankleR = 700f, hipX = 405f, hipY = y))
|
||||
// Holds a raised left wrist (y=300, well past the 500-15=485
|
||||
// threshold) continuously from t=150 through past the 5000ms hold
|
||||
// requirement.
|
||||
while (t <= 5300L) {
|
||||
y += if ((t / 200L) % 2L == 0L) 0.3f else -0.3f
|
||||
result = detector.update(
|
||||
frame(t, ankleL = 1000f + (y - 805f), ankleR = 700f, hipX = 405f, hipY = y, leftWristY = 300f)
|
||||
)
|
||||
if (result.wasReset) sawReset = true
|
||||
lastStepCount = result.stepCount
|
||||
t += 50L
|
||||
t += 200L
|
||||
}
|
||||
|
||||
assertEquals(true, sawReset)
|
||||
assertEquals(0, lastStepCount)
|
||||
assertEquals("holding the raised-hand gesture for the full duration should reset", true, sawReset)
|
||||
assertEquals(0, result.stepCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Control case for the same gesture: raising a hand but dropping it
|
||||
* before the hold duration completes should never reset, even after
|
||||
* recording continues well past when the original hold would have
|
||||
* finished -- a drop restarts the hold from zero rather than pausing
|
||||
* and resuming it.
|
||||
*/
|
||||
@Test
|
||||
fun droppingHandBeforeFullDurationNeverResets() {
|
||||
val detector = LiveStepDetector()
|
||||
|
||||
detector.update(frame(0, ankleL = 1000f, ankleR = 700f, hipX = 400f, hipY = 800f))
|
||||
detector.update(frame(50, ankleL = 1060f, ankleR = 700f, hipX = 402f, hipY = 802f))
|
||||
val afterStep = detector.update(frame(100, ankleL = 1000f, ankleR = 700f, hipX = 405f, hipY = 805f))
|
||||
assertEquals(1, afterStep.stepCount)
|
||||
|
||||
var result = afterStep
|
||||
var y = 805f
|
||||
var t = 150L
|
||||
// Raise for 2s, well under the 5s hold requirement.
|
||||
while (t <= 2100L) {
|
||||
y += if ((t / 200L) % 2L == 0L) 0.3f else -0.3f
|
||||
result = detector.update(
|
||||
frame(t, ankleL = 1000f + (y - 805f), ankleR = 700f, hipX = 405f, hipY = y, leftWristY = 300f)
|
||||
)
|
||||
t += 200L
|
||||
}
|
||||
// Drop the hand and keep recording for 4s more -- past where the
|
||||
// original hold would have completed at t=5150.
|
||||
while (t <= 6200L) {
|
||||
y += if ((t / 200L) % 2L == 0L) 0.3f else -0.3f
|
||||
result = detector.update(frame(t, ankleL = 1000f + (y - 805f), ankleR = 700f, hipX = 405f, hipY = y))
|
||||
t += 200L
|
||||
}
|
||||
|
||||
assertEquals(false, result.wasReset)
|
||||
assertEquals(1, result.stepCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reproduces the real failure found on a device recording: the bowler
|
||||
* held the gesture for 4.35 of the required 5 seconds (progress
|
||||
* climbing perfectly smoothly the whole way, so this was a genuine,
|
||||
* deliberate hold, not jitter), then one frame read as "not raised" --
|
||||
* a natural arm wobble, not a dropped attempt -- and progress fell
|
||||
* straight back to zero. That happened on every one of five attempts
|
||||
* in that recording; none ever completed. A brief drop (under the
|
||||
* 500ms default grace period) should no longer restart the hold.
|
||||
*/
|
||||
@Test
|
||||
fun briefDropDuringHoldDoesNotResetProgress() {
|
||||
val detector = LiveStepDetector()
|
||||
|
||||
detector.update(frame(0, ankleL = 1000f, ankleR = 700f, hipX = 400f, hipY = 800f))
|
||||
detector.update(frame(50, ankleL = 1060f, ankleR = 700f, hipX = 402f, hipY = 802f))
|
||||
val afterStep = detector.update(frame(100, ankleL = 1000f, ankleR = 700f, hipX = 405f, hipY = 805f))
|
||||
assertEquals(1, afterStep.stepCount)
|
||||
|
||||
var result = afterStep
|
||||
var y = 805f
|
||||
var t = 150L
|
||||
// Hold for 2s.
|
||||
while (t <= 2100L) {
|
||||
y += if ((t / 200L) % 2L == 0L) 0.3f else -0.3f
|
||||
result = detector.update(
|
||||
frame(t, ankleL = 1000f + (y - 805f), ankleR = 700f, hipX = 405f, hipY = y, leftWristY = 300f)
|
||||
)
|
||||
t += 200L
|
||||
}
|
||||
// One frame's momentary dip -- hand reads as not-raised for a
|
||||
// single 200ms tick, well inside the 500ms grace period.
|
||||
y += 0.3f
|
||||
result = detector.update(frame(t, ankleL = 1000f + (y - 805f), ankleR = 700f, hipX = 405f, hipY = y))
|
||||
t += 200L
|
||||
|
||||
var sawReset = false
|
||||
// Resume raising and continue through the full hold duration.
|
||||
while (t <= 5300L) {
|
||||
y += if ((t / 200L) % 2L == 0L) 0.3f else -0.3f
|
||||
result = detector.update(
|
||||
frame(t, ankleL = 1000f + (y - 805f), ankleR = 700f, hipX = 405f, hipY = y, leftWristY = 300f)
|
||||
)
|
||||
if (result.wasReset) sawReset = true
|
||||
t += 200L
|
||||
}
|
||||
|
||||
assertEquals("a brief drop within the grace period should not restart the hold", true, sawReset)
|
||||
assertEquals(0, result.stepCount)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +231,7 @@ class LiveStepDetectorTest {
|
||||
180L to 601f, 210L to 599f, 240L to 600f, 270L to 601f, 300L to 599f, 330L to 600f,
|
||||
360L to 580f, 390L to 560f, 420L to 540f, 450L to 520f, 480L to 500f
|
||||
)
|
||||
var result = LiveStepDetector.Result(0, emptyList(), false)
|
||||
var result = LiveStepDetector.Result(0, emptyList(), false, 0f)
|
||||
// Hip drifts steadily throughout (a real bowler's hip keeps moving
|
||||
// during the approach) -- constant hip position would itself read
|
||||
// as a held "ready" stance once enough time elapses and wipe out
|
||||
@@ -157,7 +264,7 @@ class LiveStepDetectorTest {
|
||||
fun jitterBelowThresholdNeverConfirms() {
|
||||
val detector = LiveStepDetector()
|
||||
|
||||
var result = LiveStepDetector.Result(0, emptyList(), false)
|
||||
var result = LiveStepDetector.Result(0, emptyList(), false, 0f)
|
||||
var y = 600f
|
||||
var t = 0L
|
||||
val deltas = floatArrayOf(3f, -5f, 2f, -1f, 6f, -4f, 1f, -2f, 4f, -3f)
|
||||
@@ -199,7 +306,7 @@ class LiveStepDetectorTest {
|
||||
// gets rejected outright.
|
||||
val hipY = 455f
|
||||
|
||||
var result = LiveStepDetector.Result(0, emptyList(), false)
|
||||
var result = LiveStepDetector.Result(0, emptyList(), false, 0f)
|
||||
var t = 0L
|
||||
// Establish a trusted baseline.
|
||||
result = detector.update(frame(t, ankleL = 400f, ankleR = 700f, hipX = 400f, hipY = hipY))
|
||||
@@ -232,7 +339,7 @@ class LiveStepDetectorTest {
|
||||
val detector = LiveStepDetector()
|
||||
val hipY = 455f // torsoScale = 45, same as the test above.
|
||||
|
||||
var result = LiveStepDetector.Result(0, emptyList(), false)
|
||||
var result = LiveStepDetector.Result(0, emptyList(), false, 0f)
|
||||
var t = 0L
|
||||
// Rises from 400 to 460 in 10px steps (well under the 11.25px
|
||||
// per-frame outlier cutoff), holds, then descends the same way --
|
||||
|
||||
Reference in New Issue
Block a user