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