From 3c682c1b30bb2060386bae2d5c2710f635c9e498 Mon Sep 17 00:00:00 2001 From: jingwen121 Date: Mon, 14 Sep 2026 21:38:10 +0800 Subject: [PATCH] adjusted order --- .../jnicpp/bowling/PosePhaseDetector.kt | 82 +++++++++---------- 1 file changed, 39 insertions(+), 43 deletions(-) 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 251d318..b6b98f6 100644 --- a/app/src/main/java/com/example/jnicpp/bowling/PosePhaseDetector.kt +++ b/app/src/main/java/com/example/jnicpp/bowling/PosePhaseDetector.kt @@ -64,13 +64,9 @@ class PosePhaseDetector { private var currentPhase: BowlingPhase? = null /** - * @brief The raw angle readings [update] computed for one frame, for - * callers that want to show the bowler the actual numbers (e.g. - * "Torso 12°") rather than just a pass/fail phase. + * The actual angles we calculate from the camera for one frame, so can show stuff like "Torso 12°". * - * Each field is null exactly when the landmarks it depends on weren't - * confidently detected that frame (see [reliable]) -- same meaning as a - * null field elsewhere in this codebase (e.g. [PoseAngles]). + * Each field is null if the camera isn't sure where that body part is. * * @param torsoTiltDegrees See [torsoTiltDegrees]. * @param leftKneeAngleDegrees Left hip-knee-ankle angle, or null. @@ -87,23 +83,18 @@ class PosePhaseDetector { ) /** - * @brief One [update] call's outcome: the classified phase plus the raw angles it was based on. + * get back after checking a frame: the phase and the angles. * @param phase See [update]'s return doc. * @param metrics This frame's raw angle readings, for display regardless of whether [phase] validated. */ data class Result(val phase: BowlingPhase?, val metrics: Metrics) /** - * @brief Feeds one frame's landmarks/angles into the detector. + * Check one frame from the camera to see what the bowler is doing. * - * @param landmarks Smoothed landmarks for this frame, keyed by ML Kit's `PoseLandmark` type constant. - * @param angles Joint angles computed for this same frame (see - * [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 [validFrameProgress] has climbed to [REQUIRED_CONSECUTIVE_FRAMES], - * continuing to report it through brief invalid streaks shorter - * than [REQUIRED_INVALID_FRAMES_TO_EXIT], otherwise alongside a null phase. + * @param landmarks The x,y points of all body parts (already smoothed). + * @param angles The joint angles we already calculated for this frame. + * @return The updated phase and the raw metrics for this frame. */ fun update(landmarks: Map, angles: PoseAngles): Result { val metrics = Metrics( @@ -141,9 +132,6 @@ class PosePhaseDetector { // Look through all future phases. for (i in (currentIdx + 2) until allPhases.size) { val p = allPhases[i] - // Never skip straight to Follow Through from nothing or very early. - // must at least reach Pushaway before Follow Through is a valid skip-to target. - if (p == BowlingPhase.FOLLOW_THROUGH && currentIdx < BowlingPhase.PUSHAWAY.ordinal) continue val isThisValid = when (p) { BowlingPhase.APPROACH -> isApproachValid(metrics) @@ -218,16 +206,18 @@ class PosePhaseDetector { * actually see. * * @param metrics This frame's raw angle readings. - * @return true if torso tilt, both visible knee angles, and both visible elbow angles all fall within range. + * @return true if torso lean, knees, and elbows are all in the right spot. */ fun isStartingStanceValid(metrics: Metrics): Boolean { + // Check if the body is mostly upright (tilt between 1 and 20 degrees). val torsoTilt = metrics.torsoTiltDegrees ?: return false - // Table: 1-15, widened to 20 to make it easier to start if (torsoTilt !in 1f..20f) return false + // Check if both knees are fairly straight (angle above 150 degrees). val kneeAngles = listOfNotNull(metrics.leftKneeAngleDegrees, metrics.rightKneeAngleDegrees) if (kneeAngles.isEmpty() || kneeAngles.any { it !in 150f..180f }) return false + // Check if elbows are folded (angle between 70 and 125 degrees) while holding the ball. val elbowAngles = listOfNotNull(metrics.leftElbowAngleDegrees, metrics.rightElbowAngleDegrees) if (elbowAngles.isEmpty() || elbowAngles.any { it !in 70f..125f }) return false @@ -246,14 +236,15 @@ class PosePhaseDetector { * @return true if torso tilt, knee angles, and elbow angles fall within range. */ fun isApproachValid(metrics: Metrics): Boolean { + // Check if they have a slight forward lean (5 to 30 degrees). val torsoTilt = metrics.torsoTiltDegrees ?: return false - // Widened to 30 if (torsoTilt !in 5f..30f) return false + // Check if legs are still mostly straight during the walk. val kneeAngles = listOfNotNull(metrics.leftKneeAngleDegrees, metrics.rightKneeAngleDegrees) if (kneeAngles.isEmpty() || kneeAngles.any { it !in 145f..180f }) return false - // Widened to 140 to provide overlap with Pushaway (130-180) + // Check if arm is still close to the body (angle between 60 and 140 degrees). val elbowAngles = listOfNotNull(metrics.leftElbowAngleDegrees, metrics.rightElbowAngleDegrees) if (elbowAngles.isEmpty() || elbowAngles.any { it !in 60f..140f }) return false @@ -264,7 +255,7 @@ class PosePhaseDetector { * @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) + * - Torso Tilt: 5-25 degrees (leaning more) * - Knee Angle: 145-180 degrees (legs still mostly straight) * - Elbow Angle: 130-180 degrees (bowling arm extending forward) * @@ -272,14 +263,14 @@ class PosePhaseDetector { * @return true if torso tilt, knee angles, and at least one elbow angle fall within range. */ fun isPushawayValid(metrics: Metrics): Boolean { + // Check if torso lean is between 5 and 25 degrees. 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. + // Check if at least one arm is extending forward (angle between 130 and 180 degrees). val elbowAngles = listOfNotNull(metrics.leftElbowAngleDegrees, metrics.rightElbowAngleDegrees) if (elbowAngles.isEmpty() || elbowAngles.none { it in 130f..180f }) return false @@ -293,13 +284,14 @@ class PosePhaseDetector { * - Torso Tilt: 15-45 degrees * - Knee Angle: 90-150 degrees (sliding knee) * - Elbow Angle: 150-180 degrees - * - Hand Position: Below shoulder (to distinguish from backswing) + * - Hand Position: Below shoulder (so we know it's not the backswing) * * @param metrics This frame's raw angle readings. * @param landmarks Current frame's raw landmarks. - * @return true if torso tilt, at least one knee angle, and at least one elbow angle fall within range. + * @return true if they are actually releasing the ball. */ fun isSlideReleaseValid(metrics: Metrics, landmarks: Map): Boolean { + // Check for a deeper forward lean (15 to 45 degrees). val torsoTilt = metrics.torsoTiltDegrees ?: return false if (torsoTilt !in 15f..45f) return false @@ -330,9 +322,9 @@ class PosePhaseDetector { * - Elbow Angle: 140-180 degrees * - Hand Position: Above shoulder AND in front of the body * - * @param metrics This frame's raw angle readings. - * @param landmarks Current frame's raw landmarks. - * @return true if torso tilt, at least one knee angle, and at least one elbow angle fall within range. + * @param metrics The angles for this frame. + * @param landmarks The points of the body (to check hand position). + * @return True if arm is extended high in front of the body. */ fun isFollowThroughValid(metrics: Metrics, landmarks: Map): Boolean { val torsoTilt = metrics.torsoTiltDegrees ?: return false @@ -377,10 +369,10 @@ class PosePhaseDetector { * shoulder-midpoint-to-hip-midpoint vector. * * @param landmarks Smoothed landmarks for this frame. - * @return The tilt in degrees (always >= 0, direction-agnostic), or - * null if neither shoulder or neither hip is reliably detected. + * @return The tilt in degrees, or null if we can't see the shoulders or hips. */ private fun torsoTiltDegrees(landmarks: Map): Float? { + // Find the middle of the shoulders and the middle of the hips. val shoulderMid = midpoint(landmarks, PoseLandmark.LEFT_SHOULDER, PoseLandmark.RIGHT_SHOULDER) ?: return null val hipMid = midpoint(landmarks, PoseLandmark.LEFT_HIP, PoseLandmark.RIGHT_HIP) ?: return null val dx = shoulderMid.first - hipMid.first @@ -392,33 +384,36 @@ class PosePhaseDetector { } /** - * @brief Hip-knee-ankle angle for one leg, gated by confidence. - * @param landmarks Smoothed landmarks for this frame. - * @param hipType Landmark type constant for that leg's hip. - * @param kneeType Landmark type constant for that leg's knee. - * @param ankleType Landmark type constant for that leg's ankle. - * @return The angle in degrees, or null if any of the three landmarks isn't reliably detected. + * // Calculate the angle of a knee. + * @param landmarks The smoothed points of the body. + * @param hipType Which hip to look at. + * @param kneeType Which knee to look at. + * @param ankleType Which ankle to look at. + * @return The angle in degrees, or null if the camera missed a part. */ private fun kneeAngle(landmarks: Map, hipType: Int, kneeType: Int, ankleType: Int): Float? { + // Try to get the hip, knee, and ankle from the camera data. val hip = reliable(landmarks, hipType) ?: return null val knee = reliable(landmarks, kneeType) ?: return null val ankle = reliable(landmarks, ankleType) ?: return null + // Calculate the angle between those three points. return PoseAngleCalculator.calculateAngle(hip, knee, ankle) } /** - * @brief Midpoint of two landmarks, gated by confidence, falling back to - * whichever single one is reliable if only one is. + * @brief Find the middle point between two body parts. * @param landmarks Smoothed landmarks for this frame. * @param firstType Landmark type constant for the first side. * @param secondType Landmark type constant for the second side. - * @return The midpoint as (x, y), or null if neither landmark is reliable. + * @return The middle (x, y), or null if the camera missed both. */ private fun midpoint(landmarks: Map, firstType: Int, secondType: Int): Pair? { val first = reliable(landmarks, firstType) val second = reliable(landmarks, secondType) return when { + // If we have both parts, find the average. first != null && second != null -> (first.x + second.x) / 2f to (first.y + second.y) / 2f + // If we only have one, just use that one. first != null -> first.x to first.y second != null -> second.x to second.y else -> null @@ -429,10 +424,11 @@ class PosePhaseDetector { * @brief Looks up one landmark, gated by [PoseSkeletonRenderer.MIN_LIKELIHOOD]. * @param landmarks Smoothed landmarks for this frame. * @param type Landmark type constant to look up. - * @return The landmark, or null if missing or below the confidence bar. + * @return The landmark, or null if it's missing or blurry. */ private fun reliable(landmarks: Map, type: Int): SmoothedLandmark? { val landmark = landmarks[type] ?: return null + // Only return if the camera is sure enough about where the part is. return landmark.takeIf { it.inFrameLikelihood >= PoseSkeletonRenderer.MIN_LIKELIHOOD } } }