varun2808 commited on
Commit
014cef7
·
verified ·
1 Parent(s): 63004a4

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +211 -0
app.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import math
3
+ import os
4
+ import tempfile
5
+ import uuid
6
+ import zipfile
7
+
8
+ import cv2
9
+ import mediapipe as mp
10
+ from fastapi import FastAPI, File, UploadFile, HTTPException
11
+ from fastapi.responses import StreamingResponse, JSONResponse
12
+ from mediapipe.tasks.python import BaseOptions
13
+ from mediapipe.tasks.python.vision import (
14
+ PoseLandmarker,
15
+ PoseLandmarkerOptions,
16
+ RunningMode,
17
+ )
18
+
19
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
20
+ MODEL_PATH = os.path.join(SCRIPT_DIR, "pose_landmarker_heavy.task")
21
+
22
+ # Landmark indices
23
+ LEFT_SHOULDER = 11
24
+ RIGHT_SHOULDER = 12
25
+ LEFT_HIP = 23
26
+ RIGHT_HIP = 24
27
+
28
+ TARGETS = {
29
+ "front": 0,
30
+ "front_45_clockwise": 45,
31
+ "right_side": 90,
32
+ "back_45_clockwise": 135,
33
+ "back": 180,
34
+ "back_45_anticlockwise": -135,
35
+ "left_side": -90,
36
+ "front_45_anticlockwise": -45,
37
+ }
38
+
39
+ app = FastAPI(title="Pose Frame Extractor API")
40
+
41
+
42
+ def estimate_body_angle(world_landmarks):
43
+ ls = world_landmarks[LEFT_SHOULDER]
44
+ rs = world_landmarks[RIGHT_SHOULDER]
45
+ lh = world_landmarks[LEFT_HIP]
46
+ rh = world_landmarks[RIGHT_HIP]
47
+
48
+ s_dx = ls.x - rs.x
49
+ s_dz = ls.z - rs.z
50
+ h_dx = lh.x - rh.x
51
+ h_dz = lh.z - rh.z
52
+
53
+ dx = (s_dx + h_dx) / 2
54
+ dz = (s_dz + h_dz) / 2
55
+
56
+ angle_rad = math.atan2(dz, dx)
57
+ return math.degrees(angle_rad)
58
+
59
+
60
+ def angle_distance(a, b):
61
+ diff = (a - b + 180) % 360 - 180
62
+ return abs(diff)
63
+
64
+
65
+ def process_video(video_path: str):
66
+ """Process video and return dict of pose_name -> (png_bytes, metadata)."""
67
+ if not os.path.exists(MODEL_PATH):
68
+ raise HTTPException(status_code=500, detail="Pose model file not found on server.")
69
+
70
+ cap = cv2.VideoCapture(video_path)
71
+ if not cap.isOpened():
72
+ raise HTTPException(status_code=400, detail="Cannot open uploaded video.")
73
+
74
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
75
+ fps = cap.get(cv2.CAP_PROP_FPS)
76
+ if fps == 0:
77
+ cap.release()
78
+ raise HTTPException(status_code=400, detail="Invalid video: 0 FPS detected.")
79
+
80
+ options = PoseLandmarkerOptions(
81
+ base_options=BaseOptions(model_asset_path=MODEL_PATH),
82
+ running_mode=RunningMode.VIDEO,
83
+ num_poses=1,
84
+ min_pose_detection_confidence=0.5,
85
+ min_pose_presence_confidence=0.5,
86
+ min_tracking_confidence=0.5,
87
+ )
88
+ landmarker = PoseLandmarker.create_from_options(options)
89
+
90
+ best = {
91
+ name: {"diff": float("inf"), "frame": None, "angle": None, "frame_idx": -1}
92
+ for name in TARGETS
93
+ }
94
+
95
+ frame_idx = 0
96
+ while True:
97
+ ret, frame = cap.read()
98
+ if not ret:
99
+ break
100
+
101
+ rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
102
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
103
+ timestamp_ms = int(frame_idx * 1000 / fps)
104
+ results = landmarker.detect_for_video(mp_image, timestamp_ms)
105
+
106
+ if results.pose_world_landmarks and len(results.pose_world_landmarks) > 0:
107
+ world_lms = results.pose_world_landmarks[0]
108
+ angle = estimate_body_angle(world_lms)
109
+
110
+ for name, target in TARGETS.items():
111
+ diff = angle_distance(angle, target)
112
+ if diff < best[name]["diff"]:
113
+ best[name] = {
114
+ "diff": diff,
115
+ "frame": frame.copy(),
116
+ "angle": angle,
117
+ "frame_idx": frame_idx,
118
+ }
119
+
120
+ frame_idx += 1
121
+
122
+ cap.release()
123
+ landmarker.close()
124
+
125
+ # Encode frames as PNGs
126
+ results_out = {}
127
+ for name, target_angle in TARGETS.items():
128
+ info = best[name]
129
+ if info["frame"] is None:
130
+ continue
131
+ suffix = "" if info["diff"] <= 15 else "_approx"
132
+ filename = f"{name}{suffix}.png"
133
+ _, buf = cv2.imencode(".png", info["frame"])
134
+ results_out[filename] = {
135
+ "png_bytes": buf.tobytes(),
136
+ "frame_idx": info["frame_idx"],
137
+ "detected_angle": round(info["angle"], 1),
138
+ "target_angle": target_angle,
139
+ "error": round(info["diff"], 1),
140
+ }
141
+
142
+ return results_out, total_frames, fps
143
+
144
+
145
+ @app.post("/extract-poses")
146
+ async def extract_poses(video: UploadFile = File(...)):
147
+ """Upload a video and get back a ZIP of extracted pose frames."""
148
+ # Save uploaded video to a temp file
149
+ suffix = os.path.splitext(video.filename or "video.mp4")[1]
150
+ with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
151
+ tmp.write(await video.read())
152
+ tmp_path = tmp.name
153
+
154
+ try:
155
+ results, total_frames, fps = process_video(tmp_path)
156
+ finally:
157
+ os.unlink(tmp_path)
158
+
159
+ if not results:
160
+ raise HTTPException(status_code=422, detail="No poses detected in video.")
161
+
162
+ # Build a ZIP in memory
163
+ zip_buffer = io.BytesIO()
164
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
165
+ for filename, data in results.items():
166
+ zf.writestr(filename, data["png_bytes"])
167
+
168
+ zip_buffer.seek(0)
169
+ return StreamingResponse(
170
+ zip_buffer,
171
+ media_type="application/zip",
172
+ headers={"Content-Disposition": "attachment; filename=pose_frames.zip"},
173
+ )
174
+
175
+
176
+ @app.post("/extract-poses-json")
177
+ async def extract_poses_json(video: UploadFile = File(...)):
178
+ """Upload a video and get back JSON metadata (no images, just info)."""
179
+ suffix = os.path.splitext(video.filename or "video.mp4")[1]
180
+ with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
181
+ tmp.write(await video.read())
182
+ tmp_path = tmp.name
183
+
184
+ try:
185
+ results, total_frames, fps = process_video(tmp_path)
186
+ finally:
187
+ os.unlink(tmp_path)
188
+
189
+ summary = []
190
+ for filename, data in results.items():
191
+ summary.append({
192
+ "filename": filename,
193
+ "frame_idx": data["frame_idx"],
194
+ "detected_angle": data["detected_angle"],
195
+ "target_angle": data["target_angle"],
196
+ "error_degrees": data["error"],
197
+ })
198
+
199
+ return JSONResponse({
200
+ "video_info": {
201
+ "total_frames": total_frames,
202
+ "fps": round(fps, 1),
203
+ "duration_seconds": round(total_frames / fps, 2),
204
+ },
205
+ "poses": summary,
206
+ })
207
+
208
+
209
+ @app.get("/health")
210
+ async def health():
211
+ return {"status": "ok", "model_loaded": os.path.exists(MODEL_PATH)}