from vitpose import VitPose import requests import os from config import API_URL,API_KEY from fastapi import UploadFile import logging import cv2 import numpy as np import time import json logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def process_video(file_name: str,vitpose: VitPose,user_id: str,player_id: str): video_path = file_name contents = open(video_path, "rb").read() with open(video_path, "wb") as f: f.write(contents) logger.info(f"file saved {video_path}") logger.info(f"starting task {video_path}") new_file_name = os.path.join("static", video_path) logger.info(f"new file name {new_file_name}") vitpose.output_video_path = new_file_name annotated_frames = vitpose.run(video_path) vitpose.frames_to_video(annotated_frames) logger.info(f"Video processed {video_path}") with open(new_file_name, "rb") as f: contents = f.read() url = API_URL+ "/excercises/webhooks/video-processed" logger.info(f"Sending video to {url}") files = {"file": (video_path, contents, "video/mp4")} logger.info(f"video_path: {video_path}") response = requests.post(url, files=files, data={"user_id":user_id,"typeMessage":"video_processed","file_name":video_path, "player_id":player_id}, stream=True, headers={"token":API_KEY}) logger.info(f"Response: {response.status_code}") logger.info(f"Response: {response.text}") logger.info(f"Video sent to {url}") def process_salto_alto(file_name: str, vitpose: VitPose, player_data: dict, repetitions: int, exercise_id: str): """ Process a high jump exercise video using VitPose for pose estimation. Args: file_name: Path to the input video vitpose: VitPose instance for pose estimation player_data: Dictionary containing player information repetitions: Expected number of repetitions exercise_id: ID of the exercise """ # Use the provided VitPose instance print(f"start processing") model = vitpose.pipeline # Get player parameters from player_data or use defaults reference_height = player_data.get('height', 1.68) # Altura aproximada de la persona en metros body_mass_kg = player_data.get('weight', 64) # Peso corporal en kg # Generate output paths output_video = file_name.replace('.mp4', '_analyzed.mp4') output_json = output_video.replace('.mp4', '.json') # Process the video and get the jump metrics results_dict = analyze_jump_video( model=model, input_video=file_name, output_video=output_video, reference_height=reference_height, body_mass_kg=body_mass_kg ) # Save results to JSON with open(output_json, 'w') as f: json.dumps(results_dict, indent=4) # Print summary print("\nResultados finales:") print(f"Salto Relativo máximo: {results_dict['jump_metrics']['max_relative_jump']:.2f}m") print(f"Salto Alto máximo: {results_dict['jump_metrics']['max_high_jump']:.2f}m") print(f"Potencia Sayer (estimada): {results_dict['jump_metrics']['peak_power_sayer']:.2f} W") # Return results dictionary return { "output_video": output_video, "output_json": output_json, "metrics": results_dict } def analyze_jump_video(model, input_video, output_video, reference_height=1.68, body_mass_kg=64): """ Analyze a jump video to calculate various jump metrics. Args: model: VitPose model instance input_video: Path to input video output_video: Path to output video reference_height: Height of the person in meters body_mass_kg: Weight of the person in kg Returns: Dictionary containing jump metrics and video analysis data """ # Configuration parameters JUMP_THRESHOLD_PERCENT = 0.05 # Porcentaje de cambio en la altura del tobillo para detectar el inicio del salto SMOOTHING_WINDOW = 5 # Ventana para suavizar la altura de los tobillos HORIZONTAL_OFFSET_FACTOR = 0.75 # Factor para ubicar el cuadro entre el hombro y el borde VELOCITY_WINDOW = 3 # Número de frames para calcular la velocidad METRICS_BELOW_FEET_OFFSET = 20 # Offset en píxeles para colocar los cuadros debajo de los pies # Color palette BLUE = (255, 0, 0) GREEN = (0, 255, 0) YELLOW = (0, 255, 255) WHITE = (255, 255, 255) BLACK = (0, 0, 0) GRAY = (128, 128, 128) LIGHT_GRAY = (200, 200, 200) repetition_data = [] # Open the video cap = cv2.VideoCapture(input_video) if not cap.isOpened(): print("Error al abrir el video") return {} # Get first frame to calibrate and get initial shoulder positions ret, frame = cap.read() if not ret: print("Error al leer el video") return {} # Initialize calibration variables PX_PER_METER = None initial_person_height_px = None initial_left_shoulder_x = None initial_right_shoulder_x = None # Process first frame to calibrate output = model(frame) # Detect pose in first frame keypoints = output.keypoints_xy.float().cpu().numpy() print(f"keypoints {keypoints}") labels = model.pose_estimator_config.label2id print(labels) nose_keypoint = labels["Nose"] L_ankle_keypoint = labels["L_Ankle"] R_ankle_keypoint = labels["R_Ankle"] L_shoulder_keypoint = labels["L_Shoulder"] R_shoulder_keypoint = labels["R_Shoulder"] print(f"nose_keypoint {nose_keypoint}") print(f"L_ankle_keypoint {L_ankle_keypoint}") print(f"R_ankle_keypoint {R_ankle_keypoint}") print(f"L_shoulder_keypoint {L_shoulder_keypoint}") print(f"R_shoulder_keypoint {R_shoulder_keypoint}") if ( keypoints is not None and len(keypoints) > 0 and len(keypoints[0]) > 0): kpts_first = keypoints[0] if len(kpts_first[nose_keypoint]) > 0 and len(kpts_first[L_ankle_keypoint]) > 0: # Nose and ankles initial_person_height_px = min(kpts_first[L_ankle_keypoint][1], kpts_first[R_ankle_keypoint][1]) - kpts_first[nose_keypoint][1] print(f"initial_person_height_px {initial_person_height_px}") PX_PER_METER = float(initial_person_height_px) / float(reference_height) print(f"Escala calculada: {PX_PER_METER:.2f} px/m") if len(kpts_first[L_shoulder_keypoint]) > 0 and len(kpts_first[R_shoulder_keypoint]) > 0: # Left (5) and right (6) shoulders initial_left_shoulder_x = int(kpts_first[L_shoulder_keypoint][0]) initial_right_shoulder_x = int(kpts_first[R_shoulder_keypoint][0]) if PX_PER_METER is None or initial_left_shoulder_x is None or initial_right_shoulder_x is None: print("No se pudo calibrar la escala o detectar los hombros en el primer frame.") cap.release() return {} # Reset video for processing cap.release() cap = cv2.VideoCapture(input_video) fps = cap.get(cv2.CAP_PROP_FPS) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) out = cv2.VideoWriter(output_video, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) # Variables for metrics and visualization ground_level = None takeoff_head_y = None max_jump_height = 0 # Maximum relative jump max_head_height_px = None # Maximum head height in pixels (lowest in y coordinates) jump_started = False head_y_history = [] ankle_y_history = [] last_detected_ankles_y = None head_y_buffer = [] velocity_vertical = 0.0 peak_power_sayer = 0.0 # Initialize Sayer power person_detected = False # Flag to indicate if person was detected in any frame current_power = 0.0 repetition_count = 0 jump_in_air = False # Process each frame while cap.isOpened(): ret, frame = cap.read() if not ret: break annotated_frame = frame.copy() # Add try-except block around the model inference to catch any model errors try: output = model(annotated_frame) keypoints = output.keypoints_xy.float().cpu().numpy() # Verify that keypoints array has valid data before processing if (keypoints is not None and len(keypoints) > 0 and len(keypoints[0]) > 0 and keypoints.size > 0): # Check if array is not empty person_detected = True kpts = keypoints[0] # Make sure all required keypoints are detected if (nose_keypoint < len(kpts) and L_ankle_keypoint < len(kpts) and R_ankle_keypoint < len(kpts) and L_shoulder_keypoint < len(kpts) and R_shoulder_keypoint < len(kpts)): nose = kpts[nose_keypoint] ankles = [kpts[L_ankle_keypoint], kpts[R_ankle_keypoint]] left_shoulder = kpts[L_shoulder_keypoint] right_shoulder = kpts[R_shoulder_keypoint] # Check if keypoints have valid coordinates if (nose[0] > 0 and nose[1] > 0 and all(a[0] > 0 and a[1] > 0 for a in ankles) and left_shoulder[0] > 0 and left_shoulder[1] > 0 and right_shoulder[0] > 0 and right_shoulder[1] > 0): # Continue with existing processing current_ankle_y = min(a[1] for a in ankles) last_detected_ankles_y = current_ankle_y current_head_y = nose[1] current_left_shoulder_x = int(left_shoulder[0]) current_right_shoulder_x = int(right_shoulder[0]) # Smooth ankle and head positions ankle_y_history.append(current_ankle_y) if len(ankle_y_history) > SMOOTHING_WINDOW: ankle_y_history.pop(0) smoothed_ankle_y = np.mean(ankle_y_history) head_y_history.append(current_head_y) if len(head_y_history) > SMOOTHING_WINDOW: head_y_history.pop(0) smoothed_head_y = np.mean(head_y_history) # Calculate vertical velocity (using head position) head_y_buffer.append(smoothed_head_y) if len(head_y_buffer) > VELOCITY_WINDOW: head_y_buffer.pop(0) if PX_PER_METER is not None and fps > 0: delta_y_pixels = head_y_buffer[0] - head_y_buffer[-1] delta_y_meters = delta_y_pixels / PX_PER_METER delta_t = VELOCITY_WINDOW / fps velocity_vertical = delta_y_meters / delta_t # Set ground level in first frame where ankles are detected if ground_level is None: ground_level = smoothed_ankle_y takeoff_head_y = smoothed_head_y relative_ankle_change = (ground_level - smoothed_ankle_y) / ground_level if ground_level > 0 else 0 # Detect jump start if not jump_started and relative_ankle_change > JUMP_THRESHOLD_PERCENT: jump_started = True takeoff_head_y = smoothed_head_y max_jump_height = 0 max_head_height_px = smoothed_head_y # Detect jump end if jump_started and relative_ankle_change <= JUMP_THRESHOLD_PERCENT: # Add to repetition data salto_alto = calculate_absolute_jump_height(reference_height, max_jump_height) repetition_data.append({ "repetition": repetition_count + 1, "relative_jump_m": round(max_jump_height, 3), "absolute_jump_m": round(salto_alto, 3), "peak_power_watts": round(current_power, 1) }) repetition_count += 1 jump_started = False # Update jump metrics while in air if jump_started: relative_jump = (takeoff_head_y - smoothed_head_y) / PX_PER_METER if relative_jump > max_jump_height: max_jump_height = relative_jump if smoothed_head_y < max_head_height_px: max_head_height_px = smoothed_head_y if relative_jump: current_power = calculate_peak_power_sayer(relative_jump, body_mass_kg) if current_power > peak_power_sayer: peak_power_sayer = current_power else: # Skip processing for this frame - invalid coordinates print("Skipping frame - invalid keypoint coordinates") print(f"keypoints {keypoints}") else: # Skip processing for this frame - missing required keypoints print("Skipping frame - missing required keypoints") print(f"keypoints {keypoints}") else: # Skip processing for this frame - no valid keypoints detected print("Skipping frame - no valid keypoints detected") print(f"keypoints {keypoints}") last_detected_ankles_y = None velocity_vertical = 0.0 except Exception as e: # Handle any other exceptions that might occur during model inference print(f"Error processing frame: {e}") print(f"keypoints {keypoints}") last_detected_ankles_y = None velocity_vertical = 0.0 # Calculate metrics and draw overlay even if keypoints weren't detected # This ensures video continues to show previous metrics salto_alto = calculate_absolute_jump_height(reference_height, max_jump_height) # Draw floating metric boxes annotated_frame = draw_metrics_overlay( frame=annotated_frame, max_jump_height=max_jump_height, salto_alto=salto_alto, velocity_vertical=velocity_vertical, peak_power_sayer=peak_power_sayer, repetition_count=repetition_count, last_detected_ankles_y=last_detected_ankles_y, initial_left_shoulder_x=initial_left_shoulder_x, initial_right_shoulder_x=initial_right_shoulder_x, width=width, height=height, colors={ "blue": BLUE, "green": GREEN, "yellow": YELLOW, "white": WHITE, "black": BLACK, "gray": GRAY, "light_gray": LIGHT_GRAY }, metrics_below_feet_offset=METRICS_BELOW_FEET_OFFSET, horizontal_offset_factor=HORIZONTAL_OFFSET_FACTOR ) out.write(annotated_frame) # Prepare results dictionary results_dict = { "jump_metrics": { "max_relative_jump": float(max(0, max_jump_height)), "max_high_jump": float(max(0, salto_alto)), "peak_power_sayer": float(peak_power_sayer), "repetitions": int(repetition_count), "reference_height": float(reference_height), "body_mass_kg": float(body_mass_kg), "px_per_meter": float(PX_PER_METER) if PX_PER_METER is not None else 0.0 }, "video_analysis": { "input_video": str(input_video), "output_video": str(output_video), "fps": float(fps), "resolution": f"{int(width)}x{int(height)}" }, "repetition_data": [ { "repetition": int(rep["repetition"]), "relative_jump_m": float(rep["relative_jump_m"]), "absolute_jump_m": float(rep["absolute_jump_m"]), "peak_power_watts": float(rep["peak_power_watts"]) } for rep in repetition_data ] } cap.release() out.release() return results_dict def calculate_peak_power_sayer(jump_height_m, body_mass_kg): """ Estimates peak anaerobic power using Sayer's equation. Args: jump_height_m: Jump height in meters body_mass_kg: Body mass in kg Returns: Estimated peak power in watts """ jump_height_cm = jump_height_m * 100 return (60.7 * jump_height_cm) + (45.3 * body_mass_kg) - 2055 def calculate_absolute_jump_height(reference_height, relative_jump): """ Calculate absolute jump height based on reference height and relative jump. Args: reference_height: Reference height in meters relative_jump: Relative jump height in meters Returns: Absolute jump height in meters """ absolute_jump = float(reference_height) + float(relative_jump) # Apply validation rule if absolute_jump > 1.72: return absolute_jump else: return 0 def draw_metrics_overlay(frame, max_jump_height, salto_alto, velocity_vertical, peak_power_sayer, repetition_count, last_detected_ankles_y, initial_left_shoulder_x, initial_right_shoulder_x, width, height, colors, metrics_below_feet_offset=20, horizontal_offset_factor=0.75): """ Draw metrics overlay on the frame. Args: frame: Input frame max_jump_height: Maximum jump height in meters salto_alto: Absolute jump height in meters velocity_vertical: Vertical velocity in m/s peak_power_sayer: Peak power in watts repetition_count: Number of repetitions last_detected_ankles_y: Y-coordinate of last detected ankles initial_left_shoulder_x: X-coordinate of left shoulder initial_right_shoulder_x: X-coordinate of right shoulder width: Frame width height: Frame height colors: Dictionary with color values metrics_below_feet_offset: Offset for metrics below feet horizontal_offset_factor: Factor for horizontal offset Returns: Frame with metrics overlay """ overlay = frame.copy() alpha = 0.7 font = cv2.FONT_HERSHEY_SIMPLEX font_scale_title_metric = 0.5 font_scale_value = 0.7 font_scale_title_main = 1.2 # Scale for main title (larger) font_thickness_metric = 1 font_thickness_title_main = 1 # Thickness for main title line_height_title_metric = int(20 * 1.2) line_height_value = int(25 * 1.2) padding_vertical = int(15 * 1.2) padding_horizontal = int(15 * 1.2) text_color_title = colors["light_gray"] text_color_value = colors["white"] text_color_title_main = colors["white"] bg_color = colors["gray"] border_color = colors["white"] border_thickness = 1 corner_radius = 10 spacing_horizontal = 30 title_y_offset = 50 # Lower vertical position of title metrics_y_offset_alto = 80 # Adjust Salto Alto position to leave space below metrics_y_offset_relativo = None # Will be calculated dynamically metrics_y_offset_velocidad = None # Will be calculated dynamically metrics_y_offset_potencia = None # Will be calculated dynamically # Helper function to draw rounded rectangles def draw_rounded_rect(img, pt1, pt2, color, thickness=-1, lineType=cv2.LINE_AA, radius=10): x1, y1 = pt1 x2, y2 = pt2 w = x2 - x1 h = y2 - y1 if radius > 0: img = cv2.ellipse(img, (x1 + radius, y1 + radius), (radius, radius), 0, 0, 90, color, thickness, lineType) img = cv2.ellipse(img, (x2 - radius, y1 + radius), (radius, radius), 0, 90, 180, color, thickness, lineType) img = cv2.ellipse(img, (x2 - radius, y2 - radius), (radius, radius), 0, 180, 270, color, thickness, lineType) img = cv2.ellipse(img, (x1 + radius, y2 - radius), (radius, radius), 0, 270, 360, color, thickness, lineType) img = cv2.rectangle(img, (x1, y1 + radius), (x2, y2 - radius), color, thickness, lineType) img = cv2.rectangle(img, (x1 + radius, y1), (x2 - radius, y2), color, thickness, lineType) else: img = cv2.rectangle(img, pt1, pt2, color, thickness, lineType) return img # --- Main Title --- title_text = "Ejercicio de Salto" title_text_size = cv2.getTextSize(title_text, font, font_scale_title_main, font_thickness_title_main)[0] title_x = (width - title_text_size[0]) // 2 title_y = title_y_offset cv2.putText(overlay, title_text, (title_x, title_y), font, font_scale_title_main, text_color_title_main, font_thickness_title_main, cv2.LINE_AA) # --- Relative Jump Box (dynamically positioned) --- relativo_text = "SALTO RELATIVO" relativo_value = f"{max(0, max_jump_height):.2f} m" relativo_text_size = cv2.getTextSize(relativo_text, font, font_scale_title_metric, font_thickness_metric)[0] relativo_value_size = cv2.getTextSize(relativo_value, font, font_scale_value, font_thickness_metric)[0] bg_width_relativo = max(relativo_text_size[0], relativo_value_size[0]) + 2 * padding_horizontal bg_height_relativo = line_height_title_metric + line_height_value + 2 * padding_vertical x_relativo = 20 if last_detected_ankles_y is not None and bg_height_relativo is not None: metrics_y_offset_relativo = int(last_detected_ankles_y - bg_height_relativo - 10) # 10 pixels above ankle # Make sure box doesn't go off top if metrics_y_offset_relativo < title_y_offset + 50: metrics_y_offset_relativo = int(last_detected_ankles_y + metrics_below_feet_offset) # Show below else: metrics_y_offset_relativo = height - 150 # Default position if ankles not detected if metrics_y_offset_relativo is not None: y_relativo = metrics_y_offset_relativo pt1_relativo = (x_relativo, y_relativo) pt2_relativo = (x_relativo + bg_width_relativo, y_relativo + bg_height_relativo) overlay = draw_rounded_rect(overlay, pt1_relativo, pt2_relativo, bg_color, cv2.FILLED, cv2.LINE_AA, corner_radius) cv2.rectangle(overlay, pt1_relativo, pt2_relativo, border_color, border_thickness, cv2.LINE_AA) cv2.putText(overlay, relativo_text, (x_relativo + (bg_width_relativo - relativo_text_size[0]) // 2, y_relativo + padding_vertical + line_height_title_metric // 2 + 2), font, font_scale_title_metric, text_color_title, font_thickness_metric, cv2.LINE_AA) cv2.putText(overlay, relativo_value, (x_relativo + (bg_width_relativo - relativo_value_size[0]) // 2, y_relativo + padding_vertical + line_height_title_metric + line_height_value // 2 + 5), font, font_scale_value, text_color_value, font_thickness_metric, cv2.LINE_AA) # --- High Jump Box (stays in top right) --- alto_text = "SALTO ALTO" alto_value = f"{max(0, salto_alto):.2f} m" alto_text_size = cv2.getTextSize(alto_text, font, font_scale_title_metric, font_thickness_metric)[0] alto_value_size = cv2.getTextSize(alto_value, font, font_scale_value, font_thickness_metric)[0] bg_width_alto = max(alto_text_size[0], alto_value_size[0]) + 2 * padding_horizontal bg_height_alto = line_height_title_metric + line_height_value + 2 * padding_vertical x_alto = width - bg_width_alto - 20 # Default position near right edge if initial_right_shoulder_x is not None: available_space = width - initial_right_shoulder_x x_alto_calculated = initial_right_shoulder_x + int(available_space * (1 - horizontal_offset_factor)) - bg_width_alto # Make sure doesn't go off left edge and there's space from first box if x_alto_calculated > x_relativo + bg_width_relativo + spacing_horizontal + 10 and x_alto_calculated + bg_width_alto < width - 10: x_alto = x_alto_calculated y_alto = metrics_y_offset_alto pt1_alto = (x_alto, y_alto) pt2_alto = (x_alto + bg_width_alto, y_alto + bg_height_alto) overlay = draw_rounded_rect(overlay, pt1_alto, pt2_alto, bg_color, cv2.FILLED, cv2.LINE_AA, corner_radius) cv2.rectangle(overlay, pt1_alto, pt2_alto, border_color, border_thickness, cv2.LINE_AA) cv2.putText(overlay, alto_text, (x_alto + (bg_width_alto - alto_text_size[0]) // 2, y_alto + padding_vertical + line_height_title_metric // 2 + 2), font, font_scale_title_metric, text_color_title, font_thickness_metric, cv2.LINE_AA) cv2.putText(overlay, alto_value, (x_alto + (bg_width_alto - alto_value_size[0]) // 2, y_alto + padding_vertical + line_height_title_metric + line_height_value // 2 + 5), font, font_scale_value, text_color_value, font_thickness_metric, cv2.LINE_AA) # --- Repetitions Box --- reps_text = "REPETICIONES" reps_value = f"{repetition_count}" reps_text_size = cv2.getTextSize(reps_text, font, font_scale_title_metric, font_thickness_metric)[0] reps_value_size = cv2.getTextSize(reps_value, font, font_scale_value, font_thickness_metric)[0] bg_width_reps = max(reps_text_size[0], reps_value_size[0]) + 2 * padding_horizontal bg_height_reps = line_height_title_metric + line_height_value + 2 * padding_vertical x_reps = x_relativo y_reps = y_relativo + bg_height_relativo + 10 pt1_reps = (x_reps, y_reps) pt2_reps = (x_reps + bg_width_reps, y_reps + bg_height_reps) overlay = draw_rounded_rect(overlay, pt1_reps, pt2_reps, bg_color, cv2.FILLED, cv2.LINE_AA, corner_radius) cv2.rectangle(overlay, pt1_reps, pt2_reps, border_color, border_thickness, cv2.LINE_AA) cv2.putText(overlay, reps_text, (x_reps + (bg_width_reps - reps_text_size[0]) // 2, y_reps + padding_vertical + line_height_title_metric // 2 + 2), font, font_scale_title_metric, text_color_title, font_thickness_metric, cv2.LINE_AA) cv2.putText(overlay, reps_value, (x_reps + (bg_width_reps - reps_value_size[0]) // 2, y_reps + padding_vertical + line_height_title_metric + line_height_value // 2 + 5), font, font_scale_value, text_color_value, font_thickness_metric, cv2.LINE_AA) # --- Vertical Velocity Box (below feet) --- if last_detected_ankles_y is not None: velocidad_text = "VELOCIDAD VERTICAL" velocidad_value = f"{abs(velocity_vertical):.2f} m/s" # Show absolute value velocidad_text_size = cv2.getTextSize(velocidad_text, font, font_scale_title_metric, font_thickness_metric)[0] velocidad_value_size = cv2.getTextSize(velocidad_value, font, font_scale_value, font_thickness_metric)[0] bg_width_velocidad = max(velocidad_text_size[0], velocidad_value_size[0]) + 2 * padding_horizontal bg_height_velocidad = line_height_title_metric + line_height_value + 2 * padding_vertical x_velocidad = int(width / 2 - bg_width_velocidad / 2) # Horizontally centered y_velocidad = int(last_detected_ankles_y + metrics_below_feet_offset + bg_height_velocidad) pt1_velocidad = (int(x_velocidad), int(y_velocidad - bg_height_velocidad)) pt2_velocidad = (int(x_velocidad + bg_width_velocidad), int(y_velocidad)) overlay = draw_rounded_rect(overlay, pt1_velocidad, pt2_velocidad, bg_color, cv2.FILLED, cv2.LINE_AA, corner_radius) cv2.rectangle(overlay, pt1_velocidad, pt2_velocidad, border_color, border_thickness, cv2.LINE_AA) cv2.putText(overlay, velocidad_text, (int(x_velocidad + (bg_width_velocidad - velocidad_text_size[0]) // 2), int(y_velocidad - bg_height_velocidad + padding_vertical + line_height_title_metric // 2 + 2)), font, font_scale_title_metric, text_color_title, font_thickness_metric, cv2.LINE_AA) cv2.putText(overlay, velocidad_value, (int(x_velocidad + (bg_width_velocidad - velocidad_value_size[0]) // 2), int(y_velocidad - bg_height_velocidad + padding_vertical + line_height_title_metric + line_height_value // 2 + 5)), font, font_scale_value, text_color_value, font_thickness_metric, cv2.LINE_AA) # --- Sayer Power Box (below velocity box) --- potencia_text = "POTENCIA SAYER" potencia_value = f"{peak_power_sayer:.2f} W" potencia_text_size = cv2.getTextSize(potencia_text, font, font_scale_title_metric, font_thickness_metric)[0] potencia_value_size = cv2.getTextSize(potencia_value, font, font_scale_value, font_thickness_metric)[0] bg_width_potencia = max(potencia_text_size[0], potencia_value_size[0]) + 2 * padding_horizontal bg_height_potencia = line_height_title_metric + line_height_value + 2 * padding_vertical x_potencia = x_velocidad # Same horizontal position as velocity y_potencia = y_velocidad + 5 # Below velocity box pt1_potencia = (int(x_potencia), int(y_potencia)) pt2_potencia = (int(x_potencia + bg_width_potencia), int(y_potencia + bg_height_potencia)) overlay = draw_rounded_rect(overlay, pt1_potencia, pt2_potencia, bg_color, cv2.FILLED, cv2.LINE_AA, corner_radius) cv2.rectangle(overlay, pt1_potencia, pt2_potencia, border_color, border_thickness, cv2.LINE_AA) cv2.putText(overlay, potencia_text, (int(x_potencia + (bg_width_potencia - potencia_text_size[0]) // 2), int(y_potencia + padding_vertical + line_height_title_metric // 2 + 2)), font, font_scale_title_metric, text_color_title, font_thickness_metric, cv2.LINE_AA) cv2.putText(overlay, potencia_value, (int(x_potencia + (bg_width_potencia - potencia_value_size[0]) // 2), int(y_potencia + padding_vertical + line_height_title_metric + line_height_value // 2 + 5)), font, font_scale_value, text_color_value, font_thickness_metric, cv2.LINE_AA) # Blend overlay with original frame result = cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0) return result