David-dsv commited on
Commit
998d63b
·
1 Parent(s): 7f45797

Initial commit: CourtSide-CV Tennis Analysis

Browse files
Files changed (7) hide show
  1. .gitignore +19 -0
  2. DEPLOYMENT_GUIDE.md +132 -0
  3. README.md +42 -12
  4. app.py +418 -0
  5. best.pt +3 -0
  6. bytetrack_tennis_custom.yaml +10 -0
  7. requirements.txt +8 -0
.gitignore ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ env/
7
+ venv/
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ *.mp4
12
+ *.avi
13
+ *.mov
14
+ .DS_Store
15
+ .vscode/
16
+ .idea/
17
+ *.log
18
+ tmp/
19
+ temp/
DEPLOYMENT_GUIDE.md ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 📦 Guide de Déploiement sur Hugging Face Spaces
2
+
3
+ ## 🚀 Étapes de déploiement
4
+
5
+ ### 1. Créer un compte Hugging Face
6
+ - Allez sur https://huggingface.co
7
+ - Créez un compte si vous n'en avez pas
8
+
9
+ ### 2. Créer un nouveau Space
10
+ - Cliquez sur votre profil → "New Space"
11
+ - Donnez un nom à votre Space (ex: "courtside-cv-tennis")
12
+ - Choisissez **Gradio** comme SDK
13
+ - Choisissez la visibilité (Public ou Private)
14
+ - Cliquez sur "Create Space"
15
+
16
+ ### 3. Uploader les fichiers
17
+ Vous avez deux options :
18
+
19
+ #### Option A : Via l'interface web
20
+ 1. Dans votre Space, cliquez sur "Files" → "Add file" → "Upload files"
21
+ 2. Uploadez les fichiers suivants :
22
+ - `app.py`
23
+ - `requirements.txt`
24
+ - `README.md`
25
+ - `.gitignore`
26
+
27
+ #### Option B : Via Git (recommandé)
28
+ ```bash
29
+ # Cloner votre Space
30
+ git clone https://huggingface.co/spaces/VOTRE_USERNAME/VOTRE_SPACE_NAME
31
+ cd VOTRE_SPACE_NAME
32
+
33
+ # Copier vos fichiers
34
+ cp /chemin/vers/app.py .
35
+ cp /chemin/vers/requirements.txt .
36
+ cp /chemin/vers/README.md .
37
+
38
+ # Commit et push
39
+ git add .
40
+ git commit -m "Initial commit: CourtSide-CV Tennis Analysis"
41
+ git push
42
+ ```
43
+
44
+ ### 4. Configuration du Space
45
+
46
+ Le Space va automatiquement :
47
+ 1. Installer les dépendances depuis `requirements.txt`
48
+ 2. Lancer `app.py`
49
+ 3. Démarrer l'interface Gradio
50
+
51
+ ⏱️ Le premier déploiement peut prendre 5-10 minutes.
52
+
53
+ ### 5. Ajouter votre modèle custom (optionnel)
54
+
55
+ Si vous avez un modèle YOLO custom pour les balles de tennis :
56
+
57
+ 1. Uploadez votre fichier `best.pt` dans l'onglet "Files"
58
+ 2. Modifiez dans `app.py` ligne 328 :
59
+ ```python
60
+ ball_model_path = 'best.pt' # Au lieu de 'yolov8m.pt'
61
+ ```
62
+
63
+ ### 6. Configuration avancée (optionnel)
64
+
65
+ Pour des performances optimales, vous pouvez configurer :
66
+
67
+ #### Augmenter les ressources
68
+ - Dans les paramètres du Space, passez à un hardware plus puissant (GPU si disponible)
69
+
70
+ #### Ajouter des secrets
71
+ Si vous avez des clés API ou tokens :
72
+ - Allez dans "Settings" → "Repository secrets"
73
+ - Ajoutez vos secrets
74
+
75
+ ## 🔧 Personnalisation
76
+
77
+ ### Modifier les paramètres par défaut
78
+ Dans `app.py`, vous pouvez ajuster :
79
+
80
+ ```python
81
+ # Ligne 41-44 : Seuils de détection
82
+ self.conf_thresh = 0.05 # Confiance minimale
83
+ self.smooth_window = 5 # Fenêtre de lissage
84
+ self.max_interpolate_gap = 30 # Gap max pour interpolation
85
+ ```
86
+
87
+ ### Changer l'apparence
88
+ Modifiez le thème Gradio ligne 325 :
89
+ ```python
90
+ theme=gr.themes.Soft() # Essayez aussi: Base(), Default(), Glass()
91
+ ```
92
+
93
+ ## 🐛 Dépannage
94
+
95
+ ### Le Space ne démarre pas
96
+ 1. Vérifiez les logs dans l'onglet "Logs"
97
+ 2. Assurez-vous que tous les packages dans `requirements.txt` sont compatibles
98
+ 3. Vérifiez qu'il n'y a pas d'erreurs de syntaxe dans `app.py`
99
+
100
+ ### Out of memory
101
+ 1. Réduisez `max_duration` par défaut (ligne 317)
102
+ 2. Réduisez `imgsz` dans les détections YOLO (lignes 54, 131, 223)
103
+ 3. Demandez un hardware plus puissant dans les settings
104
+
105
+ ### Détection de balle faible
106
+ 1. Ajustez `conf_thresh` (ligne 41)
107
+ 2. Utilisez un modèle YOLO custom entraîné sur des balles de tennis
108
+ 3. Assurez-vous que vos vidéos sont de bonne qualité
109
+
110
+ ## 📊 Limitations
111
+
112
+ - **Durée** : Pour éviter les timeouts, limitez les vidéos à 60 secondes
113
+ - **Résolution** : Les vidéos très haute résolution (4K) peuvent être lentes
114
+ - **Gratuit** : Les Spaces gratuits ont des limitations de CPU/RAM
115
+
116
+ ## 🎯 Prochaines étapes
117
+
118
+ 1. **Modèle custom** : Entraînez un modèle YOLO spécifiquement sur des balles de tennis
119
+ 2. **Statistiques avancées** : Ajoutez le comptage de coups, vitesse de balle, etc.
120
+ 3. **Multi-caméras** : Supportez plusieurs angles de vue
121
+ 4. **Export des données** : Exportez les trajectoires en JSON/CSV
122
+
123
+ ## 📞 Support
124
+
125
+ Si vous avez des questions :
126
+ - 💬 Créez une Discussion sur votre Space Hugging Face
127
+ - 🐛 Ouvrez une Issue sur GitHub
128
+ - 📧 Contactez-moi directement
129
+
130
+ ---
131
+
132
+ Bon déploiement ! 🚀
README.md CHANGED
@@ -1,14 +1,44 @@
1
- ---
2
- title: A
3
- emoji: 🌍
4
- colorFrom: purple
5
- colorTo: blue
6
- sdk: gradio
7
- sdk_version: 6.0.0
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- short_description: a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # 🎾 CourtSide-CV - Tennis Analysis
2
+
3
+ Analysez vos matchs de tennis avec l'intelligence artificielle !
4
+
5
+ ## 🌟 Fonctionnalités
6
+
7
+ - **🎯 Tracking de balle en temps réel** : Suivi intelligent de la balle de tennis avec interpolation pour les frames manquantes
8
+ - **🤸 Détection de pose** : Visualisation du squelette des joueurs avec estimation de pose
9
+ - **📊 Analyse de trajectoire** : Lissage avancé des trajectoires pour un rendu fluide
10
+ - **🎨 Effets visuels professionnels** : Trail de la balle, glow effects, overlay informatif
11
+
12
+ ## 🚀 Utilisation
13
+
14
+ 1. Uploadez votre vidéo de match de tennis
15
+ 2. (Optionnel) Entrez les noms des joueurs
16
+ 3. Ajustez la durée maximale si nécessaire
17
+ 4. Cliquez sur "Analyser la vidéo"
18
+ 5. Téléchargez votre vidéo annotée !
19
+
20
+ ## 🔧 Technologies
21
+
22
+ - **YOLOv8** : Détection d'objets et estimation de pose
23
+ - **ByteTrack** : Algorithme de suivi multi-objets
24
+ - **OpenCV** : Traitement et manipulation vidéo
25
+ - **Scipy** : Interpolation et lissage des trajectoires
26
+ - **Gradio** : Interface utilisateur interactive
27
+
28
+ ## 💡 Conseils
29
+
30
+ - Utilisez des vidéos de bonne qualité (720p ou plus)
31
+ - Assurez-vous que la balle est visible dans la majorité des frames
32
+ - Pour de meilleures performances, limitez la durée à 30-60 secondes
33
+
34
+ ## 📝 Note
35
+
36
+ Cette version utilise des modèles YOLO génériques. Pour de meilleurs résultats sur la détection de balle de tennis, vous pouvez utiliser un modèle fine-tuné spécifiquement sur des balles de tennis.
37
+
38
+ ## 📄 Licence
39
+
40
+ MIT License
41
+
42
  ---
43
 
44
+ Créé avec ❤️ par CourtSide-CV
app.py ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ CourtSide-CV - Tennis Analysis Space
4
+ Hugging Face Gradio App
5
+ """
6
+
7
+ import os
8
+ import cv2
9
+ import gradio as gr
10
+ import numpy as np
11
+ from pathlib import Path
12
+ from ultralytics import YOLO
13
+ from collections import defaultdict
14
+ import logging
15
+ from scipy import interpolate
16
+ import tempfile
17
+ import subprocess
18
+
19
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class BallTrackerLinkedIn:
24
+ """Tracker optimisé pour détection de balle de tennis"""
25
+
26
+ def __init__(self, model_path):
27
+ self.ball_model = YOLO(model_path)
28
+ self.tracks = {}
29
+ self.frame_idx = 0
30
+ self.all_positions = []
31
+ self.conf_thresh = 0.05
32
+ self.smooth_window = 5
33
+ self.max_interpolate_gap = 30
34
+
35
+ def process_batch(self, frames, progress=gr.Progress()):
36
+ """Process un batch de frames pour le tracking"""
37
+ positions = []
38
+
39
+ for i, frame in enumerate(frames):
40
+ if progress:
41
+ progress((i + 1) / len(frames), desc=f"Detecting ball... {i+1}/{len(frames)}")
42
+
43
+ self.frame_idx = i
44
+ results = self.ball_model.track(
45
+ source=frame,
46
+ conf=self.conf_thresh,
47
+ classes=[0],
48
+ imgsz=640,
49
+ iou=0.5,
50
+ persist=True,
51
+ verbose=False
52
+ )
53
+
54
+ ball_pos = None
55
+ if results[0].boxes is not None and len(results[0].boxes) > 0:
56
+ best_idx = results[0].boxes.conf.argmax()
57
+ x1, y1, x2, y2 = results[0].boxes.xyxy[best_idx].tolist()
58
+ cx = (x1 + x2) / 2
59
+ cy = (y1 + y2) / 2
60
+ conf = float(results[0].boxes.conf[best_idx])
61
+ ball_pos = (cx, cy, conf)
62
+
63
+ positions.append((i, ball_pos))
64
+
65
+ return positions
66
+
67
+ def interpolate_missing(self, positions):
68
+ """Interpoler les positions manquantes"""
69
+ detected_frames = []
70
+ detected_x = []
71
+ detected_y = []
72
+
73
+ for frame_idx, pos in positions:
74
+ if pos is not None:
75
+ detected_frames.append(frame_idx)
76
+ detected_x.append(pos[0])
77
+ detected_y.append(pos[1])
78
+
79
+ if len(detected_frames) < 2:
80
+ return positions
81
+
82
+ fx = interpolate.interp1d(detected_frames, detected_x, kind='linear', fill_value='extrapolate')
83
+ fy = interpolate.interp1d(detected_frames, detected_y, kind='linear', fill_value='extrapolate')
84
+
85
+ interpolated = []
86
+ for frame_idx, pos in positions:
87
+ if pos is None:
88
+ prev_detected = max([f for f in detected_frames if f < frame_idx], default=-999)
89
+ next_detected = min([f for f in detected_frames if f > frame_idx], default=999)
90
+
91
+ if (frame_idx - prev_detected <= self.max_interpolate_gap and
92
+ next_detected - frame_idx <= self.max_interpolate_gap):
93
+ ix = float(fx(frame_idx))
94
+ iy = float(fy(frame_idx))
95
+ interpolated.append((frame_idx, (ix, iy, 0.0)))
96
+ else:
97
+ interpolated.append((frame_idx, None))
98
+ else:
99
+ interpolated.append((frame_idx, pos))
100
+
101
+ return interpolated
102
+
103
+ def smooth_trajectory(self, positions):
104
+ """Lisser la trajectoire avec filtre médian"""
105
+ smoothed = []
106
+
107
+ for i, (frame_idx, pos) in enumerate(positions):
108
+ if pos is None:
109
+ smoothed.append((frame_idx, None))
110
+ continue
111
+
112
+ window_start = max(0, i - self.smooth_window // 2)
113
+ window_end = min(len(positions), i + self.smooth_window // 2 + 1)
114
+
115
+ window_x = []
116
+ window_y = []
117
+ for j in range(window_start, window_end):
118
+ if positions[j][1] is not None:
119
+ window_x.append(positions[j][1][0])
120
+ window_y.append(positions[j][1][1])
121
+
122
+ if window_x:
123
+ smooth_x = np.median(window_x)
124
+ smooth_y = np.median(window_y)
125
+ conf = pos[2] if len(pos) > 2 else 0.0
126
+ smoothed.append((frame_idx, (smooth_x, smooth_y, conf)))
127
+ else:
128
+ smoothed.append((frame_idx, pos))
129
+
130
+ return smoothed
131
+
132
+
133
+ class VideoProcessorLinkedIn:
134
+ """Processeur vidéo pour Gradio"""
135
+
136
+ def __init__(self, ball_model_path):
137
+ self.tracker = BallTrackerLinkedIn(ball_model_path)
138
+ self.person_model = YOLO('yolov8m.pt')
139
+ self.pose_model = YOLO('yolov8m-pose.pt')
140
+
141
+ self.skeleton_connections = [
142
+ (5, 6), (5, 7), (7, 9), (6, 8), (8, 10),
143
+ (5, 11), (6, 12), (11, 12), (11, 13), (13, 15),
144
+ (12, 14), (14, 16), (0, 1), (0, 2), (1, 3), (2, 4)
145
+ ]
146
+
147
+ def draw_skeleton(self, frame, keypoints, conf_threshold=0.5):
148
+ """Dessine le squelette sur la frame"""
149
+ joint_color = (0, 255, 0)
150
+ bone_color = (0, 255, 255)
151
+
152
+ for connection in self.skeleton_connections:
153
+ kp1_idx, kp2_idx = connection
154
+ if kp1_idx < len(keypoints) and kp2_idx < len(keypoints):
155
+ kp1 = keypoints[kp1_idx]
156
+ kp2 = keypoints[kp2_idx]
157
+
158
+ if len(kp1) > 2 and len(kp2) > 2:
159
+ if kp1[2] > conf_threshold and kp2[2] > conf_threshold:
160
+ pt1 = (int(kp1[0]), int(kp1[1]))
161
+ pt2 = (int(kp2[0]), int(kp2[1]))
162
+ cv2.line(frame, pt1, pt2, bone_color, 2, cv2.LINE_AA)
163
+
164
+ for keypoint in keypoints:
165
+ if len(keypoint) > 2 and keypoint[2] > conf_threshold:
166
+ x, y = int(keypoint[0]), int(keypoint[1])
167
+ cv2.circle(frame, (x, y), 4, joint_color, -1, cv2.LINE_AA)
168
+ cv2.circle(frame, (x, y), 4, (255, 255, 255), 1, cv2.LINE_AA)
169
+
170
+ def process_video(self, video_path, player1_name="PLAYER 1", player2_name="PLAYER 2",
171
+ max_duration=30, progress=gr.Progress()):
172
+ """Traiter la vidéo et retourner la version annotée"""
173
+
174
+ if video_path is None:
175
+ return None, "❌ Veuillez uploader une vidéo"
176
+
177
+ try:
178
+ logger.info(f"Processing video: {video_path}")
179
+
180
+ # Ouvrir la vidéo
181
+ cap = cv2.VideoCapture(video_path)
182
+ if not cap.isOpened():
183
+ return None, "❌ Impossible d'ouvrir la vidéo"
184
+
185
+ fps = int(cap.get(cv2.CAP_PROP_FPS))
186
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
187
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
188
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
189
+
190
+ # Limiter la durée
191
+ max_frames = min(total_frames, int(fps * max_duration))
192
+
193
+ # Lire toutes les frames
194
+ progress(0, desc="Loading video...")
195
+ frames = []
196
+ for i in range(max_frames):
197
+ ret, frame = cap.read()
198
+ if not ret:
199
+ break
200
+ frames.append(frame)
201
+ cap.release()
202
+
203
+ if len(frames) == 0:
204
+ return None, "❌ Aucune frame lue"
205
+
206
+ logger.info(f"Loaded {len(frames)} frames ({width}x{height} @ {fps}fps)")
207
+
208
+ # Phase 1: Tracking de la balle
209
+ progress(0.1, desc="Tracking ball...")
210
+ positions = self.tracker.process_batch(frames, progress)
211
+
212
+ # Phase 2: Interpolation
213
+ progress(0.4, desc="Interpolating missing positions...")
214
+ positions = self.tracker.interpolate_missing(positions)
215
+
216
+ # Phase 3: Lissage
217
+ progress(0.5, desc="Smoothing trajectory...")
218
+ positions = self.tracker.smooth_trajectory(positions)
219
+
220
+ # Stats
221
+ detected = sum(1 for _, p in positions if p is not None)
222
+ coverage = (detected / len(positions)) * 100
223
+
224
+ # Phase 4: Rendu vidéo
225
+ progress(0.6, desc="Rendering annotated video...")
226
+
227
+ # Créer fichier de sortie temporaire
228
+ temp_output = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name
229
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
230
+ out = cv2.VideoWriter(temp_output, fourcc, fps, (width, height))
231
+
232
+ trail_length = 15
233
+ trail_positions = []
234
+
235
+ for frame_idx, (_, ball_pos) in enumerate(positions):
236
+ if progress:
237
+ progress(0.6 + 0.3 * (frame_idx / len(frames)),
238
+ desc=f"Rendering... {frame_idx+1}/{len(frames)}")
239
+
240
+ annotated = frames[frame_idx].copy()
241
+
242
+ # Dessiner la balle et sa trajectoire
243
+ if ball_pos is not None:
244
+ x, y, conf = ball_pos
245
+ trail_positions.append((int(x), int(y)))
246
+ if len(trail_positions) > trail_length:
247
+ trail_positions.pop(0)
248
+
249
+ # Trail
250
+ for i in range(1, len(trail_positions)):
251
+ alpha = i / len(trail_positions)
252
+ thickness = int(2 + alpha * 2)
253
+ cv2.line(annotated, trail_positions[i-1], trail_positions[i],
254
+ (0, 255, 255), thickness, cv2.LINE_AA)
255
+
256
+ # Balle
257
+ radius = 8
258
+ cv2.circle(annotated, (int(x), int(y)), radius + 3,
259
+ (0, 255, 255), -1, cv2.LINE_AA)
260
+ cv2.circle(annotated, (int(x), int(y)), radius,
261
+ (0, 255, 0), -1, cv2.LINE_AA)
262
+ cv2.circle(annotated, (int(x), int(y)), radius,
263
+ (255, 255, 255), 2, cv2.LINE_AA)
264
+
265
+ # Détection de pose
266
+ pose_results = self.pose_model(frames[frame_idx], conf=0.3, verbose=False)
267
+ if pose_results[0].keypoints is not None:
268
+ for keypoints in pose_results[0].keypoints.data[:2]:
269
+ keypoints_np = keypoints.cpu().numpy()
270
+ keypoints_with_conf = [[kp[0], kp[1], kp[2]] for kp in keypoints_np]
271
+ self.draw_skeleton(annotated, keypoints_with_conf, conf_threshold=0.3)
272
+
273
+ # Overlay
274
+ cv2.rectangle(annotated, (0, height-45), (width, height), (0, 0, 0), -1)
275
+ cv2.putText(annotated, "CourtSide-CV", (15, height-15),
276
+ cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2, cv2.LINE_AA)
277
+
278
+ if ball_pos is not None:
279
+ status = "TRACKING" if conf > 0.1 else "PREDICTED"
280
+ color_status = (0, 255, 255) if conf > 0.1 else (255, 200, 0)
281
+ cv2.putText(annotated, f"Ball: {status}", (width//2 - 60, height-15),
282
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, color_status, 2, cv2.LINE_AA)
283
+
284
+ out.write(annotated)
285
+
286
+ out.release()
287
+
288
+ # Conversion finale en H.264 pour compatibilité
289
+ progress(0.95, desc="Finalizing video...")
290
+ final_output = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name
291
+
292
+ cmd = [
293
+ 'ffmpeg', '-i', temp_output,
294
+ '-c:v', 'libx264', '-preset', 'fast', '-crf', '22',
295
+ '-pix_fmt', 'yuv420p', '-movflags', '+faststart',
296
+ final_output, '-y', '-loglevel', 'error'
297
+ ]
298
+ subprocess.run(cmd, check=True)
299
+ os.remove(temp_output)
300
+
301
+ message = f"""
302
+ ✅ **Vidéo traitée avec succès!**
303
+
304
+ 📊 **Statistiques:**
305
+ - Frames traitées: {len(frames)}
306
+ - Couverture balle: {coverage:.1f}%
307
+ - Résolution: {width}x{height}
308
+ - FPS: {fps}
309
+
310
+ 🎾 Prêt pour LinkedIn!
311
+ """
312
+
313
+ logger.info(f"✅ Processing complete: {final_output}")
314
+ return final_output, message
315
+
316
+ except Exception as e:
317
+ logger.error(f"Error processing video: {e}", exc_info=True)
318
+ return None, f"❌ Erreur: {str(e)}"
319
+
320
+
321
+ # Télécharger les modèles au démarrage
322
+ def download_models():
323
+ """Télécharge les modèles YOLO nécessaires"""
324
+ logger.info("Downloading YOLO models...")
325
+
326
+ # Modèles de base (téléchargés automatiquement par ultralytics)
327
+ _ = YOLO('yolov8m.pt')
328
+ _ = YOLO('yolov8m-pose.pt')
329
+
330
+ logger.info("✅ Models ready!")
331
+
332
+
333
+ # Initialiser le processeur
334
+ logger.info("Initializing app...")
335
+ download_models()
336
+
337
+ # Note: Pour le modèle de balle de tennis personnalisé, vous devrez l'uploader
338
+ # Pour l'instant, on utilise le modèle YOLO standard
339
+ ball_model_path = 'yolov8m.pt' # Remplacer par le chemin de votre modèle custom
340
+ processor = VideoProcessorLinkedIn(ball_model_path)
341
+
342
+
343
+ # Interface Gradio
344
+ def process_video_gradio(video, player1, player2, max_duration):
345
+ """Wrapper pour Gradio"""
346
+ return processor.process_video(video, player1, player2, max_duration)
347
+
348
+
349
+ # Créer l'interface
350
+ with gr.Blocks(title="🎾 CourtSide-CV - Tennis Analysis", theme=gr.themes.Soft()) as demo:
351
+ gr.Markdown("""
352
+ # 🎾 CourtSide-CV - Tennis Analysis
353
+
354
+ Analysez vos matchs de tennis avec l'IA ! Cette application utilise la vision par ordinateur pour :
355
+ - 🎯 **Tracker la balle** en temps réel avec interpolation intelligente
356
+ - 🤸 **Détecter la pose** des joueurs avec visualisation du squelette
357
+ - 📊 **Analyser les trajectoires** avec lissage avancé
358
+
359
+ ---
360
+ """)
361
+
362
+ with gr.Row():
363
+ with gr.Column():
364
+ video_input = gr.Video(label="📹 Uploadez votre vidéo de tennis")
365
+
366
+ with gr.Row():
367
+ player1_input = gr.Textbox(
368
+ label="👤 Nom Joueur 1 (gauche)",
369
+ value="PLAYER 1",
370
+ max_lines=1
371
+ )
372
+ player2_input = gr.Textbox(
373
+ label="👤 Nom Joueur 2 (droite)",
374
+ value="PLAYER 2",
375
+ max_lines=1
376
+ )
377
+
378
+ max_duration_input = gr.Slider(
379
+ minimum=5,
380
+ maximum=60,
381
+ value=30,
382
+ step=5,
383
+ label="⏱️ Durée maximale (secondes)",
384
+ info="Pour des raisons de performance, limitez la durée"
385
+ )
386
+
387
+ submit_btn = gr.Button("🚀 Analyser la vidéo", variant="primary", size="lg")
388
+
389
+ with gr.Column():
390
+ video_output = gr.Video(label="🎬 Vidéo annotée")
391
+ status_output = gr.Markdown(label="📊 Résultats")
392
+
393
+ gr.Markdown("""
394
+ ---
395
+ ### 💡 Conseils
396
+ - Utilisez des vidéos de **bonne qualité** pour de meilleurs résultats
397
+ - La **balle doit être visible** dans la majorité des frames
398
+ - Pour de meilleures performances, limitez à **30 secondes**
399
+
400
+ ### 🔧 Technologies
401
+ - **YOLOv8** pour la détection d'objets et de poses
402
+ - **ByteTrack** pour le suivi d'objets
403
+ - **OpenCV** pour le traitement vidéo
404
+ - **Scipy** pour l'interpolation
405
+
406
+ ---
407
+ Créé avec ❤️ par CourtSide-CV
408
+ """)
409
+
410
+ submit_btn.click(
411
+ fn=process_video_gradio,
412
+ inputs=[video_input, player1_input, player2_input, max_duration_input],
413
+ outputs=[video_output, status_output]
414
+ )
415
+
416
+ # Lancer l'application
417
+ if __name__ == "__main__":
418
+ demo.launch()
best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8971b58d985de0da9cecb2a7699300de26c4e51ceb9585807656d5bbf7e0aa58
3
+ size 6313891
bytetrack_tennis_custom.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configuration ByteTrack optimisée pour tennis
2
+ tracker_type: bytetrack
3
+ track_high_thresh: 0.15 # Plus bas pour détecter la balle
4
+ track_low_thresh: 0.01 # Très bas pour ne pas perdre
5
+ new_track_thresh: 0.10 # Nouveau track
6
+ track_buffer: 60 # Buffer plus long (2 sec à 30fps)
7
+ match_thresh: 0.70 # Match moins strict
8
+ fuse_score: True
9
+ mot20: False
10
+ frame_rate: 50 # 50 fps pour notre vidéo
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ opencv-python-headless
3
+ ultralytics
4
+ numpy
5
+ scipy
6
+ pillow
7
+ torch
8
+ torchvision