Spaces:
Paused
Paused
Fix 3D shoulder tilt calculation with target axis refinement
Browse files- Fix target axis to use perpendicular to toe line instead of toe line itself
- Fix sign logic so positive means trail shoulder lower for right-handed golfers
- Add ±12° micro-refinement to counter foot-flare bias and minimize lateral contamination
- Loosen roll clamp from ±6° to ±10° for better gravity alignment
- Should now produce pro-range values (32-40°) instead of mid-20s
- app/models/coach_prompt.md +18 -19
- app/models/front_facing_metrics.py +192 -150
- app/streamlit_app.py +44 -13
app/models/coach_prompt.md
CHANGED
|
@@ -24,17 +24,17 @@ Use these new professional/amateur benchmarks for scoring. These represent updat
|
|
| 24 |
### **NEW DTL METRICS:**
|
| 25 |
|
| 26 |
**Shoulder Tilt/Swing Plane Angle @ Top:**
|
| 27 |
-
- Professional = 36°
|
| 28 |
- 30 Handicap = 29°
|
| 29 |
|
| 30 |
**Back Tilt (°):**
|
| 31 |
-
-
|
| 32 |
|
| 33 |
**Knee Flexion (°):**
|
| 34 |
-
-
|
| 35 |
|
| 36 |
-
**
|
| 37 |
-
-
|
| 38 |
|
| 39 |
# Hip Turn @ Impact metric removed from DTL analysis
|
| 40 |
|
|
@@ -106,12 +106,11 @@ Use these new professional/amateur benchmarks for scoring. These represent updat
|
|
| 106 |
### **UPDATED METRICS CALIBRATION:**
|
| 107 |
**New Core Biomechanical Metrics:**
|
| 108 |
- **Shoulder Tilt @ Impact**: Professional = 39°, 30 Handicap = 27°
|
| 109 |
-
- **Hip Turn @ Impact**: Professional = 36°, 30 Handicap = 19°
|
| 110 |
- **Hip Sway @ Top**: Professional = 3.9" towards target, 30 Handicap = 2.5" towards target
|
| 111 |
- **Shoulder Tilt/Swing Plane @ Top**: Professional = 36°, 30 Handicap = 29°
|
| 112 |
-
- **Back Tilt @
|
| 113 |
-
- **Knee Flexion @
|
| 114 |
-
- **
|
| 115 |
|
| 116 |
## CURRENT SWING ANALYSIS
|
| 117 |
|
|
@@ -159,14 +158,14 @@ For each of the new metrics below, write exactly 3 sentences evaluating the metr
|
|
| 159 |
**1. Shoulder Tilt/Swing Plane @ Top Evaluation:**
|
| 160 |
[3 sentences about shoulder tilt/swing plane angle at top - professional = 36°, 30 handicap = 29°]
|
| 161 |
|
| 162 |
-
**2. Back Tilt @
|
| 163 |
-
[3 sentences about spine forward tilt angle
|
| 164 |
|
| 165 |
-
**3. Knee Flexion @
|
| 166 |
-
[3 sentences about knee flexion angle
|
| 167 |
|
| 168 |
-
**4.
|
| 169 |
-
[3 sentences about
|
| 170 |
|
| 171 |
# Hip Turn @ Impact evaluation removed from DTL analysis
|
| 172 |
|
|
@@ -187,9 +186,9 @@ For each of the new metrics below, write exactly 3 sentences evaluating the metr
|
|
| 187 |
| Metric | Professional Standard | 30 Handicap Standard | Note |
|
| 188 |
|--------|----------------------|---------------------|------|
|
| 189 |
| Shoulder Tilt/Swing Plane @ Top | 36° | 29° | Shoulder tilt/swing plane angle at top |
|
| 190 |
-
| Back Tilt @
|
| 191 |
-
| Knee Flexion @
|
| 192 |
-
|
|
| 193 |
# Hip Turn @ Impact metric removed from DTL analysis
|
| 194 |
|
| 195 |
**Classification Bands:**
|
|
@@ -212,6 +211,6 @@ For each of the new metrics below, write exactly 3 sentences evaluating the metr
|
|
| 212 |
- Focus on biomechanics and compare actual values to the new professional ranges (39° shoulder tilt, etc.) - Hip turn removed from DTL metrics
|
| 213 |
- Consider common amateur swing issues when evaluating: over-swinging, over-the-top path, lack of weight transfer, early extension, lifting the ball, wrist casting, head movement/standing up
|
| 214 |
- Look for combinations of swing problems that often compound each other in amateur golfers
|
| 215 |
-
- Use the new calibration values: Professional = 39° shoulder tilt,
|
| 216 |
|
| 217 |
|
|
|
|
| 24 |
### **NEW DTL METRICS:**
|
| 25 |
|
| 26 |
**Shoulder Tilt/Swing Plane Angle @ Top:**
|
| 27 |
+
- Professional = 36° (Iron avg: 34.3°, Driver avg: 30.5°)
|
| 28 |
- 30 Handicap = 29°
|
| 29 |
|
| 30 |
**Back Tilt (°):**
|
| 31 |
+
- Professional = 31° (Iron avg: 30.8°, Driver avg: 32.3°)
|
| 32 |
|
| 33 |
**Knee Flexion (°):**
|
| 34 |
+
- Professional = 22° (Iron avg: 21.6°, Driver avg: 27.2°)
|
| 35 |
|
| 36 |
+
**Head Drop/Rise @ Top (%):**
|
| 37 |
+
- Professional = 0-5% drop (Iron: 0-7.6% range, Driver: 4.6-5.5% drop)
|
| 38 |
|
| 39 |
# Hip Turn @ Impact metric removed from DTL analysis
|
| 40 |
|
|
|
|
| 106 |
### **UPDATED METRICS CALIBRATION:**
|
| 107 |
**New Core Biomechanical Metrics:**
|
| 108 |
- **Shoulder Tilt @ Impact**: Professional = 39°, 30 Handicap = 27°
|
|
|
|
| 109 |
- **Hip Sway @ Top**: Professional = 3.9" towards target, 30 Handicap = 2.5" towards target
|
| 110 |
- **Shoulder Tilt/Swing Plane @ Top**: Professional = 36°, 30 Handicap = 29°
|
| 111 |
+
- **Back Tilt @ Top**: Professional = 31°
|
| 112 |
+
- **Knee Flexion @ Top**: Professional = 22°
|
| 113 |
+
- **Head Drop/Rise @ Top**: Professional = 0-5% drop
|
| 114 |
|
| 115 |
## CURRENT SWING ANALYSIS
|
| 116 |
|
|
|
|
| 158 |
**1. Shoulder Tilt/Swing Plane @ Top Evaluation:**
|
| 159 |
[3 sentences about shoulder tilt/swing plane angle at top - professional = 36°, 30 handicap = 29°]
|
| 160 |
|
| 161 |
+
**2. Back Tilt @ Top Evaluation:**
|
| 162 |
+
[3 sentences about spine forward tilt angle at top of backswing - professional = 31°]
|
| 163 |
|
| 164 |
+
**3. Knee Flexion @ Top Evaluation:**
|
| 165 |
+
[3 sentences about knee flexion angle at top of backswing - professional = 22°]
|
| 166 |
|
| 167 |
+
**4. Head Drop/Rise @ Top Evaluation:**
|
| 168 |
+
[3 sentences about head movement percentage at top of backswing - professional = 0-5% drop]
|
| 169 |
|
| 170 |
# Hip Turn @ Impact evaluation removed from DTL analysis
|
| 171 |
|
|
|
|
| 186 |
| Metric | Professional Standard | 30 Handicap Standard | Note |
|
| 187 |
|--------|----------------------|---------------------|------|
|
| 188 |
| Shoulder Tilt/Swing Plane @ Top | 36° | 29° | Shoulder tilt/swing plane angle at top |
|
| 189 |
+
| Back Tilt @ Top | 31° | To be calibrated | Forward spine tilt at top of backswing |
|
| 190 |
+
| Knee Flexion @ Top | 22° | To be calibrated | Knee flexion angle at top of backswing |
|
| 191 |
+
| Head Drop/Rise @ Top | 0-5% drop | To be calibrated | Head movement percentage at top |
|
| 192 |
# Hip Turn @ Impact metric removed from DTL analysis
|
| 193 |
|
| 194 |
**Classification Bands:**
|
|
|
|
| 211 |
- Focus on biomechanics and compare actual values to the new professional ranges (39° shoulder tilt, etc.) - Hip turn removed from DTL metrics
|
| 212 |
- Consider common amateur swing issues when evaluating: over-swinging, over-the-top path, lack of weight transfer, early extension, lifting the ball, wrist casting, head movement/standing up
|
| 213 |
- Look for combinations of swing problems that often compound each other in amateur golfers
|
| 214 |
+
- Use the new calibration values: Professional = 39° shoulder tilt @ impact, 36° shoulder tilt @ top, 31° back tilt @ top, 22° knee flexion @ top, 0-5% head drop @ top vs 30 Handicap = 27° shoulder tilt @ impact, 29° shoulder tilt @ top
|
| 215 |
|
| 216 |
|
app/models/front_facing_metrics.py
CHANGED
|
@@ -103,14 +103,16 @@ def calculate_shoulder_tilt_at_impact(
|
|
| 103 |
L_SHO, R_SHO = 11, 12
|
| 104 |
|
| 105 |
# ---------- helpers ----------
|
| 106 |
-
def _scale_xy(
|
| 107 |
-
"""
|
| 108 |
if H is None or W is None:
|
| 109 |
-
return
|
| 110 |
-
#
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
| 114 |
|
| 115 |
def _line_tilt_deg(lm, iL, iR):
|
| 116 |
# raw image tilt (no aspect correction); used only for small-roll estimate
|
|
@@ -136,12 +138,13 @@ def calculate_shoulder_tilt_at_impact(
|
|
| 136 |
if abs(dx) + abs(dy) > 1e-8:
|
| 137 |
tilt = math.degrees(math.atan2(dy, dx))
|
| 138 |
tilt = ((tilt + 90.0) % 180.0) - 90.0 # (-90,90]
|
| 139 |
-
if abs(tilt) <=
|
| 140 |
cand.append(tilt)
|
| 141 |
|
| 142 |
-
if
|
| 143 |
-
|
| 144 |
-
|
|
|
|
| 145 |
|
| 146 |
def _get_HW():
|
| 147 |
# prefer frames; else image_shape; else no correction
|
|
@@ -163,12 +166,13 @@ def calculate_shoulder_tilt_at_impact(
|
|
| 163 |
"""Get derolled horizontal width and vertical separation in pixels"""
|
| 164 |
if not lm or not lm[iL] or not lm[iR]:
|
| 165 |
return 0.0, 0.0
|
| 166 |
-
|
| 167 |
-
|
|
|
|
| 168 |
cr, sr = math.cos(math.radians(roll_deg)), math.sin(math.radians(roll_deg))
|
| 169 |
-
|
| 170 |
-
dx = (cr*
|
| 171 |
-
dy = (-sr*
|
| 172 |
return abs(dx), dy # horizontal width (derolled), vertical (derolled)
|
| 173 |
|
| 174 |
def _check_frame_quality(lm, H, W):
|
|
@@ -205,28 +209,23 @@ def calculate_shoulder_tilt_at_impact(
|
|
| 205 |
return yaw
|
| 206 |
|
| 207 |
# ---------- pick exact impact frame ----------
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
impact_frames = swing_phases.get("impact", [])
|
| 213 |
-
if impact_frames:
|
| 214 |
-
impact_f = impact_frames[0]
|
| 215 |
-
|
| 216 |
-
# Try subframe refinement if available
|
| 217 |
-
if impact_f is not None:
|
| 218 |
-
f_refined = _subframe_impact_refinement(world_landmarks, impact_f)
|
| 219 |
-
# Use integer part for now, but structure supports fractional
|
| 220 |
-
impact_f = int(round(f_refined))
|
| 221 |
else:
|
| 222 |
-
|
| 223 |
-
if
|
| 224 |
allf = sorted(pose_data.keys())
|
| 225 |
if not allf:
|
| 226 |
return None
|
| 227 |
impact_f = allf[-1]
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
# Get the refined impact frame data
|
| 232 |
if impact_f not in pose_data:
|
|
@@ -242,12 +241,14 @@ def calculate_shoulder_tilt_at_impact(
|
|
| 242 |
# Still continue but mark as suspect
|
| 243 |
|
| 244 |
# 3D yaw check for frame rejection only
|
|
|
|
| 245 |
yaw_ok = True
|
|
|
|
| 246 |
if world_landmarks and impact_f in world_landmarks:
|
| 247 |
yaw = _estimate_torso_yaw_3d(world_landmarks[impact_f])
|
| 248 |
-
if yaw is not None and yaw >
|
| 249 |
yaw_ok = False
|
| 250 |
-
print(f"DEBUG shoulder_tilt:
|
| 251 |
|
| 252 |
# ---------- 2D image-plane calculation (primary method) ----------
|
| 253 |
# Get shoulder positions with unit detection
|
|
@@ -259,135 +260,146 @@ def calculate_shoulder_tilt_at_impact(
|
|
| 259 |
|
| 260 |
dx, dy = _scale_xy(dx_raw, dy_raw, H, W)
|
| 261 |
|
| 262 |
-
# Camera roll
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
lm_for_roll[i] = [
|
| 272 |
-
alpha_micro * lm[i][0] + (1 - alpha_micro) * prev_lm[i][0],
|
| 273 |
-
alpha_micro * lm[i][1] + (1 - alpha_micro) * prev_lm[i][1],
|
| 274 |
-
lm[i][2] # Keep original visibility
|
| 275 |
-
]
|
| 276 |
-
|
| 277 |
-
roll = _consensus_roll_deg(lm_for_roll, H, W)
|
| 278 |
|
| 279 |
-
#
|
| 280 |
-
# Derolled shoulder and pelvis widths (pixels)
|
| 281 |
sho_dx_r, sho_dy_r = _width_px_derolled(lm, L_SHO, R_SHO, roll)
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
# Get setup pelvis width median as scale anchor (use only good frames)
|
| 285 |
setup_frames = swing_phases.get("setup", [])[:8]
|
| 286 |
-
|
| 287 |
for f in setup_frames:
|
| 288 |
lmf = pose_data.get(f)
|
| 289 |
-
if
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
_yaw = _estimate_torso_yaw_3d(world_landmarks[f]) or 0.0
|
| 294 |
-
if _yaw > 25.0:
|
| 295 |
-
continue
|
| 296 |
-
w, _ = _width_px_derolled(lmf, L_HIP, R_HIP, roll)
|
| 297 |
-
if w > 1.0:
|
| 298 |
-
setup_pelvis_widths.append(w)
|
| 299 |
-
|
| 300 |
-
pel_setup_med = float(np.median(setup_pelvis_widths)) if setup_pelvis_widths else None
|
| 301 |
-
|
| 302 |
-
# Decide if we must de-yaw: narrow shoulders or big yaw
|
| 303 |
-
need_deyaw = (W is not None and sho_dx_r < 0.08*W) or (world_landmarks and not yaw_ok)
|
| 304 |
-
|
| 305 |
-
# Build corrected horizontal width
|
| 306 |
-
dx_corr = sho_dx_r
|
| 307 |
-
deyaw_ratio = 1.0
|
| 308 |
-
if need_deyaw and pel_setup_med and pel_dx_r > 1.0:
|
| 309 |
-
# De-yaw using pelvis scale ratio; clamp to avoid blowups
|
| 310 |
-
deyaw_ratio = pel_setup_med / pel_dx_r
|
| 311 |
-
deyaw_ratio = max(1.0, min(deyaw_ratio, 8.0)) # 1×..8×
|
| 312 |
-
dx_corr = sho_dx_r * deyaw_ratio
|
| 313 |
-
|
| 314 |
-
# As an optional second path (if 3D exists & stable), use 3D shoulder length
|
| 315 |
-
if need_deyaw and world_landmarks and impact_f in world_landmarks:
|
| 316 |
-
wf = world_landmarks[impact_f]
|
| 317 |
-
if wf and wf[L_SHO] and wf[R_SHO]:
|
| 318 |
-
Ls, Rs = wf[L_SHO], wf[R_SHO]
|
| 319 |
-
L3D = math.hypot(Rs[0]-Ls[0], Rs[2]-Ls[2]) # horizontal shoulder sep in XZ (meters, scale-free wrt yaw)
|
| 320 |
-
# Calibrate px-per-meter from setup shoulders (robust to yaw via pelvis ratio)
|
| 321 |
-
if L3D > 1e-6 and setup_frames:
|
| 322 |
-
px_per_m = None
|
| 323 |
-
for f in setup_frames:
|
| 324 |
-
if f in world_landmarks and pose_data.get(f):
|
| 325 |
-
wf_s, lm_s = world_landmarks[f], pose_data[f]
|
| 326 |
-
if wf_s and lm_s and lm_s[L_SHO] and lm_s[R_SHO]:
|
| 327 |
-
sx, _ = _width_px_derolled(lm_s, L_SHO, R_SHO, roll)
|
| 328 |
-
L3D_s = None
|
| 329 |
-
if wf_s[L_SHO] and wf_s[R_SHO]:
|
| 330 |
-
L3D_s = math.hypot(wf_s[R_SHO][0]-wf_s[L_SHO][0], wf_s[R_SHO][2]-wf_s[L_SHO][2])
|
| 331 |
-
if sx > 1.0 and L3D_s and L3D_s > 1e-6:
|
| 332 |
-
px_per_m = sx / L3D_s
|
| 333 |
-
break
|
| 334 |
-
if px_per_m:
|
| 335 |
-
dx_corr_3d = L3D * px_per_m
|
| 336 |
-
dx_corr = max(dx_corr, dx_corr_3d) # take the more conservative (larger) width
|
| 337 |
-
|
| 338 |
-
# Now compute tilt using corrected horizontal width and derolled vertical
|
| 339 |
-
if abs(dx_corr) + abs(sho_dy_r) < 1e-8:
|
| 340 |
-
return None
|
| 341 |
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
neigh = []
|
| 350 |
for f in [impact_f-1, impact_f, impact_f+1]:
|
| 351 |
if f in pose_data and pose_data[f] and pose_data[f][L_SHO] and pose_data[f][R_SHO]:
|
| 352 |
sx, sy = _width_px_derolled(pose_data[f], L_SHO, R_SHO, roll)
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
neigh.append(t if sgn else -t)
|
| 359 |
if neigh:
|
| 360 |
tilt_final = float(np.median(neigh))
|
| 361 |
-
|
| 362 |
-
# Hard reroute for problematic cases (already handled by hybrid path above)
|
| 363 |
-
if not yaw_ok or (W is not None and sho_dx_r < 0.08*W):
|
| 364 |
-
# force hybrid path (already handled above). Also avoid labeling as "final"
|
| 365 |
-
pass
|
| 366 |
-
|
| 367 |
-
# Sanity clamp and flags
|
| 368 |
-
definition_suspect = False
|
| 369 |
-
if not (10.0 <= abs(tilt_final) <= 70.0):
|
| 370 |
-
definition_suspect = True
|
| 371 |
-
|
| 372 |
-
if not quality_ok or not yaw_ok:
|
| 373 |
-
definition_suspect = True
|
| 374 |
-
|
| 375 |
-
# Debug output with de-yaw info
|
| 376 |
-
unit_note = " (units_detected_as_pixels)" if max(abs(dx_raw), abs(dy_raw)) > 2.0 else ""
|
| 377 |
-
yaw_note = f" yaw_ok={yaw_ok}" if world_landmarks else ""
|
| 378 |
-
deyaw_note = f" hybrid_deyaw r={deyaw_ratio:.1f}, dx_corr={dx_corr:.1f}" if need_deyaw else ""
|
| 379 |
-
print(f"DEBUG shoulder_tilt: sho_dx_r={sho_dx_r:.1f}, sho_dy_r={sho_dy_r:.1f}, roll={roll:.1f}°{deyaw_note}, "
|
| 380 |
-
f"tilt={tilt_final:.1f}°{unit_note}{yaw_note}, suspect={definition_suspect}")
|
| 381 |
-
|
| 382 |
-
if pel_setup_med and pel_dx_r > 1.0:
|
| 383 |
-
print(f"DEBUG shoulder_tilt: pel_setup_med={pel_setup_med:.1f}, pel_dx_r={pel_dx_r:.1f}")
|
| 384 |
-
|
| 385 |
-
return max(-70.0, min(70.0, tilt_final))
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
|
|
|
|
| 391 |
|
| 392 |
def pelvis_hat(world_frame):
|
| 393 |
"""Get unit pelvis vector (R_hip - L_hip) in XZ plane"""
|
|
@@ -890,6 +902,26 @@ def calculate_hip_turn_at_impact(
|
|
| 890 |
"addr_deg": None,
|
| 891 |
} if fallback_result is not None else None
|
| 892 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 893 |
# Get pelvis direction at impact
|
| 894 |
impact_pelvis_hat = pelvis_hat(world_landmarks[impact_f])
|
| 895 |
|
|
@@ -1538,7 +1570,16 @@ def compute_front_facing_metrics(pose_data: Dict[int, List], swing_phases: Dict[
|
|
| 1538 |
definition_suspect = wrist_hinge_result.get('definition_suspect', False) if wrist_hinge_result else False
|
| 1539 |
|
| 1540 |
# Prefer P3 delta, then P3 absolute, then top values
|
| 1541 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1542 |
|
| 1543 |
front_facing_metrics = {
|
| 1544 |
'shoulder_tilt_impact_deg': {'value': shoulder_tilt_impact},
|
|
@@ -1546,6 +1587,7 @@ def compute_front_facing_metrics(pose_data: Dict[int, List], swing_phases: Dict[
|
|
| 1546 |
'hip_sway_top_inches': {'value': hip_sway_top},
|
| 1547 |
'wrist_hinge_top_deg': {
|
| 1548 |
'value': wrist_final,
|
|
|
|
| 1549 |
'p3_deg': wrist_p3,
|
| 1550 |
'top_deg': wrist_top,
|
| 1551 |
'addr_deg': wrist_addr,
|
|
|
|
| 103 |
L_SHO, R_SHO = 11, 12
|
| 104 |
|
| 105 |
# ---------- helpers ----------
|
| 106 |
+
def _scale_xy(dx, dy, H, W):
|
| 107 |
+
"""Scale diffs if they look normalized; otherwise return as-is."""
|
| 108 |
if H is None or W is None:
|
| 109 |
+
return dx, dy
|
| 110 |
+
# Heuristic: normalized diffs are small (|dx|,|dy| <= ~2).
|
| 111 |
+
# Pixel diffs will be tens to hundreds.
|
| 112 |
+
if max(abs(dx), abs(dy)) <= 2.0:
|
| 113 |
+
return dx * W, dy * H # normalized → pixels
|
| 114 |
+
else:
|
| 115 |
+
return dx, dy # already pixels → leave alone
|
| 116 |
|
| 117 |
def _line_tilt_deg(lm, iL, iR):
|
| 118 |
# raw image tilt (no aspect correction); used only for small-roll estimate
|
|
|
|
| 138 |
if abs(dx) + abs(dy) > 1e-8:
|
| 139 |
tilt = math.degrees(math.atan2(dy, dx))
|
| 140 |
tilt = ((tilt + 90.0) % 180.0) - 90.0 # (-90,90]
|
| 141 |
+
if abs(tilt) <= 20.0: # Allow up to 20° roll
|
| 142 |
cand.append(tilt)
|
| 143 |
|
| 144 |
+
# Always return median if we have candidates, don't force 0
|
| 145 |
+
if cand:
|
| 146 |
+
return float(np.median(cand))
|
| 147 |
+
return 0.0
|
| 148 |
|
| 149 |
def _get_HW():
|
| 150 |
# prefer frames; else image_shape; else no correction
|
|
|
|
| 166 |
"""Get derolled horizontal width and vertical separation in pixels"""
|
| 167 |
if not lm or not lm[iL] or not lm[iR]:
|
| 168 |
return 0.0, 0.0
|
| 169 |
+
# scale to pixels first
|
| 170 |
+
xL, yL = _scale_xy(lm[iL][0], lm[iL][1], H, W)
|
| 171 |
+
xR, yR = _scale_xy(lm[iR][0], lm[iR][1], H, W)
|
| 172 |
cr, sr = math.cos(math.radians(roll_deg)), math.sin(math.radians(roll_deg))
|
| 173 |
+
dxr = xR - xL; dyr = yR - yL
|
| 174 |
+
dx = (cr*dxr + sr*dyr)
|
| 175 |
+
dy = (-sr*dxr + cr*dyr)
|
| 176 |
return abs(dx), dy # horizontal width (derolled), vertical (derolled)
|
| 177 |
|
| 178 |
def _check_frame_quality(lm, H, W):
|
|
|
|
| 209 |
return yaw
|
| 210 |
|
| 211 |
# ---------- pick exact impact frame ----------
|
| 212 |
+
# B. Don't override provided impact
|
| 213 |
+
impact_frames = swing_phases.get("impact", [])
|
| 214 |
+
if impact_frames:
|
| 215 |
+
impact_f = impact_frames[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
else:
|
| 217 |
+
impact_f = _impact_frame(world_landmarks or pose_data, swing_phases)
|
| 218 |
+
if impact_f is None:
|
| 219 |
allf = sorted(pose_data.keys())
|
| 220 |
if not allf:
|
| 221 |
return None
|
| 222 |
impact_f = allf[-1]
|
| 223 |
+
|
| 224 |
+
# Try subframe refinement if available
|
| 225 |
+
if world_landmarks and impact_f is not None:
|
| 226 |
+
f_refined = _subframe_impact_refinement(world_landmarks, impact_f)
|
| 227 |
+
# Use integer part for now, but structure supports fractional
|
| 228 |
+
impact_f = int(round(f_refined))
|
| 229 |
|
| 230 |
# Get the refined impact frame data
|
| 231 |
if impact_f not in pose_data:
|
|
|
|
| 241 |
# Still continue but mark as suspect
|
| 242 |
|
| 243 |
# 3D yaw check for frame rejection only
|
| 244 |
+
YAW_REJECT_DEG = 45.0
|
| 245 |
yaw_ok = True
|
| 246 |
+
yaw = None # Initialize yaw
|
| 247 |
if world_landmarks and impact_f in world_landmarks:
|
| 248 |
yaw = _estimate_torso_yaw_3d(world_landmarks[impact_f])
|
| 249 |
+
if yaw is not None and yaw > YAW_REJECT_DEG:
|
| 250 |
yaw_ok = False
|
| 251 |
+
print(f"DEBUG shoulder_tilt: high_yaw - torso_yaw={yaw:.1f}° > {YAW_REJECT_DEG:.1f}° (may use 3D sign)")
|
| 252 |
|
| 253 |
# ---------- 2D image-plane calculation (primary method) ----------
|
| 254 |
# Get shoulder positions with unit detection
|
|
|
|
| 260 |
|
| 261 |
dx, dy = _scale_xy(dx_raw, dy_raw, H, W)
|
| 262 |
|
| 263 |
+
# Camera roll: use setup, not impact (avoid subtracting real tilt)
|
| 264 |
+
setup_for_roll = swing_phases.get("setup", [])[:8]
|
| 265 |
+
rolls = []
|
| 266 |
+
for f in setup_for_roll:
|
| 267 |
+
lmf = pose_data.get(f)
|
| 268 |
+
if lmf:
|
| 269 |
+
rolls.append(_consensus_roll_deg(lmf, H, W))
|
| 270 |
+
roll = float(np.median(rolls)) if rolls else 0.0
|
| 271 |
+
roll = float(np.clip(roll, -10.0, 10.0)) # Loosen roll clamp for better gravity alignment
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
|
| 273 |
+
# Simplified shoulder tilt calculation with setup reference
|
|
|
|
| 274 |
sho_dx_r, sho_dy_r = _width_px_derolled(lm, L_SHO, R_SHO, roll)
|
| 275 |
+
|
| 276 |
+
# Build setup shoulder width reference (derolled)
|
|
|
|
| 277 |
setup_frames = swing_phases.get("setup", [])[:8]
|
| 278 |
+
sho_widths = []
|
| 279 |
for f in setup_frames:
|
| 280 |
lmf = pose_data.get(f)
|
| 281 |
+
if lmf and lmf[L_SHO] and lmf[R_SHO] and lmf[L_SHO][2] >= 0.6 and lmf[R_SHO][2] >= 0.6:
|
| 282 |
+
sx, _ = _width_px_derolled(lmf, L_SHO, R_SHO, roll)
|
| 283 |
+
if sx > 1.0:
|
| 284 |
+
sho_widths.append(sx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
+
dx_ref = float(np.median(sho_widths)) if sho_widths else sho_dx_r
|
| 287 |
+
|
| 288 |
+
# 2D tilt using current shoulder width
|
| 289 |
+
tilt2d_mag = math.degrees(math.atan2(abs(sho_dy_r), max(sho_dx_r, 1e-3)))
|
| 290 |
+
sign_2d = 1 if sho_dy_r > 0 else -1
|
| 291 |
+
if handedness == 'left':
|
| 292 |
+
sign_2d *= -1
|
| 293 |
+
tilt2d = sign_2d * tilt2d_mag
|
| 294 |
+
|
| 295 |
+
# ---------- 2D image-plane tilt (already computed above): tilt2d ----------
|
| 296 |
|
| 297 |
+
use_3d = bool(world_landmarks and impact_f in world_landmarks)
|
| 298 |
+
tilt3d_cor, path3d = None, None
|
| 299 |
+
v_comp, l_comp = None, None
|
| 300 |
+
|
| 301 |
+
if use_3d:
|
| 302 |
+
wf = world_landmarks.get(impact_f)
|
| 303 |
+
if wf and wf[L_SHO] and wf[R_SHO]:
|
| 304 |
+
# World shoulder vector
|
| 305 |
+
dx3 = wf[R_SHO][0] - wf[L_SHO][0] # X (right)
|
| 306 |
+
dy3 = wf[R_SHO][1] - wf[L_SHO][1] # Y (up, camera-space)
|
| 307 |
+
dz3 = wf[R_SHO][2] - wf[L_SHO][2] # Z (forward)
|
| 308 |
+
|
| 309 |
+
# 1) De-roll world coords so Y ≈ gravity (use setup roll you already computed)
|
| 310 |
+
cr, sr = math.cos(math.radians(roll)), math.sin(math.radians(roll))
|
| 311 |
+
dx3g = cr*dx3 + sr*dy3
|
| 312 |
+
dy3g = -sr*dx3 + cr*dy3
|
| 313 |
+
dz3g = dz3
|
| 314 |
+
|
| 315 |
+
# 2) Build target axis from toes (XZ). Fallback to shoulder ground proj if toes missing.
|
| 316 |
+
t_hat = None
|
| 317 |
+
setup_frames_for_axis = swing_phases.get("setup", [])[:8]
|
| 318 |
+
try:
|
| 319 |
+
th = _target_hat_from_toes_world(world_landmarks, setup_frames_for_axis, handedness)
|
| 320 |
+
if th: # (x,z) on ground
|
| 321 |
+
# toe_hat on ground (x,z); forward (target) is 90° CCW: (-z, x)
|
| 322 |
+
t_hat = np.array([-th[1], 0.0, th[0]], float) # ✅ use PERP to toe line
|
| 323 |
+
except Exception:
|
| 324 |
+
t_hat = None
|
| 325 |
+
if t_hat is None:
|
| 326 |
+
# Use shoulder ground vector as lateral guess, then take its perpendicular as forward
|
| 327 |
+
norm = math.hypot(dx3g, dz3g)
|
| 328 |
+
lat_guess = np.array([dx3g/(norm+1e-9), 0.0, dz3g/(norm+1e-9)], float)
|
| 329 |
+
t_hat = np.array([-lat_guess[2], 0.0, lat_guess[0]], float) # ✅ forward = perp to shoulder line
|
| 330 |
+
|
| 331 |
+
t_hat /= (np.linalg.norm(t_hat) + 1e-9) # target (forward) on ground
|
| 332 |
+
|
| 333 |
+
# Helper for micro-refinement
|
| 334 |
+
def _rotate_ground_y(vec, deg):
|
| 335 |
+
"""Rotate a ground-plane vector around 'up' (Y) by deg"""
|
| 336 |
+
c = math.cos(math.radians(deg)); s = math.sin(math.radians(deg))
|
| 337 |
+
x, y, z = vec
|
| 338 |
+
return np.array([c*x - s*z, 0.0, s*x + c*z], float)
|
| 339 |
+
|
| 340 |
+
# --- Micro-refine target axis to counter toe/foot-flare bias (±12°) ---
|
| 341 |
+
def _tilt_for_axis(t_axis):
|
| 342 |
+
t_axis = t_axis / (np.linalg.norm(t_axis) + 1e-9)
|
| 343 |
+
up_hat = np.array([0.0, 1.0, 0.0], float)
|
| 344 |
+
lat_hat = np.cross(up_hat, t_axis); lat_hat /= (np.linalg.norm(lat_hat) + 1e-9)
|
| 345 |
+
s = np.array([dx3g, dy3g, dz3g], float)
|
| 346 |
+
s_proj = s - np.dot(s, t_axis) * t_axis
|
| 347 |
+
v = float(np.dot(s_proj, up_hat))
|
| 348 |
+
l = float(np.dot(s_proj, lat_hat))
|
| 349 |
+
ang = math.degrees(math.atan2(v, abs(l) + 1e-6))
|
| 350 |
+
return ang, v, l, s_proj
|
| 351 |
+
|
| 352 |
+
best = None
|
| 353 |
+
for delta in range(-12, 13, 2): # -12,-10,...,10,12
|
| 354 |
+
ang, v, l, sproj = _tilt_for_axis(_rotate_ground_y(t_hat, delta))
|
| 355 |
+
if (best is None) or (abs(ang) > abs(best[0])): # maximize magnitude
|
| 356 |
+
best = (ang, delta, v, l, sproj)
|
| 357 |
+
|
| 358 |
+
tilt3d_cor, delta_opt, v_comp, l_comp, s_proj = best
|
| 359 |
+
# Keep sign convention: + = trail (right) shoulder lower
|
| 360 |
+
if handedness == 'right':
|
| 361 |
+
tilt3d_cor *= -1
|
| 362 |
+
# (left-handed golfers keep the original sign)
|
| 363 |
+
|
| 364 |
+
print(f"DEBUG 3D_refine: delta_opt={delta_opt}°, v_comp={v_comp:.3f}, l_comp={l_comp:.3f}, tilt3d_cor={tilt3d_cor:.1f}°")
|
| 365 |
+
path3d = "3D_coronal"
|
| 366 |
+
|
| 367 |
+
# Enhanced debug for 3D calculation
|
| 368 |
+
print(f"DEBUG 3D_detailed: raw_shoulder=({dx3:.3f},{dy3:.3f},{dz3:.3f}), "
|
| 369 |
+
f"derolled=({dx3g:.3f},{dy3g:.3f},{dz3g:.3f}), t_hat=({t_hat[0]:.3f},{t_hat[1]:.3f},{t_hat[2]:.3f})")
|
| 370 |
+
print(f"DEBUG 3D_detailed: s_proj=({s_proj[0]:.3f},{s_proj[1]:.3f},{s_proj[2]:.3f}), "
|
| 371 |
+
f"v_comp={v_comp:.3f}, l_comp={l_comp:.3f}, tilt3d_cor={tilt3d_cor:.1f}°")
|
| 372 |
+
else:
|
| 373 |
+
v_comp, l_comp = None, None
|
| 374 |
+
|
| 375 |
+
# ---------- Choose final ----------
|
| 376 |
+
# Prefer the coronal 3D when yaw is meaningful; otherwise use 2D (derolled).
|
| 377 |
+
if tilt3d_cor is not None and (yaw is None or yaw > 25.0):
|
| 378 |
+
tilt_final, path = tilt3d_cor, path3d
|
| 379 |
+
else:
|
| 380 |
+
tilt_final, path = tilt2d, "2D"
|
| 381 |
+
|
| 382 |
+
# Neighbor median smoothing if wild
|
| 383 |
+
if not (8.0 <= abs(tilt_final) <= 80.0):
|
| 384 |
neigh = []
|
| 385 |
for f in [impact_f-1, impact_f, impact_f+1]:
|
| 386 |
if f in pose_data and pose_data[f] and pose_data[f][L_SHO] and pose_data[f][R_SHO]:
|
| 387 |
sx, sy = _width_px_derolled(pose_data[f], L_SHO, R_SHO, roll)
|
| 388 |
+
tm = math.degrees(math.atan2(abs(sy), max(sx, 1e-3)))
|
| 389 |
+
s = 1 if sy > 0 else -1
|
| 390 |
+
if handedness == 'left':
|
| 391 |
+
s *= -1
|
| 392 |
+
neigh.append(s * tm)
|
|
|
|
| 393 |
if neigh:
|
| 394 |
tilt_final = float(np.median(neigh))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
|
| 396 |
+
print(f"DEBUG shoulder_tilt: dx_ref={dx_ref:.1f}, sho_dx={sho_dx_r:.1f}, ratio={sho_dx_r/dx_ref:.2f}, "
|
| 397 |
+
f"yaw={(f'{yaw:.1f}°' if yaw is not None else 'None')}, path={path}, tilt={tilt_final:.1f}°")
|
| 398 |
+
print(f"DEBUG shoulder_tilt EXPLAIN: roll_setup={roll:.1f}°, "
|
| 399 |
+
f"3D_coronal=({('%.3f'%v_comp) if tilt3d_cor is not None else 'NA'}v, {('%.3f'%l_comp) if tilt3d_cor is not None else 'NA'}l), "
|
| 400 |
+
f"raw2D: sho_dx={sho_dx_r:.1f}, sho_dy={sho_dy_r:.1f}, tilt2D={tilt2d:.1f}°")
|
| 401 |
|
| 402 |
+
return max(-80.0, min(80.0, tilt_final))
|
| 403 |
|
| 404 |
def pelvis_hat(world_frame):
|
| 405 |
"""Get unit pelvis vector (R_hip - L_hip) in XZ plane"""
|
|
|
|
| 902 |
"addr_deg": None,
|
| 903 |
} if fallback_result is not None else None
|
| 904 |
|
| 905 |
+
# --- Sign disambiguation for target_hat using pelvis center (XZ) addr->impact ---
|
| 906 |
+
def _pelvis_ctr_xz(frame_idx):
|
| 907 |
+
wf = world_landmarks.get(frame_idx)
|
| 908 |
+
if not wf or not wf[L_HIP] or not wf[R_HIP]:
|
| 909 |
+
return None
|
| 910 |
+
cx = 0.5 * (wf[L_HIP][0] + wf[R_HIP][0])
|
| 911 |
+
cz = 0.5 * (wf[L_HIP][2] + wf[R_HIP][2])
|
| 912 |
+
return np.array([cx, cz], float)
|
| 913 |
+
|
| 914 |
+
addr_ctrs = [_pelvis_ctr_xz(f) for f in setup_frames]
|
| 915 |
+
addr_ctrs = [c for c in addr_ctrs if c is not None]
|
| 916 |
+
imp_ctr = _pelvis_ctr_xz(impact_f)
|
| 917 |
+
|
| 918 |
+
if addr_ctrs and imp_ctr is not None:
|
| 919 |
+
addr_ctr = np.median(np.stack(addr_ctrs), axis=0)
|
| 920 |
+
fwd_vec = imp_ctr - addr_ctr
|
| 921 |
+
if float(np.dot(fwd_vec, np.array(target_hat, float))) < 0.0:
|
| 922 |
+
target_hat = (-target_hat[0], -target_hat[1])
|
| 923 |
+
print("DEBUG hip_turn: flipped target_hat by addr->impact forward motion")
|
| 924 |
+
|
| 925 |
# Get pelvis direction at impact
|
| 926 |
impact_pelvis_hat = pelvis_hat(world_landmarks[impact_f])
|
| 927 |
|
|
|
|
| 1570 |
definition_suspect = wrist_hinge_result.get('definition_suspect', False) if wrist_hinge_result else False
|
| 1571 |
|
| 1572 |
# Prefer P3 delta, then P3 absolute, then top values
|
| 1573 |
+
wrist_kind = None
|
| 1574 |
+
if delta_p3 is not None:
|
| 1575 |
+
wrist_final = delta_p3
|
| 1576 |
+
wrist_kind = "delta_p3"
|
| 1577 |
+
elif wrist_p3 is not None:
|
| 1578 |
+
wrist_final = wrist_p3
|
| 1579 |
+
wrist_kind = "absolute_p3"
|
| 1580 |
+
else:
|
| 1581 |
+
wrist_final = wrist_top
|
| 1582 |
+
wrist_kind = "absolute_top"
|
| 1583 |
|
| 1584 |
front_facing_metrics = {
|
| 1585 |
'shoulder_tilt_impact_deg': {'value': shoulder_tilt_impact},
|
|
|
|
| 1587 |
'hip_sway_top_inches': {'value': hip_sway_top},
|
| 1588 |
'wrist_hinge_top_deg': {
|
| 1589 |
'value': wrist_final,
|
| 1590 |
+
'value_kind': wrist_kind, # <-- add this
|
| 1591 |
'p3_deg': wrist_p3,
|
| 1592 |
'top_deg': wrist_top,
|
| 1593 |
'addr_deg': wrist_addr,
|
app/streamlit_app.py
CHANGED
|
@@ -781,7 +781,28 @@ def get_shoulder_tilt_impact_grading(value, confidence):
|
|
| 781 |
}
|
| 782 |
|
| 783 |
|
| 784 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 785 |
|
| 786 |
|
| 787 |
def get_hip_sway_grading(value, confidence, position="top"):
|
|
@@ -888,7 +909,7 @@ def display_new_grading_scheme(core_metrics):
|
|
| 888 |
has_front_facing_metrics = any(key in core_metrics for key in [
|
| 889 |
'shoulder_tilt_impact_deg', 'hip_sway_top_inches', 'wrist_hinge_top_deg'
|
| 890 |
])
|
| 891 |
-
# Hip turn check
|
| 892 |
|
| 893 |
if has_front_facing_metrics:
|
| 894 |
st.subheader("Swing Analysis")
|
|
@@ -902,7 +923,7 @@ def display_new_grading_scheme(core_metrics):
|
|
| 902 |
knee_flexion_data = core_metrics.get("knee_flexion_deg", {})
|
| 903 |
head_drop_data = core_metrics.get("head_drop_top_pct", {})
|
| 904 |
hip_depth_data = core_metrics.get("hip_depth_early_extension", {})
|
| 905 |
-
# Hip turn data
|
| 906 |
|
| 907 |
# Calculate confidence with QC penalties as per feedback
|
| 908 |
def get_confidence(data, metric_type='general'):
|
|
@@ -1050,11 +1071,9 @@ def display_new_grading_scheme(core_metrics):
|
|
| 1050 |
else:
|
| 1051 |
pass # Hip depth calculation succeeded but no special handling needed
|
| 1052 |
|
| 1053 |
-
# Hip Turn @ Impact metric removed per user request
|
| 1054 |
-
|
| 1055 |
# Additional old metrics removed - focusing on new 4-metric system
|
| 1056 |
|
| 1057 |
-
# Front-facing metrics (only displayed when available) - 4 required metrics
|
| 1058 |
if has_front_facing_metrics:
|
| 1059 |
# Front-facing metrics are now always calculated
|
| 1060 |
pass
|
|
@@ -1067,7 +1086,13 @@ def display_new_grading_scheme(core_metrics):
|
|
| 1067 |
if grading:
|
| 1068 |
metrics_to_display.append(("Shoulder Tilt @ Impact", grading))
|
| 1069 |
|
| 1070 |
-
# Hip Turn at Impact
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1071 |
|
| 1072 |
# Hip Sway at Top
|
| 1073 |
hip_sway_top_data = core_metrics.get("hip_sway_top_inches", {})
|
|
@@ -1184,8 +1209,6 @@ def get_metric_evaluation(metric_name, grading):
|
|
| 1184 |
else:
|
| 1185 |
return f"Your hip depth of **{value}** indicates early extension issues. This movement pattern reduces power transfer and can cause inconsistent contact. Focus on maintaining spine angle and proper hip position throughout the downswing and impact."
|
| 1186 |
|
| 1187 |
-
# Hip Turn @ Impact metric removed per user request
|
| 1188 |
-
|
| 1189 |
# FRONT-FACING METRICS (4 current metrics)
|
| 1190 |
elif metric_name == "Shoulder Tilt @ Impact":
|
| 1191 |
if "🟢" in badge:
|
|
@@ -1195,6 +1218,14 @@ def get_metric_evaluation(metric_name, grading):
|
|
| 1195 |
else:
|
| 1196 |
return f"Your shoulder tilt of **{value}** at impact needs attention. Proper shoulder angle at impact is crucial for power transfer and ball flight control. Work on impact position for better contact and consistency."
|
| 1197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1198 |
elif metric_name == "Hip Sway @ Top":
|
| 1199 |
if "🟢" in badge:
|
| 1200 |
return f"Your hip sway of **{value}** at the top shows excellent stability. Minimal lateral movement maintains proper balance and swing center, promoting consistent contact and accuracy. This stable foundation supports powerful, controlled swings."
|
|
@@ -1312,14 +1343,14 @@ Timing Metrics:
|
|
| 1312 |
- Knee Flexion @ Setup: {format_metric_value(core_metrics.get('knee_flexion_deg', {}), '°')}
|
| 1313 |
- Head Movement @ Top: {format_metric_value(core_metrics.get('head_drop_top_pct', {}), '%')}
|
| 1314 |
- Hip Depth / Early Extension: {format_metric_value(core_metrics.get('hip_depth_early_extension', {}), '%')}
|
| 1315 |
-
# Hip Turn @ Impact metric
|
| 1316 |
|
| 1317 |
=== DTL-LIMITED METRICS (Approximate) ===
|
| 1318 |
- Shoulder Turn Quality: {format_metric_value(core_metrics.get('shoulder_turn_quality', {}))}
|
| 1319 |
|
| 1320 |
=== FRONT-FACING METRICS ===
|
| 1321 |
- Shoulder Tilt @ Impact: {format_metric_value(core_metrics.get('shoulder_tilt_impact_deg', {}), '°')}
|
| 1322 |
-
# Hip Turn @ Impact metric
|
| 1323 |
- Hip Sway @ Top: {format_metric_value(core_metrics.get('hip_sway_top_inches', {}), '"')}
|
| 1324 |
- Wrist Hinge @ Top: {format_metric_value(core_metrics.get('wrist_hinge_top_deg', {}), '°')}
|
| 1325 |
"""
|
|
@@ -2171,14 +2202,14 @@ def render_step_4():
|
|
| 2171 |
shoulder_tilt_status = core_metrics.get("shoulder_tilt_impact_deg", {}).get('status', 'n/a')
|
| 2172 |
st.caption(f"Shoulder Tilt @ Impact: {shoulder_tilt_raw}° ({shoulder_tilt_status})")
|
| 2173 |
|
| 2174 |
-
# Hip turn data
|
| 2175 |
|
| 2176 |
# Hip depth raw value
|
| 2177 |
hip_depth_raw = core_metrics.get("hip_depth_early_extension", {}).get('value')
|
| 2178 |
hip_depth_status = core_metrics.get("hip_depth_early_extension", {}).get('status', 'n/a')
|
| 2179 |
st.caption(f"Hip Depth / Early Extension: {hip_depth_raw} ({hip_depth_status})")
|
| 2180 |
|
| 2181 |
-
# Hip turn debug info
|
| 2182 |
if has_front_facing_metrics:
|
| 2183 |
st.caption("Front-facing metrics detected")
|
| 2184 |
else:
|
|
|
|
| 781 |
}
|
| 782 |
|
| 783 |
|
| 784 |
+
def get_hip_turn_impact_grading(value, confidence):
|
| 785 |
+
"""Grade hip turn at impact - professional = 36 degrees, 30 handicap = 19 degrees"""
|
| 786 |
+
if value is None:
|
| 787 |
+
return {'display_value': 'n/a', 'badge': '⚪', 'status': 'No data available'}
|
| 788 |
+
|
| 789 |
+
# Professional = 36°, 30 handicap = 19°
|
| 790 |
+
if 30 <= value <= 45:
|
| 791 |
+
badge = '🟢'
|
| 792 |
+
label = 'Excellent turn'
|
| 793 |
+
elif 20 <= value < 30 or 45 < value <= 55:
|
| 794 |
+
badge = '🟠'
|
| 795 |
+
label = 'Good turn'
|
| 796 |
+
else:
|
| 797 |
+
badge = '🔴'
|
| 798 |
+
label = 'Needs improvement'
|
| 799 |
+
|
| 800 |
+
return {
|
| 801 |
+
'display_value': f'{value:.1f}°',
|
| 802 |
+
'badge': badge,
|
| 803 |
+
'label': label,
|
| 804 |
+
'confidence': confidence
|
| 805 |
+
}
|
| 806 |
|
| 807 |
|
| 808 |
def get_hip_sway_grading(value, confidence, position="top"):
|
|
|
|
| 909 |
has_front_facing_metrics = any(key in core_metrics for key in [
|
| 910 |
'shoulder_tilt_impact_deg', 'hip_sway_top_inches', 'wrist_hinge_top_deg'
|
| 911 |
])
|
| 912 |
+
# Hip turn check for front-facing metrics
|
| 913 |
|
| 914 |
if has_front_facing_metrics:
|
| 915 |
st.subheader("Swing Analysis")
|
|
|
|
| 923 |
knee_flexion_data = core_metrics.get("knee_flexion_deg", {})
|
| 924 |
head_drop_data = core_metrics.get("head_drop_top_pct", {})
|
| 925 |
hip_depth_data = core_metrics.get("hip_depth_early_extension", {})
|
| 926 |
+
# Hip turn data for front-facing metrics
|
| 927 |
|
| 928 |
# Calculate confidence with QC penalties as per feedback
|
| 929 |
def get_confidence(data, metric_type='general'):
|
|
|
|
| 1071 |
else:
|
| 1072 |
pass # Hip depth calculation succeeded but no special handling needed
|
| 1073 |
|
|
|
|
|
|
|
| 1074 |
# Additional old metrics removed - focusing on new 4-metric system
|
| 1075 |
|
| 1076 |
+
# Front-facing metrics (only displayed when available) - 4 required metrics
|
| 1077 |
if has_front_facing_metrics:
|
| 1078 |
# Front-facing metrics are now always calculated
|
| 1079 |
pass
|
|
|
|
| 1086 |
if grading:
|
| 1087 |
metrics_to_display.append(("Shoulder Tilt @ Impact", grading))
|
| 1088 |
|
| 1089 |
+
# Hip Turn at Impact
|
| 1090 |
+
hip_turn_impact_data = core_metrics.get("hip_turn_impact_deg", {})
|
| 1091 |
+
if hip_turn_impact_data.get('value') is not None:
|
| 1092 |
+
confidence = 0.8 # High confidence for front-facing measurements
|
| 1093 |
+
grading = get_hip_turn_impact_grading(hip_turn_impact_data['value'], confidence)
|
| 1094 |
+
if grading:
|
| 1095 |
+
metrics_to_display.append(("Hip Turn @ Impact", grading))
|
| 1096 |
|
| 1097 |
# Hip Sway at Top
|
| 1098 |
hip_sway_top_data = core_metrics.get("hip_sway_top_inches", {})
|
|
|
|
| 1209 |
else:
|
| 1210 |
return f"Your hip depth of **{value}** indicates early extension issues. This movement pattern reduces power transfer and can cause inconsistent contact. Focus on maintaining spine angle and proper hip position throughout the downswing and impact."
|
| 1211 |
|
|
|
|
|
|
|
| 1212 |
# FRONT-FACING METRICS (4 current metrics)
|
| 1213 |
elif metric_name == "Shoulder Tilt @ Impact":
|
| 1214 |
if "🟢" in badge:
|
|
|
|
| 1218 |
else:
|
| 1219 |
return f"Your shoulder tilt of **{value}** at impact needs attention. Proper shoulder angle at impact is crucial for power transfer and ball flight control. Work on impact position for better contact and consistency."
|
| 1220 |
|
| 1221 |
+
elif metric_name == "Hip Turn @ Impact":
|
| 1222 |
+
if "🟢" in badge:
|
| 1223 |
+
return f"Your hip turn of **{value}** at impact shows excellent rotation. This proper hip movement indicates ideal power transfer and body sequencing. Good hip turn at impact promotes solid contact, optimal ball flight, and consistent distance control."
|
| 1224 |
+
elif "🟠" in badge:
|
| 1225 |
+
return f"Your hip turn of **{value}** at impact is acceptable with room for improvement. Hip rotation at impact affects power transfer and ball flight characteristics. Refining your hip movement can enhance consistency and distance."
|
| 1226 |
+
else:
|
| 1227 |
+
return f"Your hip turn of **{value}** at impact needs attention. Proper hip rotation at impact is crucial for power transfer and ball flight control. Work on hip movement for better contact and consistency."
|
| 1228 |
+
|
| 1229 |
elif metric_name == "Hip Sway @ Top":
|
| 1230 |
if "🟢" in badge:
|
| 1231 |
return f"Your hip sway of **{value}** at the top shows excellent stability. Minimal lateral movement maintains proper balance and swing center, promoting consistent contact and accuracy. This stable foundation supports powerful, controlled swings."
|
|
|
|
| 1343 |
- Knee Flexion @ Setup: {format_metric_value(core_metrics.get('knee_flexion_deg', {}), '°')}
|
| 1344 |
- Head Movement @ Top: {format_metric_value(core_metrics.get('head_drop_top_pct', {}), '%')}
|
| 1345 |
- Hip Depth / Early Extension: {format_metric_value(core_metrics.get('hip_depth_early_extension', {}), '%')}
|
| 1346 |
+
# Hip Turn @ Impact metric restored for front-facing analysis
|
| 1347 |
|
| 1348 |
=== DTL-LIMITED METRICS (Approximate) ===
|
| 1349 |
- Shoulder Turn Quality: {format_metric_value(core_metrics.get('shoulder_turn_quality', {}))}
|
| 1350 |
|
| 1351 |
=== FRONT-FACING METRICS ===
|
| 1352 |
- Shoulder Tilt @ Impact: {format_metric_value(core_metrics.get('shoulder_tilt_impact_deg', {}), '°')}
|
| 1353 |
+
# Hip Turn @ Impact metric restored for front-facing analysis
|
| 1354 |
- Hip Sway @ Top: {format_metric_value(core_metrics.get('hip_sway_top_inches', {}), '"')}
|
| 1355 |
- Wrist Hinge @ Top: {format_metric_value(core_metrics.get('wrist_hinge_top_deg', {}), '°')}
|
| 1356 |
"""
|
|
|
|
| 2202 |
shoulder_tilt_status = core_metrics.get("shoulder_tilt_impact_deg", {}).get('status', 'n/a')
|
| 2203 |
st.caption(f"Shoulder Tilt @ Impact: {shoulder_tilt_raw}° ({shoulder_tilt_status})")
|
| 2204 |
|
| 2205 |
+
# Hip turn data for front-facing metrics
|
| 2206 |
|
| 2207 |
# Hip depth raw value
|
| 2208 |
hip_depth_raw = core_metrics.get("hip_depth_early_extension", {}).get('value')
|
| 2209 |
hip_depth_status = core_metrics.get("hip_depth_early_extension", {}).get('status', 'n/a')
|
| 2210 |
st.caption(f"Hip Depth / Early Extension: {hip_depth_raw} ({hip_depth_status})")
|
| 2211 |
|
| 2212 |
+
# Hip turn debug info for front-facing metrics
|
| 2213 |
if has_front_facing_metrics:
|
| 2214 |
st.caption("Front-facing metrics detected")
|
| 2215 |
else:
|