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- app.py +52 -7
- main.py +51 -5
- requirements.txt +19 -6
- 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 |
-
|
|
|
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 |
-
|
60 |
-
|
|
|
|
|
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=
|
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
|
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 |
-
#
|
284 |
-
|
285 |
-
if
|
286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
287 |
|
288 |
-
return minimap_bgr,
|
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)
|