Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- app.py +241 -0
- requirements.txt +6 -0
app.py
ADDED
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Digital Review System (DRS) application for LBW decisions
|
3 |
+
========================================================
|
4 |
+
|
5 |
+
This application provides a simplified demonstration of how a cricket‑style
|
6 |
+
digital review system (DRS) could be implemented using open source
|
7 |
+
computer vision tools. It is not a complete Hawk‑Eye replacement, but
|
8 |
+
illustrates the key steps in building such a system: capturing video,
|
9 |
+
detecting and tracking the ball, estimating its flight trajectory,
|
10 |
+
analysing whether it would have hit the stumps, estimating speed and
|
11 |
+
generating a replay with annotations. A Gradio interface ties these
|
12 |
+
components together to provide an easy way to record a match, appeal
|
13 |
+
for an LBW decision and review the result.
|
14 |
+
|
15 |
+
The app has two main pages:
|
16 |
+
|
17 |
+
• **Live Match Recording** – allows the user to upload or record match video.
|
18 |
+
The video is stored on disk and can be analysed later.
|
19 |
+
|
20 |
+
• **LBW Review** – analyses the last few seconds of the recorded video
|
21 |
+
whenever an appeal is made. It performs ball tracking, trajectory
|
22 |
+
estimation and stumps intersection checks to predict whether the
|
23 |
+
batsman is out or not. An annotated replay and a 3D trajectory
|
24 |
+
visualisation are returned along with speed and impact information.
|
25 |
+
|
26 |
+
The implementation relies on simple background subtraction and circle
|
27 |
+
detection rather than proprietary tracking systems. It assumes a
|
28 |
+
single static camera behind the bowler and a fairly unobstructed view
|
29 |
+
of the pitch. See the individual modules in the ``modules`` package
|
30 |
+
for more details on each processing step.
|
31 |
+
|
32 |
+
Note: because this space is intended to run on Hugging Face, file
|
33 |
+
paths and heavy downloads are avoided wherever possible. The code is
|
34 |
+
fully self contained and uses only packages available in this runtime.
|
35 |
+
"""
|
36 |
+
|
37 |
+
from __future__ import annotations
|
38 |
+
|
39 |
+
import os
|
40 |
+
import shutil
|
41 |
+
import tempfile
|
42 |
+
from pathlib import Path
|
43 |
+
from typing import Any, Dict, Tuple
|
44 |
+
|
45 |
+
import gradio as gr
|
46 |
+
|
47 |
+
from drs_modules.modules.video_processing import trim_last_seconds, save_uploaded_video
|
48 |
+
from drs_modules.modules.detection import detect_and_track_ball
|
49 |
+
from drs_modules.modules.trajectory import estimate_trajectory, predict_stumps_intersection
|
50 |
+
from drs_modules.modules.lbw_decision import make_lbw_decision
|
51 |
+
from drs_modules.modules.visualization import (
|
52 |
+
generate_trajectory_plot,
|
53 |
+
annotate_video_with_tracking,
|
54 |
+
)
|
55 |
+
|
56 |
+
|
57 |
+
def analyse_appeal(video_path: str, review_seconds: int = 8) -> Tuple[str, Dict[str, Any]]:
|
58 |
+
"""Analyse the last few seconds of a match video and return DRS results.
|
59 |
+
|
60 |
+
Parameters
|
61 |
+
----------
|
62 |
+
video_path: str
|
63 |
+
Path to the full match video recorded on the Live Match Recording page.
|
64 |
+
review_seconds: int, optional
|
65 |
+
Number of seconds from the end of the video to analyse. Defaults to 8.
|
66 |
+
|
67 |
+
Returns
|
68 |
+
-------
|
69 |
+
Tuple[str, Dict[str, Any]]
|
70 |
+
A message summarising the decision and a dictionary with the
|
71 |
+
underlying data for display (decision text, ball speed, impact
|
72 |
+
frame number, annotated video path and trajectory plot path).
|
73 |
+
"""
|
74 |
+
# Create a temporary directory to hold intermediate files
|
75 |
+
temp_dir = tempfile.mkdtemp()
|
76 |
+
trimmed_path = os.path.join(temp_dir, "trimmed.mp4")
|
77 |
+
|
78 |
+
# Step 1: Trim the last N seconds of the input video
|
79 |
+
trim_last_seconds(video_path, trimmed_path, review_seconds)
|
80 |
+
|
81 |
+
# Step 2: Detect and track the ball through the trimmed segment
|
82 |
+
tracking_data = detect_and_track_ball(trimmed_path)
|
83 |
+
|
84 |
+
# Step 3: Estimate the ball's trajectory (2D for simplicity) and predict
|
85 |
+
# whether it will hit the stumps
|
86 |
+
trajectory_model = estimate_trajectory(tracking_data["centers"], tracking_data["timestamps"])
|
87 |
+
will_hit_stumps = predict_stumps_intersection(trajectory_model)
|
88 |
+
|
89 |
+
# Step 4: Make a decision based on trajectory and impact detection
|
90 |
+
decision, impact_frame_idx = make_lbw_decision(
|
91 |
+
tracking_data["centers"],
|
92 |
+
trajectory_model,
|
93 |
+
will_hit_stumps,
|
94 |
+
)
|
95 |
+
|
96 |
+
# Step 5: Calculate ball speed (pixels per second scaled to km/h)
|
97 |
+
total_distance_px = 0.0
|
98 |
+
for i in range(1, len(tracking_data["centers"])):
|
99 |
+
cx0, cy0 = tracking_data["centers"][i - 1]
|
100 |
+
cx1, cy1 = tracking_data["centers"][i]
|
101 |
+
total_distance_px += ((cx1 - cx0) ** 2 + (cy1 - cy0) ** 2) ** 0.5
|
102 |
+
# Duration of captured frames
|
103 |
+
duration = tracking_data["timestamps"][-1] - tracking_data["timestamps"][0]
|
104 |
+
if duration <= 0:
|
105 |
+
speed_kmh = 0.0
|
106 |
+
else:
|
107 |
+
# Convert pixel distance per second to km/h using an assumed scale
|
108 |
+
pixels_per_metre = 50.0
|
109 |
+
speed_mps = (total_distance_px / pixels_per_metre) / duration
|
110 |
+
speed_kmh = speed_mps * 3.6
|
111 |
+
|
112 |
+
# Step 6: Generate annotated replay video and trajectory plot
|
113 |
+
annotated_video_path = os.path.join(temp_dir, "annotated.mp4")
|
114 |
+
annotate_video_with_tracking(
|
115 |
+
trimmed_path,
|
116 |
+
tracking_data["centers"],
|
117 |
+
trajectory_model,
|
118 |
+
will_hit_stumps,
|
119 |
+
impact_frame_idx,
|
120 |
+
annotated_video_path,
|
121 |
+
)
|
122 |
+
plot_path = os.path.join(temp_dir, "trajectory_plot.png")
|
123 |
+
generate_trajectory_plot(
|
124 |
+
tracking_data["centers"], trajectory_model, will_hit_stumps, plot_path
|
125 |
+
)
|
126 |
+
|
127 |
+
# Compose the message and result dictionary
|
128 |
+
decision_message = f"Decision: {decision}"
|
129 |
+
result = {
|
130 |
+
"decision": decision,
|
131 |
+
"ball_speed_kmh": round(speed_kmh, 2),
|
132 |
+
"impact_frame_index": impact_frame_idx,
|
133 |
+
"annotated_video": annotated_video_path,
|
134 |
+
"trajectory_plot": plot_path,
|
135 |
+
}
|
136 |
+
|
137 |
+
return decision_message, result
|
138 |
+
|
139 |
+
|
140 |
+
def build_interface() -> gr.Blocks:
|
141 |
+
"""Construct the Gradio interface with multiple pages."""
|
142 |
+
with gr.Blocks(title="Cricket LBW DRS Demo") as demo:
|
143 |
+
gr.Markdown(
|
144 |
+
"""# Digital Review System (LBW)
|
145 |
+
|
146 |
+
This demo illustrates how a simplified digital review system can be
|
147 |
+
implemented using computer vision techniques. You can record or
|
148 |
+
upload match footage, and when an appeal occurs, the system will
|
149 |
+
analyse the last few seconds to decide whether the batsman is **OUT**
|
150 |
+
or **NOT OUT**. Alongside the decision you will receive an
|
151 |
+
annotated replay, a 3D trajectory plot and an estimate of the ball
|
152 |
+
speed.
|
153 |
+
"""
|
154 |
+
)
|
155 |
+
|
156 |
+
with gr.Tab("Live Match Recording"):
|
157 |
+
video_input = gr.Video(
|
158 |
+
label="Record or upload match video",
|
159 |
+
sources=["upload", "webcam"],
|
160 |
+
# Do not specify `type` because some versions of Gradio
|
161 |
+
# reject that argument. The file path is available via
|
162 |
+
# video_file.name in the callback.
|
163 |
+
)
|
164 |
+
out_video_path = gr.State()
|
165 |
+
|
166 |
+
def on_video_upload(video_file):
|
167 |
+
if video_file is None:
|
168 |
+
return None
|
169 |
+
save_path = save_uploaded_video(video_file.name, video_file)
|
170 |
+
return save_path
|
171 |
+
|
172 |
+
video_input.change(
|
173 |
+
fn=on_video_upload,
|
174 |
+
inputs=[video_input],
|
175 |
+
outputs=[out_video_path],
|
176 |
+
)
|
177 |
+
|
178 |
+
gr.Markdown(
|
179 |
+
"""
|
180 |
+
After recording or uploading a video, switch to the **LBW Review**
|
181 |
+
tab and press **Analyse Appeal** to review the last 8 seconds.
|
182 |
+
"""
|
183 |
+
)
|
184 |
+
|
185 |
+
with gr.Tab("LBW Review"):
|
186 |
+
with gr.Row():
|
187 |
+
analyse_button = gr.Button("Analyse Appeal")
|
188 |
+
review_seconds = gr.Number(
|
189 |
+
value=8, label="Seconds to review", minimum=2, maximum=20
|
190 |
+
)
|
191 |
+
decision_output = gr.Textbox(label="Decision", lines=1)
|
192 |
+
ball_speed_output = gr.Textbox(
|
193 |
+
label="Ball speed (km/h)", lines=1, interactive=False
|
194 |
+
)
|
195 |
+
impact_frame_output = gr.Textbox(
|
196 |
+
label="Impact frame index", lines=1, interactive=False
|
197 |
+
)
|
198 |
+
annotated_video_output = gr.Video(
|
199 |
+
label="Annotated replay video"
|
200 |
+
)
|
201 |
+
trajectory_plot_output = gr.Image(
|
202 |
+
label="3D Trajectory plot"
|
203 |
+
)
|
204 |
+
|
205 |
+
def on_analyse(_):
|
206 |
+
video_path = out_video_path.value
|
207 |
+
if not video_path or not os.path.exists(video_path):
|
208 |
+
return (
|
209 |
+
"Please record or upload a video in the first tab.",
|
210 |
+
None,
|
211 |
+
None,
|
212 |
+
None,
|
213 |
+
None,
|
214 |
+
)
|
215 |
+
message, result = analyse_appeal(video_path, int(review_seconds.value))
|
216 |
+
return (
|
217 |
+
message,
|
218 |
+
str(result["ball_speed_kmh"]),
|
219 |
+
str(result["impact_frame_index"]),
|
220 |
+
result["annotated_video"],
|
221 |
+
result["trajectory_plot"],
|
222 |
+
)
|
223 |
+
|
224 |
+
analyse_button.click(
|
225 |
+
fn=on_analyse,
|
226 |
+
inputs=[analyse_button],
|
227 |
+
outputs=[
|
228 |
+
decision_output,
|
229 |
+
ball_speed_output,
|
230 |
+
impact_frame_output,
|
231 |
+
annotated_video_output,
|
232 |
+
trajectory_plot_output,
|
233 |
+
],
|
234 |
+
)
|
235 |
+
|
236 |
+
return demo
|
237 |
+
|
238 |
+
|
239 |
+
if __name__ == "__main__":
|
240 |
+
demo = build_interface()
|
241 |
+
demo.launch()
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
opencv-python
|
3 |
+
numpy
|
4 |
+
matplotlib
|
5 |
+
gradio
|
6 |
+
pillow
|