vl-jepa-custom / vljepa /utils.py
max044's picture
Upload folder using huggingface_hub
2fd5fdc verified
Raw
History Blame Contribute Delete
4.21 kB
"""Utility functions: video I/O, temporal IoU, NMS, sliding windows."""
import cv2
import numpy as np
import torch
def load_video_frames(
video_path: str,
start_sec: float = 0.0,
end_sec: float | None = None,
num_frames: int = 16,
) -> list[np.ndarray] | None:
"""Load uniformly sampled RGB frames from a video segment.
Args:
video_path: path to .mp4 file
start_sec: start of segment in seconds
end_sec: end of segment in seconds (None = end of video)
num_frames: number of frames to sample
Returns:
List of RGB numpy arrays (H, W, 3), or None on failure.
"""
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return None
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
if fps <= 0 or total_frames <= 0:
cap.release()
return None
duration = total_frames / fps
if end_sec is None:
end_sec = duration
start_frame = max(0, int(start_sec * fps))
end_frame = min(total_frames - 1, int(end_sec * fps))
if end_frame <= start_frame:
cap.release()
return None
n_available = end_frame - start_frame + 1
n_sample = min(num_frames, n_available)
indices = np.linspace(start_frame, end_frame, n_sample, dtype=int)
frames = []
for idx in indices:
cap.set(cv2.CAP_PROP_POS_FRAMES, int(idx))
ret, frame = cap.read()
if ret:
frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
cap.release()
if len(frames) == 0:
return None
return frames
def get_video_duration(video_path: str) -> float:
"""Get video duration in seconds."""
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
return 0.0
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
cap.release()
if fps <= 0:
return 0.0
return total_frames / fps
def temporal_iou(
pred_start: float,
pred_end: float,
gt_start: float,
gt_end: float,
) -> float:
"""Compute temporal Intersection over Union between two segments."""
inter_start = max(pred_start, gt_start)
inter_end = min(pred_end, gt_end)
inter = max(0.0, inter_end - inter_start)
union = (pred_end - pred_start) + (gt_end - gt_start) - inter
if union <= 0:
return 0.0
return inter / union
def nms(
proposals: list[tuple[float, float]],
scores: list[float],
iou_threshold: float = 0.5,
) -> list[int]:
"""Non-maximum suppression for temporal proposals.
Args:
proposals: list of (start, end) tuples
scores: corresponding scores
iou_threshold: suppress proposals with IoU above this
Returns:
List of kept indices (sorted by score descending).
"""
if len(proposals) == 0:
return []
sorted_idx = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)
kept = []
for i in sorted_idx:
should_keep = True
for j in kept:
iou = temporal_iou(
proposals[i][0], proposals[i][1],
proposals[j][0], proposals[j][1],
)
if iou > iou_threshold:
should_keep = False
break
if should_keep:
kept.append(i)
return kept
def sliding_window_proposals(
duration: float,
window_sizes: list[float],
stride: float = 1.0,
) -> list[tuple[float, float]]:
"""Generate candidate temporal proposals using sliding windows.
Args:
duration: total video duration in seconds
window_sizes: list of window durations to use
stride: step size in seconds
Returns:
List of (start, end) proposals.
"""
proposals = []
for ws in window_sizes:
if ws > duration:
# Single proposal covering the whole video
proposals.append((0.0, duration))
continue
start = 0.0
while start + ws <= duration + 0.01: # small epsilon for float
end = min(start + ws, duration)
proposals.append((start, end))
start += stride
return proposals