| |
| import cv2 |
| import numpy as np |
| from typing import List |
| import imageio |
|
|
| def rotate_frame(frame: np.ndarray) -> np.ndarray: |
| """Rotate a frame 90 degrees clockwise.""" |
| return cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE) |
|
|
| def center_crop_image(frame: np.ndarray, crop_percent: float = 1.0) -> np.ndarray: |
| """ |
| Center crop an image to a specified percentage of its original size. |
| |
| Args: |
| frame (np.ndarray): Input image. |
| crop_percent (float): Percentage of original size to crop to (0 < crop_percent <= 1). |
| |
| Returns: |
| np.ndarray: Cropped image. |
| """ |
| if crop_percent == 1.0: |
| return frame |
|
|
| |
| original_height, original_width = frame.shape[:2] |
| |
| |
| new_width = int(original_width * crop_percent) |
| new_height = int(original_height * crop_percent) |
| |
| |
| start_x = (original_width - new_width) // 2 |
| start_y = (original_height - new_height) // 2 |
| |
| |
| cropped_frame = frame[start_y:start_y + new_height, start_x:start_x + new_width] |
|
|
| return cropped_frame |
|
|
| def read_video_frames( |
| cap, |
| start_frame: int = None, |
| end_frame: int = None, |
| interval: int = 1, |
| rotate: bool = False, |
| crop_percent: float = 1.0 |
| ) -> List[np.ndarray]: |
| """ |
| Read frames from a video capture object with optional rotation and cropping. |
| |
| Args: |
| cap: OpenCV VideoCapture object. |
| start_frame (int): Starting frame index. |
| end_frame (int): Ending frame index. |
| interval (int): Frame interval for sampling. |
| rotate (bool): Whether to rotate frames 90 degrees clockwise. |
| crop_percent (float): Center crop percentage. |
| |
| Returns: |
| List[np.ndarray]: List of frames. |
| """ |
| frame_count = 0 |
| frame_list = [] |
|
|
| |
| if start_frame is not None: |
| cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame) |
| frame_count += start_frame |
| |
| |
| if end_frame is None: |
| end_frame = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) |
|
|
| |
| while frame_count < end_frame: |
| ret, frame = cap.read() |
| if not ret: |
| break |
|
|
| |
| if frame_count % interval == 0: |
|
|
| |
| if rotate: |
| frame = rotate_frame(frame) |
| |
| |
| frame = center_crop_image(frame, crop_percent=crop_percent) |
| frame_list.append(frame) |
|
|
| frame_count += 1 |
|
|
| return frame_list |
|
|
| def save_to_video(frames: List[np.ndarray], output_path: str, fps: int = 30): |
| """ |
| Save a list of frames to a video file. |
| |
| Args: |
| frames (List[np.ndarray]): List of frames. |
| output_path (str): Output video path. |
| fps (int): Frames per second. |
| """ |
| imageio.mimsave(output_path, frames, fps=fps, codec='libx264') |
|
|
| def resize_frames_to_long_side(frames: List[np.ndarray], target_long_side: int) -> List[np.ndarray]: |
| """ |
| Resize frames so the longer side matches the target size, preserving aspect ratio. |
| |
| Args: |
| frames (List[np.ndarray]): List of frames. |
| target_long_side (int): Desired length of the longer side. |
| |
| Returns: |
| List[np.ndarray]: Resized frames. |
| """ |
|
|
| |
| if target_long_side is None: |
| return frames |
| |
| resized_frames = [] |
| |
| for frame in frames: |
| height, width = frame.shape[:2] |
| |
| |
| if width > height: |
| scale_factor = target_long_side / width |
| else: |
| scale_factor = target_long_side / height |
| |
| new_width = int(width * scale_factor) |
| new_height = int(height * scale_factor) |
| |
| |
| resized_frame = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_AREA) |
| resized_frames.append(resized_frame) |
| |
| return resized_frames |
|
|
| def sample_frames_evenly(video_frames: List[np.ndarray], num_frames: int) -> List[np.ndarray]: |
| """ |
| Sample frames evenly from a video sequence. |
| |
| Args: |
| video_frames (List[np.ndarray]): List of frames. |
| num_frames (int): Number of frames to sample. |
| |
| Returns: |
| List[np.ndarray]: Sampled frames. |
| """ |
| total = len(video_frames) |
| if num_frames >= total: |
| return video_frames.copy() |
|
|
| indices = np.linspace(0, total - 1, num=num_frames, dtype=int) |
| return [video_frames[i] for i in indices] |
|
|
| def wrap_text(text: str, max_width: int, font, font_scale: float) -> List[str]: |
| """ |
| Wraps text to fit within a given width. |
| |
| Args: |
| text (str): The text to wrap. |
| max_width (int): The maximum width in pixels. |
| font: The font to use. |
| font_scale (float): The scale of the font. |
| |
| Returns: |
| List of lines of wrapped text. |
| """ |
| words = text.split(' ') |
| lines = [] |
| current_line = '' |
| |
| for word in words: |
| test_line = current_line + word + ' ' |
| |
| size = cv2.getTextSize(test_line, font, font_scale, 1)[0] |
| if size[0] > max_width: |
| lines.append(current_line.strip()) |
| current_line = word + ' ' |
| else: |
| current_line = test_line |
| |
| if current_line: |
| lines.append(current_line.strip()) |
| |
| return lines |
|
|
| def add_overlay_text(frame: np.ndarray, caption: str) -> np.ndarray: |
| """ |
| Add overlay text to a frame. |
| |
| Args: |
| frame (np.ndarray): Input image. |
| caption (str): Text to overlay. |
| |
| Returns: |
| np.ndarray: Frame with text. |
| """ |
|
|
| w = frame.shape[1] |
| y0, dy = 30, 20 |
| for i, line in enumerate(wrap_text(caption, w, cv2.FONT_HERSHEY_SIMPLEX, 1.0)): |
| y = y0 + i * dy |
| cv2.putText(frame, line, (10, y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 2) |
| return frame |