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 0cc7e17..61b7580 100644 --- a/app/src/main/java/com/example/jnicpp/bowling/BowlingCameraActivity.kt +++ b/app/src/main/java/com/example/jnicpp/bowling/BowlingCameraActivity.kt @@ -278,12 +278,23 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback { binding.textPoseFeedback.visibility = View.VISIBLE // Every other BowlingPhase falls back to the "waiting" message too -- // see PosePhaseDetector's class doc, only STARTING_STANCE is detected today. - if (phase == BowlingPhase.STARTING_STANCE) { - binding.textPoseFeedback.text = getString(R.string.pose_phase_starting_stance) - binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.pose_feedback_ready)) - } else { - binding.textPoseFeedback.text = getString(R.string.pose_phase_waiting) - binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.pose_feedback_waiting)) + when (phase) { + BowlingPhase.STARTING_STANCE -> { + binding.textPoseFeedback.text = getString(R.string.pose_phase_starting_stance) + binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Starting_stance_ready)) + } + BowlingPhase.APPROACH -> { + binding.textPoseFeedback.text = getString(R.string.pose_phase_approach) + binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Approach_ready)) + } + BowlingPhase.PUSHAWAY -> { + binding.textPoseFeedback.text = getString(R.string.pose_phase_pushaway) + binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Pushaway_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/PosePhaseDetector.kt b/app/src/main/java/com/example/jnicpp/bowling/PosePhaseDetector.kt index 0167067..b3ab0be 100644 --- a/app/src/main/java/com/example/jnicpp/bowling/PosePhaseDetector.kt +++ b/app/src/main/java/com/example/jnicpp/bowling/PosePhaseDetector.kt @@ -86,7 +86,10 @@ class PosePhaseDetector( private val requiredConsecutiveFrames: Int = 8, private val requiredInvalidFramesToExit: Int = 5 ) { - // Decaying progress toward requiredConsecutiveFrames -- see update()'s + // Shared parameters for all phases (consecutive frames, etc) could be + // split out, but for now they're reused from the constructor. + + // Decaying progress toward confirming a phase -- see update()'s // doc for why this decays by one on an invalid frame rather than // resetting to 0 outright. private var validFrameProgress = 0 @@ -143,13 +146,31 @@ class PosePhaseDetector( leftElbowAngleDegrees = angles.leftElbow, rightElbowAngleDegrees = angles.rightElbow ) - val isValid = isStartingStanceValid(metrics) - if (isValid) { - // Capped at the threshold rather than left to grow unbounded, so - // a long-held stance doesn't need an equally long invalid streak - // to ever start climbing back down once it's fully confirmed. + + // Identify the next phase we are looking for in the sequence. + val targetPhase = when (currentPhase) { + null -> BowlingPhase.STARTING_STANCE + BowlingPhase.STARTING_STANCE -> BowlingPhase.APPROACH + BowlingPhase.APPROACH -> BowlingPhase.PUSHAWAY + // Placeholder for remaining sequence + else -> currentPhase + } + + // 1. Check if the user is in the NEXT phase. + val isTargetValid = when (targetPhase) { + BowlingPhase.STARTING_STANCE -> isStartingStanceValid(metrics) + BowlingPhase.APPROACH -> isApproachValid(metrics) + BowlingPhase.PUSHAWAY -> isPushawayValid(metrics) + else -> false + } + + if (isTargetValid) { validFrameProgress = (validFrameProgress + 1).coerceAtMost(requiredConsecutiveFrames) - consecutiveInvalidFrames = 0 + if (validFrameProgress >= requiredConsecutiveFrames) { + currentPhase = targetPhase + validFrameProgress = 0 + 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 @@ -160,16 +181,30 @@ class PosePhaseDetector( // 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 + } + + if (isCurrentStillValid || isTargetValid) { + consecutiveInvalidFrames = 0 + } else { consecutiveInvalidFrames++ } - currentPhase = when { - validFrameProgress >= requiredConsecutiveFrames -> BowlingPhase.STARTING_STANCE - // Already confirmed -- a short invalid streak alone (jitter, - // not necessarily a real change of posture) doesn't clear it. - currentPhase == BowlingPhase.STARTING_STANCE && consecutiveInvalidFrames < requiredInvalidFramesToExit -> BowlingPhase.STARTING_STANCE - else -> null + // 3. Handle resets: If we lose the current posture for too long, reset to null. + if (consecutiveInvalidFrames >= requiredInvalidFramesToExit) { + currentPhase = null + validFrameProgress = 0 + consecutiveInvalidFrames = 0 } + return Result(currentPhase, metrics) } @@ -205,6 +240,56 @@ class PosePhaseDetector( return true } + /** + * @brief Checks whether this single frame's [Metrics] match the approach phase. + * + * Approach is characterized by: + * - Torso Tilt: 5-20 degrees + * - Knee Angle: 145-180 degrees + * - Elbow Angle: 60-130 degrees + * + * @param metrics This frame's raw angle readings. + * @return true if torso tilt, knee angles, and elbow angles fall within range. + */ + private fun isApproachValid(metrics: Metrics): Boolean { + val torsoTilt = metrics.torsoTiltDegrees ?: return false + if (torsoTilt !in 5f..20f) return false + + val kneeAngles = listOfNotNull(metrics.leftKneeAngleDegrees, metrics.rightKneeAngleDegrees) + 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 + } + + /** + * @brief Checks whether this single frame's [Metrics] match the pushaway phase. + * + * Pushaway is characterized by: + * - Torso Tilt: 5-25 degrees (more lean than stance) + * - Knee Angle: 145-180 degrees (legs still mostly straight) + * - Elbow Angle: 130-180 degrees (bowling arm extending forward) + * + * @param metrics This frame's raw angle readings. + * @return true if torso tilt, knee angles, and at least one elbow angle fall within range. + */ + private fun isPushawayValid(metrics: Metrics): Boolean { + val torsoTilt = metrics.torsoTiltDegrees ?: return false + if (torsoTilt !in 5f..25f) return false + + val kneeAngles = listOfNotNull(metrics.leftKneeAngleDegrees, metrics.rightKneeAngleDegrees) + if (kneeAngles.isEmpty() || kneeAngles.any { it !in 145f..180f }) return false + + // 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 true + } + /** * @brief Forward/backward torso lean from vertical, from the * shoulder-midpoint-to-hip-midpoint vector. diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1da7f1c..c8112d2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -13,6 +13,8 @@ #FF00E5FF #FF76FF03 #99000000 - #CC00C853 //green - #CCFFA000 //orange + #CC00C853 //green + #CCFFA000 //orange + #CC2196F3 //blue + #FFFFD600 //yellow/gold \ 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 9c2adea..74e576d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,6 +19,8 @@ Recording failed: %1$s Pose detector error: %1$s Starting pose - Get into starting pose + Approach + Pushaway + Get into pose Torso %1$s · Knee L%2$s R%3$s · Elbow L%4$s R%5$s \ No newline at end of file