From d601efcf43dd3b41cddd1d0cc297a3693f8acecd Mon Sep 17 00:00:00 2001
From: Gabriel Low <73009315+gabriellow1111@users.noreply.github.com>
Date: Thu, 10 Sep 2026 00:13:06 +0800
Subject: [PATCH] Corrected 5 step approach phases & added placeholders for
future logic
---
.../jnicpp/bowling/BowlingCameraActivity.kt | 12 +++
.../example/jnicpp/bowling/CameraViewModel.kt | 8 +-
.../jnicpp/bowling/PosePhaseDetector.kt | 80 ++++++++++++++-----
app/src/main/res/values/colors.xml | 3 +
app/src/main/res/values/strings.xml | 10 +--
5 files changed, 84 insertions(+), 29 deletions(-)
diff --git a/app/src/main/java/com/example/jnicpp/bowling/BowlingCameraActivity.kt b/app/src/main/java/com/example/jnicpp/bowling/BowlingCameraActivity.kt
index 128b1a8..775f73e 100644
--- a/app/src/main/java/com/example/jnicpp/bowling/BowlingCameraActivity.kt
+++ b/app/src/main/java/com/example/jnicpp/bowling/BowlingCameraActivity.kt
@@ -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))
diff --git a/app/src/main/java/com/example/jnicpp/bowling/CameraViewModel.kt b/app/src/main/java/com/example/jnicpp/bowling/CameraViewModel.kt
index e177d8a..cb4c334 100644
--- a/app/src/main/java/com/example/jnicpp/bowling/CameraViewModel.kt
+++ b/app/src/main/java/com/example/jnicpp/bowling/CameraViewModel.kt
@@ -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,
)
}
diff --git a/app/src/main/java/com/example/jnicpp/bowling/PosePhaseDetector.kt b/app/src/main/java/com/example/jnicpp/bowling/PosePhaseDetector.kt
index 96c4f69..638e86c 100644
--- a/app/src/main/java/com/example/jnicpp/bowling/PosePhaseDetector.kt
+++ b/app/src/main/java/com/example/jnicpp/bowling/PosePhaseDetector.kt
@@ -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
+ }
}
/**
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index d9e5a8a..08f947d 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -18,5 +18,8 @@
#CCFFA000
#CC2196F3
#FFFFD600
+ #CC9C27B0
+ #CCFF9800
+ #CCE91E63
#FFFFD600
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 97c065d..6455dbc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -49,13 +49,11 @@
Settings saved
Enter a valid number for every field
Starting Stance
- Waiting for stance…
- Step 1
- Step 2
- Step 3
- Step 4
- End Position
Approach
Pushaway
+ Backswing
+ Power Step
+ Slide & Release
+ Waiting for stance…
Torso: %1$s · Knee L: %2$s R: %3$s\nElbow L: %4$s R: %5$s
\ No newline at end of file