|
import cv2 |
|
import numpy as np |
|
|
|
|
|
FIELD_LENGTH_YARDS = 114.83 |
|
FIELD_WIDTH_YARDS = 74.37 |
|
|
|
|
|
EXPECTED_H, EXPECTED_W = 720, 1280 |
|
|
|
|
|
from pose_estimator import (LEFT_ANKLE_KP_INDEX, RIGHT_ANKLE_KP_INDEX, |
|
CONFIDENCE_THRESHOLD_KEYPOINTS, DEFAULT_MARKER_COLOR, SKELETON_EDGES, SKELETON_THICKNESS) |
|
|
|
|
|
MARKER_RADIUS = 6 |
|
MARKER_BORDER_THICKNESS = 1 |
|
MARKER_BORDER_COLOR = (0, 0, 0) |
|
|
|
BALL_MARKER_RADIUS = 5 |
|
BALL_MARKER_COLOR = (255, 255, 255) |
|
BALL_BORDER_COLOR = (0, 0, 0) |
|
BALL_BORDER_THICKNESS = 1 |
|
|
|
|
|
DYNAMIC_SCALE_MIN_MODULATION = 0.4 |
|
DYNAMIC_SCALE_MAX_MODULATION = 1.6 |
|
|
|
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] |
|
|
|
|
|
base_green = (0, 60, 0) |
|
stripe_green = (0, 70, 0) |
|
stripe_width = 5 |
|
|
|
|
|
minimap_bgr = np.full((minimap_h, minimap_w, 3), base_green, dtype=np.uint8) |
|
|
|
|
|
scale_x = minimap_w / FIELD_LENGTH_YARDS |
|
scale_y = minimap_h / FIELD_WIDTH_YARDS |
|
scale = min(scale_x, scale_y) * 0.9 |
|
|
|
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 |
|
|
|
|
|
for x in range(offset_x, offset_x + field_length_px, stripe_width * 2): |
|
|
|
start_x = x |
|
end_x = min(x + stripe_width, offset_x + field_length_px) |
|
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) |
|
|
|
|
|
|
|
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) |
|
line_thickness = 1 |
|
goal_thickness = 1 |
|
goal_width_yards = 8 |
|
|
|
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) |
|
|
|
|
|
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) |
|
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) |
|
|
|
|
|
goal_y_top = center_y - goal_width_px // 2 |
|
goal_y_bottom = center_y + goal_width_px // 2 |
|
|
|
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) |
|
|
|
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, |
|
ball_ref_point_img: np.ndarray | None = None, |
|
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. Dessine aussi le ballon s'il est fourni. |
|
|
|
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. |
|
ball_ref_point_img: Point de référence (x, y) du ballon dans l'image originale (optionnel). |
|
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 |
|
|
|
minimap_bgr, metrics = _prepare_minimap_base(minimap_size) |
|
|
|
|
|
_draw_field_lines(minimap_bgr, metrics) |
|
|
|
S = metrics["S"] |
|
H_minimap = S @ homography |
|
|
|
players_to_draw = [] |
|
|
|
|
|
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'] |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
except Exception as e: |
|
|
|
continue |
|
|
|
|
|
players_to_draw.append({ |
|
'data': p_data, |
|
'mx': mx, |
|
'my': my, |
|
'ref_point': ref_point_img |
|
}) |
|
|
|
|
|
|
|
players_to_draw.sort(key=lambda p: p['my']) |
|
|
|
|
|
total_applied_scale = 0.0 |
|
drawn_players_count = 0 |
|
|
|
|
|
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'] |
|
|
|
drawing_color = (0, 0, 0) |
|
|
|
|
|
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 |
|
|
|
|
|
total_applied_scale += final_draw_scale |
|
drawn_players_count += 1 |
|
|
|
|
|
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)) |
|
|
|
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) |
|
|
|
|
|
ball_mx, ball_my = None, None |
|
if ball_ref_point_img is not None: |
|
try: |
|
point_to_transform = np.array([[ball_ref_point_img]], dtype=np.float32) |
|
projected_ball_point = cv2.perspectiveTransform(point_to_transform, H_minimap) |
|
ball_mx, ball_my = map(int, projected_ball_point[0, 0]) |
|
h_map, w_map = minimap_bgr.shape[:2] |
|
if not (0 <= ball_mx < w_map and 0 <= ball_my < h_map): |
|
print("Avertissement : Ballon projeté hors des limites de la minimap.") |
|
ball_mx, ball_my = None, None |
|
else: |
|
|
|
cv2.circle(minimap_bgr, (ball_mx, ball_my), BALL_MARKER_RADIUS + BALL_BORDER_THICKNESS, |
|
BALL_BORDER_COLOR, -1, cv2.LINE_AA) |
|
|
|
cv2.circle(minimap_bgr, (ball_mx, ball_my), BALL_MARKER_RADIUS, |
|
BALL_MARKER_COLOR, -1, cv2.LINE_AA) |
|
except Exception as e: |
|
print(f"Erreur lors de la projection ou du dessin du ballon : {e}") |
|
ball_mx, ball_my = None, None |
|
|
|
|
|
final_avg_scale = total_applied_scale / drawn_players_count if drawn_players_count > 0 else None |
|
|
|
return minimap_bgr, final_avg_scale |
|
|
|
|
|
|
|
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 |