Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| """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 | |