File size: 12,748 Bytes
41c03cf
 
c3429f6
462fddf
c3429f6
449d194
462fddf
ba9faee
d72a7a2
 
 
 
 
 
 
 
c3429f6
7e29aa0
 
c3429f6
c4eb395
 
 
 
 
 
 
 
 
 
c3429f6
 
462fddf
7e44cd3
42d2b87
462fddf
 
70fce4e
462fddf
7e29aa0
58199b3
 
42d2b87
 
 
449d194
c4eb395
449d194
 
 
 
 
 
 
 
db7efde
b3f7afe
42d2b87
c3429f6
462fddf
db7efde
462fddf
db7efde
c9d4715
7e44cd3
0a9dc78
c4eb395
462fddf
c4eb395
 
1213ff3
c4eb395
 
d72a7a2
449d194
d179a4e
1213ff3
d41a272
d72a7a2
1213ff3
d41a272
c4eb395
885c61f
1213ff3
58199b3
1213ff3
c4eb395
 
58199b3
 
 
b3f7afe
c4eb395
1213ff3
0a9dc78
5fb1ae2
 
 
 
 
 
 
 
 
 
 
 
c4eb395
 
0a9dc78
c4eb395
 
0a9dc78
1213ff3
c4eb395
 
0a9dc78
c4eb395
d72a7a2
c4eb395
0a9dc78
c4eb395
 
0a9dc78
c4eb395
 
 
0a9dc78
c4eb395
 
0a9dc78
70fce4e
c4eb395
70fce4e
c4eb395
70fce4e
 
 
 
b3f7afe
c4eb395
 
 
 
e24f9eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c4eb395
d72a7a2
 
c4eb395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462fddf
d72a7a2
 
 
 
 
 
 
 
 
462fddf
7e44cd3
462fddf
1213ff3
c4eb395
1213ff3
c9d4715
e24f9eb
 
 
 
d72a7a2
 
 
 
 
 
 
c4eb395
1213ff3
 
61be320
d72a7a2
462fddf
 
 
 
1213ff3
e24f9eb
462fddf
c4eb395
e24f9eb
462fddf
a295d73
c3429f6
449d194
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
import cv2
import numpy as np
import torch
from ultralytics import YOLO
import gradio as gr
from scipy.interpolate import interp1d
import uuid
import os
try:
    from OpenGL.GL import *
    from OpenGL.GLU import *
    from pygame import display, event, QUIT
    HAS_OPENGL = True
except ImportError:
    print("Warning: PyOpenGL or Pygame not found. 3D visualization will be disabled. Install with 'pip install PyOpenGL PyOpenGL_accelerate pygame'.")
    HAS_OPENGL = False

# Load the trained YOLOv8n model
model = YOLO("best.pt")

# Constants
STUMPS_WIDTH = 0.2286  # meters
FRAME_RATE = 20
SLOW_MOTION_FACTOR = 2
CONF_THRESHOLD = 0.3
PITCH_ZONE_Y = 0.8
IMPACT_ZONE_Y = 0.7
IMPACT_DELTA_Y = 20
STUMPS_HEIGHT = 0.711  # meters
PITCH_LENGTH = 20.12  # meters (22 yards)

def process_video(video_path):
    if not os.path.exists(video_path):
        return [], [], [], "Error: Video file not found"
    cap = cv2.VideoCapture(video_path)
    frames = []
    ball_positions = []
    detection_frames = []
    debug_log = []

    frame_count = 0
    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            break
        frames.append(frame.copy())
        frame = cv2.convertScaleAbs(frame, alpha=1.2, beta=10)
        results = model.predict(frame, conf=CONF_THRESHOLD)
        detections = [det for det in results[0].boxes if det.cls == 0]
        if len(detections) == 1:
            x1, y1, x2, y2 = detections[0].xyxy[0].cpu().numpy()
            ball_positions.append([(x1 + x2) / 2, (y1 + y2) / 2])
            detection_frames.append(len(frames) - 1)
            cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)
        frames[-1] = frame
        debug_log.append(f"Frame {frame_count}: {len(detections)} ball detections")
        frame_count += 1
    cap.release()

    if not ball_positions:
        debug_log.append("No valid single-ball detections in any frame")
    else:
        debug_log.append(f"Total valid single-ball detections: {len(ball_positions)}")

    return frames, ball_positions, detection_frames, "\n".join(debug_log)

def estimate_trajectory_3d(ball_positions, detection_frames, frames):
    if len(ball_positions) < 2:
        return None, None, None, None, None, None, "Error: Fewer than 2 valid single-ball detections"
    frame_height, frame_width = frames[0].shape[:2]

    x_coords = np.array([pos[0] for pos in ball_positions]) / frame_width * PITCH_LENGTH
    y_coords = np.array([frame_height - pos[1] for pos in ball_positions]) / frame_height * STUMPS_HEIGHT * 2
    z_coords = np.zeros_like(x_coords)  # Placeholder for depth
    times = np.array([i / FRAME_RATE for i in range(len(ball_positions))])

    pitch_idx = 0
    for i, y in enumerate(y_coords):
        if y < STUMPS_HEIGHT:
            pitch_idx = i
            break
    pitch_point = (x_coords[pitch_idx], y_coords[pitch_idx], 0)
    pitch_frame = detection_frames[pitch_idx]

    impact_idx = None
    for i in range(1, len(y_coords)):
        if (y_coords[i] > STUMPS_HEIGHT and
            abs(y_coords[i] - y_coords[i-1]) > IMPACT_DELTA_Y * STUMPS_HEIGHT / frame_height):
            impact_idx = i
            break
    if impact_idx is None:
        impact_idx = len(y_coords) - 1
    impact_point = (x_coords[impact_idx], y_coords[impact_idx], 0)
    impact_frame = detection_frames[impact_idx]

    # Use cubic interpolation to avoid derivative mismatch
    try:
        fx = interp1d(times[:impact_idx + 1], x_coords[:impact_idx + 1], kind='cubic', fill_value="extrapolate")
        fy = interp1d(times[:impact_idx + 1], y_coords[:impact_idx + 1], kind='cubic', fill_value="extrapolate")
        fz = interp1d(times[:impact_idx + 1], z_coords[:impact_idx + 1], kind='cubic', fill_value="extrapolate")
    except ValueError as e:
        # Fallback to linear if cubic fails (e.g., too few points)
        fx = interp1d(times[:impact_idx + 1], x_coords[:impact_idx + 1], kind='linear', fill_value="extrapolate")
        fy = interp1d(times[:impact_idx + 1], y_coords[:impact_idx + 1], kind='linear', fill_value="extrapolate")
        fz = interp1d(times[:impact_idx + 1], z_coords[:impact_idx + 1], kind='linear', fill_value="extrapolate")
        print(f"Warning: Cubic interpolation failed, falling back to linear. Error: {str(e)}")

    t_full = np.linspace(times[0], times[impact_idx] + 0.5, 50)
    full_trajectory = list(zip(fx(t_full), fy(t_full), fz(t_full)))

    vis_trajectory = list(zip(x_coords, y_coords, z_coords))[:impact_idx + 1]
    return full_trajectory, vis_trajectory, pitch_point, pitch_frame, impact_point, impact_frame, "Trajectory estimated"

def lbw_decision(ball_positions, full_trajectory, frames, pitch_point, impact_point):
    if not frames or not full_trajectory:
        return "Error: No data", None, None, None
    frame_height, frame_width = frames[0].shape[:2]
    stumps_x = PITCH_LENGTH / 2
    stumps_y = 0
    stumps_width = STUMPS_WIDTH

    pitch_x, pitch_y, _ = pitch_point
    impact_x, impact_y, _ = impact_point

    in_line_threshold = stumps_width / 2
    if abs(pitch_x - stumps_x) > in_line_threshold:
        return f"Not Out (Pitched outside line at x: {pitch_x:.1f})", full_trajectory, pitch_point, impact_point

    if abs(impact_x - stumps_x) > in_line_threshold or impact_y < stumps_y:
        return f"Not Out (Impact outside line at x: {impact_x:.1f})", full_trajectory, pitch_point, impact_point

    hit_stumps = False
    for x, y, z in full_trajectory:
        if (abs(x - stumps_x) < in_line_threshold and
            abs(y - stumps_y) < STUMPS_HEIGHT / 2):
            hit_stumps = True
            break

    if hit_stumps:
        if abs(x - stumps_x) < in_line_threshold * 0.1:
            return f"Umpire's Call - Not Out", full_trajectory, pitch_point, impact_point
        return f"Out (Ball hits stumps)", full_trajectory, pitch_point, impact_point
    return f"Not Out (Missing stumps)", full_trajectory, pitch_point, impact_point

def generate_slow_motion(frames, vis_trajectory, pitch_point, pitch_frame, impact_point, impact_frame, detection_frames, output_path, decision, frame_width, frame_height):
    if not frames:
        return None
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    out = cv2.VideoWriter(output_path, fourcc, FRAME_RATE / SLOW_MOTION_FACTOR, (frame_width, frame_height))

    trajectory_points = np.array([[p[0] * frame_width / PITCH_LENGTH, frame_height - (p[1] * frame_height / (STUMPS_HEIGHT * 2))] for p in vis_trajectory], dtype=np.int32).reshape((-1, 1, 2))

    for i, frame in enumerate(frames):
        # Draw stumps outline (scaled back to pixel coordinates)
        stumps_x = frame_width / 2
        stumps_y = frame_height * 0.8
        stumps_width_pixels = frame_width * (STUMPS_WIDTH / PITCH_LENGTH)
        stumps_height_pixels = frame_height * (STUMPS_HEIGHT / (STUMPS_HEIGHT * 2))
        cv2.line(frame, (int(stumps_x - stumps_width_pixels / 2), int(stumps_y)),
                 (int(stumps_x + stumps_width_pixels / 2), int(stumps_y)), (255, 255, 255), 2)
        cv2.line(frame, (int(stumps_x - stumps_width_pixels / 2), int(stumps_y - stumps_height_pixels)),
                 (int(stumps_x - stumps_width_pixels / 2), int(stumps_y)), (255, 255, 255), 2)
        cv2.line(frame, (int(stumps_x + stumps_width_pixels / 2), int(stumps_y - stumps_height_pixels)),
                 (int(stumps_x + stumps_width_pixels / 2), int(stumps_y)), (255, 255, 255), 2)

        # Draw crease line
        cv2.line(frame, (int(stumps_x - stumps_width_pixels / 2), int(stumps_y)),
                 (int(stumps_x + stumps_width_pixels / 2), int(stumps_y)), (255, 255, 0), 2)

        if i in detection_frames and trajectory_points.size > 0:
            idx = detection_frames.index(i) + 1
            if idx <= len(trajectory_points):
                cv2.polylines(frame, [trajectory_points[:idx]], False, (0, 0, 255), 2)  # Blue trajectory

        if pitch_point and i == pitch_frame:
            x = pitch_point[0] * frame_width / PITCH_LENGTH
            y = frame_height - (pitch_point[1] * frame_height / (STUMPS_HEIGHT * 2))
            cv2.circle(frame, (int(x), int(y)), 8, (0, 255, 0), -1)  # Green for pitching
            cv2.putText(frame, "Pitching", (int(x) + 10, int(y) - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)

        if impact_point and i == impact_frame:
            x = impact_point[0] * frame_width / PITCH_LENGTH
            y = frame_height - (impact_point[1] * frame_height / (STUMPS_HEIGHT * 2))
            cv2.circle(frame, (int(x), int(y)), 8, (0, 0, 255), -1)  # Red for impact
            cv2.putText(frame, "Impact", (int(x) + 10, int(y) + 20),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)

        if impact_point and i == impact_frame and "Out" in decision:
            cv2.putText(frame, "Wickets", (int(stumps_x) - 50, int(stumps_y) - 20),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1)  # Orange for wickets

        for _ in range(SLOW_MOTION_FACTOR):
            out.write(frame)
    out.release()
    return output_path

def draw_3d_scene(trajectory, pitch_point, impact_point, decision):
    if not HAS_OPENGL:
        return
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glBegin(GL_LINES)
    for i in range(len(trajectory) - 1):
        glColor3f(0, 0, 1)  # Blue trajectory
        glVertex3f(trajectory[i][0], trajectory[i][1], trajectory[i][2])
        glVertex3f(trajectory[i + 1][0], trajectory[i + 1][1], trajectory[i + 1][2])
    glEnd()

    glColor3f(0, 1, 0)  # Green pitch
    glBegin(GL_QUADS)
    glVertex3f(0, 0, 0)
    glVertex3f(PITCH_LENGTH, 0, 0)
    glVertex3f(PITCH_LENGTH, 0, -1)
    glVertex3f(0, 0, -1)
    glEnd()

    glColor3f(1, 1, 1)  # White stumps
    glBegin(GL_LINES)
    glVertex3f(PITCH_LENGTH / 2 - STUMPS_WIDTH / 2, 0, 0)
    glVertex3f(PITCH_LENGTH / 2 - STUMPS_WIDTH / 2, STUMPS_HEIGHT, 0)
    glVertex3f(PITCH_LENGTH / 2 + STUMPS_WIDTH / 2, 0, 0)
    glVertex3f(PITCH_LENGTH / 2 + STUMPS_WIDTH / 2, STUMPS_HEIGHT, 0)
    glEnd()

    if pitch_point:
        glColor3f(0, 1, 0)  # Green
        glPushMatrix()
        glTranslatef(pitch_point[0], pitch_point[1], pitch_point[2])
        glutSolidSphere(0.1, 20, 20)
        glPopMatrix()

    if impact_point:
        glColor3f(1, 0, 0)  # Red
        glPushMatrix()
        glTranslatef(impact_point[0], impact_point[1], impact_point[2])
        glutSolidSphere(0.1, 20, 20)
        glPopMatrix()

    if "Out" in decision:
        glColor3f(1, 0.65, 0)  # Orange
        glRasterPos3f(PITCH_LENGTH / 2, STUMPS_HEIGHT, 0)
        for char in "Wickets":
            glutBitmapCharacter(GLUT_BITMAP_HELVETICA_12, ord(char))

    display.flip()

def init_3d_window(width, height):
    if not HAS_OPENGL:
        return
    pygame.init()
    display.set_mode((width, height), DOUBLEBUF | OPENGL)
    gluPerspective(45, (width / height), 0.1, 50.0)
    glTranslatef(0.0, -5.0, -30)
    glEnable(GL_DEPTH_TEST)

def drs_review(video):
    frames, ball_positions, detection_frames, debug_log = process_video(video)
    if not frames:
        return f"Error: Failed to process video\nDebug Log:\n{debug_log}", None
    full_trajectory, vis_trajectory, pitch_point, pitch_frame, impact_point, impact_frame, trajectory_log = estimate_trajectory_3d(ball_positions, detection_frames, frames)
    decision, full_trajectory, pitch_point, impact_point = lbw_decision(ball_positions, full_trajectory, frames, pitch_point, impact_point)

    frame_height, frame_width = frames[0].shape[:2]
    output_path = f"output_{uuid.uuid4()}.mp4"
    slow_motion_path = generate_slow_motion(frames, vis_trajectory, pitch_point, pitch_frame, impact_point, impact_frame, detection_frames, output_path, decision, frame_width, frame_height)

    if HAS_OPENGL:
        init_3d_window(800, 600)
        from OpenGL.GLUT import glutInit, glutSolidSphere
        glutInit()
        for _ in range(100):  # Limited frames for demo
            draw_3d_scene(full_trajectory, pitch_point, impact_point, decision)
            event.pump()

    debug_output = f"{debug_log}\n{trajectory_log}"
    return f"DRS Decision: {decision}\nDebug Log:\n{debug_output}", slow_motion_path

# Gradio interface
iface = gr.Interface(
    fn=drs_review,
    inputs=gr.Video(label="Upload Video Clip"),
    outputs=[
        gr.Textbox(label="DRS Decision and Debug Log"),
        gr.Video(label="Slow-Motion Replay with 2D Annotations")
    ],
    title="AI-Powered 3D DRS for LBW",
    description="Upload a video clip for 3D DRS analysis with pitching (green), impact (red), and wickets (orange) visualization, and 2D annotated video output."
)

if __name__ == "__main__":
    iface.launch()