Corrected 5 step approach phases & added placeholders for future logic

This commit is contained in:
Gabriel Low
2026-09-10 00:13:06 +08:00
parent 7cf217488a
commit d601efcf43
5 changed files with 84 additions and 29 deletions
@@ -373,6 +373,18 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
binding.textPoseFeedback.text = getString(R.string.pose_phase_pushaway)
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Pushaway_ready))
}
BowlingPhase.BACK_SWING -> {
binding.textPoseFeedback.text = getString(R.string.pose_phase_back_swing)
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Back_swing_ready))
}
BowlingPhase.POWER_STEP -> {
binding.textPoseFeedback.text = getString(R.string.pose_phase_power_step)
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Power_step_ready))
}
BowlingPhase.SLIDE_AND_RELEASE -> {
binding.textPoseFeedback.text = getString(R.string.pose_phase_slide_and_release)
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Slide_and_release_ready))
}
else -> {
binding.textPoseFeedback.text = getString(R.string.pose_phase_waiting)
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Starting_stance_waiting))
@@ -151,13 +151,15 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
// Update pose phase detector with this frame's landmarks and angles
val phaseResult = posePhaseDetector.update(landmarks, angles)
_posePhase.value = phaseResult.phase
val stepCount = stepEvents.value.size
val stepPhase = PosePhaseDetector.phaseForStep(stepCount)
_posePhase.value = phaseResult.phase ?: stepPhase
_poseMetrics.value = phaseResult.metrics
// Check if the current pose matches the Starting Stance
val isStartingStance = (phaseResult.phase == BowlingPhase.STARTING_STANCE)
|| posePhaseDetector.isStartingStanceValid(phaseResult.metrics)
|| (PoseStageAdvisor.feedback(stepEvents.value.size.takeIf { it > 0 }, angles)?.contains("Starting position") == true)
|| (PoseStageAdvisor.feedback(stepCount.takeIf { it > 0 }, angles)?.contains("Starting position") == true)
if (_recordingState.value is RecordingState.Recording) {
stepCountingSession.onFrame(
@@ -167,7 +169,7 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
isStartingPosition = isStartingStance,
)
_poseStageFeedback.value = PoseStageAdvisor.feedback(
stepNumber = stepEvents.value.size.takeIf { it > 0 },
stepNumber = stepCount.takeIf { it > 0 },
angles = angles,
)
}
@@ -20,10 +20,9 @@ enum class BowlingPhase {
STARTING_STANCE,
APPROACH,
PUSHAWAY,
@Suppress("unused")
SLIDE_RELEASE,
@Suppress("unused")
FOLLOW_THROUGH
BACK_SWING,
POWER_STEP,
SLIDE_AND_RELEASE
}
/**
@@ -158,7 +157,9 @@ class PosePhaseDetector(
null -> BowlingPhase.STARTING_STANCE
BowlingPhase.STARTING_STANCE -> BowlingPhase.APPROACH
BowlingPhase.APPROACH -> BowlingPhase.PUSHAWAY
// Placeholder for remaining sequence
BowlingPhase.PUSHAWAY -> BowlingPhase.BACK_SWING
BowlingPhase.BACK_SWING -> BowlingPhase.POWER_STEP
BowlingPhase.POWER_STEP -> BowlingPhase.SLIDE_AND_RELEASE
else -> currentPhase
}
}
@@ -168,6 +169,9 @@ class PosePhaseDetector(
BowlingPhase.STARTING_STANCE -> isStartingValid
BowlingPhase.APPROACH -> isApproachValid(metrics)
BowlingPhase.PUSHAWAY -> isPushawayValid(metrics)
BowlingPhase.BACK_SWING -> isBackSwingValid(metrics)
BowlingPhase.POWER_STEP -> isPowerStepValid(metrics)
BowlingPhase.SLIDE_AND_RELEASE -> isSlideAndReleaseValid(metrics)
else -> false
}
@@ -179,24 +183,18 @@ class PosePhaseDetector(
consecutiveInvalidFrames = 0
}
} else {
// A step back, not a hard reset to 0 -- torso/knee/elbow angles
// all have to validate *simultaneously* every frame, and with
// five independent noisy readings it's easy for one to blip out
// of range for a single frame even while the bowler holds
// genuinely still. Resetting to 0 on that alone meant progress
// could almost never reach requiredConsecutiveFrames; decaying
// by one instead still requires a mostly-valid run to confirm,
// just without one blip erasing everything before it.
validFrameProgress = (validFrameProgress - 1).coerceAtLeast(0)
}
// 2. Check if the user has broken their CURRENT confirmed phase.
// If they are neither in the target phase nor the current phase, count an invalid frame.
val isCurrentStillValid = when (currentPhase) {
BowlingPhase.STARTING_STANCE -> isStartingStanceValid(metrics)
BowlingPhase.APPROACH -> isApproachValid(metrics)
BowlingPhase.PUSHAWAY -> isPushawayValid(metrics)
else -> true // If null, we only care about progress toward STARTING_STANCE
BowlingPhase.BACK_SWING -> isBackSwingValid(metrics)
BowlingPhase.POWER_STEP -> isPowerStepValid(metrics)
BowlingPhase.SLIDE_AND_RELEASE -> isSlideAndReleaseValid(metrics)
else -> true
}
if (isCurrentStillValid || isTargetValid) {
@@ -264,9 +262,7 @@ class PosePhaseDetector(
if (kneeAngles.isEmpty() || kneeAngles.any { it !in 145f..180f }) return false
val elbowAngles = listOfNotNull(metrics.leftElbowAngleDegrees, metrics.rightElbowAngleDegrees)
if (elbowAngles.isEmpty() || elbowAngles.any { it !in 60f..130f }) return false
return true
return elbowAngles.isNotEmpty() && elbowAngles.all { it in 60f..130f }
}
/**
@@ -290,9 +286,53 @@ class PosePhaseDetector(
// For Pushaway, the bowling arm extends. We look for *at least one*
// elbow to be extended (130-180), since we don't know the bowler's handedness.
val elbowAngles = listOfNotNull(metrics.leftElbowAngleDegrees, metrics.rightElbowAngleDegrees)
if (elbowAngles.isEmpty() || elbowAngles.none { it in 130f..180f }) return false
return elbowAngles.isNotEmpty() && elbowAngles.any { it in 130f..180f }
}
return true
/** @brief Placeholder validation for Backswing phase (Step 3). */
@Suppress("UNUSED_PARAMETER")
private fun isBackSwingValid(metrics: Metrics): Boolean = true
/** @brief Placeholder validation for Power Step phase (Step 4). */
@Suppress("UNUSED_PARAMETER")
private fun isPowerStepValid(metrics: Metrics): Boolean = true
/** @brief Placeholder validation for Slide & Release phase (Step 5). */
@Suppress("UNUSED_PARAMETER")
private fun isSlideAndReleaseValid(metrics: Metrics): Boolean = true
companion object {
/**
* @brief Maps a 5-step approach step count (0..5) to its corresponding [BowlingPhase].
*
* step 0 -> STARTING_STANCE
* step 1 -> APPROACH
* step 2 -> PUSHAWAY
* step 3 -> BACK_SWING
* step 4 -> POWER_STEP
* step 5 -> SLIDE_AND_RELEASE
*/
fun phaseForStep(stepCount: Int): BowlingPhase = when {
stepCount <= 0 -> BowlingPhase.STARTING_STANCE
stepCount == 1 -> BowlingPhase.APPROACH
stepCount == 2 -> BowlingPhase.PUSHAWAY
stepCount == 3 -> BowlingPhase.BACK_SWING
stepCount == 4 -> BowlingPhase.POWER_STEP
else -> BowlingPhase.SLIDE_AND_RELEASE
}
/**
* @brief Maps a [BowlingPhase] to its corresponding 5-step approach step count.
*/
@Suppress("unused")
fun stepForPhase(phase: BowlingPhase): Int = when (phase) {
BowlingPhase.STARTING_STANCE -> 0
BowlingPhase.APPROACH -> 1
BowlingPhase.PUSHAWAY -> 2
BowlingPhase.BACK_SWING -> 3
BowlingPhase.POWER_STEP -> 4
BowlingPhase.SLIDE_AND_RELEASE -> 5
}
}
/**
+3
View File
@@ -18,5 +18,8 @@
<color name="Starting_stance_ready">#CCFFA000</color> <!-- orange -->
<color name="Approach_ready">#CC2196F3</color> <!-- blue -->
<color name="Pushaway_ready">#FFFFD600</color> <!-- yellow/gold -->
<color name="Back_swing_ready">#CC9C27B0</color> <!-- purple -->
<color name="Power_step_ready">#CCFF9800</color> <!-- orange/amber -->
<color name="Slide_and_release_ready">#CCE91E63</color> <!-- pink/red -->
<color name="final_position_highlight">#FFFFD600</color>
</resources>
+4 -6
View File
@@ -49,13 +49,11 @@
<string name="editor_saved_toast">Settings saved</string>
<string name="editor_invalid_value_toast">Enter a valid number for every field</string>
<string name="pose_phase_starting_stance">Starting Stance</string>
<string name="pose_phase_waiting">Waiting for stance…</string>
<string name="first_step">Step 1</string>
<string name="second_step">Step 2</string>
<string name="third_step">Step 3</string>
<string name="fourth_step">Step 4</string>
<string name="end_position">End Position</string>
<string name="pose_phase_approach">Approach</string>
<string name="pose_phase_pushaway">Pushaway</string>
<string name="pose_phase_back_swing">Backswing</string>
<string name="pose_phase_power_step">Power Step</string>
<string name="pose_phase_slide_and_release">Slide &amp; Release</string>
<string name="pose_phase_waiting">Waiting for stance…</string>
<string name="pose_metrics_format">Torso: %1$s · Knee L: %2$s R: %3$s\nElbow L: %4$s R: %5$s</string>
</resources>