| from __future__ import annotations |
|
|
| from pozify.contracts import PoseFrame |
| from pozify.exercises.shared.analyzer import ( |
| ExerciseMetricResult, |
| max_optional, |
| mean_optional, |
| mean_pair, |
| min_optional, |
| range_optional, |
| round_optional, |
| safe_ratio, |
| score, |
| side_delta, |
| std_optional, |
| value_series, |
| width, |
| ) |
| from pozify.steps.rep_signals import average_axis, body_line_score |
|
|
|
|
| class PushUpAnalyzer: |
| def metrics(self, frames: list[PoseFrame]) -> ExerciseMetricResult: |
| elbow_angles = value_series( |
| frames, |
| lambda frame: mean_pair( |
| frame, |
| ("left_shoulder", "left_elbow", "left_wrist"), |
| ("right_shoulder", "right_elbow", "right_wrist"), |
| ), |
| ) |
| elbow_deltas = value_series( |
| frames, |
| lambda frame: side_delta( |
| frame, |
| ("left_shoulder", "left_elbow", "left_wrist"), |
| ("right_shoulder", "right_elbow", "right_wrist"), |
| ), |
| ) |
| body_line = value_series(frames, body_line_score) |
| shoulder_y = value_series( |
| frames, |
| lambda frame: average_axis(frame, ("left_shoulder", "right_shoulder"), "y"), |
| ) |
| hip_y = value_series(frames, lambda frame: average_axis(frame, ("left_hip", "right_hip"), "y")) |
| ankle_y = value_series( |
| frames, |
| lambda frame: average_axis(frame, ("left_ankle", "right_ankle"), "y"), |
| ) |
| hand_width = value_series(frames, lambda frame: width(frame, "left_wrist", "right_wrist")) |
| shoulder_width = value_series( |
| frames, |
| lambda frame: width(frame, "left_shoulder", "right_shoulder"), |
| ) |
| elbow_width = value_series(frames, lambda frame: width(frame, "left_elbow", "right_elbow")) |
| knee_y = value_series( |
| frames, |
| lambda frame: average_axis(frame, ("left_knee", "right_knee"), "y"), |
| ) |
|
|
| min_elbow = min_optional(elbow_angles) |
| max_elbow = max_optional(elbow_angles) |
| elbow_rom = 0.0 if min_elbow is None or max_elbow is None else max_elbow - min_elbow |
| chest_depth = range_optional(shoulder_y) or 0.0 |
| hand_width_ratio = safe_ratio(mean_optional(hand_width), mean_optional(shoulder_width)) |
| elbow_flare = safe_ratio(mean_optional(elbow_width), mean_optional(shoulder_width)) |
| body_line_mean = mean_optional(body_line) |
| hip_sag_score = None |
| if body_line_mean is not None: |
| hip_sag_score = max(0.0, 1.0 - body_line_mean) |
| knee_support_score = self._knee_support_score(hip_y, knee_y, ankle_y) |
|
|
| rom_score = score((elbow_rom / 80.0) * 0.65 + (chest_depth / 0.16) * 0.35) |
| hip_stability = 1.0 - min(1.0, (std_optional(hip_y) or 0.0) * 4.0) |
| stability_score = score(((body_line_mean or 0.5) * 0.75) + hip_stability * 0.25) |
| symmetry_score = score(1.0 - ((mean_optional(elbow_deltas) or 0.0) / 45.0)) |
|
|
| metrics = { |
| "min_elbow_angle_deg": round_optional(min_elbow), |
| "max_elbow_angle_deg": round_optional(max_elbow), |
| "body_line_score": round_optional(body_line_mean), |
| "hip_sag_score": round_optional(hip_sag_score), |
| "hip_pike_score": round_optional(max(0.0, ((body_line_mean or 1.0) - 1.0) * -1.0), 4), |
| "chest_depth_proxy": round_optional(chest_depth, 4), |
| "hand_width_ratio": round_optional(hand_width_ratio, 3), |
| "elbow_flare_ratio": round_optional(elbow_flare, 3), |
| "lockout_quality": score(((max_elbow or 120.0) - 120.0) / 55.0), |
| "knee_support_score": knee_support_score, |
| } |
| hints = [] |
| if hand_width_ratio is not None and hand_width_ratio > 1.45: |
| hints.append("wide_grip_push_up") |
| elif hand_width_ratio is not None and hand_width_ratio < 0.95: |
| hints.append("close_grip_push_up") |
| if knee_support_score >= 0.8: |
| hints.append("knee_push_up") |
| return metrics, rom_score, stability_score, symmetry_score, hints |
|
|
| def _knee_support_score( |
| self, |
| hip_y: list[float | None], |
| knee_y: list[float | None], |
| ankle_y: list[float | None], |
| ) -> float: |
| scores: list[float] = [] |
| for hip, knee, ankle in zip(hip_y, knee_y, ankle_y, strict=False): |
| if hip is None or knee is None or ankle is None: |
| continue |
|
|
| hip_to_ankle = abs(ankle - hip) |
| if hip_to_ankle <= 1e-6: |
| continue |
| knee_to_hip = abs(knee - hip) |
| bent_leg_score = score(1.0 - knee_to_hip / max(hip_to_ankle * 0.55, 0.01)) |
| scores.append(bent_leg_score) |
|
|
| return score(mean_optional(scores) or 0.0) |
|
|