Spaces:
Sleeping
Sleeping
| import cv2 | |
| import numpy as np | |
| from PIL import Image, ImageDraw, ImageFont | |
| def extract_frame(video_path: str, timestamp: str, output_image_path: str) -> None: | |
| """ | |
| Extracts a frame from a video at the specified timestamp and saves it as an image. | |
| Parameters: | |
| - video_path (str): Path to the input video file. | |
| - timestamp (str): Timestamp in the format "HH:MM:SS.sss". | |
| - output_image_path (str): Path to save the extracted frame image. | |
| """ | |
| # Convert timestamp to milliseconds | |
| h, m, s = map(float, timestamp.split(':')) | |
| target_msec = int((h * 3600 + m * 60 + s) * 1000) | |
| # Open the video | |
| cap = cv2.VideoCapture(video_path) | |
| if not cap.isOpened(): | |
| raise IOError(f"Cannot open video file: {video_path}") | |
| # Set position in milliseconds | |
| cap.set(cv2.CAP_PROP_POS_MSEC, target_msec) | |
| # Read the frame | |
| ret, frame = cap.read() | |
| if not ret: | |
| raise RuntimeError(f"Failed to retrieve frame at {timestamp} ({target_msec} ms)") | |
| # Save frame as PNG | |
| cv2.imwrite(output_image_path, frame) | |
| print(f"Frame saved as {output_image_path}") | |
| # Clean up | |
| cap.release() | |
| def center_bbox_in_circle( | |
| input_image_path: str, | |
| bbox: tuple, | |
| output_path: str, | |
| background_path = "src/assets/BACKGROUND.png", | |
| mask_path = "src/assets/circle_mask.png", | |
| circle_center=(296, 126), | |
| circle_radius=77, | |
| blend_alpha: float = 0.3 | |
| ) -> None: | |
| """ | |
| Centers a bounding box from an input image into a circular area on a background image. | |
| Parameters: | |
| - input_image_path (str): Path to the input image. | |
| - background_path (str): Path to the background image with a circle. | |
| - mask_path (str): Path to the circular mask image (grayscale). | |
| - bbox (tuple): Bounding box in normalized format (y_min, x_min, y_max, x_max), range 0–1000. | |
| - circle_center (tuple): (x, y) center of the target circle on the background. | |
| - circle_radius (int): Radius of the target circle. | |
| - output_path (str): Path to save the resulting image. | |
| - blend_alpha (float): Blending factor for the background (default: 0.3). | |
| """ | |
| background = cv2.imread(background_path) | |
| input_image = cv2.imread(input_image_path) | |
| img_height, img_width = input_image.shape[:2] | |
| y_min = int(bbox[0] / 1000.0 * img_height) | |
| x_min = int(bbox[1] / 1000.0 * img_width) | |
| y_max = int(bbox[2] / 1000.0 * img_height) | |
| x_max = int(bbox[3] / 1000.0 * img_width) | |
| bbox_w = x_max - x_min | |
| bbox_h = y_max - y_min | |
| bbox_center_x = x_min + bbox_w // 2 | |
| bbox_center_y = y_min + bbox_h // 2 | |
| circle_diameter = 2 * circle_radius | |
| scale = min(circle_diameter / bbox_w, circle_diameter / bbox_h) | |
| new_width = int(img_width * scale) | |
| new_height = int(img_height * scale) | |
| resized_image = cv2.resize(input_image, (new_width, new_height)) | |
| new_bbox_center_x = int(bbox_center_x * scale) | |
| new_bbox_center_y = int(bbox_center_y * scale) | |
| offset_x = circle_center[0] - new_bbox_center_x | |
| offset_y = circle_center[1] - new_bbox_center_y | |
| canvas = np.zeros_like(background) | |
| start_x = max(offset_x, 0) | |
| start_y = max(offset_y, 0) | |
| end_x = min(start_x + new_width, background.shape[1]) | |
| end_y = min(start_y + new_height, background.shape[0]) | |
| src_start_x = max(-offset_x, 0) | |
| src_start_y = max(-offset_y, 0) | |
| target_h = min(end_y - start_y, resized_image.shape[0] - src_start_y) | |
| target_w = min(end_x - start_x, resized_image.shape[1] - src_start_x) | |
| canvas[start_y:start_y+target_h, start_x:start_x+target_w] = resized_image[ | |
| src_start_y:src_start_y + target_h, | |
| src_start_x:src_start_x + target_w | |
| ] | |
| mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) | |
| mask = cv2.resize(mask, (background.shape[1], background.shape[0])) | |
| blended = cv2.addWeighted(canvas, blend_alpha, background, 1.0 - blend_alpha, 0) | |
| mask_3c = cv2.merge([mask, mask, mask]) | |
| inv_mask = cv2.bitwise_not(mask_3c) | |
| outside = cv2.bitwise_and(blended, mask_3c) | |
| inside = cv2.bitwise_and(canvas, inv_mask) | |
| combined = cv2.add(outside, inside) | |
| cv2.imwrite(output_path, combined) | |
| print(f"Saved result to {output_path}") | |
| def overlay_image_bottom_right( | |
| background_path: str, | |
| overlay_path: str, | |
| output_path: str | |
| ) -> None: | |
| """ | |
| Overlays an image (optionally with transparency) onto the bottom-right corner of another image. | |
| Parameters: | |
| - background_path (str): Path to the background image. | |
| - overlay_path (str): Path to the overlay image (can have alpha channel). | |
| - output_path (str): Path to save the combined image. | |
| """ | |
| # Load images (support alpha if available) | |
| background = cv2.imread(background_path, cv2.IMREAD_UNCHANGED) | |
| overlay = cv2.imread(overlay_path, cv2.IMREAD_UNCHANGED) | |
| h_bg, w_bg = background.shape[:2] | |
| h_ov, w_ov = overlay.shape[:2] | |
| x_offset = w_bg - w_ov | |
| y_offset = h_bg - h_ov | |
| if overlay.shape[2] == 4: | |
| # Split channels | |
| overlay_rgb = overlay[:, :, :3] | |
| alpha = overlay[:, :, 3] / 255.0 | |
| # Handle 3-channel background (add alpha if missing) | |
| if background.shape[2] == 3: | |
| background = cv2.cvtColor(background, cv2.COLOR_BGR2BGRA) | |
| for c in range(3): # Only blend RGB channels | |
| background[y_offset:y_offset+h_ov, x_offset:x_offset+w_ov, c] = ( | |
| alpha * overlay_rgb[:, :, c] + | |
| (1 - alpha) * background[y_offset:y_offset+h_ov, x_offset:x_offset+w_ov, c] | |
| ) | |
| else: | |
| # No alpha channel: paste directly | |
| background[y_offset:y_offset+h_ov, x_offset:x_offset+w_ov] = overlay | |
| # Ensure saving as 3-channel PNG | |
| if background.shape[2] == 4: | |
| background = cv2.cvtColor(background, cv2.COLOR_BGRA2BGR) | |
| cv2.imwrite(output_path, background) | |
| print(f"Combined image saved to {output_path}") | |
| def overlay_image_top_left( | |
| background_path: str, | |
| overlay_path: str, | |
| output_path: str, | |
| padding_x: int = 20, | |
| padding_y: int = 20 | |
| ) -> None: | |
| """ | |
| Overlays an image (optionally with transparency) onto the top-left corner of another image with padding. | |
| Parameters: | |
| - background_path (str): Path to the background image. | |
| - overlay_path (str): Path to the overlay image (can have alpha channel). | |
| - output_path (str): Path to save the resulting image. | |
| - padding_x (int): Horizontal padding from the left. | |
| - padding_y (int): Vertical padding from the top. | |
| """ | |
| # Load images (support alpha if available) | |
| background = cv2.imread(background_path, cv2.IMREAD_UNCHANGED) | |
| overlay = cv2.imread(overlay_path, cv2.IMREAD_UNCHANGED) | |
| h_bg, w_bg = background.shape[:2] | |
| h_ov, w_ov = overlay.shape[:2] | |
| x_offset = padding_x | |
| y_offset = padding_y | |
| # Ensure overlay fits in background with padding | |
| if x_offset + w_ov > w_bg or y_offset + h_ov > h_bg: | |
| raise ValueError("Overlay image does not fit within background with given padding.") | |
| if overlay.shape[2] == 4: | |
| overlay_rgb = overlay[:, :, :3] | |
| alpha = overlay[:, :, 3] / 255.0 | |
| # Add alpha channel to background if needed | |
| if background.shape[2] == 3: | |
| background = cv2.cvtColor(background, cv2.COLOR_BGR2BGRA) | |
| for c in range(3): # Blend RGB channels | |
| background[y_offset:y_offset+h_ov, x_offset:x_offset+w_ov, c] = ( | |
| alpha * overlay_rgb[:, :, c] + | |
| (1 - alpha) * background[y_offset:y_offset+h_ov, x_offset:x_offset+w_ov, c] | |
| ) | |
| else: | |
| # No alpha: paste directly | |
| background[y_offset:y_offset+h_ov, x_offset:x_offset+w_ov] = overlay | |
| # Convert back to BGR before saving (if needed) | |
| if background.shape[2] == 4: | |
| background = cv2.cvtColor(background, cv2.COLOR_BGRA2BGR) | |
| cv2.imwrite(output_path, background) | |
| print(f"Image with overlay saved to {output_path}") | |
| def annotate_image_with_phrase_and_label(image_path, output_path, phrase, label): | |
| font_path = "src/assets/Herokid-BoldUltraCondensed.otf" # Path to your custom font file | |
| img_cv2 = cv2.imread(image_path) | |
| if img_cv2 is None: | |
| raise ValueError("Could not load the image.") | |
| # Convert BGR (OpenCV) to RGB (Pillow) | |
| img_rgb = cv2.cvtColor(img_cv2, cv2.COLOR_BGR2RGB) | |
| img_pil = Image.fromarray(img_rgb) | |
| draw = ImageDraw.Draw(img_pil) | |
| # Load custom font | |
| font_size = 50 | |
| font = ImageFont.truetype(font_path, font_size) | |
| # Image dimensions | |
| width, height = img_pil.size | |
| margin = 10 | |
| # Phrase (bottom-left in white) | |
| phrase_bbox = draw.textbbox((0, 0), phrase, font=font) | |
| phrase_width = phrase_bbox[2] - phrase_bbox[0] | |
| phrase_height = phrase_bbox[3] - phrase_bbox[1] | |
| phrase_pos = (margin, height - phrase_height - margin) | |
| draw.text(phrase_pos, phrase, font=font, fill="white") | |
| # Label (bottom-right in black) | |
| label_bbox = draw.textbbox((0, 0), label, font=font) | |
| label_width = label_bbox[2] - label_bbox[0] | |
| label_height = label_bbox[3] - label_bbox[1] | |
| label_pos = (width - label_width - margin, height - label_height - margin) | |
| draw.text(label_pos, label, font=font, fill="black") | |
| # Convert back to OpenCV and save | |
| img_output = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR) | |
| cv2.imwrite(output_path, img_output) |