added two poses for now but still need to make the angle detection more accurate
This commit is contained in:
@@ -278,12 +278,23 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
|||||||
binding.textPoseFeedback.visibility = View.VISIBLE
|
binding.textPoseFeedback.visibility = View.VISIBLE
|
||||||
// Every other BowlingPhase falls back to the "waiting" message too --
|
// Every other BowlingPhase falls back to the "waiting" message too --
|
||||||
// see PosePhaseDetector's class doc, only STARTING_STANCE is detected today.
|
// 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.text = getString(R.string.pose_phase_starting_stance)
|
||||||
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.pose_feedback_ready))
|
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Starting_stance_ready))
|
||||||
} else {
|
}
|
||||||
|
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.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 requiredConsecutiveFrames: Int = 8,
|
||||||
private val requiredInvalidFramesToExit: Int = 5
|
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
|
// doc for why this decays by one on an invalid frame rather than
|
||||||
// resetting to 0 outright.
|
// resetting to 0 outright.
|
||||||
private var validFrameProgress = 0
|
private var validFrameProgress = 0
|
||||||
@@ -143,13 +146,31 @@ class PosePhaseDetector(
|
|||||||
leftElbowAngleDegrees = angles.leftElbow,
|
leftElbowAngleDegrees = angles.leftElbow,
|
||||||
rightElbowAngleDegrees = angles.rightElbow
|
rightElbowAngleDegrees = angles.rightElbow
|
||||||
)
|
)
|
||||||
val isValid = isStartingStanceValid(metrics)
|
|
||||||
if (isValid) {
|
// Identify the next phase we are looking for in the sequence.
|
||||||
// Capped at the threshold rather than left to grow unbounded, so
|
val targetPhase = when (currentPhase) {
|
||||||
// a long-held stance doesn't need an equally long invalid streak
|
null -> BowlingPhase.STARTING_STANCE
|
||||||
// to ever start climbing back down once it's fully confirmed.
|
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)
|
validFrameProgress = (validFrameProgress + 1).coerceAtMost(requiredConsecutiveFrames)
|
||||||
|
if (validFrameProgress >= requiredConsecutiveFrames) {
|
||||||
|
currentPhase = targetPhase
|
||||||
|
validFrameProgress = 0
|
||||||
consecutiveInvalidFrames = 0
|
consecutiveInvalidFrames = 0
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// A step back, not a hard reset to 0 -- torso/knee/elbow angles
|
// A step back, not a hard reset to 0 -- torso/knee/elbow angles
|
||||||
// all have to validate *simultaneously* every frame, and with
|
// 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,
|
// by one instead still requires a mostly-valid run to confirm,
|
||||||
// just without one blip erasing everything before it.
|
// just without one blip erasing everything before it.
|
||||||
validFrameProgress = (validFrameProgress - 1).coerceAtLeast(0)
|
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++
|
consecutiveInvalidFrames++
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPhase = when {
|
// 3. Handle resets: If we lose the current posture for too long, reset to null.
|
||||||
validFrameProgress >= requiredConsecutiveFrames -> BowlingPhase.STARTING_STANCE
|
if (consecutiveInvalidFrames >= requiredInvalidFramesToExit) {
|
||||||
// Already confirmed -- a short invalid streak alone (jitter,
|
currentPhase = null
|
||||||
// not necessarily a real change of posture) doesn't clear it.
|
validFrameProgress = 0
|
||||||
currentPhase == BowlingPhase.STARTING_STANCE && consecutiveInvalidFrames < requiredInvalidFramesToExit -> BowlingPhase.STARTING_STANCE
|
consecutiveInvalidFrames = 0
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result(currentPhase, metrics)
|
return Result(currentPhase, metrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +240,56 @@ class PosePhaseDetector(
|
|||||||
return true
|
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
|
* @brief Forward/backward torso lean from vertical, from the
|
||||||
* shoulder-midpoint-to-hip-midpoint vector.
|
* shoulder-midpoint-to-hip-midpoint vector.
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
<color name="skeleton_joint">#FF00E5FF</color>
|
<color name="skeleton_joint">#FF00E5FF</color>
|
||||||
<color name="skeleton_bone">#FF76FF03</color>
|
<color name="skeleton_bone">#FF76FF03</color>
|
||||||
<color name="overlay_scrim">#99000000</color>
|
<color name="overlay_scrim">#99000000</color>
|
||||||
<color name="pose_feedback_ready">#CC00C853</color> //green
|
<color name="Starting_stance_waiting">#CC00C853</color> //green
|
||||||
<color name="pose_feedback_waiting">#CCFFA000</color> //orange
|
<color name="Starting_stance_ready">#CCFFA000</color> //orange
|
||||||
|
<color name="Approach_ready">#CC2196F3</color> //blue
|
||||||
|
<color name="Pushaway_ready">#FFFFD600</color> //yellow/gold
|
||||||
</resources>
|
</resources>
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
<string name="error_recording_failed">Recording failed: %1$s</string>
|
<string name="error_recording_failed">Recording failed: %1$s</string>
|
||||||
<string name="error_pose_detector">Pose detector error: %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_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>
|
<string name="pose_metrics_format">Torso %1$s · Knee L%2$s R%3$s · Elbow L%4$s R%5$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user