| """ |
| JSON generator for converting movement annotations and keypoint data to structured JSON format. |
| """ |
|
|
| import json |
| from typing import List, Dict, Any, Optional |
| from datetime import datetime |
|
|
| from .pose_estimation import PoseResult, Keypoint |
| from .notation_engine import MovementMetrics, Direction, Intensity, Speed |
| import numpy as np |
|
|
|
|
| def generate_json( |
| movement_metrics: List[MovementMetrics], |
| pose_results: Optional[List[List[PoseResult]]] = None, |
| video_metadata: Optional[Dict[str, Any]] = None, |
| include_keypoints: bool = False |
| ) -> Dict[str, Any]: |
| """ |
| Generate structured JSON output from movement analysis results. |
| |
| Args: |
| movement_metrics: List of movement metrics per frame |
| pose_results: Optional pose keypoints per frame |
| video_metadata: Optional video metadata (fps, dimensions, etc.) |
| include_keypoints: Whether to include raw keypoint data |
| |
| Returns: |
| Dictionary containing formatted analysis results |
| """ |
| output = { |
| "analysis_metadata": { |
| "timestamp": datetime.now().isoformat(), |
| "version": "1.0.0", |
| "model_info": video_metadata.get("model_info", {}) if video_metadata else {} |
| }, |
| "video_info": {}, |
| "movement_analysis": { |
| "frame_count": len(movement_metrics), |
| "frames": [] |
| } |
| } |
| |
| |
| if video_metadata: |
| output["video_info"] = { |
| "fps": video_metadata.get("fps", 30.0), |
| "duration_seconds": len(movement_metrics) / video_metadata.get("fps", 30.0), |
| "width": video_metadata.get("width"), |
| "height": video_metadata.get("height"), |
| "frame_count": video_metadata.get("frame_count", len(movement_metrics)) |
| } |
| |
| |
| for i, metrics in enumerate(movement_metrics): |
| frame_data = { |
| "frame_index": metrics.frame_index, |
| "timestamp": metrics.timestamp, |
| "metrics": { |
| "direction": metrics.direction.value, |
| "intensity": metrics.intensity.value, |
| "speed": metrics.speed.value, |
| "velocity": round(metrics.velocity, 4), |
| "acceleration": round(metrics.acceleration, 4), |
| "fluidity": round(metrics.fluidity, 3), |
| "expansion": round(metrics.expansion, 3), |
| "total_displacement": round(metrics.total_displacement, 4) |
| } |
| } |
| |
| |
| if metrics.center_displacement: |
| frame_data["metrics"]["center_displacement"] = { |
| "x": round(metrics.center_displacement[0], 4), |
| "y": round(metrics.center_displacement[1], 4) |
| } |
| |
| |
| if include_keypoints and pose_results and i < len(pose_results): |
| frame_poses = pose_results[i] |
| if frame_poses: |
| frame_data["keypoints"] = [] |
| for pose in frame_poses: |
| keypoint_data = { |
| "person_id": pose.person_id, |
| "points": [] |
| } |
| for kp in pose.keypoints: |
| keypoint_data["points"].append({ |
| "name": kp.name, |
| "x": round(kp.x, 4), |
| "y": round(kp.y, 4), |
| "confidence": round(kp.confidence, 3) |
| }) |
| frame_data["keypoints"].append(keypoint_data) |
| |
| output["movement_analysis"]["frames"].append(frame_data) |
| |
| |
| output["movement_analysis"]["summary"] = _generate_summary(movement_metrics) |
| |
| return output |
|
|
|
|
| def _generate_summary(metrics: List[MovementMetrics]) -> Dict[str, Any]: |
| """Generate summary statistics from movement metrics.""" |
| if not metrics: |
| return {} |
| |
| |
| direction_counts = {} |
| intensity_counts = {} |
| speed_counts = {} |
| |
| velocities = [] |
| accelerations = [] |
| fluidities = [] |
| expansions = [] |
| |
| for m in metrics: |
| |
| direction_counts[m.direction.value] = direction_counts.get(m.direction.value, 0) + 1 |
| intensity_counts[m.intensity.value] = intensity_counts.get(m.intensity.value, 0) + 1 |
| speed_counts[m.speed.value] = speed_counts.get(m.speed.value, 0) + 1 |
| |
| |
| velocities.append(m.velocity) |
| accelerations.append(m.acceleration) |
| fluidities.append(m.fluidity) |
| expansions.append(m.expansion) |
| |
| |
| import numpy as np |
| |
| summary = { |
| "direction": { |
| "distribution": direction_counts, |
| "dominant": max(direction_counts, key=direction_counts.get) |
| }, |
| "intensity": { |
| "distribution": intensity_counts, |
| "dominant": max(intensity_counts, key=intensity_counts.get) |
| }, |
| "speed": { |
| "distribution": speed_counts, |
| "dominant": max(speed_counts, key=speed_counts.get) |
| }, |
| "velocity": { |
| "mean": round(float(np.mean(velocities)), 4), |
| "std": round(float(np.std(velocities)), 4), |
| "min": round(float(np.min(velocities)), 4), |
| "max": round(float(np.max(velocities)), 4) |
| }, |
| "acceleration": { |
| "mean": round(float(np.mean(accelerations)), 4), |
| "std": round(float(np.std(accelerations)), 4), |
| "min": round(float(np.min(accelerations)), 4), |
| "max": round(float(np.max(accelerations)), 4) |
| }, |
| "fluidity": { |
| "mean": round(float(np.mean(fluidities)), 3), |
| "std": round(float(np.std(fluidities)), 3) |
| }, |
| "expansion": { |
| "mean": round(float(np.mean(expansions)), 3), |
| "std": round(float(np.std(expansions)), 3) |
| } |
| } |
| |
| |
| summary["movement_segments"] = _identify_movement_segments(metrics) |
| |
| return summary |
|
|
|
|
| def _identify_movement_segments(metrics: List[MovementMetrics]) -> List[Dict[str, Any]]: |
| """Identify significant movement segments (e.g., bursts of activity).""" |
| segments = [] |
| |
| |
| current_segment = None |
| intensity_threshold = Intensity.MEDIUM |
| |
| for i, m in enumerate(metrics): |
| if m.intensity.value >= intensity_threshold.value: |
| if current_segment is None: |
| |
| current_segment = { |
| "start_frame": i, |
| "start_time": m.timestamp, |
| "peak_velocity": m.velocity, |
| "dominant_direction": m.direction.value |
| } |
| else: |
| |
| if m.velocity > current_segment["peak_velocity"]: |
| current_segment["peak_velocity"] = m.velocity |
| current_segment["dominant_direction"] = m.direction.value |
| else: |
| if current_segment is not None: |
| |
| current_segment["end_frame"] = i - 1 |
| current_segment["end_time"] = metrics[i-1].timestamp if i > 0 else 0 |
| current_segment["duration"] = ( |
| current_segment["end_time"] - current_segment["start_time"] |
| ) |
| current_segment["peak_velocity"] = round(current_segment["peak_velocity"], 4) |
| segments.append(current_segment) |
| current_segment = None |
| |
| |
| if current_segment is not None: |
| current_segment["end_frame"] = len(metrics) - 1 |
| current_segment["end_time"] = metrics[-1].timestamp |
| current_segment["duration"] = ( |
| current_segment["end_time"] - current_segment["start_time"] |
| ) |
| current_segment["peak_velocity"] = round(current_segment["peak_velocity"], 4) |
| segments.append(current_segment) |
| |
| return segments |
|
|
|
|
| def save_json(data: Dict[str, Any], output_path: str, pretty: bool = True) -> None: |
| """ |
| Save JSON data to file. |
| |
| Args: |
| data: Dictionary to save |
| output_path: Path to output file |
| pretty: Whether to format JSON with indentation |
| """ |
| with open(output_path, 'w') as f: |
| if pretty: |
| json.dump(data, f, indent=2, sort_keys=False) |
| else: |
| json.dump(data, f) |
|
|
|
|
| def format_for_display(data: Dict[str, Any]) -> str: |
| """ |
| Format JSON data for display in Gradio. |
| |
| Args: |
| data: Dictionary to format |
| |
| Returns: |
| Formatted JSON string |
| """ |
| return json.dumps(data, indent=2, sort_keys=False) |