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