RamziBm commited on
Commit
41f1119
·
1 Parent(s): 528ff57

feat: Intégration de la détection de ballon avec YOLO et amélioration des vérifications de modèles dans app.py et main.py.

Browse files
Files changed (4) hide show
  1. app.py +52 -7
  2. main.py +51 -5
  3. requirements.txt +19 -6
  4. visualizer.py +33 -6
app.py CHANGED
@@ -5,13 +5,16 @@ import torch
5
  from pathlib import Path
6
  import time
7
  import traceback
 
 
8
 
9
  # Importer les éléments nécessaires depuis les autres modules du projet
10
  try:
11
  from tvcalib.infer.module import TvCalibInferModule
12
  # On essaie d'importer la fonction de pré-traitement depuis main.py
13
  # Si main.py n'est pas conçu pour être importé, il faudra peut-être copier/coller cette fonction ici
14
- from main import preprocess_image_tvcalib, IMAGE_SHAPE, SEGMENTATION_MODEL_PATH
 
15
  from visualizer import (
16
  create_minimap_view,
17
  create_minimap_with_offset_skeletons,
@@ -40,6 +43,11 @@ if not SEGMENTATION_MODEL_PATH.exists():
40
  print("L'application risque de ne pas fonctionner. Assurez-vous que le fichier est présent.")
41
  # Gradio peut quand même démarrer, mais le traitement échouera.
42
 
 
 
 
 
 
43
  # --- Fonction Principale de Traitement ---
44
  def process_image_and_generate_minimaps(input_image_bgr, optim_steps, target_avg_scale):
45
  """
@@ -54,12 +62,23 @@ def process_image_and_generate_minimaps(input_image_bgr, optim_steps, target_avg
54
  # Vérifier si le modèle de segmentation existe (important car on ne peut pas l'afficher dans l'UI facilement)
55
  if not SEGMENTATION_MODEL_PATH.exists():
56
  # Retourner des images noires ou des messages d'erreur
57
- error_msg = f"Erreur: Modèle {SEGMENTATION_MODEL_PATH} introuvable."
58
  print(error_msg)
59
- placeholder = np.zeros((300, 500, 3), dtype=np.uint8) # Placeholder noir
60
- cv2.putText(placeholder, error_msg, (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
 
 
61
  return placeholder, placeholder.copy() # Retourner deux placeholders
62
 
 
 
 
 
 
 
 
 
 
63
  try:
64
  # 1. Initialisation du modèle TvCalib (peut être lent si fait à chaque fois)
65
  # Pourrait être optimisé en chargeant globalement (voir commentaire plus haut)
@@ -86,6 +105,31 @@ def process_image_and_generate_minimaps(input_image_bgr, optim_steps, target_avg
86
  image_tensor = image_tensor.to(model_device)
87
  print(f"Temps de prétraitement TvCalib : {time.time() - start_preprocess:.3f}s")
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  # 3. Exécuter la calibration (Segmentation + Optimisation)
91
  print("Exécution de la segmentation...")
@@ -136,11 +180,12 @@ def process_image_and_generate_minimaps(input_image_bgr, optim_steps, target_avg
136
  # Minimap avec projection (image RGB attendue par la fonction)
137
  minimap_original = create_minimap_view(image_rgb_resized, homography_np)
138
 
139
- # Minimap avec squelettes (utilise l'échelle estimée)
140
  minimap_offset_skeletons, actual_avg_scale = create_minimap_with_offset_skeletons(
141
  player_list,
142
  homography_np,
143
- base_skeleton_scale=estimated_base_scale
 
144
  )
145
  print(f"Temps de génération des minimaps : {time.time() - start_viz:.3f}s")
146
  if actual_avg_scale is not None:
@@ -194,7 +239,7 @@ with gr.Blocks() as demo:
194
  info="Number of iterations to refine homography."
195
  )
196
  target_scale_slider = gr.Slider(
197
- minimum=0.1, maximum=2.5, step=0.05, value=0.35,
198
  label="Target Average Skeleton Scale",
199
  info="Adjusts the desired average size of skeletons on the minimap."
200
  )
 
5
  from pathlib import Path
6
  import time
7
  import traceback
8
+ # Import YOLO
9
+ from ultralytics import YOLO
10
 
11
  # Importer les éléments nécessaires depuis les autres modules du projet
12
  try:
13
  from tvcalib.infer.module import TvCalibInferModule
14
  # On essaie d'importer la fonction de pré-traitement depuis main.py
15
  # Si main.py n'est pas conçu pour être importé, il faudra peut-être copier/coller cette fonction ici
16
+ # Importer aussi les constantes YOLO depuis main.py (ou les redéfinir ici)
17
+ from main import preprocess_image_tvcalib, IMAGE_SHAPE, SEGMENTATION_MODEL_PATH, YOLO_MODEL_PATH, BALL_CLASS_INDEX
18
  from visualizer import (
19
  create_minimap_view,
20
  create_minimap_with_offset_skeletons,
 
43
  print("L'application risque de ne pas fonctionner. Assurez-vous que le fichier est présent.")
44
  # Gradio peut quand même démarrer, mais le traitement échouera.
45
 
46
+ # Vérifier si le modèle YOLO existe aussi
47
+ if not YOLO_MODEL_PATH.exists():
48
+ print(f"AVERTISSEMENT : Modèle YOLO introuvable : {YOLO_MODEL_PATH}")
49
+ print("L'application risque de ne pas fonctionner. Assurez-vous que le fichier est présent.")
50
+
51
  # --- Fonction Principale de Traitement ---
52
  def process_image_and_generate_minimaps(input_image_bgr, optim_steps, target_avg_scale):
53
  """
 
62
  # Vérifier si le modèle de segmentation existe (important car on ne peut pas l'afficher dans l'UI facilement)
63
  if not SEGMENTATION_MODEL_PATH.exists():
64
  # Retourner des images noires ou des messages d'erreur
65
+ error_msg = f"Erreur: Modèle {SEGMENTATION_MODEL_PATH.name} introuvable."
66
  print(error_msg)
67
+ # Créer un placeholder plus informatif
68
+ placeholder = np.zeros((300, 500, 3), dtype=np.uint8)
69
+ cv2.putText(placeholder, "Model Error:", (10, 130), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 1, cv2.LINE_AA)
70
+ cv2.putText(placeholder, error_msg, (10, 155), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
71
  return placeholder, placeholder.copy() # Retourner deux placeholders
72
 
73
+ # Vérifier aussi le modèle YOLO
74
+ if not YOLO_MODEL_PATH.exists():
75
+ error_msg = f"Erreur: Modèle {YOLO_MODEL_PATH.name} introuvable."
76
+ print(error_msg)
77
+ placeholder = np.zeros((300, 500, 3), dtype=np.uint8)
78
+ cv2.putText(placeholder, "Model Error:", (10, 130), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 1, cv2.LINE_AA)
79
+ cv2.putText(placeholder, error_msg, (10, 155), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
80
+ return placeholder, placeholder.copy()
81
+
82
  try:
83
  # 1. Initialisation du modèle TvCalib (peut être lent si fait à chaque fois)
84
  # Pourrait être optimisé en chargeant globalement (voir commentaire plus haut)
 
105
  image_tensor = image_tensor.to(model_device)
106
  print(f"Temps de prétraitement TvCalib : {time.time() - start_preprocess:.3f}s")
107
 
108
+ # --- Détection du ballon avec YOLO ---
109
+ print("Chargement du modèle YOLO et détection du ballon...")
110
+ start_yolo = time.time()
111
+ ball_ref_point_img = None # Point de référence du ballon sur l'image originale redimensionnée
112
+ try:
113
+ # Charger le modèle YOLO (pourrait être chargé globalement pour la perf, mais attention)
114
+ yolo_model = YOLO(YOLO_MODEL_PATH)
115
+ # Utiliser l'image BGR redimensionnée pour YOLO
116
+ results = yolo_model.predict(image_bgr_resized, classes=[BALL_CLASS_INDEX], verbose=False)
117
+
118
+ if results and len(results[0].boxes) > 0:
119
+ # Prendre la détection avec la plus haute confiance
120
+ best_ball_box = results[0].boxes[results[0].boxes.conf.argmax()]
121
+ x1, y1, x2, y2 = map(int, best_ball_box.xyxy[0].tolist())
122
+ conf = best_ball_box.conf[0].item()
123
+
124
+ # Calculer le point de référence (centre bas de la bbox)
125
+ ball_ref_point_img = np.array([(x1 + x2) / 2, y2], dtype=np.float32)
126
+ print(f" ✓ Ballon trouvé (conf: {conf:.2f}) à la bbox [{x1},{y1},{x2},{y2}]. Point réf: {ball_ref_point_img}")
127
+ else:
128
+ print(" Aucun ballon détecté.")
129
+
130
+ except Exception as e_yolo:
131
+ print(f" Erreur pendant la détection YOLO : {e_yolo}")
132
+ print(f"Temps de détection YOLO : {time.time() - start_yolo:.3f}s")
133
 
134
  # 3. Exécuter la calibration (Segmentation + Optimisation)
135
  print("Exécution de la segmentation...")
 
180
  # Minimap avec projection (image RGB attendue par la fonction)
181
  minimap_original = create_minimap_view(image_rgb_resized, homography_np)
182
 
183
+ # Minimap avec squelettes ET LE BALLON (utilise l'échelle estimée)
184
  minimap_offset_skeletons, actual_avg_scale = create_minimap_with_offset_skeletons(
185
  player_list,
186
  homography_np,
187
+ base_skeleton_scale=estimated_base_scale,
188
+ ball_ref_point_img=ball_ref_point_img # Passer le point de référence du ballon
189
  )
190
  print(f"Temps de génération des minimaps : {time.time() - start_viz:.3f}s")
191
  if actual_avg_scale is not None:
 
239
  info="Number of iterations to refine homography."
240
  )
241
  target_scale_slider = gr.Slider(
242
+ minimum=0.1, maximum=2.5, step=0.05, value=1,
243
  label="Target Average Skeleton Scale",
244
  info="Adjusts the desired average size of skeletons on the minimap."
245
  )
main.py CHANGED
@@ -5,6 +5,8 @@ import torch
5
  from pathlib import Path
6
  import time
7
  import traceback
 
 
8
 
9
  # Assurez-vous que le répertoire tvcalib est dans le PYTHONPATH
10
  # ou exécutez depuis le répertoire tvcalib_image_processor
@@ -22,6 +24,10 @@ from pose_estimator import get_player_data
22
  # Constantes
23
  IMAGE_SHAPE = (720, 1280) # Hauteur, Largeur
24
  SEGMENTATION_MODEL_PATH = Path("models/segmentation/train_59.pt")
 
 
 
 
25
 
26
  def preprocess_image_tvcalib(image_bgr):
27
  """Prétraite l'image BGR pour TvCalib et retourne le tenseur et l'image RGB redimensionnée."""
@@ -72,6 +78,12 @@ def main():
72
  print("Assurez-vous d'avoir copié train_59.pt dans le dossier models/segmentation/")
73
  return
74
 
 
 
 
 
 
 
75
  print("Initialisation de TvCalibInferModule...")
76
  try:
77
  model = TvCalibInferModule(
@@ -87,16 +99,49 @@ def main():
87
 
88
  print(f"Traitement de l'image : {args.image_path}")
89
  try:
 
 
 
 
 
 
 
 
90
  # Charger l'image (en BGR par défaut avec OpenCV)
91
  image_bgr_orig = cv2.imread(args.image_path)
92
  if image_bgr_orig is None:
93
- raise FileNotFoundError(f"Impossible de lire le fichier image: {args.image_path}")
94
 
95
- # Prétraiter l'image
96
  start_preprocess = time.time()
97
  image_tensor, image_bgr_resized, image_rgb_resized = preprocess_image_tvcalib(image_bgr_orig)
98
  print(f"Temps de prétraitement TvCalib : {time.time() - start_preprocess:.3f}s")
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  # Exécuter la segmentation
101
  print("Exécution de la segmentation...")
102
  start_segment = time.time()
@@ -155,12 +200,13 @@ def main():
155
  # 1. Minimap avec l'image originale (RGB)
156
  minimap_original = create_minimap_view(image_rgb_resized, homography_np)
157
 
158
- # 2. Minimap avec les squelettes
159
- # Utiliser l'échelle de base ESTIMÉE
160
  minimap_offset_skeletons, actual_avg_scale = create_minimap_with_offset_skeletons(
161
  player_list,
162
  homography_np,
163
- base_skeleton_scale=estimated_base_scale # Utiliser l'estimation
 
164
  )
165
 
166
  # Afficher la cible et le résultat réel
 
5
  from pathlib import Path
6
  import time
7
  import traceback
8
+ # Import YOLO
9
+ from ultralytics import YOLO
10
 
11
  # Assurez-vous que le répertoire tvcalib est dans le PYTHONPATH
12
  # ou exécutez depuis le répertoire tvcalib_image_processor
 
24
  # Constantes
25
  IMAGE_SHAPE = (720, 1280) # Hauteur, Largeur
26
  SEGMENTATION_MODEL_PATH = Path("models/segmentation/train_59.pt")
27
+ # Chemin vers le modèle YOLO pour la détection du ballon
28
+ YOLO_MODEL_PATH = Path("models/detection/yolo_football.pt")
29
+ # Index de classe pour le ballon (basé sur votre exemple)
30
+ BALL_CLASS_INDEX = 2
31
 
32
  def preprocess_image_tvcalib(image_bgr):
33
  """Prétraite l'image BGR pour TvCalib et retourne le tenseur et l'image RGB redimensionnée."""
 
78
  print("Assurez-vous d'avoir copié train_59.pt dans le dossier models/segmentation/")
79
  return
80
 
81
+ # Vérifier l'existence du modèle YOLO
82
+ if not YOLO_MODEL_PATH.exists():
83
+ print(f"Erreur : Modèle YOLO introuvable : {YOLO_MODEL_PATH}")
84
+ print(f"Assurez-vous d'avoir téléchargé {YOLO_MODEL_PATH.name} et de l'avoir placé dans {YOLO_MODEL_PATH.parent}/")
85
+ return
86
+
87
  print("Initialisation de TvCalibInferModule...")
88
  try:
89
  model = TvCalibInferModule(
 
99
 
100
  print(f"Traitement de l'image : {args.image_path}")
101
  try:
102
+ # Vérification supplémentaire avant imread
103
+ image_path_obj = Path(args.image_path)
104
+ absolute_path = image_path_obj.resolve()
105
+ print(f"Tentative de lecture de l'image via cv2.imread depuis : {absolute_path}")
106
+ if not absolute_path.is_file():
107
+ print(f"ERREUR : Le chemin absolu {absolute_path} ne pointe pas vers un fichier existant juste avant imread !")
108
+ return # Arrêter ici si le fichier n'est pas trouvé à ce stade
109
+
110
  # Charger l'image (en BGR par défaut avec OpenCV)
111
  image_bgr_orig = cv2.imread(args.image_path)
112
  if image_bgr_orig is None:
113
+ raise FileNotFoundError(f"Impossible de lire le fichier image: {args.image_path} (vérifié comme existant juste avant, problème avec imread)")
114
 
115
+ # Prétraiter l'image pour TvCalib (redimensionne aussi)
116
  start_preprocess = time.time()
117
  image_tensor, image_bgr_resized, image_rgb_resized = preprocess_image_tvcalib(image_bgr_orig)
118
  print(f"Temps de prétraitement TvCalib : {time.time() - start_preprocess:.3f}s")
119
 
120
+ # --- Détection du ballon avec YOLO ---
121
+ print("\nChargement du modèle YOLO et détection du ballon...")
122
+ start_yolo = time.time()
123
+ ball_ref_point_img = None # Point de référence du ballon sur l'image originale redimensionnée
124
+ try:
125
+ yolo_model = YOLO(YOLO_MODEL_PATH)
126
+ # Utiliser l'image BGR redimensionnée pour YOLO
127
+ results = yolo_model.predict(image_bgr_resized, classes=[BALL_CLASS_INDEX], verbose=False)
128
+
129
+ if results and len(results[0].boxes) > 0:
130
+ # Prendre la détection avec la plus haute confiance
131
+ best_ball_box = results[0].boxes[results[0].boxes.conf.argmax()]
132
+ x1, y1, x2, y2 = map(int, best_ball_box.xyxy[0].tolist())
133
+ conf = best_ball_box.conf[0].item()
134
+
135
+ # Calculer le point de référence (centre bas de la bbox)
136
+ ball_ref_point_img = np.array([(x1 + x2) / 2, y2], dtype=np.float32)
137
+ print(f" ✓ Ballon trouvé (conf: {conf:.2f}) à la bbox [{x1},{y1},{x2},{y2}]. Point réf: {ball_ref_point_img}")
138
+ else:
139
+ print(" Aucun ballon détecté.")
140
+
141
+ except Exception as e_yolo:
142
+ print(f" Erreur pendant la détection YOLO : {e_yolo}")
143
+ print(f"Temps de détection YOLO : {time.time() - start_yolo:.3f}s")
144
+
145
  # Exécuter la segmentation
146
  print("Exécution de la segmentation...")
147
  start_segment = time.time()
 
200
  # 1. Minimap avec l'image originale (RGB)
201
  minimap_original = create_minimap_view(image_rgb_resized, homography_np)
202
 
203
+ # 2. Minimap avec les squelettes ET LE BALLON
204
+ # Utiliser l'échelle de base ESTIMÉE et passer les coordonnées du ballon
205
  minimap_offset_skeletons, actual_avg_scale = create_minimap_with_offset_skeletons(
206
  player_list,
207
  homography_np,
208
+ base_skeleton_scale=estimated_base_scale, # Utiliser l'estimation
209
+ ball_ref_point_img=ball_ref_point_img # Passer le point de référence du ballon
210
  )
211
 
212
  # Afficher la cible et le résultat réel
requirements.txt CHANGED
@@ -2,21 +2,34 @@
2
  # pip install torch==2.5.1 torchvision==0.20.1 torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cu121
3
 
4
  # Dépendances principales
5
- torch
6
  torchvision
7
  torchaudio
8
  numpy
9
  opencv-python
10
- pytorch-lightning
11
  soccernet
12
- kornia
13
 
14
  # Ajouté car requis par sn_segmentation
15
 
16
  # Dépendances pour l'estimation de pose (ViTPose)
17
- transformers
18
  supervision
19
- Pillow # Souvent une dépendance de transformers/supervision, mais explicite ici
20
  accelerate
21
  # scikit-learn # Retiré car K-Means n'est plus utilisé
22
- gradio
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  # pip install torch==2.5.1 torchvision==0.20.1 torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cu121
3
 
4
  # Dépendances principales
5
+ torch>=1.8.0
6
  torchvision
7
  torchaudio
8
  numpy
9
  opencv-python
10
+ pytorch-lightning>=1.4.0
11
  soccernet
12
+ kornia>=0.6.0
13
 
14
  # Ajouté car requis par sn_segmentation
15
 
16
  # Dépendances pour l'estimation de pose (ViTPose)
17
+ transformers>=4.0.0
18
  supervision
19
+ Pillow
20
  accelerate
21
  # scikit-learn # Retiré car K-Means n'est plus utilisé
22
+ gradio
23
+
24
+ # Ball detection
25
+ ultralytics
26
+
27
+ # Optionnel mais recommandé pour la gestion des chemins
28
+ pathlib
29
+
30
+ # Dépendances possibles de TvCalib (à vérifier)
31
+ # tensorboard
32
+ # matplotlib
33
+ # scipy
34
+ # tqdm
35
+ # scikit-image
visualizer.py CHANGED
@@ -16,6 +16,11 @@ from pose_estimator import (LEFT_ANKLE_KP_INDEX, RIGHT_ANKLE_KP_INDEX,
16
  MARKER_RADIUS = 6
17
  MARKER_BORDER_THICKNESS = 1
18
  MARKER_BORDER_COLOR = (0, 0, 0) # Noir
 
 
 
 
 
19
 
20
  # Plage de modulation pour l'échelle dynamique inverseé
21
  DYNAMIC_SCALE_MIN_MODULATION = 0.4 # Pour les joueurs les plus loin (haut de la minimap)
@@ -163,14 +168,16 @@ def create_minimap_view(image_rgb, homography, minimap_size=(EXPECTED_W, EXPECTE
163
 
164
  def create_minimap_with_offset_skeletons(player_data_list, homography,
165
  base_skeleton_scale: float,
 
166
  minimap_size=(EXPECTED_W, EXPECTED_H)) -> tuple[np.ndarray | None, float | None]:
167
  """Crée une vue minimap en dessinant le squelette original (réduit/agrandi dynamiquement et inversé)
168
- à la position projetée du joueur, trié par position Y.
169
 
170
  Args:
171
  player_data_list: Liste de dictionnaires retournée par get_player_data.
172
  homography: Matrice d'homographie (numpy array).
173
  base_skeleton_scale: Facteur d'échelle de base pour dessiner les squelettes.
 
174
  minimap_size: Taille de la minimap de sortie (largeur, hauteur).
175
 
176
  Returns:
@@ -280,12 +287,32 @@ def create_minimap_with_offset_skeletons(player_data_list, homography,
280
  0 <= pt2_draw[0] < w_map and 0 <= pt2_draw[1] < h_map):
281
  cv2.line(minimap_bgr, pt1_draw, pt2_draw, drawing_color, SKELETON_THICKNESS, cv2.LINE_AA) # Utiliser drawing_color (noir)
282
 
283
- # Calculer l'échelle moyenne finale
284
- average_draw_scale = base_skeleton_scale # Valeur par défaut si aucun joueur n'est dessiné
285
- if drawn_players_count > 0:
286
- average_draw_scale = total_applied_scale / drawn_players_count
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
- return minimap_bgr, average_draw_scale # Retourner aussi l'échelle moyenne
289
 
290
  # Définition simplifiée de SoccerPitchSN juste pour les constantes de dimension
291
  # (pour éviter d'importer toute la classe complexe)
 
16
  MARKER_RADIUS = 6
17
  MARKER_BORDER_THICKNESS = 1
18
  MARKER_BORDER_COLOR = (0, 0, 0) # Noir
19
+ # Constantes pour le ballon
20
+ BALL_MARKER_RADIUS = 5
21
+ BALL_MARKER_COLOR = (255, 255, 255) # Blanc
22
+ BALL_BORDER_COLOR = (0, 0, 0) # Noir
23
+ BALL_BORDER_THICKNESS = 1
24
 
25
  # Plage de modulation pour l'échelle dynamique inverseé
26
  DYNAMIC_SCALE_MIN_MODULATION = 0.4 # Pour les joueurs les plus loin (haut de la minimap)
 
168
 
169
  def create_minimap_with_offset_skeletons(player_data_list, homography,
170
  base_skeleton_scale: float,
171
+ ball_ref_point_img: np.ndarray | None = None, # Ajout du paramètre optionnel pour le ballon
172
  minimap_size=(EXPECTED_W, EXPECTED_H)) -> tuple[np.ndarray | None, float | None]:
173
  """Crée une vue minimap en dessinant le squelette original (réduit/agrandi dynamiquement et inversé)
174
+ à la position projetée du joueur, trié par position Y. Dessine aussi le ballon s'il est fourni.
175
 
176
  Args:
177
  player_data_list: Liste de dictionnaires retournée par get_player_data.
178
  homography: Matrice d'homographie (numpy array).
179
  base_skeleton_scale: Facteur d'échelle de base pour dessiner les squelettes.
180
+ ball_ref_point_img: Point de référence (x, y) du ballon dans l'image originale (optionnel).
181
  minimap_size: Taille de la minimap de sortie (largeur, hauteur).
182
 
183
  Returns:
 
287
  0 <= pt2_draw[0] < w_map and 0 <= pt2_draw[1] < h_map):
288
  cv2.line(minimap_bgr, pt1_draw, pt2_draw, drawing_color, SKELETON_THICKNESS, cv2.LINE_AA) # Utiliser drawing_color (noir)
289
 
290
+ # --- Étape 5: Dessiner le ballon (si trouvé et projeté) ---
291
+ ball_mx, ball_my = None, None
292
+ if ball_ref_point_img is not None:
293
+ try:
294
+ point_to_transform = np.array([[ball_ref_point_img]], dtype=np.float32)
295
+ projected_ball_point = cv2.perspectiveTransform(point_to_transform, H_minimap)
296
+ ball_mx, ball_my = map(int, projected_ball_point[0, 0])
297
+ h_map, w_map = minimap_bgr.shape[:2]
298
+ if not (0 <= ball_mx < w_map and 0 <= ball_my < h_map):
299
+ print("Avertissement : Ballon projeté hors des limites de la minimap.")
300
+ ball_mx, ball_my = None, None # Ne pas dessiner
301
+ else:
302
+ # Dessiner la bordure noire
303
+ cv2.circle(minimap_bgr, (ball_mx, ball_my), BALL_MARKER_RADIUS + BALL_BORDER_THICKNESS,
304
+ BALL_BORDER_COLOR, -1, cv2.LINE_AA)
305
+ # Dessiner le cercle blanc intérieur
306
+ cv2.circle(minimap_bgr, (ball_mx, ball_my), BALL_MARKER_RADIUS,
307
+ BALL_MARKER_COLOR, -1, cv2.LINE_AA)
308
+ except Exception as e:
309
+ print(f"Erreur lors de la projection ou du dessin du ballon : {e}")
310
+ ball_mx, ball_my = None, None
311
+
312
+ # Calculer l'échelle moyenne si des joueurs ont été dessinés
313
+ final_avg_scale = total_applied_scale / drawn_players_count if drawn_players_count > 0 else None
314
 
315
+ return minimap_bgr, final_avg_scale
316
 
317
  # Définition simplifiée de SoccerPitchSN juste pour les constantes de dimension
318
  # (pour éviter d'importer toute la classe complexe)