Spaces:
Sleeping
Sleeping
| import cv2 | |
| import numpy as np | |
| import torch | |
| import os | |
| from typing import List, Tuple, Optional | |
| def get_video_properties(video_path: str) -> dict: | |
| """ | |
| Get video properties. | |
| """ | |
| cap = cv2.VideoCapture(video_path) | |
| if not cap.isOpened(): | |
| return {} | |
| props = { | |
| 'width': int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), | |
| 'height': int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), | |
| 'fps': cap.get(cv2.CAP_PROP_FPS), | |
| 'frame_count': int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |
| } | |
| cap.release() | |
| return props | |
| def load_video_frames( | |
| video_path: str, | |
| num_frames: int = 30, | |
| frame_size: Tuple[int, int] = (224, 224), | |
| frame_skip: int = 1 | |
| ) -> Optional[np.ndarray]: | |
| """ | |
| Load frames from a video with robust preprocessing: | |
| 1. Center crop to square (min dimension). | |
| 2. Resize to frame_size. | |
| 3. Sample frames uniformly. | |
| """ | |
| cap = cv2.VideoCapture(video_path) | |
| if not cap.isOpened(): | |
| print(f"Error opening video: {video_path}") | |
| return None | |
| total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |
| if total_frames <= 0: | |
| cap.release() | |
| return None | |
| width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| # Calculate crop coordinates for center square crop | |
| min_dim = min(width, height) | |
| start_x = (width - min_dim) // 2 | |
| start_y = (height - min_dim) // 2 | |
| # Calculate frame indices to sample | |
| # We want 'num_frames' frames. | |
| # Strategy: evenly space them across the video duration we look at. | |
| # But for simplicity and consistency, let's just grab them with a stride, | |
| # or if video is short, grab all and pad. | |
| # Let's try to span as much of the video as possible? | |
| # Or just stick to the requested architecture of sampling segments. | |
| # The prompt asked for "preprocess ... not based on aspect ratio". | |
| # Simple strategy: Read frames with skip, up to num_frames. | |
| # If video is too short, loop/pad? | |
| # Better: Reservoir sampling or Linspace if we want fixed count? | |
| # Let's stick to the user's likely need: Fixed number of frames. | |
| sampled_frames = [] | |
| # We'll seek effectively. | |
| # But looping is safer for some codecs. | |
| # Improved sampling: pick indices using linspace if we want to span whole video? | |
| # Or just sequential for temporal consistency (RNN prefer sequences). | |
| # Let's do sequential with skip. | |
| frame_idx = 0 | |
| frames_collected = 0 | |
| while frames_collected < num_frames: | |
| ret, frame = cap.read() | |
| if not ret: | |
| break | |
| if frame_idx % frame_skip == 0: | |
| # Preprocess Frame | |
| # 1. Center Crop | |
| crop = frame[start_y:start_y+min_dim, start_x:start_x+min_dim] | |
| # 2. Resize | |
| resized = cv2.resize(crop, frame_size) | |
| # 3. Convert BGR to RGB | |
| rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB) | |
| sampled_frames.append(rgb) | |
| frames_collected += 1 | |
| frame_idx += 1 | |
| cap.release() | |
| # Handle insufficient frames | |
| if len(sampled_frames) == 0: | |
| return None | |
| if len(sampled_frames) < num_frames: | |
| # Pad with last frame or zeros? | |
| # Let's pad with zeros (black frames) or loop? | |
| # Zero padding is safer to avoid motion artifacts. | |
| padding = [np.zeros((frame_size[1], frame_size[0], 3), dtype=np.uint8)] * (num_frames - len(sampled_frames)) | |
| sampled_frames.extend(padding) | |
| return np.array(sampled_frames) | |
| def normalize_frames(frames: np.ndarray) -> np.ndarray: | |
| """ | |
| Normalize frames to [0, 1] and then standard ImageNet mean/std. | |
| Frames input: (N, H, W, C) in RGB, uint8 [0,255] | |
| """ | |
| # Convert to float32 [0, 1] | |
| frames_norm = frames.astype(np.float32) / 255.0 | |
| # Standard ImageNet mean and std | |
| mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) | |
| std = np.array([0.229, 0.224, 0.225], dtype=np.float32) | |
| # Apply normalization | |
| # frames is (N, H, W, C), mean/std are (3,) | |
| # We allow broadcasting on the last dimension | |
| frames_norm = (frames_norm - mean) / std | |
| return frames_norm | |
| def validate_video(video_path: str) -> bool: | |
| """ | |
| Check if video is valid and openable. | |
| """ | |
| if not os.path.exists(video_path): | |
| return False | |
| try: | |
| cap = cv2.VideoCapture(video_path) | |
| if not cap.isOpened(): | |
| return False | |
| # Read one frame to be sure | |
| ret, _ = cap.read() | |
| cap.release() | |
| return ret | |
| except Exception: | |
| return False | |