#!/opt/homebrew/bin/python3.11 """ Find "Kids" in pose data based on Head-to-Body Ratio. Heuristic: Kids have a larger head relative to their body height (approx 1:5 or 1:6) compared to adults (approx 1:7.5). """ import json import math # Configuration POSE_JSON_PATH = "output/384b0ff44aaaa1f1/384b0ff44aaaa1f1.pose.json" # Heuristic Threshold: Kids typically have a body length < 6.0 * head_width # Adults are usually > 6.5. # We look for Ratio < 5.5 to be safe (smaller is "more kid-like" relative to head size) BODY_TO_HEAD_RATIO_THRESHOLD = 5.8 def distance(p1, p2): return math.sqrt((p1['x'] - p2['x'])**2 + (p1['y'] - p2['y'])**2) def get_midpoint(p1, p2): return {'x': (p1['x'] + p2['x'])/2, 'y': (p1['y'] + p2['y'])/2} def find_kids(): try: with open(POSE_JSON_PATH, 'r') as f: data = json.load(f) except Exception as e: print(f"Error loading JSON: {e}") return frames = data.get("frames", {}) potential_kids = [] # Counters for debugging total_poses = 0 analyzed_poses = 0 for frame_idx_str, frame_data in frames.items(): # Structure: frames -> { "frame_index": { "timestamp": ..., "poses": [...] } } # Or maybe just "poses" list directly? # Checking structure: result["frames"][str(idx)] = { "timestamp": ..., "poses": frame_poses } # Wait, in the processor code: # result["frames"][str(idx)] = { "timestamp": idx / fps ..., "poses": frame_poses } # But the loop iterates over `frames.items()`. # Actually, looking at the JSON structure saved: # It saves the whole result dict. # result = { ... "frames": { "0": { ... }, "10": { ... } } } # So `frame_data` is { "timestamp": ..., "poses": [...] } timestamp = frame_data.get("timestamp", 0) # "poses" in this JSON is the list of person detections # Each detection has "keypoints" list # But wait, looking at the processor code: # frame_poses.append({"keypoints": person_keypoints, "person_id": person_idx}) # The saved JSON structure in process_video_pose is: # result["frames"][str(idx)] = { "timestamp": ..., "poses": frame_poses } # Let's check the actual JSON structure of the file generated. # It is likely: frames -> { "frame_index": { "timestamp": ..., "poses": [...] } } people_in_frame = frame_data.get("poses", []) for person in people_in_frame: total_poses += 1 kps_list = person.get("keypoints", []) # Map keypoints by name for easier access kp_dict = {kp['name']: kp for kp in kps_list} # We need visible keypoints nose = kp_dict.get('nose') l_shoulder = kp_dict.get('left_shoulder') r_shoulder = kp_dict.get('right_shoulder') l_hip = kp_dict.get('left_hip') r_hip = kp_dict.get('right_hip') l_ankle = kp_dict.get('left_ankle') r_ankle = kp_dict.get('right_ankle') # Check visibility if not nose or not (l_shoulder or r_shoulder): continue analyzed_poses += 1 # Estimate Head Size # Distance Nose -> Mid-Shoulders is approx half head height. if l_shoulder and r_shoulder: mid_shoulder = get_midpoint(l_shoulder, r_shoulder) elif l_shoulder: mid_shoulder = l_shoulder else: mid_shoulder = r_shoulder if not mid_shoulder: continue # Head Height approx = 2 * distance(Nose, Mid_Shoulder) # Why 2? Nose is roughly in the middle of the face vertically (eyes/nose/mouth). # Distance from nose to shoulder top is roughly "Neck + Half Head". # A rough proxy for Head Height is 1/2 shoulder width? No. # Let's use: Head_Height ~ 1.0 * distance(Nose, Shoulder) is risky. # Let's assume Head_Height is roughly constant relative to shoulder width. # Better metric: Body Length / Shoulder Width? # No, shoulder width varies with build. # Let's go back to: Total Visible Height / Estimated Head Height. # Head Height Estimate = Distance(Nose, Mid_Shoulder) * 2.5 (Rough guess for full head). # Actually, let's use: Head_Height = Distance(Left Ear, Right Ear) if visible? No, usually not reliable. # Let's use: Head_Height = Distance(Nose, Mid_Shoulder) * 1.8 (Empirical factor). head_height_est = distance(nose, mid_shoulder) * 1.8 if head_height_est < 10: # Too small/noisy continue # Body Height: Distance from Nose to lowest visible point (Hip or Ankle) # We want to estimate Total Height. # If Ankles visible: if l_ankle and r_ankle: mid_ankle = get_midpoint(l_ankle, r_ankle) # Height from Top of Head to Ankle # Nose is inside head. Distance(Nose, Ankle) + Top_of_Head_offset. # Let's just use Distance(Nose, Ankle) as the "Body Length below nose". # Total Height ≈ Dist(Nose, Ankle) + Head_Height/2. dist_nose_ankle = distance(nose, mid_ankle) total_height = dist_nose_ankle + (head_height_est / 2) # Check for valid height (avoid division by zero or weird angles) if total_height > head_height_est: ratio = total_height / head_height_est # Heuristic: # Adults: ~7.0 - 8.0 # Kids: ~4.5 - 6.0 # We look for < 6.5 if ratio < BODY_TO_HEAD_RATIO_THRESHOLD: potential_kids.append({ "frame": frame_idx_str, "timestamp": timestamp, "ratio": round(ratio, 2), "person_id": person.get("person_id", "?") }) else: # If legs not visible (sitting/crouching), harder to judge ratio. # We could use Shoulder-to-Hip vs Head, but let's stick to full body for safety. pass print(f"Analyzed {analyzed_poses} poses out of {total_poses} total detections.") print(f"Found {len(potential_kids)} potential 'kids' (Ratio < {BODY_TO_HEAD_RATIO_THRESHOLD}).") # Group by timestamp to avoid duplicates (same person in consecutive frames) unique_kids = {} for k in potential_kids: ts = round(k['timestamp'], 1) # Round to 0.1s if ts not in unique_kids: unique_kids[ts] = k # Sort by timestamp sorted_kids = sorted(unique_kids.values(), key=lambda x: x['timestamp']) print("\nUnique potential kid detections (timestamps):") for k in sorted_kids: print(f" -> Timestamp: {k['timestamp']:.2f}s | Ratio: {k['ratio']}") if __name__ == "__main__": find_kids()