Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| """Utilities for extracting and displaying frames from images and videos. | |
| The ``gradio`` import is deferred so that Gradio-free functions | |
| (``load_media_frames_raw``) can be used inside worker subprocesses | |
| without pulling in the full Gradio dependency. | |
| """ | |
| import base64 | |
| import cv2 | |
| import numpy as np | |
| _VIDEO_EXTENSIONS = (".mp4", ".avi", ".mov", ".mkv", ".webm") | |
| def load_media_frames_raw(media_path: str) -> list[np.ndarray]: | |
| """Load BGR frames from a media file (image or video). | |
| Gradio-free — raises ``RuntimeError`` on failure. Safe to call inside | |
| worker subprocesses that do not have Gradio installed / imported. | |
| Args: | |
| media_path: Path to an image or video file. | |
| Returns: | |
| List of BGR numpy arrays (one per frame). | |
| Raises: | |
| RuntimeError: If the file cannot be opened or contains no frames. | |
| """ | |
| if media_path.lower().endswith(_VIDEO_EXTENSIONS): | |
| cap = cv2.VideoCapture(media_path) | |
| if not cap.isOpened(): | |
| raise RuntimeError(f"Could not open video: {media_path}") | |
| frames: list[np.ndarray] = [] | |
| while True: | |
| ret, frame = cap.read() | |
| if not ret: | |
| break | |
| frames.append(frame) | |
| cap.release() | |
| if not frames: | |
| raise RuntimeError(f"No frames read from video: {media_path}") | |
| return frames | |
| img = cv2.imread(media_path) | |
| if img is None: | |
| raise RuntimeError(f"Could not read image: {media_path}") | |
| return [img] | |
| def extract_frames(image_path: str | None, video_path: str | None) -> list[np.ndarray]: | |
| """Extract BGR frames from an image or video. | |
| Args: | |
| image_path: Path to an image file, or None. | |
| video_path: Path to a video file, or None. | |
| Returns: | |
| List of BGR numpy arrays. | |
| Raises: | |
| gr.Error: If neither input is provided or the media cannot be read. | |
| """ | |
| import gradio as gr | |
| if image_path is None and video_path is None: | |
| raise gr.Error("Please upload an image or video.") | |
| media_path = image_path or video_path | |
| assert media_path is not None # guaranteed by the check above | |
| try: | |
| return load_media_frames_raw(media_path) | |
| except RuntimeError as exc: | |
| raise gr.Error(str(exc)) from exc | |
| def load_media_frames(media_path: str) -> list[np.ndarray]: | |
| """Load BGR frames from a stored media file (image or video). | |
| Gradio-aware wrapper around :func:`load_media_frames_raw` that converts | |
| ``RuntimeError`` to ``gr.Error`` for UI display. | |
| Args: | |
| media_path: Path to the media file. | |
| Returns: | |
| List of BGR numpy arrays. | |
| """ | |
| import gradio as gr | |
| try: | |
| return load_media_frames_raw(media_path) | |
| except RuntimeError as exc: | |
| raise gr.Error(str(exc)) from exc | |
| def get_thumbnail(media_path: str) -> np.ndarray | None: | |
| """Get an RGB thumbnail from a media file for display. | |
| For videos, returns the first frame. For images, returns the image itself. | |
| Args: | |
| media_path: Path to the media file. | |
| Returns: | |
| RGB numpy array suitable for gr.Image display, or None on failure. | |
| """ | |
| if media_path.lower().endswith(_VIDEO_EXTENSIONS): | |
| cap = cv2.VideoCapture(media_path) | |
| ret, frame = cap.read() | |
| cap.release() | |
| if ret: | |
| return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) | |
| return None | |
| img = cv2.imread(media_path) | |
| if img is not None: | |
| return cv2.cvtColor(img, cv2.COLOR_BGR2RGB) | |
| return None | |
| def get_thumbnail_base64(media_path: str) -> str | None: | |
| """Return a base64-encoded JPEG string of the first frame from a media file. | |
| Args: | |
| media_path: Path to an image or video file. | |
| Returns: | |
| Base64-encoded JPEG string, or None on failure. | |
| """ | |
| thumb = get_thumbnail(media_path) | |
| if thumb is None: | |
| return None | |
| bgr = cv2.cvtColor(thumb, cv2.COLOR_RGB2BGR) | |
| ok, buf = cv2.imencode(".jpg", bgr, [cv2.IMWRITE_JPEG_QUALITY, 80]) | |
| if not ok: | |
| return None | |
| return base64.b64encode(buf.tobytes()).decode() | |
| def get_video_frame_count(video_path: str) -> int: | |
| """Return the total number of frames in a video file. | |
| Reads ``CAP_PROP_FRAME_COUNT`` from the container metadata. Falls back | |
| to a sequential scan when the metadata is missing or unreliable (common | |
| with browser-recorded WebM files). | |
| Args: | |
| video_path: Path to a video file. | |
| Returns: | |
| Total frame count (always >= 1). | |
| Raises: | |
| RuntimeError: If the video cannot be opened or contains no frames. | |
| """ | |
| cap = cv2.VideoCapture(video_path) | |
| if not cap.isOpened(): | |
| raise RuntimeError(f"Could not open video: {video_path}") | |
| count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |
| if count > 0: | |
| cap.release() | |
| return count | |
| # Metadata unreliable — count by reading through | |
| count = 0 | |
| while True: | |
| ret, _ = cap.read() | |
| if not ret: | |
| break | |
| count += 1 | |
| cap.release() | |
| if count == 0: | |
| raise RuntimeError(f"No frames read from video: {video_path}") | |
| return count | |
| def extract_frame_at_index(video_path: str, frame_index: int) -> np.ndarray: | |
| """Extract a single BGR frame at the given index from a video. | |
| Uses ``CAP_PROP_POS_FRAMES`` to seek directly to the requested frame, | |
| avoiding the need to decode the entire video. | |
| Args: | |
| video_path: Path to a video file. | |
| frame_index: Zero-based frame index. | |
| Returns: | |
| BGR numpy array of the requested frame. | |
| Raises: | |
| RuntimeError: If the video cannot be opened or the frame cannot be read. | |
| """ | |
| cap = cv2.VideoCapture(video_path) | |
| if not cap.isOpened(): | |
| raise RuntimeError(f"Could not open video: {video_path}") | |
| cap.set(cv2.CAP_PROP_POS_FRAMES, frame_index) | |
| ret, frame = cap.read() | |
| cap.release() | |
| if not ret: | |
| raise RuntimeError(f"Could not read frame {frame_index} from: {video_path}") | |
| return frame | |