Spaces:
Paused
Paused
Fix head drop DTL metric and clean up feedback interface
Browse files- Fix head drop DTL metric sign and labeling logic
- Change metric name from 'Head Drop @ Top' to 'Head Movement @ Top'
- Implement proper direction display (drop/rise) and absolute value grading
- Apply suggested rubric: ≤3% excellent, 3-6% good, 6-10% borderline, >10% excessive
- Remove tips and confidence percentages from feedback interface
- Clean up all grading functions to remove tip generation
- Simplify metric display to show only essential information
- app/models/llm_analyzer.py +18 -0
- app/models/metrics_calculator.py +129 -58
- app/streamlit_app.py +76 -100
app/models/llm_analyzer.py
CHANGED
|
@@ -47,6 +47,23 @@ except ImportError:
|
|
| 47 |
return {'value': value, 'status': 'n/a', 'badge': '⚪'}
|
| 48 |
def get_wrist_hinge_grading(value, confidence, position):
|
| 49 |
return {'value': value, 'status': 'n/a', 'badge': '⚪'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
def safe_fmt_deg(v):
|
|
@@ -184,6 +201,7 @@ def compute_dtl_core_metrics(pose_data, swing_phases, frame_timestamps_ms=None,
|
|
| 184 |
'shoulder_plane_top_deg': ('shoulder_tilt_swing_plane_top_deg', get_shoulder_tilt_swing_plane_grading),
|
| 185 |
'back_tilt_setup_deg': ('back_tilt_deg', get_back_tilt_grading),
|
| 186 |
'knee_flexion_deg': ('knee_flexion_deg', get_knee_flexion_grading),
|
|
|
|
| 187 |
# Hip turn mapping removed per user request
|
| 188 |
}
|
| 189 |
|
|
|
|
| 47 |
return {'value': value, 'status': 'n/a', 'badge': '⚪'}
|
| 48 |
def get_wrist_hinge_grading(value, confidence, position):
|
| 49 |
return {'value': value, 'status': 'n/a', 'badge': '⚪'}
|
| 50 |
+
def get_head_drop_grading(value, confidence):
|
| 51 |
+
"""Grade head drop metric based on percentage of torso length
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
value: percentage value (positive = head moved down)
|
| 55 |
+
confidence: confidence score (not used in current implementation)
|
| 56 |
+
"""
|
| 57 |
+
if value is None:
|
| 58 |
+
return {'value': None, 'status': 'error', 'badge': '❌'}
|
| 59 |
+
|
| 60 |
+
# Address → Top: ~2–6% typical; >8% = too much "sit"
|
| 61 |
+
if 2 <= value <= 6:
|
| 62 |
+
return {'value': f"{value:.1f}%", 'status': 'good', 'badge': '✅'}
|
| 63 |
+
elif 6 < value <= 8:
|
| 64 |
+
return {'value': f"{value:.1f}%", 'status': 'caution', 'badge': '⚠️'}
|
| 65 |
+
else:
|
| 66 |
+
return {'value': f"{value:.1f}%", 'status': 'poor', 'badge': '❌'}
|
| 67 |
|
| 68 |
|
| 69 |
def safe_fmt_deg(v):
|
|
|
|
| 201 |
'shoulder_plane_top_deg': ('shoulder_tilt_swing_plane_top_deg', get_shoulder_tilt_swing_plane_grading),
|
| 202 |
'back_tilt_setup_deg': ('back_tilt_deg', get_back_tilt_grading),
|
| 203 |
'knee_flexion_deg': ('knee_flexion_deg', get_knee_flexion_grading),
|
| 204 |
+
'head_drop_top_pct': ('head_drop_top_pct', get_head_drop_grading),
|
| 205 |
# Hip turn mapping removed per user request
|
| 206 |
}
|
| 207 |
|
app/models/metrics_calculator.py
CHANGED
|
@@ -14,15 +14,68 @@ L_EYE, R_EYE = 2, 5
|
|
| 14 |
L_EAR, R_EAR = 7, 8
|
| 15 |
NOSE = 0
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# Enable debug output for head height calculations
|
| 18 |
VERBOSE = True
|
| 19 |
|
|
|
|
|
|
|
|
|
|
| 20 |
def dbg(msg):
|
| 21 |
"""Debug print helper"""
|
| 22 |
if VERBOSE:
|
| 23 |
print(f"[DEBUG] {msg}")
|
| 24 |
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
def to_pixels_if_needed(kp, frame_w=None, frame_h=None):
|
| 27 |
"""Convert keypoints to pixels if they appear normalized (0..1).
|
| 28 |
Works even if frame_w/frame_h are None by assuming a sane default size.
|
|
@@ -318,26 +371,23 @@ def _median_head_y(frames_lm, idx, k=5, vis_thr=0.5):
|
|
| 318 |
return float(np.median(hys))
|
| 319 |
|
| 320 |
|
| 321 |
-
def
|
| 322 |
-
|
|
|
|
| 323 |
|
| 324 |
-
This measures head
|
|
|
|
| 325 |
|
| 326 |
Args:
|
| 327 |
frames_lm: list of pose landmarks for all frames (pose_data as list)
|
| 328 |
addr_idx: address frame index
|
| 329 |
-
top_idx: top of backswing frame index
|
| 330 |
-
|
| 331 |
-
inch_scale: pixels per inch conversion factor
|
| 332 |
vis_thr: visibility threshold
|
| 333 |
|
| 334 |
Returns:
|
| 335 |
-
|
| 336 |
"""
|
| 337 |
-
dbg(f"=== HEAD HEIGHT CHANGE CALCULATION ===")
|
| 338 |
-
dbg(f"Frame indices - Address: {addr_idx}, Top: {top_idx}, Impact: {imp_idx}")
|
| 339 |
-
dbg(f"Inch scale: {inch_scale:.3f} px/in, Visibility threshold: {vis_thr}")
|
| 340 |
-
|
| 341 |
# Convert pose_data dict to list format for compatibility
|
| 342 |
if isinstance(frames_lm, dict):
|
| 343 |
max_frame = max(frames_lm.keys()) if frames_lm else 0
|
|
@@ -345,53 +395,57 @@ def head_height_change(frames_lm, addr_idx, top_idx, imp_idx, inch_scale, vis_th
|
|
| 345 |
for frame_idx, landmarks in frames_lm.items():
|
| 346 |
frames_list[frame_idx] = landmarks
|
| 347 |
frames_lm = frames_list
|
| 348 |
-
dbg(f"Converted pose_data dict to list format ({len(frames_list)} frames)")
|
| 349 |
|
|
|
|
| 350 |
y_addr = _median_head_y(frames_lm, addr_idx, vis_thr=vis_thr)
|
| 351 |
y_top = _median_head_y(frames_lm, top_idx, vis_thr=vis_thr)
|
| 352 |
-
y_imp = _median_head_y(frames_lm, imp_idx, vis_thr=vis_thr)
|
| 353 |
|
| 354 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
|
| 356 |
-
if None in (y_addr, y_imp):
|
| 357 |
-
dbg(f"❌ Missing head Y coordinates - cannot calculate head height change")
|
| 358 |
-
return None
|
| 359 |
-
|
| 360 |
-
if not inch_scale or inch_scale <= 0:
|
| 361 |
-
dbg(f"❌ Invalid inch scale ({inch_scale}) - cannot calculate head height change")
|
| 362 |
-
return None
|
| 363 |
|
| 364 |
-
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
-
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
dbg(f" Address → Top: {y_top - y_addr:+.1f} px = {drop_top_in:+.2f} inches (debug only)")
|
| 373 |
-
dbg(f" Address → Impact: {y_imp - y_addr:+.1f} px = {head_change_in:+.2f} inches (PRIMARY METRIC)")
|
| 374 |
|
| 375 |
-
#
|
| 376 |
-
if
|
| 377 |
-
|
| 378 |
-
elif abs(head_change_in) <= 2.5:
|
| 379 |
-
dbg(f"⚠️ Moderate head movement ({head_change_in:+.2f}\") - caution zone")
|
| 380 |
else:
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
if drop_top_in is not None:
|
| 385 |
-
if drop_top_in > 4.0:
|
| 386 |
-
dbg(f"⚠️ Excessive head drop at top ({drop_top_in:.2f}\") - too much 'sit' (debug)")
|
| 387 |
-
elif 1.0 <= drop_top_in <= 3.0:
|
| 388 |
-
dbg(f"✅ Good head drop at top ({drop_top_in:.2f}\") - within typical range (debug)")
|
| 389 |
-
elif drop_top_in < 0:
|
| 390 |
-
dbg(f"⚠️ Head rose at top ({drop_top_in:.2f}\") - unusual movement (debug)")
|
| 391 |
-
|
| 392 |
-
dbg(f"=== HEAD HEIGHT CALCULATION COMPLETE ===")
|
| 393 |
-
|
| 394 |
-
return head_change_in
|
| 395 |
|
| 396 |
|
| 397 |
def calculate_inch_scale_from_shoulders(pose_data, address_idx, frame_w=None, frame_h=None, assumed_shoulder_width_in=15.5):
|
|
@@ -1384,13 +1438,14 @@ def validate_metric_ranges(metrics):
|
|
| 1384 |
|
| 1385 |
|
| 1386 |
def compute_dtl_three(pose_data, swing_phases, frame_w, frame_h, frames=None, camera_side="trail"):
|
| 1387 |
-
"""Compute DTL metrics with
|
| 1388 |
|
| 1389 |
Metrics included:
|
| 1390 |
- Shoulder plane at top (degrees)
|
| 1391 |
- Back tilt at address (degrees)
|
| 1392 |
- Knee flexion at address (degrees)
|
| 1393 |
-
- Head
|
|
|
|
| 1394 |
|
| 1395 |
Args:
|
| 1396 |
pose_data (dict): Dictionary mapping frame indices to pose keypoints
|
|
@@ -1401,7 +1456,7 @@ def compute_dtl_three(pose_data, swing_phases, frame_w, frame_h, frames=None, ca
|
|
| 1401 |
camera_side (str): "trail" or "lead" - which side camera is on
|
| 1402 |
|
| 1403 |
Returns:
|
| 1404 |
-
dict: DTL metrics
|
| 1405 |
"""
|
| 1406 |
setup_frames = swing_phases.get("setup", [])
|
| 1407 |
backswing_frames = swing_phases.get("backswing", [])
|
|
@@ -1424,14 +1479,27 @@ def compute_dtl_three(pose_data, swing_phases, frame_w, frame_h, frames=None, ca
|
|
| 1424 |
knee_data = calculate_knee_bend_degree(pose_data, addr_idx)
|
| 1425 |
knee_avg = knee_data.get("average_flexion") if knee_data else None
|
| 1426 |
|
| 1427 |
-
# Calculate head
|
| 1428 |
-
|
| 1429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1430 |
head_height_change_in = None
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
|
| 1434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1435 |
|
| 1436 |
# Hip depth/early extension metric removed per user request
|
| 1437 |
ee_pct = None
|
|
@@ -1442,6 +1510,9 @@ def compute_dtl_three(pose_data, swing_phases, frame_w, frame_h, frames=None, ca
|
|
| 1442 |
"shoulder_plane_top_deg": shoulder_top,
|
| 1443 |
"back_tilt_setup_deg": back_tilt,
|
| 1444 |
"knee_flexion_deg": knee_avg,
|
|
|
|
|
|
|
|
|
|
| 1445 |
"head_height_change_in": round(head_height_change_in, 2) if head_height_change_in is not None else None,
|
| 1446 |
"inch_scale": round(inch_scale, 3) if inch_scale is not None else None,
|
| 1447 |
# Hip depth and hip turn metrics removed per user request
|
|
|
|
| 14 |
L_EAR, R_EAR = 7, 8
|
| 15 |
NOSE = 0
|
| 16 |
|
| 17 |
+
# Shoulder and hip landmark indices for torso length calculation
|
| 18 |
+
L_SHO, R_SHO = 11, 12
|
| 19 |
+
L_HIP, R_HIP = 23, 24
|
| 20 |
+
|
| 21 |
# Enable debug output for head height calculations
|
| 22 |
VERBOSE = True
|
| 23 |
|
| 24 |
+
# Minimum pixels per inch threshold for reliable scale detection
|
| 25 |
+
MIN_PX_PER_IN = 2.5 # anything below this is unreliable (DTL foreshortening, wide shot, etc.)
|
| 26 |
+
|
| 27 |
def dbg(msg):
|
| 28 |
"""Debug print helper"""
|
| 29 |
if VERBOSE:
|
| 30 |
print(f"[DEBUG] {msg}")
|
| 31 |
|
| 32 |
|
| 33 |
+
def _midy(lm, a, b):
|
| 34 |
+
"""Calculate midpoint y coordinate between two landmarks"""
|
| 35 |
+
if (lm is None or a >= len(lm) or b >= len(lm) or
|
| 36 |
+
lm[a] is None or lm[b] is None or
|
| 37 |
+
len(lm[a]) < 2 or len(lm[b]) < 2):
|
| 38 |
+
return None
|
| 39 |
+
return 0.5 * (lm[a][1] + lm[b][1])
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _torso_len_px(lm):
|
| 43 |
+
"""Calculate torso length in pixels from shoulder to hip midpoint"""
|
| 44 |
+
y_sho = _midy(lm, L_SHO, R_SHO)
|
| 45 |
+
y_hip = _midy(lm, L_HIP, R_HIP)
|
| 46 |
+
if y_sho is None or y_hip is None:
|
| 47 |
+
return None
|
| 48 |
+
return abs(y_sho - y_hip)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def safe_inch_scale_from_shoulders(lm, assumed_shoulder_in=15.5):
|
| 52 |
+
"""Calculate inch scale from shoulders with safety checks for DTL foreshortening
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
lm: pose landmarks for a single frame
|
| 56 |
+
assumed_shoulder_in: assumed shoulder width in inches (default 15.5")
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
tuple: (px_per_in, error_reason) where error_reason is None if successful
|
| 60 |
+
"""
|
| 61 |
+
# DTL shoulders are foreshortened → this often returns too small; guard with MIN_PX_PER_IN
|
| 62 |
+
try:
|
| 63 |
+
if (lm is None or R_SHO >= len(lm) or L_SHO >= len(lm) or
|
| 64 |
+
lm[R_SHO] is None or lm[L_SHO] is None or
|
| 65 |
+
len(lm[R_SHO]) < 2 or len(lm[L_SHO]) < 2):
|
| 66 |
+
return None, "no-shoulders"
|
| 67 |
+
|
| 68 |
+
sw_px = ((lm[R_SHO][0] - lm[L_SHO][0])**2 + (lm[R_SHO][1] - lm[L_SHO][1])**2) ** 0.5
|
| 69 |
+
except Exception:
|
| 70 |
+
return None, "no-shoulders"
|
| 71 |
+
if sw_px <= 0:
|
| 72 |
+
return None, "zero-width"
|
| 73 |
+
px_per_in = sw_px / float(assumed_shoulder_in)
|
| 74 |
+
if px_per_in < MIN_PX_PER_IN:
|
| 75 |
+
return None, f"foreshortened (px/in={px_per_in:.2f})"
|
| 76 |
+
return px_per_in, None
|
| 77 |
+
|
| 78 |
+
|
| 79 |
def to_pixels_if_needed(kp, frame_w=None, frame_h=None):
|
| 80 |
"""Convert keypoints to pixels if they appear normalized (0..1).
|
| 81 |
Works even if frame_w/frame_h are None by assuming a sane default size.
|
|
|
|
| 371 |
return float(np.median(hys))
|
| 372 |
|
| 373 |
|
| 374 |
+
def head_height_change_normalized(frames_lm, addr_idx, top_idx,
|
| 375 |
+
assumed_shoulder_in=15.5, vis_thr=0.5):
|
| 376 |
+
"""Calculate head drop at top using normalized DTL metrics (percentage of torso length)
|
| 377 |
|
| 378 |
+
This measures head drop during the backswing using torso length as the scale,
|
| 379 |
+
which is stable in DTL view unlike shoulder width which suffers from foreshortening.
|
| 380 |
|
| 381 |
Args:
|
| 382 |
frames_lm: list of pose landmarks for all frames (pose_data as list)
|
| 383 |
addr_idx: address frame index
|
| 384 |
+
top_idx: top of backswing frame index
|
| 385 |
+
assumed_shoulder_in: assumed shoulder width in inches for fallback inch calculation
|
|
|
|
| 386 |
vis_thr: visibility threshold
|
| 387 |
|
| 388 |
Returns:
|
| 389 |
+
dict: Head drop data with percentages and optional inches
|
| 390 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
# Convert pose_data dict to list format for compatibility
|
| 392 |
if isinstance(frames_lm, dict):
|
| 393 |
max_frame = max(frames_lm.keys()) if frames_lm else 0
|
|
|
|
| 395 |
for frame_idx, landmarks in frames_lm.items():
|
| 396 |
frames_list[frame_idx] = landmarks
|
| 397 |
frames_lm = frames_list
|
|
|
|
| 398 |
|
| 399 |
+
# Get median head y's (existing helpers)
|
| 400 |
y_addr = _median_head_y(frames_lm, addr_idx, vis_thr=vis_thr)
|
| 401 |
y_top = _median_head_y(frames_lm, top_idx, vis_thr=vis_thr)
|
|
|
|
| 402 |
|
| 403 |
+
if None in (y_addr, y_top):
|
| 404 |
+
return {"ok": False, "reason": "head-visibility"}
|
| 405 |
+
|
| 406 |
+
lm_addr = frames_lm[addr_idx]
|
| 407 |
+
tlen = _torso_len_px(lm_addr)
|
| 408 |
+
if tlen is None or tlen <= 1:
|
| 409 |
+
return {"ok": False, "reason": "torso-too-small"}
|
| 410 |
+
|
| 411 |
+
# Primary: percentage (DTL-safe)
|
| 412 |
+
drop_top_pct = 100.0 * (y_top - y_addr) / tlen
|
| 413 |
+
|
| 414 |
+
# Secondary: inches only if scale is safe
|
| 415 |
+
px_per_in, why = safe_inch_scale_from_shoulders(lm_addr, assumed_shoulder_in)
|
| 416 |
+
inches = None
|
| 417 |
+
if px_per_in:
|
| 418 |
+
drop_top_in = (y_top - y_addr) / px_per_in
|
| 419 |
+
inches = drop_top_in
|
| 420 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
|
| 422 |
+
return {
|
| 423 |
+
"ok": True,
|
| 424 |
+
"drop_top_pct": drop_top_pct,
|
| 425 |
+
"inches": inches # may be None if scale is unsafe
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def head_height_change(frames_lm, addr_idx, top_idx, imp_idx, inch_scale, vis_thr=0.5):
|
| 430 |
+
"""DEPRECATED - use head_height_change_normalized instead
|
| 431 |
|
| 432 |
+
This function is kept for backward compatibility but will use the new normalized implementation.
|
| 433 |
+
Note: imp_idx parameter is ignored as we only calculate head drop at top now.
|
| 434 |
+
"""
|
| 435 |
+
# Use the new normalized function (ignoring imp_idx)
|
| 436 |
+
result = head_height_change_normalized(frames_lm, addr_idx, top_idx,
|
| 437 |
+
assumed_shoulder_in=15.5, vis_thr=vis_thr)
|
| 438 |
|
| 439 |
+
if not result["ok"]:
|
| 440 |
+
return None
|
|
|
|
|
|
|
| 441 |
|
| 442 |
+
# Return the head drop in inches for backward compatibility
|
| 443 |
+
if result["inches"] is not None:
|
| 444 |
+
return result["inches"] # drop_top_in
|
|
|
|
|
|
|
| 445 |
else:
|
| 446 |
+
# Fallback: convert percentage to approximate inches using torso length
|
| 447 |
+
# This is less accurate but maintains backward compatibility
|
| 448 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
|
| 450 |
|
| 451 |
def calculate_inch_scale_from_shoulders(pose_data, address_idx, frame_w=None, frame_h=None, assumed_shoulder_width_in=15.5):
|
|
|
|
| 1438 |
|
| 1439 |
|
| 1440 |
def compute_dtl_three(pose_data, swing_phases, frame_w, frame_h, frames=None, camera_side="trail"):
|
| 1441 |
+
"""Compute DTL metrics with normalized head drop using torso length scale
|
| 1442 |
|
| 1443 |
Metrics included:
|
| 1444 |
- Shoulder plane at top (degrees)
|
| 1445 |
- Back tilt at address (degrees)
|
| 1446 |
- Knee flexion at address (degrees)
|
| 1447 |
+
- Head drop at top (percentage of torso length)
|
| 1448 |
+
- Legacy inch metrics (only when scale is reliable, otherwise None)
|
| 1449 |
|
| 1450 |
Args:
|
| 1451 |
pose_data (dict): Dictionary mapping frame indices to pose keypoints
|
|
|
|
| 1456 |
camera_side (str): "trail" or "lead" - which side camera is on
|
| 1457 |
|
| 1458 |
Returns:
|
| 1459 |
+
dict: DTL metrics with normalized head drop (replaces hip turn)
|
| 1460 |
"""
|
| 1461 |
setup_frames = swing_phases.get("setup", [])
|
| 1462 |
backswing_frames = swing_phases.get("backswing", [])
|
|
|
|
| 1479 |
knee_data = calculate_knee_bend_degree(pose_data, addr_idx)
|
| 1480 |
knee_avg = knee_data.get("average_flexion") if knee_data else None
|
| 1481 |
|
| 1482 |
+
# Calculate head drop using normalized DTL metrics
|
| 1483 |
+
head_drop_data = head_height_change_normalized(
|
| 1484 |
+
pose_data, addr_idx, top_idx
|
| 1485 |
+
)
|
| 1486 |
+
|
| 1487 |
+
# Extract metrics from normalized data
|
| 1488 |
+
head_drop_top_pct = None
|
| 1489 |
head_height_change_in = None
|
| 1490 |
+
inch_scale = None
|
| 1491 |
+
|
| 1492 |
+
if head_drop_data["ok"]:
|
| 1493 |
+
head_drop_top_pct = head_drop_data["drop_top_pct"]
|
| 1494 |
+
|
| 1495 |
+
# Get inch values if available
|
| 1496 |
+
if head_drop_data["inches"] is not None:
|
| 1497 |
+
head_height_change_in = head_drop_data["inches"] # drop_top_in
|
| 1498 |
+
# Calculate inch scale from the data for backward compatibility
|
| 1499 |
+
if head_drop_data["inches"] != 0: # avoid division by zero
|
| 1500 |
+
pixel_change = _median_head_y(pose_data, top_idx) - _median_head_y(pose_data, addr_idx)
|
| 1501 |
+
if pixel_change != 0:
|
| 1502 |
+
inch_scale = abs(pixel_change / head_drop_data["inches"])
|
| 1503 |
|
| 1504 |
# Hip depth/early extension metric removed per user request
|
| 1505 |
ee_pct = None
|
|
|
|
| 1510 |
"shoulder_plane_top_deg": shoulder_top,
|
| 1511 |
"back_tilt_setup_deg": back_tilt,
|
| 1512 |
"knee_flexion_deg": knee_avg,
|
| 1513 |
+
# New normalized head drop metric (primary)
|
| 1514 |
+
"head_drop_top_pct": round(head_drop_top_pct, 2) if head_drop_top_pct is not None else None,
|
| 1515 |
+
# Legacy inch metrics (secondary, may be None if scale is unsafe)
|
| 1516 |
"head_height_change_in": round(head_height_change_in, 2) if head_height_change_in is not None else None,
|
| 1517 |
"inch_scale": round(inch_scale, 3) if inch_scale is not None else None,
|
| 1518 |
# Hip depth and hip turn metrics removed per user request
|
app/streamlit_app.py
CHANGED
|
@@ -231,7 +231,6 @@ def get_shaft_angle_grading(value, confidence):
|
|
| 231 |
'badge': badge,
|
| 232 |
'label': label,
|
| 233 |
'confidence': confidence,
|
| 234 |
-
'tip': "Target: within ±10° of the target line at the top." if badge != "🟢" else None
|
| 235 |
}
|
| 236 |
|
| 237 |
|
|
@@ -263,7 +262,6 @@ def get_head_sway_grading(value, confidence):
|
|
| 263 |
'label': label,
|
| 264 |
'confidence': confidence,
|
| 265 |
'approx': True, # DTL-limited
|
| 266 |
-
'tip': "Aim ≤25% for most players." if value > 25 else None
|
| 267 |
}
|
| 268 |
|
| 269 |
|
|
@@ -294,7 +292,6 @@ def get_back_tilt_grading(value, confidence, camera_roll=0):
|
|
| 294 |
'label': label,
|
| 295 |
'confidence': confidence,
|
| 296 |
'status': label, # Use label as status for legacy compatibility
|
| 297 |
-
'tip': "Typical tour range ≈30–40°." if badge != "🟢" else None
|
| 298 |
}
|
| 299 |
|
| 300 |
|
|
@@ -338,8 +335,7 @@ def get_wrist_pattern_grading(pattern_data, confidence):
|
|
| 338 |
'display_value': "Unstable",
|
| 339 |
'badge': "⚪",
|
| 340 |
'label': "club not visible",
|
| 341 |
-
'confidence': 0
|
| 342 |
-
'tip': None
|
| 343 |
}
|
| 344 |
|
| 345 |
# Interpret pattern - prioritize actual pattern value over status
|
|
@@ -350,30 +346,24 @@ def get_wrist_pattern_grading(pattern_data, confidence):
|
|
| 350 |
if pattern_value == 'set_hold_release':
|
| 351 |
badge = "🟢"
|
| 352 |
label = "Set-Hold-Release — Excellent"
|
| 353 |
-
tip = None
|
| 354 |
elif pattern_value == 'early_cast' or status == 'early_cast':
|
| 355 |
badge = "🔴"
|
| 356 |
label = "Early Cast — Needs work"
|
| 357 |
-
tip = "Feel more lag; try the pump drill."
|
| 358 |
elif pattern_value == 'late_set' or (status == 'needs_work' and pattern_value != 'set_hold_release'):
|
| 359 |
badge = "🟠"
|
| 360 |
label = "Late Set — Caution"
|
| 361 |
-
tip = "Pump-drill (3 holds) before release."
|
| 362 |
elif status == 'good_sequence':
|
| 363 |
badge = "🟢"
|
| 364 |
label = "Good Sequence"
|
| 365 |
-
tip = None
|
| 366 |
else:
|
| 367 |
badge = "🟠"
|
| 368 |
label = "Inconsistent Pattern"
|
| 369 |
-
tip = "Work on consistent wrist angles."
|
| 370 |
|
| 371 |
return {
|
| 372 |
'display_value': label,
|
| 373 |
'badge': badge,
|
| 374 |
'label': "Needs work", # Add descriptive text after badge
|
| 375 |
-
'confidence': confidence
|
| 376 |
-
'tip': tip
|
| 377 |
}
|
| 378 |
|
| 379 |
|
|
@@ -412,7 +402,6 @@ def get_qualitative_shaft_angle_grading(raw_value, confidence):
|
|
| 412 |
'badge': badge,
|
| 413 |
'label': description,
|
| 414 |
'confidence': confidence,
|
| 415 |
-
'tip': "Focus on plane consistency at the top" if badge == "🟠" else None
|
| 416 |
}
|
| 417 |
|
| 418 |
|
|
@@ -434,39 +423,32 @@ def get_qualitative_hip_rotation_grading(raw_value, confidence):
|
|
| 434 |
category = "High Hip Turn"
|
| 435 |
badge = "🟢"
|
| 436 |
description = "Strong hip rotation"
|
| 437 |
-
tip = "Excellent lower body engagement"
|
| 438 |
elif raw_value >= 18:
|
| 439 |
category = "Good Hip Turn"
|
| 440 |
badge = "🟢"
|
| 441 |
description = "Good hip rotation"
|
| 442 |
-
tip = None
|
| 443 |
elif raw_value >= 12:
|
| 444 |
category = "Moderate Hip Turn"
|
| 445 |
badge = "🟠"
|
| 446 |
description = "Moderate hip rotation"
|
| 447 |
-
tip = "Work on hip initiation"
|
| 448 |
elif raw_value >= 8:
|
| 449 |
category = "Limited Hip Turn"
|
| 450 |
badge = "🟠"
|
| 451 |
description = "Limited hip rotation"
|
| 452 |
-
tip = "Increase hip drive"
|
| 453 |
else:
|
| 454 |
category = "Minimal Hip Turn"
|
| 455 |
badge = "🔴"
|
| 456 |
description = "Minimal hip rotation"
|
| 457 |
-
tip = "Focus on lower body sequence"
|
| 458 |
else:
|
| 459 |
category = "Moderate"
|
| 460 |
badge = "🟠"
|
| 461 |
description = "Mixed rotation pattern"
|
| 462 |
-
tip = "Work on hip-arm sequence"
|
| 463 |
|
| 464 |
return {
|
| 465 |
'display_value': category,
|
| 466 |
'badge': badge,
|
| 467 |
'label': description,
|
| 468 |
'confidence': confidence,
|
| 469 |
-
'tip': tip
|
| 470 |
}
|
| 471 |
|
| 472 |
|
|
@@ -489,39 +471,32 @@ def get_shoulder_turn_grading(value):
|
|
| 489 |
badge = "🟢"
|
| 490 |
quality = "Excellent"
|
| 491 |
description = "Complete shoulder rotation"
|
| 492 |
-
tip = None
|
| 493 |
elif turn_desc == "normal":
|
| 494 |
badge = "🟢"
|
| 495 |
quality = "Good"
|
| 496 |
description = "Adequate shoulder turn"
|
| 497 |
-
tip = None
|
| 498 |
elif turn_desc == "small":
|
| 499 |
badge = "🟠"
|
| 500 |
quality = "Limited"
|
| 501 |
description = "Restricted shoulder turn"
|
| 502 |
-
tip = "Work on better shoulder rotation"
|
| 503 |
else:
|
| 504 |
badge = "🟠"
|
| 505 |
quality = turn_desc.title()
|
| 506 |
description = "Mixed shoulder action"
|
| 507 |
-
tip = None
|
| 508 |
else:
|
| 509 |
# Numeric assessment
|
| 510 |
if value >= 90:
|
| 511 |
badge = "🟢"
|
| 512 |
quality = "Excellent"
|
| 513 |
description = "Complete shoulder rotation"
|
| 514 |
-
tip = None
|
| 515 |
elif value >= 75:
|
| 516 |
badge = "🟢"
|
| 517 |
quality = "Good"
|
| 518 |
description = "Adequate shoulder turn"
|
| 519 |
-
tip = None
|
| 520 |
else:
|
| 521 |
badge = "🟠"
|
| 522 |
quality = "Limited"
|
| 523 |
description = "Restricted shoulder turn"
|
| 524 |
-
tip = "Work on better shoulder rotation"
|
| 525 |
|
| 526 |
return {
|
| 527 |
'display_value': quality,
|
|
@@ -560,19 +535,14 @@ def get_hip_depth_grading(hip_depth_data, confidence):
|
|
| 560 |
if depth_loss_pct < 5:
|
| 561 |
badge = '🟢'
|
| 562 |
label = 'Good depth maintenance'
|
| 563 |
-
tip = 'Excellent hip posture through impact'
|
| 564 |
elif depth_loss_pct < 15:
|
| 565 |
badge = '🟠'
|
| 566 |
label = 'Some early extension'
|
| 567 |
-
tip = 'Work on maintaining hip flexion through impact'
|
| 568 |
else:
|
| 569 |
badge = '🔴'
|
| 570 |
label = 'Early extension'
|
| 571 |
-
tip = 'Focus on maintaining hip depth through impact for better power'
|
| 572 |
|
| 573 |
-
#
|
| 574 |
-
if confidence < 0.5:
|
| 575 |
-
tip += f"\n\nTechnical: Hip width {hip_depth_data.get('hip_width_ref', 0):.1f}px, threshold {adaptive_threshold:.1f}px"
|
| 576 |
|
| 577 |
return {
|
| 578 |
'value': depth_loss_pct, # Raw value for display logic
|
|
@@ -581,7 +551,6 @@ def get_hip_depth_grading(hip_depth_data, confidence):
|
|
| 581 |
'label': label,
|
| 582 |
'confidence': confidence,
|
| 583 |
'status': label, # Use label as status for legacy compatibility
|
| 584 |
-
'tip': tip,
|
| 585 |
'calculation_confidence': calculation_confidence,
|
| 586 |
'adaptive_threshold': adaptive_threshold
|
| 587 |
}
|
|
@@ -595,22 +564,18 @@ def get_shaft_address_grading(angle_value, confidence):
|
|
| 595 |
if 45 <= angle_value <= 65:
|
| 596 |
badge = '🟢'
|
| 597 |
label = 'Good setup angle'
|
| 598 |
-
tip = 'Excellent shaft plane at address'
|
| 599 |
elif (35 <= angle_value < 45) or (65 < angle_value <= 75):
|
| 600 |
badge = '🟠'
|
| 601 |
label = 'Acceptable angle'
|
| 602 |
-
tip = 'Shaft plane is workable but could be optimized'
|
| 603 |
else:
|
| 604 |
badge = '🔴'
|
| 605 |
label = 'Needs adjustment'
|
| 606 |
-
tip = 'Work on shaft angle at address for better swing plane'
|
| 607 |
|
| 608 |
return {
|
| 609 |
'display_value': f'{angle_value:.1f}°',
|
| 610 |
'badge': badge,
|
| 611 |
'label': label,
|
| 612 |
'confidence': confidence,
|
| 613 |
-
'tip': tip
|
| 614 |
}
|
| 615 |
|
| 616 |
|
|
@@ -626,15 +591,12 @@ def get_head_displacement_grading(displacement_data, confidence):
|
|
| 626 |
if displacement_pct < 5:
|
| 627 |
badge = '🟢'
|
| 628 |
label = 'Very stable head'
|
| 629 |
-
tip = 'Excellent head stability throughout swing'
|
| 630 |
elif displacement_pct < 15:
|
| 631 |
badge = '🟠'
|
| 632 |
label = 'Some movement'
|
| 633 |
-
tip = 'Good stability with minor movement'
|
| 634 |
else:
|
| 635 |
badge = '🔴'
|
| 636 |
label = 'Excessive movement'
|
| 637 |
-
tip = 'Focus on keeping head stable during swing'
|
| 638 |
|
| 639 |
display_text = f'{displacement_pct:.1f}% {direction}' if 'displacement_pct' in displacement_data else f'{displacement_abs:.1f}px {direction}'
|
| 640 |
|
|
@@ -643,7 +605,6 @@ def get_head_displacement_grading(displacement_data, confidence):
|
|
| 643 |
'badge': badge,
|
| 644 |
'label': label,
|
| 645 |
'confidence': confidence,
|
| 646 |
-
'tip': tip
|
| 647 |
}
|
| 648 |
|
| 649 |
|
|
@@ -657,35 +618,28 @@ def get_shoulder_rotation_grading(value, confidence, position="top"):
|
|
| 657 |
if 85 <= value <= 115:
|
| 658 |
badge = '🟢'
|
| 659 |
label = 'Excellent rotation'
|
| 660 |
-
tip = 'Great shoulder turn creating power and width'
|
| 661 |
elif 75 <= value < 85 or 115 < value <= 125:
|
| 662 |
badge = '🟠'
|
| 663 |
label = 'Good rotation'
|
| 664 |
-
tip = 'Solid shoulder turn with room for optimization'
|
| 665 |
else:
|
| 666 |
badge = '🔴'
|
| 667 |
label = 'Needs improvement'
|
| 668 |
-
tip = 'Work on shoulder mobility and turn for more power'
|
| 669 |
else: # impact
|
| 670 |
if 30 <= value <= 50:
|
| 671 |
badge = '🟢'
|
| 672 |
label = 'Excellent impact position'
|
| 673 |
-
tip = 'Great unwinding through impact'
|
| 674 |
elif 20 <= value < 30 or 50 < value <= 60:
|
| 675 |
badge = '🟠'
|
| 676 |
label = 'Good impact position'
|
| 677 |
-
tip = 'Solid rotation with room for fine-tuning'
|
| 678 |
else:
|
| 679 |
badge = '🔴'
|
| 680 |
label = 'Needs improvement'
|
| 681 |
-
tip = 'Focus on proper sequence and rotation timing'
|
| 682 |
|
| 683 |
return {
|
| 684 |
'display_value': f"{value:.1f}°",
|
| 685 |
'badge': badge,
|
| 686 |
'label': label,
|
| 687 |
'confidence': confidence,
|
| 688 |
-
'tip': tip
|
| 689 |
}
|
| 690 |
|
| 691 |
|
|
@@ -699,35 +653,28 @@ def get_hip_rotation_grading(value, confidence, position="top"):
|
|
| 699 |
if 40 <= value <= 65:
|
| 700 |
badge = '🟢'
|
| 701 |
label = 'Excellent turn'
|
| 702 |
-
tip = 'Great hip turn creating coil and power'
|
| 703 |
elif 30 <= value < 40 or 65 < value <= 75:
|
| 704 |
badge = '🟠'
|
| 705 |
label = 'Good turn'
|
| 706 |
-
tip = 'Solid hip turn with room for optimization'
|
| 707 |
else:
|
| 708 |
badge = '🔴'
|
| 709 |
label = 'Needs improvement'
|
| 710 |
-
tip = 'Work on hip mobility and rotation range'
|
| 711 |
else: # impact
|
| 712 |
if 35 <= value <= 65:
|
| 713 |
badge = '🟢'
|
| 714 |
label = 'Excellent impact position'
|
| 715 |
-
tip = 'Great hip clearing through impact'
|
| 716 |
elif 25 <= value < 35 or 65 < value <= 75:
|
| 717 |
badge = '🟠'
|
| 718 |
label = 'Good impact position'
|
| 719 |
-
tip = 'Solid hip action with room for improvement'
|
| 720 |
else:
|
| 721 |
badge = '🔴'
|
| 722 |
label = 'Needs improvement'
|
| 723 |
-
tip = 'Focus on proper hip sequence and timing'
|
| 724 |
|
| 725 |
return {
|
| 726 |
'display_value': f"{value:.1f}°",
|
| 727 |
'badge': badge,
|
| 728 |
'label': label,
|
| 729 |
'confidence': confidence,
|
| 730 |
-
'tip': tip
|
| 731 |
}
|
| 732 |
|
| 733 |
|
|
@@ -741,27 +688,75 @@ def get_shoulder_tilt_swing_plane_grading(value, confidence):
|
|
| 741 |
if 30 <= value <= 40:
|
| 742 |
badge = '🟢'
|
| 743 |
label = 'Excellent plane'
|
| 744 |
-
tip = 'Great shoulder tilt creating proper swing plane'
|
| 745 |
elif 25 <= value < 30 or 40 < value <= 45:
|
| 746 |
badge = '🟠'
|
| 747 |
label = 'Good plane'
|
| 748 |
-
tip = 'Solid shoulder tilt with room for optimization'
|
| 749 |
else:
|
| 750 |
badge = '🔴'
|
| 751 |
label = 'Needs adjustment'
|
| 752 |
-
tip = 'Work on shoulder tilt for better swing plane'
|
| 753 |
|
| 754 |
return {
|
| 755 |
'value': value, # Raw value for display logic
|
| 756 |
'display_value': f'{value:.1f}°',
|
| 757 |
'badge': badge,
|
| 758 |
'label': label,
|
| 759 |
-
'tip': tip,
|
| 760 |
'confidence': confidence,
|
| 761 |
'status': label # Use label as status for legacy compatibility
|
| 762 |
}
|
| 763 |
|
| 764 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 765 |
def get_shoulder_tilt_impact_grading(value, confidence):
|
| 766 |
"""Grade shoulder tilt at impact - professional = 39°, 30 handicap = 27°"""
|
| 767 |
if value is None:
|
|
@@ -771,21 +766,17 @@ def get_shoulder_tilt_impact_grading(value, confidence):
|
|
| 771 |
if 35 <= value <= 45:
|
| 772 |
badge = '🟢'
|
| 773 |
label = 'Excellent tilt'
|
| 774 |
-
tip = 'Great shoulder tilt creating proper impact position'
|
| 775 |
elif 25 <= value < 35 or 45 < value <= 55:
|
| 776 |
badge = '🟠'
|
| 777 |
label = 'Good tilt'
|
| 778 |
-
tip = 'Solid shoulder tilt with room for optimization'
|
| 779 |
else:
|
| 780 |
badge = '🔴'
|
| 781 |
label = 'Needs improvement'
|
| 782 |
-
tip = 'Work on shoulder tilt for better impact position'
|
| 783 |
|
| 784 |
return {
|
| 785 |
'display_value': f'{value:.1f}°',
|
| 786 |
'badge': badge,
|
| 787 |
'label': label,
|
| 788 |
-
'tip': tip,
|
| 789 |
'confidence': confidence
|
| 790 |
}
|
| 791 |
|
|
@@ -803,34 +794,27 @@ def get_hip_sway_grading(value, confidence, position="top"):
|
|
| 803 |
if 3.0 <= value <= 5.0:
|
| 804 |
badge = '🟢'
|
| 805 |
label = 'Excellent sway'
|
| 806 |
-
tip = 'Great hip movement creating proper coil'
|
| 807 |
elif 2.0 <= value < 3.0 or 5.0 < value <= 6.0:
|
| 808 |
badge = '🟠'
|
| 809 |
label = 'Good sway'
|
| 810 |
-
tip = 'Solid hip movement with room for optimization'
|
| 811 |
else:
|
| 812 |
badge = '🔴'
|
| 813 |
label = 'Needs improvement'
|
| 814 |
-
tip = 'Work on hip movement for better coil and power'
|
| 815 |
else: # impact
|
| 816 |
if 2.0 <= value <= 4.0:
|
| 817 |
badge = '🟢'
|
| 818 |
label = 'Excellent sway'
|
| 819 |
-
tip = 'Great hip movement through impact'
|
| 820 |
elif 1.0 <= value < 2.0 or 4.0 < value <= 5.0:
|
| 821 |
badge = '🟠'
|
| 822 |
label = 'Good sway'
|
| 823 |
-
tip = 'Solid hip movement with room for optimization'
|
| 824 |
else:
|
| 825 |
badge = '🔴'
|
| 826 |
label = 'Needs improvement'
|
| 827 |
-
tip = 'Work on hip movement for better impact position'
|
| 828 |
|
| 829 |
return {
|
| 830 |
'display_value': f'{value:.1f}"',
|
| 831 |
'badge': badge,
|
| 832 |
'label': label,
|
| 833 |
-
'tip': tip,
|
| 834 |
'confidence': confidence
|
| 835 |
}
|
| 836 |
|
|
@@ -845,35 +829,28 @@ def get_wrist_hinge_grading(value, confidence, position="top"):
|
|
| 845 |
if 85 <= value <= 125:
|
| 846 |
badge = '🟢'
|
| 847 |
label = 'Excellent hinge'
|
| 848 |
-
tip = 'Great wrist set for power and control'
|
| 849 |
elif 70 <= value < 85 or 125 < value <= 140:
|
| 850 |
badge = '🟠'
|
| 851 |
label = 'Good hinge'
|
| 852 |
-
tip = 'Solid wrist position with room for optimization'
|
| 853 |
else:
|
| 854 |
badge = '🔴'
|
| 855 |
label = 'Needs improvement'
|
| 856 |
-
tip = 'Work on proper wrist hinge for more lag and power'
|
| 857 |
else: # impact
|
| 858 |
if 15 <= value <= 40:
|
| 859 |
badge = '🟢'
|
| 860 |
label = 'Excellent release'
|
| 861 |
-
tip = 'Great lag retention and release through impact'
|
| 862 |
elif 10 <= value < 15 or 40 < value <= 50:
|
| 863 |
badge = '🟠'
|
| 864 |
label = 'Good release'
|
| 865 |
-
tip = 'Solid release timing with room for improvement'
|
| 866 |
else:
|
| 867 |
badge = '🔴'
|
| 868 |
label = 'Needs improvement'
|
| 869 |
-
tip = 'Focus on maintaining lag longer through downswing'
|
| 870 |
|
| 871 |
return {
|
| 872 |
'display_value': f"{value:.1f}°",
|
| 873 |
'badge': badge,
|
| 874 |
'label': label,
|
| 875 |
'confidence': confidence,
|
| 876 |
-
'tip': tip
|
| 877 |
}
|
| 878 |
|
| 879 |
|
|
@@ -886,26 +863,21 @@ def get_head_lateral_sway_grading(value, confidence):
|
|
| 886 |
if abs(value) <= 5.0: # <= 5% of shoulder width
|
| 887 |
badge = '🟢'
|
| 888 |
label = 'Excellent stability'
|
| 889 |
-
tip = 'Outstanding head stability throughout swing'
|
| 890 |
elif abs(value) <= 10.0: # <= 10% of shoulder width
|
| 891 |
badge = '🟡'
|
| 892 |
label = 'Good stability'
|
| 893 |
-
tip = 'Solid head control with minor movement'
|
| 894 |
elif abs(value) <= 15.0: # <= 15% of shoulder width
|
| 895 |
badge = '🟠'
|
| 896 |
label = 'Moderate sway'
|
| 897 |
-
tip = 'Some head movement - focus on stability'
|
| 898 |
else: # > 15% of shoulder width
|
| 899 |
badge = '🔴'
|
| 900 |
label = 'Excessive sway'
|
| 901 |
-
tip = 'Work on keeping head stable for consistent contact'
|
| 902 |
|
| 903 |
return {
|
| 904 |
'display_value': f"{value:.1f}%",
|
| 905 |
'badge': badge,
|
| 906 |
'label': label,
|
| 907 |
'confidence': confidence,
|
| 908 |
-
'tip': tip
|
| 909 |
}
|
| 910 |
|
| 911 |
|
|
@@ -928,6 +900,7 @@ def display_new_grading_scheme(core_metrics):
|
|
| 928 |
shoulder_tilt_swing_plane_data = core_metrics.get("shoulder_tilt_swing_plane_top_deg", {})
|
| 929 |
back_tilt_data = core_metrics.get("back_tilt_deg", {})
|
| 930 |
knee_flexion_data = core_metrics.get("knee_flexion_deg", {})
|
|
|
|
| 931 |
hip_depth_data = core_metrics.get("hip_depth_early_extension", {})
|
| 932 |
# Hip turn data removed per user request
|
| 933 |
|
|
@@ -1041,6 +1014,14 @@ def display_new_grading_scheme(core_metrics):
|
|
| 1041 |
if knee_grading:
|
| 1042 |
metrics_to_display.append(("Knee Flexion", knee_grading))
|
| 1043 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1044 |
# Removed: Wrist Pattern, Kinematic Sequence (DTL-approx), and Shoulder Turn Quality metrics
|
| 1045 |
|
| 1046 |
# 8. Hip Depth / Early Extension (New DTL metric)
|
|
@@ -1064,7 +1045,6 @@ def display_new_grading_scheme(core_metrics):
|
|
| 1064 |
'badge': '🔴',
|
| 1065 |
'label': 'Calculation failed',
|
| 1066 |
'confidence': 0.0,
|
| 1067 |
-
'tip': f'Debug info: {error_data}'
|
| 1068 |
}
|
| 1069 |
metrics_to_display.append(("Hip Depth / Early Extension", error_grading))
|
| 1070 |
else:
|
|
@@ -1131,27 +1111,13 @@ def display_metric_card(metric_name, grading):
|
|
| 1131 |
result_line = f"**{grading['display_value']}** — {grading['badge']} {grading.get('label', '')}"
|
| 1132 |
st.markdown(result_line)
|
| 1133 |
|
| 1134 |
-
# Confidence
|
| 1135 |
-
if grading.get('confidence'):
|
| 1136 |
-
confidence_pct = int(grading['confidence'] * 100)
|
| 1137 |
-
confidence_line = f"Confidence: ~{confidence_pct}%"
|
| 1138 |
-
|
| 1139 |
-
# Add indicators as text
|
| 1140 |
-
if grading.get('confidence') and grading['confidence'] <= 0.75:
|
| 1141 |
-
confidence_line += " *approx.*"
|
| 1142 |
-
|
| 1143 |
-
if grading.get('dtl_only') or 'DTL' in metric_name or 'approx' in str(grading.get('label', '')):
|
| 1144 |
-
confidence_line += " *DTL-only*"
|
| 1145 |
-
|
| 1146 |
-
st.caption(confidence_line)
|
| 1147 |
|
| 1148 |
# Detailed evaluation text
|
| 1149 |
evaluation = get_metric_evaluation(metric_name, grading)
|
| 1150 |
st.write(evaluation)
|
| 1151 |
|
| 1152 |
-
#
|
| 1153 |
-
if grading.get('tip'):
|
| 1154 |
-
st.info(f"💡 {grading['tip']}")
|
| 1155 |
|
| 1156 |
# Add spacing
|
| 1157 |
st.write("")
|
|
@@ -1163,6 +1129,7 @@ def get_metric_definition(metric_name):
|
|
| 1163 |
"Shoulder Tilt/Swing Plane @ Top": "Measures shoulder swing plane angle at the top of backswing.",
|
| 1164 |
"Back Tilt @ Setup": "Measures spine angle from vertical at address position.",
|
| 1165 |
"Knee Flexion": "Measures knee bend angle at address position.",
|
|
|
|
| 1166 |
"Hip Depth / Early Extension": "Tracks loss of hip flexion through impact.",
|
| 1167 |
"Hip Turn @ Impact": "Measures hip rotation at ball contact.",
|
| 1168 |
"Shoulder Tilt @ Impact": "Measures shoulder angle at ball contact.",
|
|
@@ -1201,6 +1168,14 @@ def get_metric_evaluation(metric_name, grading):
|
|
| 1201 |
else:
|
| 1202 |
return f"Your knee flexion of **{value}** may be limiting your swing potential. Proper knee bend is essential for balance, power generation, and maintaining spine angle. Too little or too much knee flexion can cause balance issues and inconsistent contact."
|
| 1203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1204 |
elif metric_name == "Hip Depth / Early Extension":
|
| 1205 |
if "🟢" in badge:
|
| 1206 |
return f"Your hip depth of **{value}** shows excellent posture maintenance. This indicates you're maintaining proper spine angle and avoiding early extension through impact. Good hip depth promotes solid contact, power transfer, and consistent ball striking patterns."
|
|
@@ -1335,6 +1310,7 @@ Timing Metrics:
|
|
| 1335 |
- Shoulder Tilt/Swing Plane @ Top: {format_metric_value(core_metrics.get('shoulder_tilt_swing_plane_top_deg', {}), '°')}
|
| 1336 |
- Back Tilt @ Setup: {format_metric_value(core_metrics.get('back_tilt_deg', {}), '°')}
|
| 1337 |
- Knee Flexion @ Setup: {format_metric_value(core_metrics.get('knee_flexion_deg', {}), '°')}
|
|
|
|
| 1338 |
- Hip Depth / Early Extension: {format_metric_value(core_metrics.get('hip_depth_early_extension', {}), '%')}
|
| 1339 |
# Hip Turn @ Impact metric removed per user request
|
| 1340 |
|
|
|
|
| 231 |
'badge': badge,
|
| 232 |
'label': label,
|
| 233 |
'confidence': confidence,
|
|
|
|
| 234 |
}
|
| 235 |
|
| 236 |
|
|
|
|
| 262 |
'label': label,
|
| 263 |
'confidence': confidence,
|
| 264 |
'approx': True, # DTL-limited
|
|
|
|
| 265 |
}
|
| 266 |
|
| 267 |
|
|
|
|
| 292 |
'label': label,
|
| 293 |
'confidence': confidence,
|
| 294 |
'status': label, # Use label as status for legacy compatibility
|
|
|
|
| 295 |
}
|
| 296 |
|
| 297 |
|
|
|
|
| 335 |
'display_value': "Unstable",
|
| 336 |
'badge': "⚪",
|
| 337 |
'label': "club not visible",
|
| 338 |
+
'confidence': 0
|
|
|
|
| 339 |
}
|
| 340 |
|
| 341 |
# Interpret pattern - prioritize actual pattern value over status
|
|
|
|
| 346 |
if pattern_value == 'set_hold_release':
|
| 347 |
badge = "🟢"
|
| 348 |
label = "Set-Hold-Release — Excellent"
|
|
|
|
| 349 |
elif pattern_value == 'early_cast' or status == 'early_cast':
|
| 350 |
badge = "🔴"
|
| 351 |
label = "Early Cast — Needs work"
|
|
|
|
| 352 |
elif pattern_value == 'late_set' or (status == 'needs_work' and pattern_value != 'set_hold_release'):
|
| 353 |
badge = "🟠"
|
| 354 |
label = "Late Set — Caution"
|
|
|
|
| 355 |
elif status == 'good_sequence':
|
| 356 |
badge = "🟢"
|
| 357 |
label = "Good Sequence"
|
|
|
|
| 358 |
else:
|
| 359 |
badge = "🟠"
|
| 360 |
label = "Inconsistent Pattern"
|
|
|
|
| 361 |
|
| 362 |
return {
|
| 363 |
'display_value': label,
|
| 364 |
'badge': badge,
|
| 365 |
'label': "Needs work", # Add descriptive text after badge
|
| 366 |
+
'confidence': confidence
|
|
|
|
| 367 |
}
|
| 368 |
|
| 369 |
|
|
|
|
| 402 |
'badge': badge,
|
| 403 |
'label': description,
|
| 404 |
'confidence': confidence,
|
|
|
|
| 405 |
}
|
| 406 |
|
| 407 |
|
|
|
|
| 423 |
category = "High Hip Turn"
|
| 424 |
badge = "🟢"
|
| 425 |
description = "Strong hip rotation"
|
|
|
|
| 426 |
elif raw_value >= 18:
|
| 427 |
category = "Good Hip Turn"
|
| 428 |
badge = "🟢"
|
| 429 |
description = "Good hip rotation"
|
|
|
|
| 430 |
elif raw_value >= 12:
|
| 431 |
category = "Moderate Hip Turn"
|
| 432 |
badge = "🟠"
|
| 433 |
description = "Moderate hip rotation"
|
|
|
|
| 434 |
elif raw_value >= 8:
|
| 435 |
category = "Limited Hip Turn"
|
| 436 |
badge = "🟠"
|
| 437 |
description = "Limited hip rotation"
|
|
|
|
| 438 |
else:
|
| 439 |
category = "Minimal Hip Turn"
|
| 440 |
badge = "🔴"
|
| 441 |
description = "Minimal hip rotation"
|
|
|
|
| 442 |
else:
|
| 443 |
category = "Moderate"
|
| 444 |
badge = "🟠"
|
| 445 |
description = "Mixed rotation pattern"
|
|
|
|
| 446 |
|
| 447 |
return {
|
| 448 |
'display_value': category,
|
| 449 |
'badge': badge,
|
| 450 |
'label': description,
|
| 451 |
'confidence': confidence,
|
|
|
|
| 452 |
}
|
| 453 |
|
| 454 |
|
|
|
|
| 471 |
badge = "🟢"
|
| 472 |
quality = "Excellent"
|
| 473 |
description = "Complete shoulder rotation"
|
|
|
|
| 474 |
elif turn_desc == "normal":
|
| 475 |
badge = "🟢"
|
| 476 |
quality = "Good"
|
| 477 |
description = "Adequate shoulder turn"
|
|
|
|
| 478 |
elif turn_desc == "small":
|
| 479 |
badge = "🟠"
|
| 480 |
quality = "Limited"
|
| 481 |
description = "Restricted shoulder turn"
|
|
|
|
| 482 |
else:
|
| 483 |
badge = "🟠"
|
| 484 |
quality = turn_desc.title()
|
| 485 |
description = "Mixed shoulder action"
|
|
|
|
| 486 |
else:
|
| 487 |
# Numeric assessment
|
| 488 |
if value >= 90:
|
| 489 |
badge = "🟢"
|
| 490 |
quality = "Excellent"
|
| 491 |
description = "Complete shoulder rotation"
|
|
|
|
| 492 |
elif value >= 75:
|
| 493 |
badge = "🟢"
|
| 494 |
quality = "Good"
|
| 495 |
description = "Adequate shoulder turn"
|
|
|
|
| 496 |
else:
|
| 497 |
badge = "🟠"
|
| 498 |
quality = "Limited"
|
| 499 |
description = "Restricted shoulder turn"
|
|
|
|
| 500 |
|
| 501 |
return {
|
| 502 |
'display_value': quality,
|
|
|
|
| 535 |
if depth_loss_pct < 5:
|
| 536 |
badge = '🟢'
|
| 537 |
label = 'Good depth maintenance'
|
|
|
|
| 538 |
elif depth_loss_pct < 15:
|
| 539 |
badge = '🟠'
|
| 540 |
label = 'Some early extension'
|
|
|
|
| 541 |
else:
|
| 542 |
badge = '🔴'
|
| 543 |
label = 'Early extension'
|
|
|
|
| 544 |
|
| 545 |
+
# Technical details removed per user request
|
|
|
|
|
|
|
| 546 |
|
| 547 |
return {
|
| 548 |
'value': depth_loss_pct, # Raw value for display logic
|
|
|
|
| 551 |
'label': label,
|
| 552 |
'confidence': confidence,
|
| 553 |
'status': label, # Use label as status for legacy compatibility
|
|
|
|
| 554 |
'calculation_confidence': calculation_confidence,
|
| 555 |
'adaptive_threshold': adaptive_threshold
|
| 556 |
}
|
|
|
|
| 564 |
if 45 <= angle_value <= 65:
|
| 565 |
badge = '🟢'
|
| 566 |
label = 'Good setup angle'
|
|
|
|
| 567 |
elif (35 <= angle_value < 45) or (65 < angle_value <= 75):
|
| 568 |
badge = '🟠'
|
| 569 |
label = 'Acceptable angle'
|
|
|
|
| 570 |
else:
|
| 571 |
badge = '🔴'
|
| 572 |
label = 'Needs adjustment'
|
|
|
|
| 573 |
|
| 574 |
return {
|
| 575 |
'display_value': f'{angle_value:.1f}°',
|
| 576 |
'badge': badge,
|
| 577 |
'label': label,
|
| 578 |
'confidence': confidence,
|
|
|
|
| 579 |
}
|
| 580 |
|
| 581 |
|
|
|
|
| 591 |
if displacement_pct < 5:
|
| 592 |
badge = '🟢'
|
| 593 |
label = 'Very stable head'
|
|
|
|
| 594 |
elif displacement_pct < 15:
|
| 595 |
badge = '🟠'
|
| 596 |
label = 'Some movement'
|
|
|
|
| 597 |
else:
|
| 598 |
badge = '🔴'
|
| 599 |
label = 'Excessive movement'
|
|
|
|
| 600 |
|
| 601 |
display_text = f'{displacement_pct:.1f}% {direction}' if 'displacement_pct' in displacement_data else f'{displacement_abs:.1f}px {direction}'
|
| 602 |
|
|
|
|
| 605 |
'badge': badge,
|
| 606 |
'label': label,
|
| 607 |
'confidence': confidence,
|
|
|
|
| 608 |
}
|
| 609 |
|
| 610 |
|
|
|
|
| 618 |
if 85 <= value <= 115:
|
| 619 |
badge = '🟢'
|
| 620 |
label = 'Excellent rotation'
|
|
|
|
| 621 |
elif 75 <= value < 85 or 115 < value <= 125:
|
| 622 |
badge = '🟠'
|
| 623 |
label = 'Good rotation'
|
|
|
|
| 624 |
else:
|
| 625 |
badge = '🔴'
|
| 626 |
label = 'Needs improvement'
|
|
|
|
| 627 |
else: # impact
|
| 628 |
if 30 <= value <= 50:
|
| 629 |
badge = '🟢'
|
| 630 |
label = 'Excellent impact position'
|
|
|
|
| 631 |
elif 20 <= value < 30 or 50 < value <= 60:
|
| 632 |
badge = '🟠'
|
| 633 |
label = 'Good impact position'
|
|
|
|
| 634 |
else:
|
| 635 |
badge = '🔴'
|
| 636 |
label = 'Needs improvement'
|
|
|
|
| 637 |
|
| 638 |
return {
|
| 639 |
'display_value': f"{value:.1f}°",
|
| 640 |
'badge': badge,
|
| 641 |
'label': label,
|
| 642 |
'confidence': confidence,
|
|
|
|
| 643 |
}
|
| 644 |
|
| 645 |
|
|
|
|
| 653 |
if 40 <= value <= 65:
|
| 654 |
badge = '🟢'
|
| 655 |
label = 'Excellent turn'
|
|
|
|
| 656 |
elif 30 <= value < 40 or 65 < value <= 75:
|
| 657 |
badge = '🟠'
|
| 658 |
label = 'Good turn'
|
|
|
|
| 659 |
else:
|
| 660 |
badge = '🔴'
|
| 661 |
label = 'Needs improvement'
|
|
|
|
| 662 |
else: # impact
|
| 663 |
if 35 <= value <= 65:
|
| 664 |
badge = '🟢'
|
| 665 |
label = 'Excellent impact position'
|
|
|
|
| 666 |
elif 25 <= value < 35 or 65 < value <= 75:
|
| 667 |
badge = '🟠'
|
| 668 |
label = 'Good impact position'
|
|
|
|
| 669 |
else:
|
| 670 |
badge = '🔴'
|
| 671 |
label = 'Needs improvement'
|
|
|
|
| 672 |
|
| 673 |
return {
|
| 674 |
'display_value': f"{value:.1f}°",
|
| 675 |
'badge': badge,
|
| 676 |
'label': label,
|
| 677 |
'confidence': confidence,
|
|
|
|
| 678 |
}
|
| 679 |
|
| 680 |
|
|
|
|
| 688 |
if 30 <= value <= 40:
|
| 689 |
badge = '🟢'
|
| 690 |
label = 'Excellent plane'
|
|
|
|
| 691 |
elif 25 <= value < 30 or 40 < value <= 45:
|
| 692 |
badge = '🟠'
|
| 693 |
label = 'Good plane'
|
|
|
|
| 694 |
else:
|
| 695 |
badge = '🔴'
|
| 696 |
label = 'Needs adjustment'
|
|
|
|
| 697 |
|
| 698 |
return {
|
| 699 |
'value': value, # Raw value for display logic
|
| 700 |
'display_value': f'{value:.1f}°',
|
| 701 |
'badge': badge,
|
| 702 |
'label': label,
|
|
|
|
| 703 |
'confidence': confidence,
|
| 704 |
'status': label # Use label as status for legacy compatibility
|
| 705 |
}
|
| 706 |
|
| 707 |
|
| 708 |
+
def get_head_drop_grading(value, confidence):
|
| 709 |
+
"""Grade head movement at top based on percentage of torso length
|
| 710 |
+
|
| 711 |
+
Convention: positive = moved DOWN (drop), negative = moved UP (rise)
|
| 712 |
+
Grades by absolute movement magnitude, shows direction in display.
|
| 713 |
+
"""
|
| 714 |
+
if value is None:
|
| 715 |
+
return {'value': None, 'display_value': 'n/a', 'badge': '⚪', 'status': 'No data available'}
|
| 716 |
+
|
| 717 |
+
# Convert string to float if needed
|
| 718 |
+
try:
|
| 719 |
+
if isinstance(value, str):
|
| 720 |
+
# Remove any non-numeric characters except decimal point and minus sign
|
| 721 |
+
import re
|
| 722 |
+
clean_value = re.sub(r'[^\d.-]', '', value)
|
| 723 |
+
if clean_value:
|
| 724 |
+
value = float(clean_value)
|
| 725 |
+
else:
|
| 726 |
+
return {'value': None, 'display_value': 'n/a', 'badge': '⚪', 'status': 'Invalid value'}
|
| 727 |
+
else:
|
| 728 |
+
value = float(value)
|
| 729 |
+
except (ValueError, TypeError):
|
| 730 |
+
return {'value': None, 'display_value': 'n/a', 'badge': '⚪', 'status': 'Invalid value'}
|
| 731 |
+
|
| 732 |
+
# Determine direction and absolute magnitude
|
| 733 |
+
direction = "drop" if value >= 0 else "rise"
|
| 734 |
+
abs_movement = abs(value)
|
| 735 |
+
|
| 736 |
+
# Grade by absolute movement magnitude (suggested rubric)
|
| 737 |
+
if abs_movement <= 3:
|
| 738 |
+
badge = '🟢'
|
| 739 |
+
grade = 'Excellent head stability'
|
| 740 |
+
elif abs_movement <= 6:
|
| 741 |
+
badge = '🟠'
|
| 742 |
+
grade = 'Good/typical'
|
| 743 |
+
elif abs_movement <= 10:
|
| 744 |
+
badge = '⚠️'
|
| 745 |
+
grade = 'Borderline (work on stability)'
|
| 746 |
+
else: # abs_movement > 10
|
| 747 |
+
badge = '🔴'
|
| 748 |
+
grade = 'Excessive movement'
|
| 749 |
+
|
| 750 |
+
return {
|
| 751 |
+
'value': value, # Raw value for display logic
|
| 752 |
+
'display_value': f'{abs_movement:.1f}% {direction}',
|
| 753 |
+
'badge': badge,
|
| 754 |
+
'label': grade,
|
| 755 |
+
'confidence': confidence,
|
| 756 |
+
'status': grade # Use grade as status for legacy compatibility
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
|
| 760 |
def get_shoulder_tilt_impact_grading(value, confidence):
|
| 761 |
"""Grade shoulder tilt at impact - professional = 39°, 30 handicap = 27°"""
|
| 762 |
if value is None:
|
|
|
|
| 766 |
if 35 <= value <= 45:
|
| 767 |
badge = '🟢'
|
| 768 |
label = 'Excellent tilt'
|
|
|
|
| 769 |
elif 25 <= value < 35 or 45 < value <= 55:
|
| 770 |
badge = '🟠'
|
| 771 |
label = 'Good tilt'
|
|
|
|
| 772 |
else:
|
| 773 |
badge = '🔴'
|
| 774 |
label = 'Needs improvement'
|
|
|
|
| 775 |
|
| 776 |
return {
|
| 777 |
'display_value': f'{value:.1f}°',
|
| 778 |
'badge': badge,
|
| 779 |
'label': label,
|
|
|
|
| 780 |
'confidence': confidence
|
| 781 |
}
|
| 782 |
|
|
|
|
| 794 |
if 3.0 <= value <= 5.0:
|
| 795 |
badge = '🟢'
|
| 796 |
label = 'Excellent sway'
|
|
|
|
| 797 |
elif 2.0 <= value < 3.0 or 5.0 < value <= 6.0:
|
| 798 |
badge = '🟠'
|
| 799 |
label = 'Good sway'
|
|
|
|
| 800 |
else:
|
| 801 |
badge = '🔴'
|
| 802 |
label = 'Needs improvement'
|
|
|
|
| 803 |
else: # impact
|
| 804 |
if 2.0 <= value <= 4.0:
|
| 805 |
badge = '🟢'
|
| 806 |
label = 'Excellent sway'
|
|
|
|
| 807 |
elif 1.0 <= value < 2.0 or 4.0 < value <= 5.0:
|
| 808 |
badge = '🟠'
|
| 809 |
label = 'Good sway'
|
|
|
|
| 810 |
else:
|
| 811 |
badge = '🔴'
|
| 812 |
label = 'Needs improvement'
|
|
|
|
| 813 |
|
| 814 |
return {
|
| 815 |
'display_value': f'{value:.1f}"',
|
| 816 |
'badge': badge,
|
| 817 |
'label': label,
|
|
|
|
| 818 |
'confidence': confidence
|
| 819 |
}
|
| 820 |
|
|
|
|
| 829 |
if 85 <= value <= 125:
|
| 830 |
badge = '🟢'
|
| 831 |
label = 'Excellent hinge'
|
|
|
|
| 832 |
elif 70 <= value < 85 or 125 < value <= 140:
|
| 833 |
badge = '🟠'
|
| 834 |
label = 'Good hinge'
|
|
|
|
| 835 |
else:
|
| 836 |
badge = '🔴'
|
| 837 |
label = 'Needs improvement'
|
|
|
|
| 838 |
else: # impact
|
| 839 |
if 15 <= value <= 40:
|
| 840 |
badge = '🟢'
|
| 841 |
label = 'Excellent release'
|
|
|
|
| 842 |
elif 10 <= value < 15 or 40 < value <= 50:
|
| 843 |
badge = '🟠'
|
| 844 |
label = 'Good release'
|
|
|
|
| 845 |
else:
|
| 846 |
badge = '🔴'
|
| 847 |
label = 'Needs improvement'
|
|
|
|
| 848 |
|
| 849 |
return {
|
| 850 |
'display_value': f"{value:.1f}°",
|
| 851 |
'badge': badge,
|
| 852 |
'label': label,
|
| 853 |
'confidence': confidence,
|
|
|
|
| 854 |
}
|
| 855 |
|
| 856 |
|
|
|
|
| 863 |
if abs(value) <= 5.0: # <= 5% of shoulder width
|
| 864 |
badge = '🟢'
|
| 865 |
label = 'Excellent stability'
|
|
|
|
| 866 |
elif abs(value) <= 10.0: # <= 10% of shoulder width
|
| 867 |
badge = '🟡'
|
| 868 |
label = 'Good stability'
|
|
|
|
| 869 |
elif abs(value) <= 15.0: # <= 15% of shoulder width
|
| 870 |
badge = '🟠'
|
| 871 |
label = 'Moderate sway'
|
|
|
|
| 872 |
else: # > 15% of shoulder width
|
| 873 |
badge = '🔴'
|
| 874 |
label = 'Excessive sway'
|
|
|
|
| 875 |
|
| 876 |
return {
|
| 877 |
'display_value': f"{value:.1f}%",
|
| 878 |
'badge': badge,
|
| 879 |
'label': label,
|
| 880 |
'confidence': confidence,
|
|
|
|
| 881 |
}
|
| 882 |
|
| 883 |
|
|
|
|
| 900 |
shoulder_tilt_swing_plane_data = core_metrics.get("shoulder_tilt_swing_plane_top_deg", {})
|
| 901 |
back_tilt_data = core_metrics.get("back_tilt_deg", {})
|
| 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 removed per user request
|
| 906 |
|
|
|
|
| 1014 |
if knee_grading:
|
| 1015 |
metrics_to_display.append(("Knee Flexion", knee_grading))
|
| 1016 |
|
| 1017 |
+
# 5. Head Movement @ Top (New DTL metric)
|
| 1018 |
+
head_drop_value = head_drop_data.get('value')
|
| 1019 |
+
if head_drop_value is not None:
|
| 1020 |
+
head_drop_confidence = get_confidence(head_drop_data, 'head_drop')
|
| 1021 |
+
head_drop_grading = get_head_drop_grading(head_drop_value, head_drop_confidence)
|
| 1022 |
+
if head_drop_grading:
|
| 1023 |
+
metrics_to_display.append(("Head Movement @ Top", head_drop_grading))
|
| 1024 |
+
|
| 1025 |
# Removed: Wrist Pattern, Kinematic Sequence (DTL-approx), and Shoulder Turn Quality metrics
|
| 1026 |
|
| 1027 |
# 8. Hip Depth / Early Extension (New DTL metric)
|
|
|
|
| 1045 |
'badge': '🔴',
|
| 1046 |
'label': 'Calculation failed',
|
| 1047 |
'confidence': 0.0,
|
|
|
|
| 1048 |
}
|
| 1049 |
metrics_to_display.append(("Hip Depth / Early Extension", error_grading))
|
| 1050 |
else:
|
|
|
|
| 1111 |
result_line = f"**{grading['display_value']}** — {grading['badge']} {grading.get('label', '')}"
|
| 1112 |
st.markdown(result_line)
|
| 1113 |
|
| 1114 |
+
# Confidence display removed per user request
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
|
| 1116 |
# Detailed evaluation text
|
| 1117 |
evaluation = get_metric_evaluation(metric_name, grading)
|
| 1118 |
st.write(evaluation)
|
| 1119 |
|
| 1120 |
+
# Tips display removed per user request
|
|
|
|
|
|
|
| 1121 |
|
| 1122 |
# Add spacing
|
| 1123 |
st.write("")
|
|
|
|
| 1129 |
"Shoulder Tilt/Swing Plane @ Top": "Measures shoulder swing plane angle at the top of backswing.",
|
| 1130 |
"Back Tilt @ Setup": "Measures spine angle from vertical at address position.",
|
| 1131 |
"Knee Flexion": "Measures knee bend angle at address position.",
|
| 1132 |
+
"Head Movement @ Top": "Measures head movement during backswing as percentage of torso length.",
|
| 1133 |
"Hip Depth / Early Extension": "Tracks loss of hip flexion through impact.",
|
| 1134 |
"Hip Turn @ Impact": "Measures hip rotation at ball contact.",
|
| 1135 |
"Shoulder Tilt @ Impact": "Measures shoulder angle at ball contact.",
|
|
|
|
| 1168 |
else:
|
| 1169 |
return f"Your knee flexion of **{value}** may be limiting your swing potential. Proper knee bend is essential for balance, power generation, and maintaining spine angle. Too little or too much knee flexion can cause balance issues and inconsistent contact."
|
| 1170 |
|
| 1171 |
+
elif metric_name == "Head Movement @ Top":
|
| 1172 |
+
if "🟢" in badge:
|
| 1173 |
+
return f"Your head drop of **{value}** shows excellent head stability during the backswing. This minimal movement indicates proper head control and balance, which promotes consistent contact and accuracy. Good head stability is fundamental for reliable ball striking."
|
| 1174 |
+
elif "🟠" in badge:
|
| 1175 |
+
return f"Your head drop of **{value}** shows moderate movement during the backswing. Some head movement can affect balance and consistency. Working on head stability and maintaining your spine angle can improve ball striking and accuracy."
|
| 1176 |
+
else:
|
| 1177 |
+
return f"Your head drop of **{value}** indicates excessive head movement during the backswing. Too much head drop disrupts balance and swing center, leading to inconsistent contact. Focus on keeping your head stable and maintaining spine angle for better results."
|
| 1178 |
+
|
| 1179 |
elif metric_name == "Hip Depth / Early Extension":
|
| 1180 |
if "🟢" in badge:
|
| 1181 |
return f"Your hip depth of **{value}** shows excellent posture maintenance. This indicates you're maintaining proper spine angle and avoiding early extension through impact. Good hip depth promotes solid contact, power transfer, and consistent ball striking patterns."
|
|
|
|
| 1310 |
- Shoulder Tilt/Swing Plane @ Top: {format_metric_value(core_metrics.get('shoulder_tilt_swing_plane_top_deg', {}), '°')}
|
| 1311 |
- Back Tilt @ Setup: {format_metric_value(core_metrics.get('back_tilt_deg', {}), '°')}
|
| 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 removed per user request
|
| 1316 |
|