File size: 14,691 Bytes
bdb955e |
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 292 293 294 295 296 297 298 |
import cv2
import numpy as np
# Dimensions du terrain en yards
FIELD_LENGTH_YARDS = 114.83
FIELD_WIDTH_YARDS = 74.37
# Constantes de taille d'image attendue
EXPECTED_H, EXPECTED_W = 720, 1280
# Import des constantes d'indices depuis pose_estimator si nécessaire (ou les redéfinir ici)
from pose_estimator import (LEFT_ANKLE_KP_INDEX, RIGHT_ANKLE_KP_INDEX,
CONFIDENCE_THRESHOLD_KEYPOINTS, DEFAULT_MARKER_COLOR, SKELETON_EDGES, SKELETON_THICKNESS)
# Constantes pour les marqueurs
MARKER_RADIUS = 6
MARKER_BORDER_THICKNESS = 1
MARKER_BORDER_COLOR = (0, 0, 0) # Noir
# Plage de modulation pour l'échelle dynamique inverseé
DYNAMIC_SCALE_MIN_MODULATION = 0.4 # Pour les joueurs les plus loin (haut de la minimap)
DYNAMIC_SCALE_MAX_MODULATION = 1.6 # Pour les joueurs les plus près (bas de la minimap)
def calculate_dynamic_scale(y_position, frame_height, min_scale=1.0, max_scale=2):
"""Calcule le facteur d'échelle en fonction de la position verticale (non utilisé dans cette version simplifiée)."""
normalized_position = y_position / frame_height
return min_scale + (max_scale - min_scale) * normalized_position
def _prepare_minimap_base(minimap_size=(EXPECTED_W, EXPECTED_H)):
"""Prépare le fond de la minimap (vert texturé verticalement dans la zone terrain) et calcule les métriques du terrain."""
minimap_h, minimap_w = minimap_size[1], minimap_size[0]
# Définir les couleurs et la largeur des bandes verticales
base_green = (0, 60, 0) # Vert foncé (fond)
stripe_green = (0, 70, 0) #
stripe_width = 5 # Largeur de chaque bande verticale (pixels)
# Initialiser TOUTE la minimap avec la couleur de base
minimap_bgr = np.full((minimap_h, minimap_w, 3), base_green, dtype=np.uint8)
# --- Calculer les métriques et les limites du terrain D'ABORD ---
scale_x = minimap_w / FIELD_LENGTH_YARDS
scale_y = minimap_h / FIELD_WIDTH_YARDS
scale = min(scale_x, scale_y) * 0.9 # Marge
field_width_px = int(FIELD_WIDTH_YARDS * scale)
field_length_px = int(FIELD_LENGTH_YARDS * scale)
offset_x = (minimap_w - field_length_px) // 2
offset_y = (minimap_h - field_width_px) // 2
# --- Dessiner les bandes VERTICALES alternées UNIQUEMENT dans la zone du terrain ---
for x in range(offset_x, offset_x + field_length_px, stripe_width * 2):
# Coordonnées du rectangle vertical pour la bande claire
start_x = x
end_x = min(x + stripe_width, offset_x + field_length_px) # Ne pas dépasser la limite droite
start_y = offset_y
end_y = offset_y + field_width_px
cv2.rectangle(minimap_bgr, (start_x, start_y), (end_x, end_y), stripe_green, thickness=-1)
# La bande foncée suivante est déjà là (couleur de base)
# --- Préparer la matrice S et les métriques à retourner ---
S = np.array([
[scale, 0, offset_x],
[0, scale, offset_y],
[0, 0, 1]
], dtype=np.float32)
metrics = {
"scale": scale,
"offset_x": offset_x,
"offset_y": offset_y,
"field_width_px": field_width_px,
"field_length_px": field_length_px,
"S": S
}
return minimap_bgr, metrics
def _draw_field_lines(minimap_bgr, metrics):
"""Dessine les lignes du terrain et les buts sur la minimap."""
scale = metrics["scale"]
offset_x = metrics["offset_x"]
offset_y = metrics["offset_y"]
field_width_px = metrics["field_width_px"]
field_length_px = metrics["field_length_px"]
line_color = (255, 255, 255) # Blanc
line_thickness = 1
goal_thickness = 1 # Épaisseur pour les poteaux de but
goal_width_yards = 8 # Largeur standard du but
center_x = offset_x + field_length_px // 2
center_y = offset_y + field_width_px // 2
penalty_area_width_px = int(SoccerPitchSN.PENALTY_AREA_WIDTH * scale)
penalty_area_length_px = int(SoccerPitchSN.PENALTY_AREA_LENGTH * scale)
goal_area_width_px = int(SoccerPitchSN.GOAL_AREA_WIDTH * scale)
goal_area_length_px = int(SoccerPitchSN.GOAL_AREA_LENGTH * scale)
center_circle_radius_px = int(SoccerPitchSN.CENTER_CIRCLE_RADIUS * scale)
goal_width_px = int(goal_width_yards * scale)
# Dessiner les lignes principales
cv2.rectangle(minimap_bgr, (offset_x, offset_y), (offset_x + field_length_px, offset_y + field_width_px), line_color, line_thickness)
cv2.line(minimap_bgr, (center_x, offset_y), (center_x, offset_y + field_width_px), line_color, line_thickness)
cv2.circle(minimap_bgr, (center_x, center_y), center_circle_radius_px, line_color, line_thickness)
cv2.circle(minimap_bgr, (center_x, center_y), 3, line_color, -1) # Point central
cv2.rectangle(minimap_bgr, (offset_x, center_y - penalty_area_width_px//2), (offset_x + penalty_area_length_px, center_y + penalty_area_width_px//2), line_color, line_thickness)
cv2.rectangle(minimap_bgr, (offset_x + field_length_px - penalty_area_length_px, center_y - penalty_area_width_px//2), (offset_x + field_length_px, center_y + penalty_area_width_px//2), line_color, line_thickness)
cv2.rectangle(minimap_bgr, (offset_x, center_y - goal_area_width_px//2), (offset_x + goal_area_length_px, center_y + goal_area_width_px//2), line_color, line_thickness)
cv2.rectangle(minimap_bgr, (offset_x + field_length_px - goal_area_length_px, center_y - goal_area_width_px//2), (offset_x + field_length_px, center_y + goal_area_width_px//2), line_color, line_thickness)
# Dessiner les buts (rectangles épais sur les lignes de but)
goal_y_top = center_y - goal_width_px // 2
goal_y_bottom = center_y + goal_width_px // 2
# But gauche
cv2.rectangle(minimap_bgr, (offset_x-6 - goal_thickness // 2, goal_y_top), (offset_x + goal_thickness // 2, goal_y_bottom), line_color, thickness=goal_thickness)
# But droit
cv2.rectangle(minimap_bgr, (offset_x + field_length_px - goal_thickness // 2, goal_y_top), (offset_x +6 + field_length_px + goal_thickness // 2, goal_y_bottom), line_color, thickness=goal_thickness)
def create_minimap_view(image_rgb, homography, minimap_size=(EXPECTED_W, EXPECTED_H)):
"""Crée une vue minimap avec l'image RGB originale projetée et les lignes du terrain.
Args:
image_rgb: Image source en format RGB (720p attendu).
homography: Matrice d'homographie (numpy array) pour projeter l'image.
minimap_size: Taille de la minimap de sortie (largeur, hauteur).
Returns:
L'image de la minimap (numpy array BGR) ou None si l'homographie est invalide.
"""
if homography is None:
print("Avertissement : Homographie invalide, impossible de créer la minimap (vue originale).")
return None
h, w = image_rgb.shape[:2]
if h != EXPECTED_H or w != EXPECTED_W:
print(f"Avertissement : L'image RGB d'entrée n'est pas en {EXPECTED_W}x{EXPECTED_H}, redimensionnement...")
image_rgb = cv2.resize(image_rgb, (EXPECTED_W, EXPECTED_H), interpolation=cv2.INTER_LINEAR)
minimap_bgr, metrics = _prepare_minimap_base(minimap_size)
S = metrics["S"]
try:
overlay = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR)
overlay = cv2.convertScaleAbs(overlay, alpha=1.2, beta=10)
overlay = cv2.addWeighted(overlay, 0.5, np.zeros_like(overlay), 0.5, 0)
H_minimap = S @ homography
warped = cv2.warpPerspective(overlay, H_minimap, minimap_size, flags=cv2.INTER_LINEAR)
mask = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
_, mask = cv2.threshold(mask, 1, 255, cv2.THRESH_BINARY)
minimap_bgr = np.where(mask[..., None] > 0, warped, minimap_bgr)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(minimap_bgr, contours, -1, (255, 255, 255), 2)
except Exception as e:
print(f"Erreur lors de la projection sur la mini-carte (vue originale) : {str(e)}")
_draw_field_lines(minimap_bgr, metrics)
return minimap_bgr
def create_minimap_with_offset_skeletons(player_data_list, homography,
base_skeleton_scale: float,
minimap_size=(EXPECTED_W, EXPECTED_H)) -> tuple[np.ndarray | None, float | None]:
"""Crée une vue minimap en dessinant le squelette original (réduit/agrandi dynamiquement et inversé)
à la position projetée du joueur, trié par position Y.
Args:
player_data_list: Liste de dictionnaires retournée par get_player_data.
homography: Matrice d'homographie (numpy array).
base_skeleton_scale: Facteur d'échelle de base pour dessiner les squelettes.
minimap_size: Taille de la minimap de sortie (largeur, hauteur).
Returns:
Tuple: (L'image de la minimap (numpy array BGR) ou None, Échelle moyenne appliquée ou None)
"""
if homography is None:
print("Avertissement : Homographie invalide, impossible de créer la minimap (squelettes décalés).")
return None, None # Retourner None pour l'image et l'échelle
minimap_bgr, metrics = _prepare_minimap_base(minimap_size)
# --- Dessiner les lignes du terrain D'ABORD ---
_draw_field_lines(minimap_bgr, metrics)
S = metrics["S"]
H_minimap = S @ homography
players_to_draw = [] # Liste pour stocker les joueurs valides avec leur position Y
# --- Étape 1 & 2: Calculer la position projetée pour tous les joueurs valides ---
for p_data in player_data_list:
kps_img = p_data['keypoints']
scores = p_data['scores']
bbox = p_data['bbox']
color = p_data['avg_color']
# -- Calculer le point de référence sur l'image --
l_ankle_pt = kps_img[LEFT_ANKLE_KP_INDEX]
r_ankle_pt = kps_img[RIGHT_ANKLE_KP_INDEX]
l_ankle_score = scores[LEFT_ANKLE_KP_INDEX]
r_ankle_score = scores[RIGHT_ANKLE_KP_INDEX]
ref_point_img = None
if l_ankle_score > CONFIDENCE_THRESHOLD_KEYPOINTS and r_ankle_score > CONFIDENCE_THRESHOLD_KEYPOINTS:
ref_point_img = (l_ankle_pt + r_ankle_pt) / 2
elif l_ankle_score > CONFIDENCE_THRESHOLD_KEYPOINTS:
ref_point_img = l_ankle_pt
elif r_ankle_score > CONFIDENCE_THRESHOLD_KEYPOINTS:
ref_point_img = r_ankle_pt
else:
x1, _, x2, y2 = bbox
ref_point_img = np.array([(x1 + x2) / 2, y2], dtype=np.float32)
if ref_point_img is None: continue
# -- Projeter ce point de référence sur la minimap --
try:
point_to_transform = np.array([[ref_point_img]], dtype=np.float32)
projected_point = cv2.perspectiveTransform(point_to_transform, H_minimap)
mx, my = map(int, projected_point[0, 0])
h_map, w_map = minimap_bgr.shape[:2]
if not (0 <= mx < w_map and 0 <= my < h_map):
continue # Ignorer si hors des limites de la minimap
except Exception as e:
# print(f"Erreur lors de la projection du point de référence {ref_point_img}: {e}") # Optionnel: décommenter pour debug
continue
# Stocker les données nécessaires pour le tri et le dessin
players_to_draw.append({
'data': p_data,
'mx': mx,
'my': my,
'ref_point': ref_point_img
})
# --- Étape 3: Trier les joueurs par position Y (ordre croissant) ---
# Ceux avec Y plus petit (plus haut) seront dessinés en premier
players_to_draw.sort(key=lambda p: p['my'])
# Variables pour calculer l'échelle moyenne appliquée
total_applied_scale = 0.0
drawn_players_count = 0
# --- Étape 4: Dessiner les joueurs dans l'ordre trié (MAINTENANT AU-DESSUS DES LIGNES) ---
for player_info in players_to_draw:
p_data = player_info['data']
mx = player_info['mx']
my = player_info['my']
ref_point_img = player_info['ref_point']
kps_img = p_data['keypoints']
scores = p_data['scores']
# color = p_data['avg_color'] # Ignorer la couleur calculée
drawing_color = (0, 0, 0) # Utiliser le noir pour tous les joueurs
# -- Calculer l'échelle dynamique INVERSÉE pour CE joueur --
minimap_height = minimap_bgr.shape[0]
if minimap_height == 0: continue
ref_y_normalized = my / minimap_height
dynamic_modulation = DYNAMIC_SCALE_MIN_MODULATION + \
(DYNAMIC_SCALE_MAX_MODULATION - DYNAMIC_SCALE_MIN_MODULATION) * (1.0 - ref_y_normalized)
dynamic_modulation = np.clip(dynamic_modulation, DYNAMIC_SCALE_MIN_MODULATION * 0.8, DYNAMIC_SCALE_MAX_MODULATION * 1.2)
final_draw_scale = base_skeleton_scale * dynamic_modulation
# Ajouter à la somme pour la moyenne
total_applied_scale += final_draw_scale
drawn_players_count += 1
# -- Dessiner le squelette --
kps_relative_to_ref = kps_img - ref_point_img
for kp_idx1, kp_idx2 in SKELETON_EDGES:
if scores[kp_idx1] > CONFIDENCE_THRESHOLD_KEYPOINTS and scores[kp_idx2] > CONFIDENCE_THRESHOLD_KEYPOINTS:
pt1_map_offset = (mx, my) + kps_relative_to_ref[kp_idx1] * final_draw_scale
pt2_map_offset = (mx, my) + kps_relative_to_ref[kp_idx2] * final_draw_scale
pt1_draw = tuple(map(int, pt1_map_offset))
pt2_draw = tuple(map(int, pt2_map_offset))
# Vérifier si les points sont dans les limites avant de dessiner (sécurité)
h_map, w_map = minimap_bgr.shape[:2]
if (0 <= pt1_draw[0] < w_map and 0 <= pt1_draw[1] < h_map and
0 <= pt2_draw[0] < w_map and 0 <= pt2_draw[1] < h_map):
cv2.line(minimap_bgr, pt1_draw, pt2_draw, drawing_color, SKELETON_THICKNESS, cv2.LINE_AA) # Utiliser drawing_color (noir)
# Calculer l'échelle moyenne finale
average_draw_scale = base_skeleton_scale # Valeur par défaut si aucun joueur n'est dessiné
if drawn_players_count > 0:
average_draw_scale = total_applied_scale / drawn_players_count
return minimap_bgr, average_draw_scale # Retourner aussi l'échelle moyenne
# Définition simplifiée de SoccerPitchSN juste pour les constantes de dimension
# (pour éviter d'importer toute la classe complexe)
class SoccerPitchSN:
GOAL_LINE_TO_PENALTY_MARK = 11.0
PENALTY_AREA_WIDTH = 42
PENALTY_AREA_LENGTH = 19
GOAL_AREA_WIDTH = 18.32
GOAL_AREA_LENGTH = 5.5
CENTER_CIRCLE_RADIUS = 10 |