Added tuning feature and hand palm action to reset the timer
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @file StepCountingSession.kt
|
||||
* @brief Owns pose-frame buffering and live step counting for one recording attempt.
|
||||
*/
|
||||
package com.example.jnicpp.bowling
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* @brief Bundles everything [CameraViewModel] needs to buffer pose frames
|
||||
* and run live step counting during a recording, so that ViewModel
|
||||
* stays a thin state machine rather than also holding this
|
||||
* machinery directly.
|
||||
*
|
||||
* Knows nothing about CameraX/ML Kit or Android component lifecycle --
|
||||
* same reasoning as [CameraXController] and [PoseAnalyzer] -- so it's
|
||||
* trivially unit-testable and reusable if a second recording surface is
|
||||
* ever added.
|
||||
*/
|
||||
class StepCountingSession {
|
||||
|
||||
// Extra SMA smoothing for ankle/hip landmarks specifically, on top of
|
||||
// PoseLandmarkSmoother's per-frame EMA -- see AnkleHipMovingAverageFilter.
|
||||
private val ankleHipSmoother = AnkleHipMovingAverageFilter()
|
||||
|
||||
// Live, incremental step counting -- see LiveStepDetector. Resets
|
||||
// mid-recording when the bowler holds a hand raised for the gesture's
|
||||
// full hold duration, so one recording can capture several practice
|
||||
// approaches back to back. Rebuilt fresh (not just .reset()) in
|
||||
// startNewSession from whatever DetectorSettings are current at that
|
||||
// moment, so tuning changes made in ParameterEditorActivity take
|
||||
// effect on the very next recording without needing an app restart.
|
||||
private var liveStepDetector = LiveStepDetector()
|
||||
|
||||
// Time-ordered pose samples for the current/most recent recording
|
||||
// session, one appended per analyzed frame while actually recording --
|
||||
// see [onFrame]. Cleared at the start of each new session (see
|
||||
// [startNewSession]). Exposed as a read-only snapshot;
|
||||
// StepDetector.detect() can consume it once a recording finishes.
|
||||
private val poseFrameBuffer = mutableListOf<PoseFrame>()
|
||||
/** @brief Time-ordered pose samples buffered for the current/most recent recording session. */
|
||||
val poseFrames: List<PoseFrame> get() = poseFrameBuffer
|
||||
|
||||
// Steps detected so far in the current attempt (since the last reset,
|
||||
// whether that reset was a new session starting or the bowler
|
||||
// completing the hand-raise reset gesture mid-recording). The UI reads
|
||||
// events.size as the "Step N" counter. Stays populated after recording
|
||||
// stops so the last attempt's count remains visible.
|
||||
private val _stepEvents = MutableStateFlow<List<StepEvent>>(emptyList())
|
||||
/** @brief Steps detected so far in the current attempt, since the last reset. */
|
||||
val stepEvents: StateFlow<List<StepEvent>> = _stepEvents.asStateFlow()
|
||||
|
||||
// How far through the hold-to-reset gesture the bowler currently is --
|
||||
// see LiveStepDetector.Result.handRaiseProgress. Drives the on-screen
|
||||
// hold indicator so a reset is never a surprise; 0 whenever not recording.
|
||||
private val _handRaiseProgress = MutableStateFlow(0f)
|
||||
/** @brief Progress toward the hold-to-reset gesture, from 0 to 1. */
|
||||
val handRaiseProgress: StateFlow<Float> = _handRaiseProgress.asStateFlow()
|
||||
|
||||
/**
|
||||
* @brief Feeds one analyzed frame's landmarks/angles into buffering and
|
||||
* live step counting. Only meant to be called while a recording
|
||||
* is actually in progress -- see [CameraViewModel.onPoseFrameUpdated].
|
||||
* @param landmarks EMA-smoothed landmarks for this frame, keyed by ML Kit's `PoseLandmark` type constant.
|
||||
* @param angles Joint angles computed for this same frame.
|
||||
* @param timestampMs Wall-clock time this frame was analyzed, in milliseconds.
|
||||
*/
|
||||
fun onFrame(landmarks: Map<Int, SmoothedLandmark>, angles: PoseAngles, timestampMs: Long) {
|
||||
val smoothedAnkleHip = ankleHipSmoother.smooth(landmarks)
|
||||
val frame = buildPoseFrame(
|
||||
timestampMs = timestampMs,
|
||||
landmarks = landmarks,
|
||||
smoothedAnkleHip = smoothedAnkleHip,
|
||||
angles = angles
|
||||
)
|
||||
poseFrameBuffer.add(frame)
|
||||
|
||||
val result = liveStepDetector.update(frame)
|
||||
if (result.wasReset) {
|
||||
_stepEvents.value = emptyList()
|
||||
}
|
||||
if (result.newSteps.isNotEmpty()) {
|
||||
_stepEvents.value = _stepEvents.value + result.newSteps
|
||||
}
|
||||
_handRaiseProgress.value = result.handRaiseProgress
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clears all buffering/detection state and rebuilds the step
|
||||
* detector from [settings]; call when a new recording starts.
|
||||
* @param settings Tuning parameters to build this session's [LiveStepDetector] with.
|
||||
*/
|
||||
fun startNewSession(settings: DetectorSettings = DetectorSettings.DEFAULT) {
|
||||
poseFrameBuffer.clear()
|
||||
ankleHipSmoother.reset()
|
||||
liveStepDetector = LiveStepDetector(
|
||||
minSpacingMs = settings.minSpacingMs,
|
||||
minProminenceRatio = settings.minProminenceRatio,
|
||||
maxFrameJumpRatio = settings.maxFrameJumpRatio,
|
||||
handRaiseHoldMs = settings.handRaiseHoldMs,
|
||||
handRaiseMarginRatio = settings.handRaiseMarginRatio
|
||||
)
|
||||
_stepEvents.value = emptyList()
|
||||
_handRaiseProgress.value = 0f
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user