Added tuning feature and hand palm action to reset the timer
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
package com.example.jnicpp.bowling
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -25,8 +26,12 @@ import kotlinx.coroutines.launch
|
||||
* [BowlingCameraActivity] and [CameraXController] report events into it,
|
||||
* and the UI observes it back out. That keeps this class trivially
|
||||
* unit-testable (no Android camera framework involved).
|
||||
*
|
||||
* Extends [AndroidViewModel] rather than a plain ViewModel solely to reach
|
||||
* an Application [android.content.Context] for [DetectorSettings.load] --
|
||||
* see [onRecordingStarting].
|
||||
*/
|
||||
class CameraViewModel : ViewModel() {
|
||||
class CameraViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
/** @brief The camera screen's overall recording state. */
|
||||
sealed interface RecordingState {
|
||||
@@ -59,38 +64,18 @@ class CameraViewModel : ViewModel() {
|
||||
/** @brief Joint angles computed for the most recent analyzed frame, or null if none available. */
|
||||
val poseAngles: StateFlow<PoseAngles?> = _poseAngles.asStateFlow()
|
||||
|
||||
// Time-ordered pose samples for the current/most recent recording
|
||||
// session, one appended per analyzed frame while actually recording
|
||||
// (see onPoseFrameUpdated) -- a live preview with pose overlay on but
|
||||
// not recording doesn't fill this. Cleared at the start of each new
|
||||
// recording (see onRecordingStarting). Exposed as a read-only snapshot;
|
||||
// StepDetector.detect() consumes it once a recording finishes (see
|
||||
// onRecordingStopped).
|
||||
private val poseFrameBuffer = mutableListOf<PoseFrame>()
|
||||
// Pose-frame buffering and live step counting for the current/most
|
||||
// recent recording session -- see StepCountingSession's class doc for
|
||||
// why this lives in its own class rather than inline here. Frames only
|
||||
// flow into it while actually recording (see onPoseFrameUpdated) -- a
|
||||
// live preview with pose overlay on but not recording doesn't feed it.
|
||||
private val stepCountingSession = StepCountingSession()
|
||||
/** @brief Time-ordered pose samples buffered for the current/most recent recording session. */
|
||||
val poseFrames: List<PoseFrame> get() = poseFrameBuffer
|
||||
|
||||
// Extra SMA smoothing for ankle/hip landmarks specifically, on top of
|
||||
// PoseLandmarkSmoother's per-frame EMA -- see AnkleHipMovingAverageFilter.
|
||||
// Reset (not replaced) alongside the buffer so a new session's window
|
||||
// doesn't lerp in from the previous one's last few frames.
|
||||
private val ankleHipSmoother = AnkleHipMovingAverageFilter()
|
||||
|
||||
// Live, incremental step counting for the current recording -- see
|
||||
// LiveStepDetector. Resets itself mid-recording when the bowler holds
|
||||
// a stationary "ready" stance again, so one recording can capture
|
||||
// several practice approaches back to back.
|
||||
private val liveStepDetector = LiveStepDetector()
|
||||
|
||||
// Steps detected so far in the current attempt (since the last reset,
|
||||
// whether that reset was a new recording starting or the bowler
|
||||
// returning to a stationary stance mid-recording -- see
|
||||
// onPoseFrameUpdated and LiveStepDetector). 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())
|
||||
val poseFrames: List<PoseFrame> get() = stepCountingSession.poseFrames
|
||||
/** @brief Steps detected so far in the current attempt, since the last reset. */
|
||||
val stepEvents: StateFlow<List<StepEvent>> = _stepEvents.asStateFlow()
|
||||
val stepEvents: StateFlow<List<StepEvent>> get() = stepCountingSession.stepEvents
|
||||
/** @brief Progress toward the hold-to-reset gesture, from 0 to 1. */
|
||||
val handRaiseProgress: StateFlow<Float> get() = stepCountingSession.handRaiseProgress
|
||||
|
||||
private val _permissionsGranted = MutableStateFlow(false)
|
||||
/** @brief Whether all required camera/microphone/storage permissions are currently granted. */
|
||||
@@ -136,32 +121,22 @@ class CameraViewModel : ViewModel() {
|
||||
fun onPoseFrameUpdated(landmarks: Map<Int, SmoothedLandmark>, angles: PoseAngles) {
|
||||
_poseAngles.value = angles
|
||||
if (_recordingState.value is RecordingState.Recording) {
|
||||
val smoothedAnkleHip = ankleHipSmoother.smooth(landmarks)
|
||||
val frame = buildPoseFrame(
|
||||
timestampMs = System.currentTimeMillis(),
|
||||
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
|
||||
}
|
||||
stepCountingSession.onFrame(landmarks, angles, System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Marks a recording as being requested and resets all per-session buffering/detection state. */
|
||||
/**
|
||||
* @brief Marks a recording as being requested and resets all
|
||||
* per-session buffering/detection state.
|
||||
*
|
||||
* Reloads [DetectorSettings] fresh here (rather than once at
|
||||
* construction) so a tuning change made in [ParameterEditorActivity]
|
||||
* takes effect on the very next recording, without needing to
|
||||
* restart this screen.
|
||||
*/
|
||||
fun onRecordingStarting() {
|
||||
_recordingState.value = RecordingState.Starting
|
||||
poseFrameBuffer.clear()
|
||||
ankleHipSmoother.reset()
|
||||
liveStepDetector.reset()
|
||||
_stepEvents.value = emptyList()
|
||||
stepCountingSession.startNewSession(DetectorSettings.load(getApplication()))
|
||||
}
|
||||
|
||||
/** @brief Marks a recording as actively writing and starts the elapsed-time timer. */
|
||||
|
||||
Reference in New Issue
Block a user