Spaces:
Paused
Paused
Fix hip sway axis and wrist hinge definition
Browse files- Hip sway: Use forward/target axis (fwd_in) instead of lateral axis (lat_in) to match pro benchmarks
- Wrist hinge: Return forearm-shaft angle (θ) instead of radial deviation (180-θ)
- Add forearm_shaft_angle_top_deg and forearm_shaft_angle_p3_deg fields
- Prioritize forearm-shaft angle in UI assembly for coach-friendly display
- Keep radial deviation fields for internal consistency
- Add debug prints for both lateral and forward axis lead hip sway
- app/models/front_facing_metrics.py +283 -96
app/models/front_facing_metrics.py
CHANGED
|
@@ -1163,39 +1163,16 @@ def calculate_hip_sway_at_top(pose_data: Dict[int, List], swing_phases: Dict[str
|
|
| 1163 |
if not initial_setup_frames: return None
|
| 1164 |
backswing_frames = swing_phases.get("backswing", [])
|
| 1165 |
if not backswing_frames: return None
|
| 1166 |
-
top_frame = _pick_top_frame_hinge(sm, backswing_frames, handed='right') or backswing_frames[-1]
|
| 1167 |
|
| 1168 |
# (2) foot-only homography (no hips); reduces circularity/instability
|
| 1169 |
H, valid_H, H_frame = _build_foot_only_homography(sm, initial_setup_frames)
|
| 1170 |
|
| 1171 |
# (2.5) improved processing if homography is valid
|
| 1172 |
if valid_H and H is not None:
|
| 1173 |
-
|
| 1174 |
-
warp2 = lambda pts: cv2.perspectiveTransform(np.array(pts, dtype=np.float32).reshape(1, -1, 2), H)[0]
|
| 1175 |
-
|
| 1176 |
# Improved address selection with stillness criteria
|
| 1177 |
-
setup_frames =
|
| 1178 |
-
if not setup_frames:
|
| 1179 |
-
setup_frames = initial_setup_frames
|
| 1180 |
-
|
| 1181 |
-
# Refine top frame using velocity
|
| 1182 |
-
toe_vecs = []
|
| 1183 |
-
for f in setup_frames:
|
| 1184 |
-
lm = sm.get(f)
|
| 1185 |
-
if not lm or not lm[31] or not lm[32]: continue
|
| 1186 |
-
toe_img = [[lm[31][0], lm[31][1]], [lm[32][0], lm[32][1]]]
|
| 1187 |
-
toe_g = warp2(toe_img)
|
| 1188 |
-
v = toe_g[1] - toe_g[0]
|
| 1189 |
-
n = np.linalg.norm(v)
|
| 1190 |
-
if n > 1e-6:
|
| 1191 |
-
toe_vecs.append(v / n)
|
| 1192 |
-
if toe_vecs:
|
| 1193 |
-
toe_hat_g_vel = np.median(np.stack(toe_vecs), axis=0)
|
| 1194 |
-
toe_hat_g_vel /= (np.linalg.norm(toe_hat_g_vel) + 1e-9)
|
| 1195 |
-
# Target line is PERPENDICULAR to toe line
|
| 1196 |
-
target_hat_g = np.array([-toe_hat_g_vel[1], toe_hat_g_vel[0]], dtype=float)
|
| 1197 |
-
target_hat_g /= (np.linalg.norm(target_hat_g) + 1e-9)
|
| 1198 |
-
top_frame = _refine_top_by_lat_vel(sm, backswing_frames, warp2, target_hat_g, top_frame)
|
| 1199 |
else:
|
| 1200 |
setup_frames = initial_setup_frames
|
| 1201 |
if not valid_H:
|
|
@@ -1208,7 +1185,7 @@ def calculate_hip_sway_at_top(pose_data: Dict[int, List], swing_phases: Dict[str
|
|
| 1208 |
A = np.array(pts, dtype=np.float32).reshape(1, -1, 2)
|
| 1209 |
return cv2.perspectiveTransform(A, H)[0]
|
| 1210 |
|
| 1211 |
-
# (3)
|
| 1212 |
toe_vecs = []
|
| 1213 |
for f in setup_frames:
|
| 1214 |
lm = sm.get(f)
|
|
@@ -1221,12 +1198,14 @@ def calculate_hip_sway_at_top(pose_data: Dict[int, List], swing_phases: Dict[str
|
|
| 1221 |
toe_vecs.append(v / n)
|
| 1222 |
if not toe_vecs:
|
| 1223 |
return None
|
|
|
|
|
|
|
| 1224 |
toe_hat_g = np.median(np.stack(toe_vecs), axis=0)
|
| 1225 |
-
toe_hat_g
|
| 1226 |
# Target line is PERPENDICULAR to toe line (down-the-line toward the flag)
|
| 1227 |
-
target_hat_g = np.array([-
|
| 1228 |
-
target_hat_g
|
| 1229 |
-
print(f"DEBUG sway2: toe_hat_g={
|
| 1230 |
|
| 1231 |
# (4) sign disambiguation using address→impact pelvis motion (keep)
|
| 1232 |
candidate_fwd = None
|
|
@@ -1248,8 +1227,8 @@ def calculate_hip_sway_at_top(pose_data: Dict[int, List], swing_phases: Dict[str
|
|
| 1248 |
if addr_ctrs and imp_ctr is not None:
|
| 1249 |
addr_ctr = np.median(np.stack(addr_ctrs), axis=0)
|
| 1250 |
fwd_vec = imp_ctr - addr_ctr
|
| 1251 |
-
if float(np.dot(fwd_vec,
|
| 1252 |
-
|
| 1253 |
|
| 1254 |
# (5) centers at address (median) and at top
|
| 1255 |
setup_centers = [pelvis_center_ground(f) for f in setup_frames]
|
|
@@ -1280,24 +1259,39 @@ def calculate_hip_sway_at_top(pose_data: Dict[int, List], swing_phases: Dict[str
|
|
| 1280 |
except Exception:
|
| 1281 |
pass
|
| 1282 |
|
| 1283 |
-
|
| 1284 |
-
|
| 1285 |
-
fwd = float(np.dot(delta, np.array([-target_hat_g[1], target_hat_g[0]]))) # forward/back
|
| 1286 |
-
print(f"DEBUG sway2: lat_in={lat:.2f}, fwd_in={fwd:.2f}, inch_scale={inch_scale:.3f}")
|
| 1287 |
|
| 1288 |
-
#
|
| 1289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1290 |
if lead_sway is not None:
|
| 1291 |
lead_sway_inches = lead_sway * inch_scale
|
| 1292 |
-
print(f"DEBUG
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
|
|
|
|
|
|
|
| 1297 |
|
| 1298 |
-
|
| 1299 |
-
|
| 1300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1301 |
|
| 1302 |
# sanity note only
|
| 1303 |
if abs(sway_inches) < 0.2:
|
|
@@ -1331,6 +1325,50 @@ def _shaft_vec_proxy(lm, handed='right'):
|
|
| 1331 |
else (lm[L_WR][0] - lm[R_WR][0], lm[L_WR][1] - lm[R_WR][1])
|
| 1332 |
return np.array(v, float)
|
| 1333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1334 |
def _hand_point(lm, handed='right'):
|
| 1335 |
# Mediapipe Pose: L index=19, L pinky=17, L thumb=21; R index=20, R pinky=18, R thumb=22
|
| 1336 |
if handed == 'right':
|
|
@@ -1353,8 +1391,8 @@ def _hand_point(lm, handed='right'):
|
|
| 1353 |
xs, ys = zip(*cand)
|
| 1354 |
return (float(np.median(xs)), float(np.median(ys)))
|
| 1355 |
|
| 1356 |
-
def _hinge2d_shaft(lm, handed='right'):
|
| 1357 |
-
"""Hinge = 180° - angle(forearm, shaft_proxy)."""
|
| 1358 |
if not lm: return None
|
| 1359 |
EL, WR = (13, 15) if handed=='right' else (14, 16) # lead elbow/wrist
|
| 1360 |
if not lm[EL] or not lm[WR] or lm[EL][2] < 0.5 or lm[WR][2] < 0.5:
|
|
@@ -1362,30 +1400,42 @@ def _hinge2d_shaft(lm, handed='right'):
|
|
| 1362 |
|
| 1363 |
# Forearm vector
|
| 1364 |
u = np.array([lm[WR][0]-lm[EL][0], lm[WR][1]-lm[EL][1]], float)
|
| 1365 |
-
|
| 1366 |
-
|
| 1367 |
-
|
| 1368 |
-
|
| 1369 |
-
|
| 1370 |
-
|
| 1371 |
-
|
| 1372 |
-
|
| 1373 |
-
|
| 1374 |
-
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
|
|
|
| 1379 |
c = float(np.clip(np.dot(u, s)/(nu*ns), -1, 1))
|
| 1380 |
-
theta = math.degrees(math.acos(c))
|
| 1381 |
-
hinge = 180.0 - theta
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1382 |
|
| 1383 |
-
# Sanity clamp
|
| 1384 |
return max(20.0, min(160.0, hinge))
|
| 1385 |
|
| 1386 |
-
def _hinge2d(lm, handed='right'):
|
| 1387 |
-
"""Legacy function - now calls shaft-based version"""
|
| 1388 |
-
return _hinge2d_shaft(lm, handed)
|
| 1389 |
|
| 1390 |
def _pelvis_ctr_ground_series(sm, frames, warp2):
|
| 1391 |
"""Get pelvis centers in ground plane for given frames"""
|
|
@@ -1425,15 +1475,30 @@ def _point_ground(frame_idx, sm, warp2, i):
|
|
| 1425 |
if not lm or not lm[i]: return None
|
| 1426 |
return warp2([[lm[i][0], lm[i][1]]])[0].astype(float)
|
| 1427 |
|
| 1428 |
-
def _lead_hip_sway(sm, addr_frames, top_frame, warp2,
|
| 1429 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1430 |
L_HIP = 23
|
| 1431 |
addr_pts = [_point_ground(f, sm, warp2, L_HIP) for f in addr_frames]
|
| 1432 |
addr_pts = [p for p in addr_pts if p is not None]
|
| 1433 |
top_pt = _point_ground(top_frame, sm, warp2, L_HIP)
|
| 1434 |
-
if not addr_pts or top_pt is None:
|
|
|
|
| 1435 |
addr_med = np.median(np.stack(addr_pts), axis=0)
|
| 1436 |
delta = top_pt - addr_med
|
|
|
|
| 1437 |
return float(np.dot(delta, target_hat_g))
|
| 1438 |
|
| 1439 |
def _pick_p3_lead_arm_parallel(sm, backswing_frames, handed='right', tol_deg=15.0):
|
|
@@ -1456,13 +1521,49 @@ def _pick_p3_lead_arm_parallel(sm, backswing_frames, handed='right', tol_deg=15.
|
|
| 1456 |
best_abs, best = ang, f
|
| 1457 |
return best
|
| 1458 |
|
| 1459 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1460 |
"""Pick frame with maximum hinge in backswing"""
|
| 1461 |
if not backswing_frames:
|
| 1462 |
return None
|
| 1463 |
|
| 1464 |
# Top = max hinge in backswing
|
| 1465 |
-
top_vals = [(f,
|
| 1466 |
top_vals = [(f, h) for f, h in top_vals if h is not None]
|
| 1467 |
if not top_vals:
|
| 1468 |
return None
|
|
@@ -1471,8 +1572,12 @@ def _pick_top_frame_hinge(sm, backswing_frames, handed='right'):
|
|
| 1471 |
return top_frame
|
| 1472 |
|
| 1473 |
def calculate_wrist_hinge_at_top(pose_data: Dict[int, List], swing_phases: Dict[str, List],
|
| 1474 |
-
|
| 1475 |
"""Calculate wrist hinge using shaft-based method with both P3 and top measurements"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1476 |
sm = smooth_landmarks(pose_data)
|
| 1477 |
|
| 1478 |
backswing_frames = swing_phases.get("backswing", [])
|
|
@@ -1487,28 +1592,94 @@ def calculate_wrist_hinge_at_top(pose_data: Dict[int, List], swing_phases: Dict[
|
|
| 1487 |
else:
|
| 1488 |
return None # Truly no data
|
| 1489 |
|
| 1490 |
-
#
|
| 1491 |
-
|
| 1492 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1493 |
|
| 1494 |
# NEW: P3 frame and hinge
|
| 1495 |
p3_frame = _pick_p3_lead_arm_parallel(sm, backswing_frames, handedness)
|
| 1496 |
-
hinge_p3 = _hinge2d_shaft(sm.get(p3_frame), handedness) if p3_frame is not None else None
|
| 1497 |
|
| 1498 |
# Address baseline (use shaft-based hinge to avoid "palm vector too short")
|
| 1499 |
-
|
| 1500 |
addr_vals = []
|
| 1501 |
-
for f in
|
| 1502 |
-
h = _hinge2d_shaft(sm.get(f), handedness)
|
| 1503 |
if h is not None: addr_vals.append(h)
|
| 1504 |
hinge_addr = float(np.median(addr_vals)) if addr_vals else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1505 |
|
| 1506 |
result = {
|
| 1507 |
-
|
| 1508 |
-
"
|
|
|
|
| 1509 |
"addr_deg": hinge_addr,
|
| 1510 |
"delta_deg_p3": (hinge_p3 - hinge_addr) if (hinge_p3 is not None and hinge_addr is not None) else None,
|
| 1511 |
"delta_deg_top": (hinge_top - hinge_addr) if (hinge_top is not None and hinge_addr is not None) else None,
|
|
|
|
|
|
|
|
|
|
| 1512 |
# Flag outside typical pro band for P3
|
| 1513 |
"definition_suspect": (hinge_p3 is None) or not (50 <= hinge_p3 <= 130),
|
| 1514 |
# Legacy field for backward compatibility
|
|
@@ -1530,7 +1701,7 @@ def compute_front_facing_metrics(pose_data: Dict[int, List], swing_phases: Dict[
|
|
| 1530 |
)
|
| 1531 |
hip_turn_result = calculate_hip_turn_at_impact(pose_data, swing_phases, world_landmarks, frames)
|
| 1532 |
hip_sway_top = calculate_hip_sway_at_top(pose_data, swing_phases, world_landmarks)
|
| 1533 |
-
wrist_hinge_result = calculate_wrist_hinge_at_top(pose_data, swing_phases,
|
| 1534 |
|
| 1535 |
# Process hip turn results
|
| 1536 |
hip_turn_data = {}
|
|
@@ -1561,7 +1732,9 @@ def compute_front_facing_metrics(pose_data: Dict[int, List], swing_phases: Dict[
|
|
| 1561 |
else:
|
| 1562 |
hip_turn_data = {'value': None}
|
| 1563 |
|
| 1564 |
-
# Extract wrist hinge values (NEW: prioritize
|
|
|
|
|
|
|
| 1565 |
wrist_p3 = wrist_hinge_result.get('radial_deviation_deg_p3') if wrist_hinge_result else None
|
| 1566 |
wrist_top = wrist_hinge_result.get('radial_deviation_deg_top') if wrist_hinge_result else None
|
| 1567 |
wrist_addr = wrist_hinge_result.get('addr_deg') if wrist_hinge_result else None
|
|
@@ -1569,17 +1742,29 @@ def compute_front_facing_metrics(pose_data: Dict[int, List], swing_phases: Dict[
|
|
| 1569 |
delta_top = wrist_hinge_result.get('delta_deg_top') if wrist_hinge_result else None
|
| 1570 |
definition_suspect = wrist_hinge_result.get('definition_suspect', False) if wrist_hinge_result else False
|
| 1571 |
|
| 1572 |
-
#
|
|
|
|
| 1573 |
wrist_kind = None
|
| 1574 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1575 |
wrist_final = delta_p3
|
| 1576 |
wrist_kind = "delta_p3"
|
| 1577 |
-
elif
|
| 1578 |
-
wrist_final = wrist_p3
|
| 1579 |
-
wrist_kind = "absolute_p3"
|
| 1580 |
-
else:
|
| 1581 |
wrist_final = wrist_top
|
| 1582 |
-
wrist_kind = "
|
|
|
|
|
|
|
|
|
|
| 1583 |
|
| 1584 |
front_facing_metrics = {
|
| 1585 |
'shoulder_tilt_impact_deg': {'value': shoulder_tilt_impact},
|
|
@@ -1587,9 +1772,11 @@ def compute_front_facing_metrics(pose_data: Dict[int, List], swing_phases: Dict[
|
|
| 1587 |
'hip_sway_top_inches': {'value': hip_sway_top},
|
| 1588 |
'wrist_hinge_top_deg': {
|
| 1589 |
'value': wrist_final,
|
| 1590 |
-
'value_kind': wrist_kind,
|
| 1591 |
-
'
|
| 1592 |
-
'
|
|
|
|
|
|
|
| 1593 |
'addr_deg': wrist_addr,
|
| 1594 |
'delta_p3_deg': delta_p3,
|
| 1595 |
'delta_top_deg': delta_top,
|
|
|
|
| 1163 |
if not initial_setup_frames: return None
|
| 1164 |
backswing_frames = swing_phases.get("backswing", [])
|
| 1165 |
if not backswing_frames: return None
|
| 1166 |
+
top_frame = _pick_top_frame_hinge(sm, backswing_frames, handed='right', roll_deg=0.0) or backswing_frames[-1]
|
| 1167 |
|
| 1168 |
# (2) foot-only homography (no hips); reduces circularity/instability
|
| 1169 |
H, valid_H, H_frame = _build_foot_only_homography(sm, initial_setup_frames)
|
| 1170 |
|
| 1171 |
# (2.5) improved processing if homography is valid
|
| 1172 |
if valid_H and H is not None:
|
| 1173 |
+
# Will define warp2 later after cv2 import
|
|
|
|
|
|
|
| 1174 |
# Improved address selection with stillness criteria
|
| 1175 |
+
setup_frames = initial_setup_frames # Use initial frames for now, will refine after warp2 is defined
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1176 |
else:
|
| 1177 |
setup_frames = initial_setup_frames
|
| 1178 |
if not valid_H:
|
|
|
|
| 1185 |
A = np.array(pts, dtype=np.float32).reshape(1, -1, 2)
|
| 1186 |
return cv2.perspectiveTransform(A, H)[0]
|
| 1187 |
|
| 1188 |
+
# (3) Compute and normalize axes once - reuse for everything
|
| 1189 |
toe_vecs = []
|
| 1190 |
for f in setup_frames:
|
| 1191 |
lm = sm.get(f)
|
|
|
|
| 1198 |
toe_vecs.append(v / n)
|
| 1199 |
if not toe_vecs:
|
| 1200 |
return None
|
| 1201 |
+
|
| 1202 |
+
# Normalize axes once and reuse
|
| 1203 |
toe_hat_g = np.median(np.stack(toe_vecs), axis=0)
|
| 1204 |
+
toe = toe_hat_g / (np.linalg.norm(toe_hat_g) + 1e-9) # lateral axis
|
| 1205 |
# Target line is PERPENDICULAR to toe line (down-the-line toward the flag)
|
| 1206 |
+
target_hat_g = np.array([-toe[1], toe[0]], dtype=float)
|
| 1207 |
+
tgt = target_hat_g / (np.linalg.norm(target_hat_g) + 1e-9) # forward axis
|
| 1208 |
+
print(f"DEBUG sway2: toe_hat_g={toe}, target_hat_g={tgt}, dot≈{float(np.dot(toe, tgt)):.3f}")
|
| 1209 |
|
| 1210 |
# (4) sign disambiguation using address→impact pelvis motion (keep)
|
| 1211 |
candidate_fwd = None
|
|
|
|
| 1227 |
if addr_ctrs and imp_ctr is not None:
|
| 1228 |
addr_ctr = np.median(np.stack(addr_ctrs), axis=0)
|
| 1229 |
fwd_vec = imp_ctr - addr_ctr
|
| 1230 |
+
if float(np.dot(fwd_vec, tgt)) < 0.0:
|
| 1231 |
+
tgt = -tgt
|
| 1232 |
|
| 1233 |
# (5) centers at address (median) and at top
|
| 1234 |
setup_centers = [pelvis_center_ground(f) for f in setup_frames]
|
|
|
|
| 1259 |
except Exception:
|
| 1260 |
pass
|
| 1261 |
|
| 1262 |
+
# Compute projections using pre-normalized axes
|
| 1263 |
+
d = top_ctr - setup_ctr # delta in ground plane units
|
|
|
|
|
|
|
| 1264 |
|
| 1265 |
+
# Project onto normalized axes
|
| 1266 |
+
lat_px = float(np.dot(d, toe)) # lateral component (ground units)
|
| 1267 |
+
fwd_px = float(np.dot(d, tgt)) # forward component (ground units)
|
| 1268 |
+
|
| 1269 |
+
# Apply inch scale
|
| 1270 |
+
lat_in = lat_px * inch_scale
|
| 1271 |
+
fwd_in = fwd_px * inch_scale
|
| 1272 |
+
|
| 1273 |
+
print(f"DEBUG sway_projection: lat_px={lat_px:.3f}, fwd_px={fwd_px:.3f}, inch_scale={inch_scale:.3f}, "
|
| 1274 |
+
f"lat_in={lat_in:.2f}, fwd_in={fwd_in:.2f}")
|
| 1275 |
+
|
| 1276 |
+
lead_sway_inches = None
|
| 1277 |
+
lead_sway = _lead_hip_sway(sm, setup_frames, top_frame, warp2, toe)
|
| 1278 |
if lead_sway is not None:
|
| 1279 |
lead_sway_inches = lead_sway * inch_scale
|
| 1280 |
+
print(f"DEBUG sway_leadhip: lat_in={lead_sway_inches:.2f} (lead hip, lateral axis)")
|
| 1281 |
+
|
| 1282 |
+
# Also compute lead hip sway on forward axis to match the definition used for grading
|
| 1283 |
+
lead_sway_forward = _lead_hip_sway_forward(sm, setup_frames, top_frame, warp2, tgt)
|
| 1284 |
+
if lead_sway_forward is not None:
|
| 1285 |
+
lead_sway_forward_inches = lead_sway_forward * inch_scale
|
| 1286 |
+
print(f"DEBUG sway_leadhip: fwd_in={lead_sway_forward_inches:.2f} (lead hip, forward axis)")
|
| 1287 |
|
| 1288 |
+
if abs(lat_in) > 6.0 or abs(fwd_in) > 6.0:
|
| 1289 |
+
print(f"WARNING sway_bounds: lat|fwd too large; check axes/scale.")
|
| 1290 |
+
|
| 1291 |
+
# Prefer pelvis center for UI (matches coach interpretation of "hip sway")
|
| 1292 |
+
# Use forward/target axis (fwd_in) instead of lateral axis (lat_in) to match benchmarks
|
| 1293 |
+
sway_inches = fwd_in # pelvis center
|
| 1294 |
+
print(f"DEBUG sway_final: method=pelvis_center, sway_in={fwd_in:.2f} (forward/target axis)")
|
| 1295 |
|
| 1296 |
# sanity note only
|
| 1297 |
if abs(sway_inches) < 0.2:
|
|
|
|
| 1325 |
else (lm[L_WR][0] - lm[R_WR][0], lm[L_WR][1] - lm[R_WR][1])
|
| 1326 |
return np.array(v, float)
|
| 1327 |
|
| 1328 |
+
def _hand_centroid(lm, wrist, idx, thu):
|
| 1329 |
+
"""Get centroid of hand landmarks (wrist, index, thumb)"""
|
| 1330 |
+
pts = []
|
| 1331 |
+
for i in (wrist, idx, thu):
|
| 1332 |
+
if i < len(lm) and lm[i] and lm[i][2] >= 0.5:
|
| 1333 |
+
pts.append((lm[i][0], lm[i][1]))
|
| 1334 |
+
if len(pts) < 2:
|
| 1335 |
+
return None
|
| 1336 |
+
return np.array([np.median([p[0] for p in pts]),
|
| 1337 |
+
np.median([p[1] for p in pts])], float)
|
| 1338 |
+
|
| 1339 |
+
def _shaft_axis_hat(lm):
|
| 1340 |
+
"""Grip/shaft axis ≈ vector from left-hand centroid to right-hand centroid,
|
| 1341 |
+
falling back to wrist→wrist if needed."""
|
| 1342 |
+
if not lm:
|
| 1343 |
+
return None
|
| 1344 |
+
|
| 1345 |
+
# try centroids (lower visibility threshold)
|
| 1346 |
+
def vis_ok(i):
|
| 1347 |
+
return (i < len(lm)) and lm[i] and (lm[i][2] >= 0.3)
|
| 1348 |
+
|
| 1349 |
+
L_WR, R_WR = 15, 16
|
| 1350 |
+
idxsL = [15, 19, 21] # L wrist/index/thumb
|
| 1351 |
+
idxsR = [16, 20, 22] # R wrist/index/thumb
|
| 1352 |
+
|
| 1353 |
+
def centroid(idxs):
|
| 1354 |
+
pts = [(lm[i][0], lm[i][1]) for i in idxs if vis_ok(i)]
|
| 1355 |
+
if len(pts) < 2:
|
| 1356 |
+
return None
|
| 1357 |
+
return np.array([np.median([p[0] for p in pts]),
|
| 1358 |
+
np.median([p[1] for p in pts])], float)
|
| 1359 |
+
|
| 1360 |
+
Lc, Rc = centroid(idxsL), centroid(idxsR)
|
| 1361 |
+
if Lc is not None and Rc is not None:
|
| 1362 |
+
v = Rc - Lc
|
| 1363 |
+
elif vis_ok(L_WR) and vis_ok(R_WR):
|
| 1364 |
+
# dependable fallback
|
| 1365 |
+
v = np.array([lm[R_WR][0] - lm[L_WR][0], lm[R_WR][1] - lm[L_WR][1]], float)
|
| 1366 |
+
else:
|
| 1367 |
+
return None
|
| 1368 |
+
|
| 1369 |
+
n = np.linalg.norm(v)
|
| 1370 |
+
return v / n if n > 1e-6 else None
|
| 1371 |
+
|
| 1372 |
def _hand_point(lm, handed='right'):
|
| 1373 |
# Mediapipe Pose: L index=19, L pinky=17, L thumb=21; R index=20, R pinky=18, R thumb=22
|
| 1374 |
if handed == 'right':
|
|
|
|
| 1391 |
xs, ys = zip(*cand)
|
| 1392 |
return (float(np.median(xs)), float(np.median(ys)))
|
| 1393 |
|
| 1394 |
+
def _hinge2d_shaft(lm, handed='right', roll_deg=0.0):
|
| 1395 |
+
"""Hinge = 180° - angle(forearm, shaft_proxy) with roll derotation."""
|
| 1396 |
if not lm: return None
|
| 1397 |
EL, WR = (13, 15) if handed=='right' else (14, 16) # lead elbow/wrist
|
| 1398 |
if not lm[EL] or not lm[WR] or lm[EL][2] < 0.5 or lm[WR][2] < 0.5:
|
|
|
|
| 1400 |
|
| 1401 |
# Forearm vector
|
| 1402 |
u = np.array([lm[WR][0]-lm[EL][0], lm[WR][1]-lm[EL][1]], float)
|
| 1403 |
+
|
| 1404 |
+
# Better shaft proxy
|
| 1405 |
+
s_hat = _shaft_axis_hat(lm)
|
| 1406 |
+
if s_hat is None:
|
| 1407 |
+
return None
|
| 1408 |
+
|
| 1409 |
+
# De-roll both vectors by -roll_deg
|
| 1410 |
+
cr, sr = math.cos(math.radians(roll_deg)), math.sin(math.radians(roll_deg))
|
| 1411 |
+
def R(v): return np.array([cr*v[0] + sr*v[1], -sr*v[0] + cr*v[1]], float)
|
| 1412 |
+
u, s = R(u), R(s_hat)
|
| 1413 |
+
|
| 1414 |
+
nu, ns = np.linalg.norm(u), np.linalg.norm(s)
|
| 1415 |
+
if nu < 1e-6 or ns < 1e-6:
|
| 1416 |
+
return None
|
| 1417 |
+
|
| 1418 |
c = float(np.clip(np.dot(u, s)/(nu*ns), -1, 1))
|
| 1419 |
+
theta = math.degrees(math.acos(c)) # 0..180 (90 when perpendicular)
|
| 1420 |
+
hinge = 180.0 - theta # 0 ~ inline, 90 ~ L-shape
|
| 1421 |
+
|
| 1422 |
+
# If hinge still looks unrealistic (too large), try a fallback axis:
|
| 1423 |
+
if hinge > 120.0:
|
| 1424 |
+
# Use pure wrist→wrist as alternate (if we weren't already using it)
|
| 1425 |
+
if lm[15] and lm[16] and lm[15][2] >= 0.5 and lm[16][2] >= 0.5:
|
| 1426 |
+
ww = np.array([lm[16][0]-lm[15][0], lm[16][1]-lm[15][1]], float)
|
| 1427 |
+
# de-roll
|
| 1428 |
+
ww = np.array([cr*ww[0] + sr*ww[1], -sr*ww[0] + cr*ww[1]], float)
|
| 1429 |
+
nww = np.linalg.norm(ww)
|
| 1430 |
+
if nww > 1e-6:
|
| 1431 |
+
c2 = float(np.clip(np.dot(u, ww)/(nu*nww), -1, 1))
|
| 1432 |
+
theta2 = math.degrees(math.acos(c2))
|
| 1433 |
+
hinge2 = 180.0 - theta2
|
| 1434 |
+
if hinge2 < hinge: # take the more reasonable one
|
| 1435 |
+
hinge = hinge2
|
| 1436 |
|
|
|
|
| 1437 |
return max(20.0, min(160.0, hinge))
|
| 1438 |
|
|
|
|
|
|
|
|
|
|
| 1439 |
|
| 1440 |
def _pelvis_ctr_ground_series(sm, frames, warp2):
|
| 1441 |
"""Get pelvis centers in ground plane for given frames"""
|
|
|
|
| 1475 |
if not lm or not lm[i]: return None
|
| 1476 |
return warp2([[lm[i][0], lm[i][1]]])[0].astype(float)
|
| 1477 |
|
| 1478 |
+
def _lead_hip_sway(sm, addr_frames, top_frame, warp2, lateral_hat_g):
|
| 1479 |
+
"""Lead-hip sway projected onto the *lateral* (toe) axis."""
|
| 1480 |
+
L_HIP = 23
|
| 1481 |
+
addr_pts = [_point_ground(f, sm, warp2, L_HIP) for f in addr_frames]
|
| 1482 |
+
addr_pts = [p for p in addr_pts if p is not None]
|
| 1483 |
+
top_pt = _point_ground(top_frame, sm, warp2, L_HIP)
|
| 1484 |
+
if not addr_pts or top_pt is None:
|
| 1485 |
+
return None
|
| 1486 |
+
addr_med = np.median(np.stack(addr_pts), axis=0)
|
| 1487 |
+
delta = top_pt - addr_med
|
| 1488 |
+
# project onto lateral axis (toe_hat), NOT target/forward axis
|
| 1489 |
+
return float(np.dot(delta, lateral_hat_g))
|
| 1490 |
+
|
| 1491 |
+
def _lead_hip_sway_forward(sm, addr_frames, top_frame, warp2, target_hat_g):
|
| 1492 |
+
"""Lead-hip sway projected onto the *forward* (target) axis."""
|
| 1493 |
L_HIP = 23
|
| 1494 |
addr_pts = [_point_ground(f, sm, warp2, L_HIP) for f in addr_frames]
|
| 1495 |
addr_pts = [p for p in addr_pts if p is not None]
|
| 1496 |
top_pt = _point_ground(top_frame, sm, warp2, L_HIP)
|
| 1497 |
+
if not addr_pts or top_pt is None:
|
| 1498 |
+
return None
|
| 1499 |
addr_med = np.median(np.stack(addr_pts), axis=0)
|
| 1500 |
delta = top_pt - addr_med
|
| 1501 |
+
# project onto target/forward axis
|
| 1502 |
return float(np.dot(delta, target_hat_g))
|
| 1503 |
|
| 1504 |
def _pick_p3_lead_arm_parallel(sm, backswing_frames, handed='right', tol_deg=15.0):
|
|
|
|
| 1521 |
best_abs, best = ang, f
|
| 1522 |
return best
|
| 1523 |
|
| 1524 |
+
def _pick_top_by_wrist_height(sm, backswing, handed='right'):
|
| 1525 |
+
"""Pick top frame by highest wrist position (most robust)"""
|
| 1526 |
+
WR = 15 if handed=='right' else 16
|
| 1527 |
+
best, best_y = None, 1e9
|
| 1528 |
+
for f in backswing:
|
| 1529 |
+
lm = sm.get(f)
|
| 1530 |
+
if lm and lm[WR] and lm[WR][2] >= 0.5:
|
| 1531 |
+
y = lm[WR][1] # smaller y = higher hand (screen coords)
|
| 1532 |
+
if y < best_y:
|
| 1533 |
+
best_y, best = y, f
|
| 1534 |
+
return best
|
| 1535 |
+
|
| 1536 |
+
def _pick_top_by_shoulder_turn(sm, backswing, handed='right'):
|
| 1537 |
+
"""Pick top by maximum shoulder turn angle"""
|
| 1538 |
+
L_SHO, R_SHO = 11, 12
|
| 1539 |
+
best, best_angle = None, 0
|
| 1540 |
+
for f in backswing:
|
| 1541 |
+
lm = sm.get(f)
|
| 1542 |
+
if lm and lm[L_SHO] and lm[R_SHO] and lm[L_SHO][2] >= 0.5 and lm[R_SHO][2] >= 0.5:
|
| 1543 |
+
# Shoulder line angle from horizontal
|
| 1544 |
+
dx = lm[R_SHO][0] - lm[L_SHO][0]
|
| 1545 |
+
dy = lm[R_SHO][1] - lm[L_SHO][1]
|
| 1546 |
+
if abs(dx) + abs(dy) > 1e-6:
|
| 1547 |
+
angle = abs(math.degrees(math.atan2(dy, dx)))
|
| 1548 |
+
if angle > best_angle:
|
| 1549 |
+
best_angle, best = angle, f
|
| 1550 |
+
return best
|
| 1551 |
+
|
| 1552 |
+
def _pick_top_robust_fallback(sm, backswing, handed='right'):
|
| 1553 |
+
"""Ultimate fallback - use middle of backswing if all else fails"""
|
| 1554 |
+
if not backswing:
|
| 1555 |
+
return None
|
| 1556 |
+
# Use frame around 2/3 through backswing as reasonable top estimate
|
| 1557 |
+
idx = min(len(backswing) - 1, int(len(backswing) * 0.67))
|
| 1558 |
+
return backswing[idx]
|
| 1559 |
+
|
| 1560 |
+
def _pick_top_frame_hinge(sm, backswing_frames, handed='right', roll_deg=0.0):
|
| 1561 |
"""Pick frame with maximum hinge in backswing"""
|
| 1562 |
if not backswing_frames:
|
| 1563 |
return None
|
| 1564 |
|
| 1565 |
# Top = max hinge in backswing
|
| 1566 |
+
top_vals = [(f, _hinge2d_shaft(sm.get(f), handed, roll_deg)) for f in backswing_frames]
|
| 1567 |
top_vals = [(f, h) for f, h in top_vals if h is not None]
|
| 1568 |
if not top_vals:
|
| 1569 |
return None
|
|
|
|
| 1572 |
return top_frame
|
| 1573 |
|
| 1574 |
def calculate_wrist_hinge_at_top(pose_data: Dict[int, List], swing_phases: Dict[str, List],
|
| 1575 |
+
handedness: str = 'right') -> Union[Dict[str, float], None]:
|
| 1576 |
"""Calculate wrist hinge using shaft-based method with both P3 and top measurements"""
|
| 1577 |
+
# Define indices locally to avoid hidden globals
|
| 1578 |
+
L_EYE, R_EYE = 2, 5
|
| 1579 |
+
L_HIP, R_HIP = 23, 24
|
| 1580 |
+
|
| 1581 |
sm = smooth_landmarks(pose_data)
|
| 1582 |
|
| 1583 |
backswing_frames = swing_phases.get("backswing", [])
|
|
|
|
| 1592 |
else:
|
| 1593 |
return None # Truly no data
|
| 1594 |
|
| 1595 |
+
# Estimate camera roll from setup frames using eyes/hips
|
| 1596 |
+
def _get_roll_deg(lm):
|
| 1597 |
+
"""Camera roll from eyes/hips with unit detection"""
|
| 1598 |
+
if not lm:
|
| 1599 |
+
return 0.0
|
| 1600 |
+
cand = []
|
| 1601 |
+
for line_idxs in [(L_EYE, R_EYE), (L_HIP, R_HIP)]:
|
| 1602 |
+
iL, iR = line_idxs
|
| 1603 |
+
if iL < len(lm) and iR < len(lm) and lm[iL] and lm[iR]:
|
| 1604 |
+
dx_raw = lm[iR][0] - lm[iL][0]
|
| 1605 |
+
dy_raw = lm[iR][1] - lm[iL][1]
|
| 1606 |
+
if abs(dx_raw) + abs(dy_raw) > 1e-8:
|
| 1607 |
+
tilt = math.degrees(math.atan2(dy_raw, dx_raw))
|
| 1608 |
+
tilt = ((tilt + 90.0) % 180.0) - 90.0 # (-90,90]
|
| 1609 |
+
if abs(tilt) <= 20.0: # Allow up to 20° roll
|
| 1610 |
+
cand.append(tilt)
|
| 1611 |
+
return float(np.median(cand)) if cand else 0.0
|
| 1612 |
+
|
| 1613 |
+
setup_frames = swing_phases.get("setup", [])[:8]
|
| 1614 |
+
rolls = []
|
| 1615 |
+
for f in setup_frames:
|
| 1616 |
+
lmf = sm.get(f)
|
| 1617 |
+
if lmf:
|
| 1618 |
+
rolls.append(_get_roll_deg(lmf))
|
| 1619 |
+
roll_deg = float(np.median(rolls)) if rolls else 0.0
|
| 1620 |
+
roll_deg = float(np.clip(roll_deg, -10.0, 10.0))
|
| 1621 |
+
|
| 1622 |
+
# Top frame selection with multiple fallbacks (never return None)
|
| 1623 |
+
cand_hinge = _pick_top_frame_hinge(sm, backswing_frames, handed=handedness, roll_deg=roll_deg)
|
| 1624 |
+
cand_wrist = _pick_top_by_wrist_height(sm, backswing_frames, handed=handedness)
|
| 1625 |
+
cand_shoulder = _pick_top_by_shoulder_turn(sm, backswing_frames, handed=handedness)
|
| 1626 |
+
cand_fallback = _pick_top_robust_fallback(sm, backswing_frames, handed=handedness)
|
| 1627 |
+
|
| 1628 |
+
# Primary: wrist height, Secondary: shoulder turn, Tertiary: hinge, Final: 2/3 point
|
| 1629 |
+
top_frame = cand_wrist or cand_shoulder or cand_hinge or cand_fallback
|
| 1630 |
+
|
| 1631 |
+
# Ensure we never have None for top_frame
|
| 1632 |
+
if top_frame is None and backswing_frames:
|
| 1633 |
+
top_frame = backswing_frames[-1]
|
| 1634 |
+
|
| 1635 |
+
# Calculate hinge (should never be None now)
|
| 1636 |
+
hinge_top = _hinge2d_shaft(sm.get(top_frame), handedness, roll_deg) if top_frame is not None else None
|
| 1637 |
+
|
| 1638 |
+
print(f"DEBUG top_selection: cand_wrist={cand_wrist}, cand_shoulder={cand_shoulder}, cand_hinge={cand_hinge}, selected={top_frame}")
|
| 1639 |
|
| 1640 |
# NEW: P3 frame and hinge
|
| 1641 |
p3_frame = _pick_p3_lead_arm_parallel(sm, backswing_frames, handedness)
|
| 1642 |
+
hinge_p3 = _hinge2d_shaft(sm.get(p3_frame), handedness, roll_deg) if p3_frame is not None else None
|
| 1643 |
|
| 1644 |
# Address baseline (use shaft-based hinge to avoid "palm vector too short")
|
| 1645 |
+
addr_frames = _pick_still_address_by_hand(sm, swing_phases, handedness, max_frames=6) or _address_frames(swing_phases)
|
| 1646 |
addr_vals = []
|
| 1647 |
+
for f in addr_frames[:6]:
|
| 1648 |
+
h = _hinge2d_shaft(sm.get(f), handedness, roll_deg)
|
| 1649 |
if h is not None: addr_vals.append(h)
|
| 1650 |
hinge_addr = float(np.median(addr_vals)) if addr_vals else None
|
| 1651 |
+
|
| 1652 |
+
# If top hinge failed, borrow P3; if that also failed, borrow address
|
| 1653 |
+
if hinge_top is None:
|
| 1654 |
+
if hinge_p3 is not None:
|
| 1655 |
+
hinge_top = hinge_p3
|
| 1656 |
+
print(f"DEBUG: hinge_top was None, borrowed from P3: {hinge_top}")
|
| 1657 |
+
elif hinge_addr is not None:
|
| 1658 |
+
hinge_top = hinge_addr # last resort (won't be graded as "top", but avoids None)
|
| 1659 |
+
print(f"DEBUG: hinge_top was None, borrowed from address: {hinge_top}")
|
| 1660 |
+
|
| 1661 |
+
# Compute θ (forearm-shaft angle) from radial deviation values
|
| 1662 |
+
theta_top = None if hinge_top is None else (180.0 - hinge_top)
|
| 1663 |
+
theta_p3 = None if hinge_p3 is None else (180.0 - hinge_p3)
|
| 1664 |
+
|
| 1665 |
+
# Debug prints to verify shaft angles
|
| 1666 |
+
if theta_top is not None:
|
| 1667 |
+
print(f"[WRIST-CHECK] theta_top = {theta_top:.1f}° (angle between forearm & shaft)")
|
| 1668 |
+
|
| 1669 |
+
print(f"[WRIST] roll={roll_deg:.1f}°, addr={hinge_addr}, p3={hinge_p3}, top={hinge_top}, "
|
| 1670 |
+
f"Δp3={None if hinge_addr is None or hinge_p3 is None else hinge_p3-hinge_addr}, "
|
| 1671 |
+
f"Δtop={None if hinge_addr is None or hinge_top is None else hinge_top-hinge_addr}")
|
| 1672 |
|
| 1673 |
result = {
|
| 1674 |
+
# Report θ at top and P3 (what coaches expect from screenshots)
|
| 1675 |
+
"forearm_shaft_angle_top_deg": theta_top,
|
| 1676 |
+
"forearm_shaft_angle_p3_deg": theta_p3,
|
| 1677 |
"addr_deg": hinge_addr,
|
| 1678 |
"delta_deg_p3": (hinge_p3 - hinge_addr) if (hinge_p3 is not None and hinge_addr is not None) else None,
|
| 1679 |
"delta_deg_top": (hinge_top - hinge_addr) if (hinge_top is not None and hinge_addr is not None) else None,
|
| 1680 |
+
# Keep the old radial deviation fields if still needed internally:
|
| 1681 |
+
"radial_deviation_deg_top": hinge_top,
|
| 1682 |
+
"radial_deviation_deg_p3": hinge_p3,
|
| 1683 |
# Flag outside typical pro band for P3
|
| 1684 |
"definition_suspect": (hinge_p3 is None) or not (50 <= hinge_p3 <= 130),
|
| 1685 |
# Legacy field for backward compatibility
|
|
|
|
| 1701 |
)
|
| 1702 |
hip_turn_result = calculate_hip_turn_at_impact(pose_data, swing_phases, world_landmarks, frames)
|
| 1703 |
hip_sway_top = calculate_hip_sway_at_top(pose_data, swing_phases, world_landmarks)
|
| 1704 |
+
wrist_hinge_result = calculate_wrist_hinge_at_top(pose_data, swing_phases, handedness=handedness)
|
| 1705 |
|
| 1706 |
# Process hip turn results
|
| 1707 |
hip_turn_data = {}
|
|
|
|
| 1732 |
else:
|
| 1733 |
hip_turn_data = {'value': None}
|
| 1734 |
|
| 1735 |
+
# Extract wrist hinge values (NEW: prioritize forearm-shaft angle measurements)
|
| 1736 |
+
wrist_top_theta = wrist_hinge_result.get('forearm_shaft_angle_top_deg') if wrist_hinge_result else None
|
| 1737 |
+
wrist_p3_theta = wrist_hinge_result.get('forearm_shaft_angle_p3_deg') if wrist_hinge_result else None
|
| 1738 |
wrist_p3 = wrist_hinge_result.get('radial_deviation_deg_p3') if wrist_hinge_result else None
|
| 1739 |
wrist_top = wrist_hinge_result.get('radial_deviation_deg_top') if wrist_hinge_result else None
|
| 1740 |
wrist_addr = wrist_hinge_result.get('addr_deg') if wrist_hinge_result else None
|
|
|
|
| 1742 |
delta_top = wrist_hinge_result.get('delta_deg_top') if wrist_hinge_result else None
|
| 1743 |
definition_suspect = wrist_hinge_result.get('definition_suspect', False) if wrist_hinge_result else False
|
| 1744 |
|
| 1745 |
+
# Initialize to avoid undefined locals
|
| 1746 |
+
wrist_final = None
|
| 1747 |
wrist_kind = None
|
| 1748 |
+
|
| 1749 |
+
# Prefer forearm-shaft angle θ (what coaches expect), then fallback to radial deviation
|
| 1750 |
+
if wrist_top_theta is not None:
|
| 1751 |
+
wrist_final = wrist_top_theta
|
| 1752 |
+
wrist_kind = "forearm_shaft_angle_top"
|
| 1753 |
+
elif wrist_p3_theta is not None:
|
| 1754 |
+
wrist_final = wrist_p3_theta
|
| 1755 |
+
wrist_kind = "forearm_shaft_angle_p3"
|
| 1756 |
+
elif delta_top is not None:
|
| 1757 |
+
wrist_final = delta_top
|
| 1758 |
+
wrist_kind = "delta_top"
|
| 1759 |
+
elif delta_p3 is not None:
|
| 1760 |
wrist_final = delta_p3
|
| 1761 |
wrist_kind = "delta_p3"
|
| 1762 |
+
elif wrist_top is not None:
|
|
|
|
|
|
|
|
|
|
| 1763 |
wrist_final = wrist_top
|
| 1764 |
+
wrist_kind = "radial_deviation_top"
|
| 1765 |
+
else:
|
| 1766 |
+
wrist_final = wrist_p3
|
| 1767 |
+
wrist_kind = "radial_deviation_p3"
|
| 1768 |
|
| 1769 |
front_facing_metrics = {
|
| 1770 |
'shoulder_tilt_impact_deg': {'value': shoulder_tilt_impact},
|
|
|
|
| 1772 |
'hip_sway_top_inches': {'value': hip_sway_top},
|
| 1773 |
'wrist_hinge_top_deg': {
|
| 1774 |
'value': wrist_final,
|
| 1775 |
+
'value_kind': wrist_kind,
|
| 1776 |
+
'forearm_shaft_angle_top_deg': wrist_top_theta,
|
| 1777 |
+
'forearm_shaft_angle_p3_deg': wrist_p3_theta,
|
| 1778 |
+
'radial_deviation_p3_deg': wrist_p3,
|
| 1779 |
+
'radial_deviation_top_deg': wrist_top,
|
| 1780 |
'addr_deg': wrist_addr,
|
| 1781 |
'delta_p3_deg': delta_p3,
|
| 1782 |
'delta_top_deg': delta_top,
|