Ai_video_detector / utils /video_utils.py
Chethanand's picture
Upload 24 files
eb3afa1 verified
Raw
History Blame Contribute Delete
4.92 kB
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