fixed issue with step counter
This commit is contained in:
@@ -42,14 +42,21 @@ class LiveStepDetectorTest {
|
||||
val detector = LiveStepDetector()
|
||||
|
||||
// Left-foot step: rising, peak, falling -- confirms on the 3rd call.
|
||||
// Amplitude (60px) is comfortably above the 45px prominence
|
||||
// threshold at this torsoScale (~300) but stays under the outlier
|
||||
// gate's 75px single-frame cutoff (maxFrameJumpRatio 0.25 * 300),
|
||||
// since these three frames are a simplified stand-in for what a
|
||||
// real footfall spreads across several -- see
|
||||
// gradualPeakOnAPlateauIsDetected for the shape that actually
|
||||
// reaches the detector in production.
|
||||
detector.update(frame(0, ankleL = 1000f, ankleR = 700f, hipX = 400f, hipY = 800f))
|
||||
detector.update(frame(50, ankleL = 1100f, ankleR = 700f, hipX = 402f, hipY = 802f))
|
||||
detector.update(frame(50, ankleL = 1060f, ankleR = 700f, hipX = 402f, hipY = 802f))
|
||||
var result = detector.update(frame(100, ankleL = 1000f, ankleR = 700f, hipX = 405f, hipY = 805f))
|
||||
assertEquals(1, result.stepCount)
|
||||
|
||||
// Right-foot step, past the 300ms refractory window.
|
||||
detector.update(frame(350, ankleL = 1000f, ankleR = 700f, hipX = 410f, hipY = 810f))
|
||||
detector.update(frame(400, ankleL = 1000f, ankleR = 800f, hipX = 415f, hipY = 815f))
|
||||
detector.update(frame(400, ankleL = 1000f, ankleR = 760f, hipX = 415f, hipY = 815f))
|
||||
result = detector.update(frame(450, ankleL = 1000f, ankleR = 700f, hipX = 420f, hipY = 820f))
|
||||
assertEquals(2, result.stepCount)
|
||||
|
||||
@@ -73,8 +80,9 @@ class LiveStepDetectorTest {
|
||||
fun genuineStillnessStillResets() {
|
||||
val detector = LiveStepDetector()
|
||||
|
||||
// Amplitude 60px -- see the comment in stalledFramesDoNotResetAnInProgressCount.
|
||||
detector.update(frame(0, ankleL = 1000f, ankleR = 700f, hipX = 400f, hipY = 800f))
|
||||
detector.update(frame(50, ankleL = 1100f, ankleR = 700f, hipX = 402f, hipY = 802f))
|
||||
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)
|
||||
|
||||
@@ -161,4 +169,80 @@ class LiveStepDetectorTest {
|
||||
|
||||
assertEquals(0, result.stepCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reproduces the over-counting bug found on a real device trace where
|
||||
* the bowler's torso scale was ~45-79px (small/distant subject in
|
||||
* frame) rather than the ~300px used elsewhere in this file: single-frame
|
||||
* ankle-y jumps of 15-88px showed up dozens of times in that trace --
|
||||
* physically implausible movement in one ~30-60ms frame at that scale
|
||||
* -- and each got read as its own step, running the live counter to 27
|
||||
* "steps" in 24 seconds of a recording with 5 real steps. A prominence
|
||||
* floor can't fix this: that same recording's genuine footfalls had as
|
||||
* little as ~10-12px of prominence, smaller than the glitch jumps
|
||||
* themselves, so no fixed threshold can separate the two by amplitude
|
||||
* alone -- confirmed separately by replaying both a floored and an
|
||||
* unfloored threshold against a clean reference recording with a known
|
||||
* step count, where flooring high enough to reject the glitch jumps
|
||||
* also rejected 4 of the 5 real steps. The actual fix instead rejects
|
||||
* any one frame whose ankle-y moved further than maxFrameJumpRatio *
|
||||
* torsoScale since the last *trusted* reading, before it ever reaches
|
||||
* the peak tracker.
|
||||
*/
|
||||
@Test
|
||||
fun implausibleSingleFrameJumpNeverConfirms() {
|
||||
val detector = LiveStepDetector()
|
||||
// shoulder is fixed at (400,500) -- see frame() -- so hipY=455
|
||||
// gives a shoulder-to-hip distance of 45, matching the real trace's
|
||||
// median torsoScale. maxFrameJumpRatio defaults to 0.25, so
|
||||
// anything over 11.25px in one frame from the last trusted reading
|
||||
// gets rejected outright.
|
||||
val hipY = 455f
|
||||
|
||||
var result = LiveStepDetector.Result(0, emptyList(), false)
|
||||
var t = 0L
|
||||
// Establish a trusted baseline.
|
||||
result = detector.update(frame(t, ankleL = 400f, ankleR = 700f, hipX = 400f, hipY = hipY))
|
||||
t += 30L
|
||||
result = detector.update(frame(t, ankleL = 402f, ankleR = 700f, hipX = 400f, hipY = hipY))
|
||||
t += 30L
|
||||
|
||||
// A single implausible spike -- 80px in one frame -- then straight
|
||||
// back. Before the outlier gate, this pair alone was enough to
|
||||
// read as a confirmed peak: the spike became the running high, and
|
||||
// the drop right back down cleared the (much smaller) ratio-only
|
||||
// prominence threshold at this torso scale.
|
||||
result = detector.update(frame(t, ankleL = 482f, ankleR = 700f, hipX = 400f, hipY = hipY))
|
||||
t += 30L
|
||||
result = detector.update(frame(t, ankleL = 403f, ankleR = 700f, hipX = 400f, hipY = hipY))
|
||||
t += 30L
|
||||
|
||||
assertEquals("an implausible single-frame jump should never read as a step", 0, result.stepCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Control case for the same fix: genuine motion at the same small
|
||||
* torso scale, arriving gradually (each frame's move well within
|
||||
* maxFrameJumpRatio) rather than as one implausible jump, should still
|
||||
* confirm -- the outlier gate isn't just disabling small-scale
|
||||
* detection outright.
|
||||
*/
|
||||
@Test
|
||||
fun gradualMotionAtSmallTorsoScaleStillConfirms() {
|
||||
val detector = LiveStepDetector()
|
||||
val hipY = 455f // torsoScale = 45, same as the test above.
|
||||
|
||||
var result = LiveStepDetector.Result(0, emptyList(), false)
|
||||
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 --
|
||||
// a 60px prominence, comfortably past the 6.75px ratio threshold.
|
||||
val path = listOf(400f, 410f, 420f, 430f, 440f, 450f, 460f, 450f, 440f, 430f, 420f, 410f, 400f)
|
||||
for (y in path) {
|
||||
result = detector.update(frame(t, ankleL = y, ankleR = 700f, hipX = 400f, hipY = hipY))
|
||||
t += 30L
|
||||
}
|
||||
|
||||
assertEquals("gradual real motion at small torso scale should still confirm", 1, result.stepCount)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user