edited the values

This commit is contained in:
2026-09-05 23:38:23 +08:00
parent cb941006c5
commit b28cc4d508
2 changed files with 67 additions and 20 deletions
@@ -36,12 +36,16 @@ enum class BowlingPhase {
* completely independent state, and neither calls into the other.
*
* [update] is fed one frame's landmarks/angles at a time, in recording (or
* live-preview) order. A posture only "counts" once it holds for
* [requiredConsecutiveFrames] frames in a row -- this filters out a
* momentary, correct-looking pose caught mid-transition (e.g. a fleeting
* instant during the approach where the knee angle briefly passes through
* the starting-stance range) -- and keeps reporting that phase for as long
* as the posture keeps validating, reverting to null the instant it doesn't.
* live-preview) order. A posture only "counts" once a decaying progress
* counter (see [validFrameProgress]) climbs to [requiredConsecutiveFrames]
* -- this filters out a momentary, correct-looking pose caught mid-transition
* (e.g. a fleeting instant during the approach where the knee angle briefly
* passes through the starting-stance range) while still tolerating the
* occasional single-frame jitter a held stance sees in practice (see
* [update]'s doc for why this decays rather than resets outright). Once
* confirmed, it keeps reporting that phase through any invalid streak
* shorter than [requiredInvalidFramesToExit], reverting to null only once
* that streak runs longer.
*
* @param torsoTiltMinDegrees Minimum forward torso lean from vertical
* (shoulder-midpoint-to-hip-midpoint vector vs. vertical) still
@@ -56,19 +60,37 @@ enum class BowlingPhase {
* considered "holding the ball in front", in degrees.
* @param elbowAngleMaxDegrees Maximum shoulder-elbow-wrist angle still
* considered "holding the ball in front", in degrees.
* @param requiredConsecutiveFrames How many consecutive frames the posture
* must validate before [update] starts reporting [BowlingPhase.STARTING_STANCE].
* @param requiredConsecutiveFrames Target value for [validFrameProgress]
* (which increments by 1 on a valid frame, decrements by 1 -- not
* reset to 0 -- on an invalid one) before [update] starts reporting
* [BowlingPhase.STARTING_STANCE]. Despite the name, this is no
* longer a strict run of consecutive valid frames; see [update]'s doc.
* @param requiredInvalidFramesToExit How many consecutive frames the
* posture must fail to validate before [update] stops reporting
* [BowlingPhase.STARTING_STANCE] once it's already been confirmed.
* Deliberately separate from [requiredConsecutiveFrames] -- angle
* readings jitter a couple of degrees frame-to-frame even when the
* bowler is genuinely holding still, so reverting to null on the very
* first out-of-range frame (as opposed to requiring several in a
* row, same as confirming the stance in the first place) makes the
* label flicker between "confirmed" and "waiting" on that jitter
* alone rather than on an actual change of posture.
*/
class PosePhaseDetector(
private val torsoTiltMinDegrees: Float = 10f,
private val torsoTiltMinDegrees: Float = 2f,
private val torsoTiltMaxDegrees: Float = 15f,
private val kneeAngleMinDegrees: Float = 160f,
private val kneeAngleMaxDegrees: Float = 175f,
private val kneeAngleMinDegrees: Float = 150f,
private val kneeAngleMaxDegrees: Float = 180f,
private val elbowAngleMinDegrees: Float = 70f,
private val elbowAngleMaxDegrees: Float = 110f,
private val requiredConsecutiveFrames: Int = 8
private val requiredConsecutiveFrames: Int = 8,
private val requiredInvalidFramesToExit: Int = 5
) {
private var consecutiveValidFrames = 0
// Decaying progress toward requiredConsecutiveFrames -- see update()'s
// doc for why this decays by one on an invalid frame rather than
// resetting to 0 outright.
private var validFrameProgress = 0
private var consecutiveInvalidFrames = 0
private var currentPhase: BowlingPhase? = null
/**
@@ -109,9 +131,9 @@ class PosePhaseDetector(
* [PoseAngleCalculator]) -- elbow angles are reused from here
* rather than recomputed, so this detector doesn't duplicate that math.
* @return This frame's [Metrics] alongside [BowlingPhase.STARTING_STANCE]
* once the posture has validated for [requiredConsecutiveFrames]
* frames in a row and keeps validating on this frame too,
* otherwise alongside a null phase.
* once [validFrameProgress] has climbed to [requiredConsecutiveFrames],
* continuing to report it through brief invalid streaks shorter
* than [requiredInvalidFramesToExit], otherwise alongside a null phase.
*/
fun update(landmarks: Map<Int, SmoothedLandmark>, angles: PoseAngles): Result {
val metrics = Metrics(
@@ -122,14 +144,39 @@ class PosePhaseDetector(
rightElbowAngleDegrees = angles.rightElbow
)
val isValid = isStartingStanceValid(metrics)
consecutiveValidFrames = if (isValid) consecutiveValidFrames + 1 else 0
currentPhase = if (consecutiveValidFrames >= requiredConsecutiveFrames) BowlingPhase.STARTING_STANCE else null
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.
validFrameProgress = (validFrameProgress + 1).coerceAtMost(requiredConsecutiveFrames)
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)
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
}
return Result(currentPhase, metrics)
}
/** @brief Clears all detection state. Call at the start of a new session/attempt. */
fun reset() {
consecutiveValidFrames = 0
validFrameProgress = 0
consecutiveInvalidFrames = 0
currentPhase = null
}
+1 -1
View File
@@ -18,7 +18,7 @@
<string name="error_camera_unavailable">Camera unavailable: %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="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_metrics_format">Torso %1$s · Knee L%2$s R%3$s · Elbow L%4$s R%5$s</string>
</resources>