added two poses for now but still need to make the angle detection more accurate

This commit is contained in:
2026-09-07 23:02:54 +08:00
parent c2990cc51a
commit 5d0abb1f60
4 changed files with 122 additions and 22 deletions
@@ -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) {
when (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.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.pose_feedback_waiting))
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Starting_stance_waiting))
}
}
}
@@ -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)
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.
+4 -2
View File
@@ -13,6 +13,8 @@
<color name="skeleton_joint">#FF00E5FF</color>
<color name="skeleton_bone">#FF76FF03</color>
<color name="overlay_scrim">#99000000</color>
<color name="pose_feedback_ready">#CC00C853</color> //green
<color name="pose_feedback_waiting">#CCFFA000</color> //orange
<color name="Starting_stance_waiting">#CC00C853</color> //green
<color name="Starting_stance_ready">#CCFFA000</color> //orange
<color name="Approach_ready">#CC2196F3</color> //blue
<color name="Pushaway_ready">#FFFFD600</color> //yellow/gold
</resources>
+3 -1
View File
@@ -19,6 +19,8 @@
<string name="error_recording_failed">Recording failed: %1$s</string>
<string name="error_pose_detector">Pose detector error: %1$s</string>
<string name="pose_phase_starting_stance">Starting pose</string>
<string name="pose_phase_waiting">Get into starting pose</string>
<string name="pose_phase_approach">Approach</string>
<string name="pose_phase_pushaway">Pushaway</string>
<string name="pose_phase_waiting">Get into pose</string>
<string name="pose_metrics_format">Torso %1$s · Knee L%2$s R%3$s · Elbow L%4$s R%5$s</string>
</resources>