Spaces:
Paused
Paused
frame analysis
Browse files- app/models/swing_analyzer.py +39 -45
app/models/swing_analyzer.py
CHANGED
|
@@ -17,74 +17,68 @@ def segment_swing(pose_data, detections, sample_rate=1):
|
|
| 17 |
angles = calculate_joint_angles(keypoints)
|
| 18 |
angles_by_frame[idx] = angles
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
setup_end = frame_indices[0]
|
| 21 |
initial_angles = angles_by_frame[frame_indices[0]]
|
| 22 |
initial_shoulder = initial_angles.get("right_shoulder")
|
| 23 |
initial_wrist = initial_angles.get("right_elbow")
|
| 24 |
-
|
| 25 |
for idx in frame_indices[1:]:
|
| 26 |
angles = angles_by_frame[idx]
|
| 27 |
shoulder = angles.get("right_shoulder")
|
| 28 |
wrist = angles.get("right_elbow")
|
| 29 |
-
if shoulder and initial_shoulder and abs(shoulder - initial_shoulder) >
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
if wrist and initial_wrist and abs(wrist - initial_wrist) > 10:
|
| 33 |
-
setup_end = idx
|
| 34 |
break
|
| 35 |
|
|
|
|
| 36 |
max_shoulder_angle = -1
|
| 37 |
-
top_backswing_frame = setup_end
|
| 38 |
for idx in frame_indices:
|
| 39 |
-
if idx
|
| 40 |
continue
|
| 41 |
shoulder = angles_by_frame[idx].get("right_shoulder")
|
| 42 |
if shoulder and shoulder > max_shoulder_angle:
|
| 43 |
max_shoulder_angle = shoulder
|
| 44 |
top_backswing_frame = idx
|
| 45 |
|
| 46 |
-
#
|
| 47 |
-
# during the downswing, before it starts rising in the follow-through
|
| 48 |
impact_frame = None
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
if
|
| 57 |
continue
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
idx, velocity = wrist_velocities[i]
|
| 73 |
-
prev_idx, prev_velocity = wrist_velocities[i-1]
|
| 74 |
-
if prev_velocity < 0 and velocity > 0: # Velocity changes from negative to positive
|
| 75 |
-
impact_frame = prev_idx
|
| 76 |
-
break
|
| 77 |
-
|
| 78 |
-
# If no clear impact point found, use the frame with minimum wrist Y position
|
| 79 |
-
if impact_frame is None:
|
| 80 |
-
for idx, wrist_y in wrist_positions:
|
| 81 |
-
if wrist_y < min_wrist_y:
|
| 82 |
-
min_wrist_y = wrist_y
|
| 83 |
-
impact_frame = idx
|
| 84 |
-
|
| 85 |
if impact_frame is None:
|
| 86 |
impact_frame = frame_indices[-1]
|
| 87 |
|
|
|
|
| 88 |
for idx in frame_indices:
|
| 89 |
if idx <= setup_end:
|
| 90 |
swing_phases["setup"].append(idx)
|
|
|
|
| 17 |
angles = calculate_joint_angles(keypoints)
|
| 18 |
angles_by_frame[idx] = angles
|
| 19 |
|
| 20 |
+
# --- Dynamic Phase Segmentation ---
|
| 21 |
+
# 1. Setup: before any significant movement
|
| 22 |
+
# 2. Backswing: from end of setup to top of backswing
|
| 23 |
+
# 3. Downswing: from top of backswing to just before impact
|
| 24 |
+
# 4. Impact: frame(s) where ball first moves
|
| 25 |
+
# 5. Follow-through: after impact
|
| 26 |
+
|
| 27 |
+
# --- 1. Find end of setup (first significant movement) ---
|
| 28 |
setup_end = frame_indices[0]
|
| 29 |
initial_angles = angles_by_frame[frame_indices[0]]
|
| 30 |
initial_shoulder = initial_angles.get("right_shoulder")
|
| 31 |
initial_wrist = initial_angles.get("right_elbow")
|
| 32 |
+
movement_threshold = 8 # degrees, can be tuned
|
| 33 |
for idx in frame_indices[1:]:
|
| 34 |
angles = angles_by_frame[idx]
|
| 35 |
shoulder = angles.get("right_shoulder")
|
| 36 |
wrist = angles.get("right_elbow")
|
| 37 |
+
if (shoulder and initial_shoulder and abs(shoulder - initial_shoulder) > movement_threshold) or \
|
| 38 |
+
(wrist and initial_wrist and abs(wrist - initial_wrist) > movement_threshold):
|
| 39 |
+
setup_end = idx - 1
|
|
|
|
|
|
|
| 40 |
break
|
| 41 |
|
| 42 |
+
# --- 2. Top of backswing (max shoulder angle after setup) ---
|
| 43 |
max_shoulder_angle = -1
|
| 44 |
+
top_backswing_frame = setup_end + 1
|
| 45 |
for idx in frame_indices:
|
| 46 |
+
if idx <= setup_end:
|
| 47 |
continue
|
| 48 |
shoulder = angles_by_frame[idx].get("right_shoulder")
|
| 49 |
if shoulder and shoulder > max_shoulder_angle:
|
| 50 |
max_shoulder_angle = shoulder
|
| 51 |
top_backswing_frame = idx
|
| 52 |
|
| 53 |
+
# --- 3. Impact: first significant ball movement ---
|
|
|
|
| 54 |
impact_frame = None
|
| 55 |
+
ball_detections = [d for d in detections if d.class_name == "sports ball"]
|
| 56 |
+
ball_detections.sort(key=lambda x: x.frame_idx)
|
| 57 |
+
movement_threshold_px = 2
|
| 58 |
+
prev_x = prev_y = None
|
| 59 |
+
prev_frame = None
|
| 60 |
+
for detection in ball_detections:
|
| 61 |
+
frame_idx = detection.frame_idx
|
| 62 |
+
if frame_idx < top_backswing_frame:
|
| 63 |
continue
|
| 64 |
+
x1, y1, x2, y2 = detection.bbox
|
| 65 |
+
ball_x = (x1 + x2) / 2
|
| 66 |
+
ball_y = (y1 + y2) / 2
|
| 67 |
+
if prev_x is not None and prev_y is not None:
|
| 68 |
+
dx = abs(ball_x - prev_x)
|
| 69 |
+
dy = abs(ball_y - prev_y)
|
| 70 |
+
if dx > movement_threshold_px or dy > movement_threshold_px:
|
| 71 |
+
impact_frame = frame_idx
|
| 72 |
+
break
|
| 73 |
+
prev_x = ball_x
|
| 74 |
+
prev_y = ball_y
|
| 75 |
+
prev_frame = frame_idx
|
| 76 |
+
if impact_frame is None and prev_frame is not None:
|
| 77 |
+
impact_frame = prev_frame
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
if impact_frame is None:
|
| 79 |
impact_frame = frame_indices[-1]
|
| 80 |
|
| 81 |
+
# --- 4. Assign phases dynamically ---
|
| 82 |
for idx in frame_indices:
|
| 83 |
if idx <= setup_end:
|
| 84 |
swing_phases["setup"].append(idx)
|