Corrected 5 step approach phases & added placeholders for future logic
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
@@ -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 & 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>
|
||||
Reference in New Issue
Block a user