from personal_analysis.drawers.utils import get_center def valid_point(p): return ( p is not None and len(p) == 2 and p[0] is not None and p[1] is not None ) def valid_bbox(b): """Return False for None, 0, empty, or invalid bbox.""" if b == 0: return False else: return True def distance(p1, p2): if p1 is None or p2 is None: return None return ((p1**2 + p2**2)**0.5) def ball_hand(ball_loco, points, frames): leave_frames = [] in_hand_prev = False is_dribble = False ball_is_head = False prev_ball_valid = False dist_thresh=40 for i, frame_id in enumerate(frames): # ---- Safe Ball Center ---- ball_bbox = ball_loco[i] if i < len(ball_loco) else None if valid_bbox(ball_bbox): ball_center = get_center(ball_bbox) else: ball_center = None # No ball detected prev_ball_valid = False continue # ---- Right wrist and face parts ---- joints = points[i] if i < len(points) else None if joints is None: in_hand = False prev_ball_valid = True in_hand_prev = in_hand continue right_wrist = joints[10] right_soulder = joints[6] nose = joints[0] l_eye = joints[1] r_eye = joints[2] l_ear = joints[3] r_ear = joints[4] # ---- Validity check ---- if not (valid_point(ball_center) and valid_point(right_wrist)): in_hand = False else: dx = ball_center[0] - right_wrist[0] dy = ball_center[1] - right_wrist[1] dist = distance(dx, dy) # Increased threshold for high-res videos (1080p/4K) if (dist < 100): in_hand = True else: in_hand = False # Only mark as dribble if it's significantly below the shoulder if (ball_center[1] > (right_soulder[1] + 50)): is_dribble = True # Check if the head is wrongly detected as the ball if not (valid_point(nose) and valid_point(r_eye) and valid_point(l_eye) and valid_point(r_ear) and valid_point(l_ear)): ball_is_head = False else: # More robust head check dist_thresh_head = 80 distance_nose = distance(ball_center[0] - nose[0], ball_center[1] - nose[1]) distance_r_eye = distance(ball_center[0] - r_eye[0], ball_center[1] - r_eye[1]) distance_l_eye = distance(ball_center[0] - l_eye[0], ball_center[1] - l_eye[1]) distance_r_ear = distance(ball_center[0] - r_ear[0], ball_center[1] - r_ear[1]) distance_l_ear = distance(ball_center[0] - l_ear[0], ball_center[1] - l_ear[1]) if (distance_nose <= dist_thresh_head or distance_r_eye <= dist_thresh_head or distance_l_eye <= dist_thresh_head or distance_r_ear <= dist_thresh_head or distance_l_ear <= dist_thresh_head): ball_is_head = True # ---- Detect transition: ball was in hand → now not ---- if prev_ball_valid and in_hand_prev and not in_hand: leave_frames.append(i) prev_ball_valid = True in_hand_prev = in_hand is_dribble = False ball_is_head = False accurate_leave_frames = [] buffer_frames=10 last_kept = -10000000000 for f in leave_frames: if f - last_kept > buffer_frames: accurate_leave_frames.append(f) last_kept = f return accurate_leave_frames def shot_started(points, leave_frames): shot_start_frames = [] for frame_num in leave_frames: start_found = False start = max(0, frame_num - 20) end = frame_num for i in range(start, end): if i >= len(points): continue joints = points[i] if joints is None: continue right_shoulder = joints[6] right_elbow = joints[8] # Heuristic: Shot starts when elbow is near or above shoulder height if (right_elbow[1] - 30) <= right_shoulder[1]: shot_start_frames.append(i) start_found = True break if not start_found: # Fallback: Use the release frame itself minus a small buffer shot_start_frames.append(max(0, frame_num - 10)) return shot_start_frames