"""Frame overlay and annotation drawing utilities. Provides functions for compositing visual overlays onto video frames (banners, timers, centered text). Extracted from ``frame_utils`` to keep that module focused on frame extraction/loading. """ import cv2 import numpy as np def draw_countdown_banner(frame: np.ndarray, text: str) -> np.ndarray: """Draw a warning banner at the top of the frame (doesn't obscure the feed). Args: frame: Input frame (any color space). text: Text to display in the banner. Returns: Copy of the frame with the banner composited in. """ result = frame.copy() h, w = result.shape[:2] banner_h = max(40, h // 12) overlay = result.copy() cv2.rectangle(overlay, (0, 0), (w, banner_h), (0, 0, 180), -1) cv2.addWeighted(overlay, 0.7, result, 0.3, 0, result) font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 0.7 thickness = 2 text_size = cv2.getTextSize(text, font, font_scale, thickness)[0] x = (w - text_size[0]) // 2 y = (banner_h + text_size[1]) // 2 cv2.putText(result, text, (x, y), font, font_scale, (255, 255, 255), thickness) return result def draw_session_timer(frame: np.ndarray, remaining_seconds: float) -> np.ndarray: """Draw a small session timer in the bottom-right corner of the frame. Shows remaining time as "M:SS". Uses a subtle semi-transparent dark background pill with white text — informational, not a warning. Args: frame: Input frame (any color space). remaining_seconds: Seconds remaining in the session. Returns: Copy of the frame with the timer composited in. """ minutes = int(remaining_seconds) // 60 seconds = int(remaining_seconds) % 60 text = f"{minutes}:{seconds:02d}" result = frame.copy() h, w = result.shape[:2] font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 0.5 thickness = 1 text_size = cv2.getTextSize(text, font, font_scale, thickness)[0] pad_x, pad_y = 8, 6 margin = 10 box_w = text_size[0] + 2 * pad_x box_h = text_size[1] + 2 * pad_y x1 = w - box_w - margin y1 = h - box_h - margin x2 = x1 + box_w y2 = y1 + box_h # Semi-transparent dark background overlay = result.copy() cv2.rectangle(overlay, (x1, y1), (x2, y2), (0, 0, 0), -1) cv2.addWeighted(overlay, 0.5, result, 0.5, 0, result) text_x = x1 + pad_x text_y = y2 - pad_y cv2.putText(result, text, (text_x, text_y), font, font_scale, (255, 255, 255), thickness) return result def draw_overlay(frame: np.ndarray, text: str) -> np.ndarray: """Draw a semi-transparent overlay with centered text on the frame. Args: frame: Input frame (any color space). text: Text to display in the center of the frame. Returns: Copy of the frame with the overlay composited in. """ overlay = frame.copy() h, w = overlay.shape[:2] cv2.rectangle(overlay, (0, h // 3), (w, 2 * h // 3), (0, 0, 0), -1) result = cv2.addWeighted(frame, 0.4, overlay, 0.6, 0) font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 1.0 thickness = 2 lines = text.split("\n") line_sizes = [cv2.getTextSize(line, font, font_scale, thickness)[0] for line in lines] line_height = max(s[1] for s in line_sizes) line_gap = int(line_height * 0.6) total_height = len(lines) * line_height + (len(lines) - 1) * line_gap # Start y so the block is vertically centered y = h // 2 - total_height // 2 + line_height for line, (tw, th) in zip(lines, line_sizes): x = (w - tw) // 2 cv2.putText(result, line, (x, y), font, font_scale, (255, 255, 255), thickness) y += line_height + line_gap return result