Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -4,22 +4,25 @@ import torch
|
|
4 |
from ultralytics import YOLO
|
5 |
import gradio as gr
|
6 |
from scipy.interpolate import interp1d
|
7 |
-
from scipy.ndimage import uniform_filter1d
|
8 |
import uuid
|
9 |
import os
|
|
|
|
|
|
|
10 |
|
11 |
# Load the trained YOLOv8n model
|
12 |
model = YOLO("best.pt")
|
13 |
|
14 |
-
# Constants
|
15 |
-
STUMPS_WIDTH = 0.2286 # meters
|
16 |
-
FRAME_RATE = 20
|
17 |
-
SLOW_MOTION_FACTOR = 2
|
18 |
-
CONF_THRESHOLD = 0.3
|
19 |
-
PITCH_ZONE_Y = 0.8
|
20 |
-
IMPACT_ZONE_Y = 0.7
|
21 |
-
IMPACT_DELTA_Y = 20
|
22 |
-
STUMPS_HEIGHT = 0.711 # meters
|
|
|
23 |
|
24 |
def process_video(video_path):
|
25 |
if not os.path.exists(video_path):
|
@@ -35,10 +38,8 @@ def process_video(video_path):
|
|
35 |
ret, frame = cap.read()
|
36 |
if not ret:
|
37 |
break
|
38 |
-
# Process every frame for better tracking
|
39 |
frames.append(frame.copy())
|
40 |
-
|
41 |
-
frame = cv2.convertScaleAbs(frame, alpha=1.2, beta=10) # Enhance contrast
|
42 |
results = model.predict(frame, conf=CONF_THRESHOLD)
|
43 |
detections = [det for det in results[0].boxes if det.cls == 0]
|
44 |
if len(detections) == 1:
|
@@ -58,168 +59,166 @@ def process_video(video_path):
|
|
58 |
|
59 |
return frames, ball_positions, detection_frames, "\n".join(debug_log)
|
60 |
|
61 |
-
def
|
62 |
if len(ball_positions) < 2:
|
63 |
-
return None, None, None, None, None, None, "Error: Fewer than 2 valid single-ball detections
|
64 |
-
frame_height = frames[0].shape[
|
65 |
|
66 |
-
#
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
times = np.array([i / FRAME_RATE for i in range(len(ball_positions))])
|
71 |
|
72 |
pitch_idx = 0
|
73 |
for i, y in enumerate(y_coords):
|
74 |
-
if y
|
75 |
pitch_idx = i
|
76 |
break
|
77 |
-
pitch_point =
|
78 |
pitch_frame = detection_frames[pitch_idx]
|
79 |
|
80 |
impact_idx = None
|
81 |
for i in range(1, len(y_coords)):
|
82 |
-
if (y_coords[i] >
|
83 |
-
abs(y_coords[i] - y_coords[i-1]) > IMPACT_DELTA_Y):
|
84 |
impact_idx = i
|
85 |
break
|
86 |
if impact_idx is None:
|
87 |
impact_idx = len(y_coords) - 1
|
88 |
-
impact_point =
|
89 |
impact_frame = detection_frames[impact_idx]
|
90 |
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
fy = interp1d(times, y_coords, kind='quadratic', fill_value="extrapolate")
|
98 |
-
except Exception as e:
|
99 |
-
return None, None, None, None, None, None, f"Error in trajectory interpolation: {str(e)}"
|
100 |
-
|
101 |
-
vis_trajectory = list(zip(x_coords, y_coords))
|
102 |
-
t_full = np.linspace(times[0], times[-1] + 0.5, len(times) + 5)
|
103 |
-
x_full = fx(t_full)
|
104 |
-
y_full = fy(t_full)
|
105 |
-
full_trajectory = list(zip(x_full, y_full))
|
106 |
|
107 |
-
|
108 |
-
|
109 |
-
f"Impact point at frame {impact_frame + 1}: ({impact_point[0]:.1f}, {impact_point[1]:.1f})")
|
110 |
-
return full_trajectory, vis_trajectory, pitch_point, pitch_frame, impact_point, impact_frame, debug_log
|
111 |
|
112 |
def lbw_decision(ball_positions, full_trajectory, frames, pitch_point, impact_point):
|
113 |
-
if not frames:
|
114 |
-
return "Error: No
|
115 |
-
if not full_trajectory or len(ball_positions) < 2:
|
116 |
-
return "Not enough data (insufficient valid single-ball detections)", None, None, None
|
117 |
-
|
118 |
frame_height, frame_width = frames[0].shape[:2]
|
119 |
-
stumps_x =
|
120 |
-
stumps_y =
|
121 |
-
|
122 |
-
batsman_area_y = frame_height * 0.7
|
123 |
|
124 |
-
pitch_x, pitch_y = pitch_point
|
125 |
-
impact_x, impact_y = impact_point
|
126 |
|
127 |
-
in_line_threshold =
|
128 |
-
if pitch_x
|
129 |
-
return f"Not Out (Pitched outside line at x: {pitch_x:.1f}
|
130 |
|
131 |
-
if
|
132 |
-
return f"Not Out (Impact outside line
|
133 |
|
134 |
hit_stumps = False
|
135 |
-
for x, y in full_trajectory:
|
136 |
if (abs(x - stumps_x) < in_line_threshold and
|
137 |
-
abs(y - stumps_y) <
|
138 |
hit_stumps = True
|
139 |
break
|
140 |
|
141 |
if hit_stumps:
|
142 |
if abs(x - stumps_x) < in_line_threshold * 0.1:
|
143 |
-
return f"Umpire's Call - Not Out
|
144 |
-
return f"Out (Ball hits stumps
|
145 |
-
return f"Not Out (Missing stumps
|
146 |
-
|
147 |
-
def
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
|
|
|
|
|
|
|
|
199 |
|
200 |
def drs_review(video):
|
201 |
frames, ball_positions, detection_frames, debug_log = process_video(video)
|
202 |
if not frames:
|
203 |
return f"Error: Failed to process video\nDebug Log:\n{debug_log}", None
|
204 |
-
full_trajectory, vis_trajectory, pitch_point, pitch_frame, impact_point, impact_frame, trajectory_log =
|
205 |
decision, full_trajectory, pitch_point, impact_point = lbw_decision(ball_positions, full_trajectory, frames, pitch_point, impact_point)
|
206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
207 |
output_path = f"output_{uuid.uuid4()}.mp4"
|
208 |
-
|
|
|
209 |
|
210 |
debug_output = f"{debug_log}\n{trajectory_log}"
|
211 |
return f"DRS Decision: {decision}\nDebug Log:\n{debug_output}", slow_motion_path
|
212 |
|
213 |
-
# Gradio interface
|
214 |
iface = gr.Interface(
|
215 |
fn=drs_review,
|
216 |
inputs=gr.Video(label="Upload Video Clip"),
|
217 |
outputs=[
|
218 |
gr.Textbox(label="DRS Decision and Debug Log"),
|
219 |
-
gr.Video(label="
|
220 |
],
|
221 |
-
title="AI-Powered DRS for LBW
|
222 |
-
description="Upload a video clip
|
223 |
)
|
224 |
|
225 |
if __name__ == "__main__":
|
|
|
4 |
from ultralytics import YOLO
|
5 |
import gradio as gr
|
6 |
from scipy.interpolate import interp1d
|
|
|
7 |
import uuid
|
8 |
import os
|
9 |
+
from OpenGL.GL import *
|
10 |
+
from OpenGL.GLU import *
|
11 |
+
from pygame import display, event, QUIT
|
12 |
|
13 |
# Load the trained YOLOv8n model
|
14 |
model = YOLO("best.pt")
|
15 |
|
16 |
+
# Constants
|
17 |
+
STUMPS_WIDTH = 0.2286 # meters
|
18 |
+
FRAME_RATE = 20
|
19 |
+
SLOW_MOTION_FACTOR = 2
|
20 |
+
CONF_THRESHOLD = 0.3
|
21 |
+
PITCH_ZONE_Y = 0.8
|
22 |
+
IMPACT_ZONE_Y = 0.7
|
23 |
+
IMPACT_DELTA_Y = 20
|
24 |
+
STUMPS_HEIGHT = 0.711 # meters
|
25 |
+
PITCH_LENGTH = 20.12 # meters (22 yards)
|
26 |
|
27 |
def process_video(video_path):
|
28 |
if not os.path.exists(video_path):
|
|
|
38 |
ret, frame = cap.read()
|
39 |
if not ret:
|
40 |
break
|
|
|
41 |
frames.append(frame.copy())
|
42 |
+
frame = cv2.convertScaleAbs(frame, alpha=1.2, beta=10)
|
|
|
43 |
results = model.predict(frame, conf=CONF_THRESHOLD)
|
44 |
detections = [det for det in results[0].boxes if det.cls == 0]
|
45 |
if len(detections) == 1:
|
|
|
59 |
|
60 |
return frames, ball_positions, detection_frames, "\n".join(debug_log)
|
61 |
|
62 |
+
def estimate_trajectory_3d(ball_positions, detection_frames, frames):
|
63 |
if len(ball_positions) < 2:
|
64 |
+
return None, None, None, None, None, None, "Error: Fewer than 2 valid single-ball detections"
|
65 |
+
frame_height, frame_width = frames[0].shape[:2]
|
66 |
|
67 |
+
# Simple 2D to 3D mapping (approximation)
|
68 |
+
x_coords = np.array([pos[0] for pos in ball_positions]) / frame_width * PITCH_LENGTH
|
69 |
+
y_coords = np.array([frame_height - pos[1] for pos in ball_positions]) / frame_height * STUMPS_HEIGHT * 2
|
70 |
+
z_coords = np.zeros_like(x_coords) # Depth estimation needed (simplified as 0 for now)
|
71 |
times = np.array([i / FRAME_RATE for i in range(len(ball_positions))])
|
72 |
|
73 |
pitch_idx = 0
|
74 |
for i, y in enumerate(y_coords):
|
75 |
+
if y < STUMPS_HEIGHT: # Pitch near ground
|
76 |
pitch_idx = i
|
77 |
break
|
78 |
+
pitch_point = (x_coords[pitch_idx], y_coords[pitch_idx], 0)
|
79 |
pitch_frame = detection_frames[pitch_idx]
|
80 |
|
81 |
impact_idx = None
|
82 |
for i in range(1, len(y_coords)):
|
83 |
+
if (y_coords[i] > STUMPS_HEIGHT and
|
84 |
+
abs(y_coords[i] - y_coords[i-1]) > IMPACT_DELTA_Y * STUMPS_HEIGHT / frame_height):
|
85 |
impact_idx = i
|
86 |
break
|
87 |
if impact_idx is None:
|
88 |
impact_idx = len(y_coords) - 1
|
89 |
+
impact_point = (x_coords[impact_idx], y_coords[impact_idx], 0)
|
90 |
impact_frame = detection_frames[impact_idx]
|
91 |
|
92 |
+
# Interpolate 3D trajectory
|
93 |
+
fx = interp1d(times[:impact_idx + 1], x_coords[:impact_idx + 1], kind='linear', fill_value="extrapolate")
|
94 |
+
fy = interp1d(times[:impact_idx + 1], y_coords[:impact_idx + 1], kind='quadratic', fill_value="extrapolate")
|
95 |
+
fz = interp1d(times[:impact_idx + 1], z_coords[:impact_idx + 1], kind='linear', fill_value="extrapolate")
|
96 |
+
t_full = np.linspace(times[0], times[impact_idx] + 0.5, 50)
|
97 |
+
full_trajectory = list(zip(fx(t_full), fy(t_full), fz(t_full)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
|
99 |
+
vis_trajectory = list(zip(x_coords, y_coords, z_coords))[:impact_idx + 1]
|
100 |
+
return full_trajectory, vis_trajectory, pitch_point, pitch_frame, impact_point, impact_frame, "Trajectory estimated"
|
|
|
|
|
101 |
|
102 |
def lbw_decision(ball_positions, full_trajectory, frames, pitch_point, impact_point):
|
103 |
+
if not frames or not full_trajectory:
|
104 |
+
return "Error: No data", None, None, None
|
|
|
|
|
|
|
105 |
frame_height, frame_width = frames[0].shape[:2]
|
106 |
+
stumps_x = PITCH_LENGTH / 2
|
107 |
+
stumps_y = 0 # Ground level
|
108 |
+
stumps_width = STUMPS_WIDTH
|
|
|
109 |
|
110 |
+
pitch_x, pitch_y, _ = pitch_point
|
111 |
+
impact_x, impact_y, _ = impact_point
|
112 |
|
113 |
+
in_line_threshold = stumps_width / 2
|
114 |
+
if abs(pitch_x - stumps_x) > in_line_threshold:
|
115 |
+
return f"Not Out (Pitched outside line at x: {pitch_x:.1f})", full_trajectory, pitch_point, impact_point
|
116 |
|
117 |
+
if abs(impact_x - stumps_x) > in_line_threshold or impact_y < stumps_y:
|
118 |
+
return f"Not Out (Impact outside line at x: {impact_x:.1f})", full_trajectory, pitch_point, impact_point
|
119 |
|
120 |
hit_stumps = False
|
121 |
+
for x, y, z in full_trajectory:
|
122 |
if (abs(x - stumps_x) < in_line_threshold and
|
123 |
+
abs(y - stumps_y) < STUMPS_HEIGHT / 2):
|
124 |
hit_stumps = True
|
125 |
break
|
126 |
|
127 |
if hit_stumps:
|
128 |
if abs(x - stumps_x) < in_line_threshold * 0.1:
|
129 |
+
return f"Umpire's Call - Not Out", full_trajectory, pitch_point, impact_point
|
130 |
+
return f"Out (Ball hits stumps)", full_trajectory, pitch_point, impact_point
|
131 |
+
return f"Not Out (Missing stumps)", full_trajectory, pitch_point, impact_point
|
132 |
+
|
133 |
+
def init_3d_window(width, height):
|
134 |
+
pygame.init()
|
135 |
+
display.set_mode((width, height), DOUBLEBUF | OPENGL)
|
136 |
+
gluPerspective(45, (width / height), 0.1, 50.0)
|
137 |
+
glTranslatef(0.0, -5.0, -30)
|
138 |
+
glEnable(GL_DEPTH_TEST)
|
139 |
+
|
140 |
+
def draw_3d_scene(trajectory, pitch_point, impact_point, decision):
|
141 |
+
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
142 |
+
glBegin(GL_LINES)
|
143 |
+
for i in range(len(trajectory) - 1):
|
144 |
+
glColor3f(0, 0, 1) # Blue trajectory
|
145 |
+
glVertex3f(trajectory[i][0], trajectory[i][1], trajectory[i][2])
|
146 |
+
glVertex3f(trajectory[i + 1][0], trajectory[i + 1][1], trajectory[i + 1][2])
|
147 |
+
glEnd()
|
148 |
+
|
149 |
+
# Draw pitch and stumps
|
150 |
+
glColor3f(0, 1, 0) # Green pitch
|
151 |
+
glBegin(GL_QUADS)
|
152 |
+
glVertex3f(0, 0, 0)
|
153 |
+
glVertex3f(PITCH_LENGTH, 0, 0)
|
154 |
+
glVertex3f(PITCH_LENGTH, 0, -1)
|
155 |
+
glVertex3f(0, 0, -1)
|
156 |
+
glEnd()
|
157 |
+
|
158 |
+
glColor3f(1, 1, 1) # White stumps
|
159 |
+
glBegin(GL_LINES)
|
160 |
+
glVertex3f(PITCH_LENGTH / 2 - STUMPS_WIDTH / 2, 0, 0)
|
161 |
+
glVertex3f(PITCH_LENGTH / 2 - STUMPS_WIDTH / 2, STUMPS_HEIGHT, 0)
|
162 |
+
glVertex3f(PITCH_LENGTH / 2 + STUMPS_WIDTH / 2, 0, 0)
|
163 |
+
glVertex3f(PITCH_LENGTH / 2 + STUMPS_WIDTH / 2, STUMPS_HEIGHT, 0)
|
164 |
+
glEnd()
|
165 |
+
|
166 |
+
# Draw pitching and impact points
|
167 |
+
if pitch_point:
|
168 |
+
glColor3f(0, 1, 0) # Green
|
169 |
+
glPushMatrix()
|
170 |
+
glTranslatef(pitch_point[0], pitch_point[1], pitch_point[2])
|
171 |
+
glutSolidSphere(0.1, 20, 20)
|
172 |
+
glPopMatrix()
|
173 |
+
|
174 |
+
if impact_point:
|
175 |
+
glColor3f(1, 0, 0) # Red
|
176 |
+
glPushMatrix()
|
177 |
+
glTranslatef(impact_point[0], impact_point[1], impact_point[2])
|
178 |
+
glutSolidSphere(0.1, 20, 20)
|
179 |
+
glPopMatrix()
|
180 |
+
|
181 |
+
# Display decision (simplified text)
|
182 |
+
if "Out" in decision:
|
183 |
+
glColor3f(1, 0.65, 0) # Orange
|
184 |
+
glRasterPos3f(PITCH_LENGTH / 2, STUMPS_HEIGHT, 0)
|
185 |
+
for char in "Wickets":
|
186 |
+
glutBitmapCharacter(GLUT_BITMAP_HELVETICA_12, ord(char))
|
187 |
+
|
188 |
+
display.flip()
|
189 |
|
190 |
def drs_review(video):
|
191 |
frames, ball_positions, detection_frames, debug_log = process_video(video)
|
192 |
if not frames:
|
193 |
return f"Error: Failed to process video\nDebug Log:\n{debug_log}", None
|
194 |
+
full_trajectory, vis_trajectory, pitch_point, pitch_frame, impact_point, impact_frame, trajectory_log = estimate_trajectory_3d(ball_positions, detection_frames, frames)
|
195 |
decision, full_trajectory, pitch_point, impact_point = lbw_decision(ball_positions, full_trajectory, frames, pitch_point, impact_point)
|
196 |
|
197 |
+
# Generate 3D visualization (separate window)
|
198 |
+
init_3d_window(800, 600)
|
199 |
+
from OpenGL.GLUT import glutInit, glutSolidSphere
|
200 |
+
glutInit()
|
201 |
+
for _ in range(100): # Limited frames for demo
|
202 |
+
draw_3d_scene(full_trajectory, pitch_point, impact_point, decision)
|
203 |
+
event.pump()
|
204 |
+
|
205 |
output_path = f"output_{uuid.uuid4()}.mp4"
|
206 |
+
# Note: 3D rendering is separate; 2D video output is placeholder
|
207 |
+
slow_motion_path = None # To be enhanced with 3D export
|
208 |
|
209 |
debug_output = f"{debug_log}\n{trajectory_log}"
|
210 |
return f"DRS Decision: {decision}\nDebug Log:\n{debug_output}", slow_motion_path
|
211 |
|
212 |
+
# Gradio interface (placeholder for 3D)
|
213 |
iface = gr.Interface(
|
214 |
fn=drs_review,
|
215 |
inputs=gr.Video(label="Upload Video Clip"),
|
216 |
outputs=[
|
217 |
gr.Textbox(label="DRS Decision and Debug Log"),
|
218 |
+
gr.Video(label="3D Visualization (Separate Window)")
|
219 |
],
|
220 |
+
title="AI-Powered 3D DRS for LBW",
|
221 |
+
description="Upload a video clip for 3D DRS analysis with pitching (green), impact (red), and wickets (orange) visualization."
|
222 |
)
|
223 |
|
224 |
if __name__ == "__main__":
|