""" Streaming frame reader for memory-efficient video processing """ import cv2 import numpy as np from pathlib import Path from typing import Optional, Dict, Any, Tuple class StreamingFrameReader: """Read frames one at a time from video file with seeking support""" def __init__(self, video_path: str, start_frame: int = 0): self.video_path = Path(video_path) if not self.video_path.exists(): raise FileNotFoundError(f"Video file not found: {video_path}") self.cap = cv2.VideoCapture(str(self.video_path)) if not self.cap.isOpened(): raise RuntimeError(f"Failed to open video: {video_path}") # Get video properties self.fps = self.cap.get(cv2.CAP_PROP_FPS) self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # Set start position self.current_frame_idx = 0 if start_frame > 0: self.seek(start_frame) print(f"📹 Streaming reader initialized:") print(f" Video: {self.video_path.name}") print(f" Resolution: {self.width}x{self.height}") print(f" FPS: {self.fps}") print(f" Total frames: {self.total_frames}") print(f" Starting at frame: {start_frame}") def read_frame(self) -> Optional[np.ndarray]: """ Read next frame from video Returns: Frame as numpy array or None if end of video """ ret, frame = self.cap.read() if ret: self.current_frame_idx += 1 return frame return None def seek(self, frame_idx: int) -> bool: """ Seek to specific frame Args: frame_idx: Target frame index Returns: True if seek successful """ if 0 <= frame_idx < self.total_frames: self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx) self.current_frame_idx = frame_idx return True return False def get_video_info(self) -> Dict[str, Any]: """Get video metadata""" return { 'width': self.width, 'height': self.height, 'fps': self.fps, 'total_frames': self.total_frames, 'path': str(self.video_path) } def get_progress(self) -> float: """Get current progress as percentage""" if self.total_frames > 0: return (self.current_frame_idx / self.total_frames) * 100 return 0.0 def reset(self) -> None: """Reset to beginning of video""" self.seek(0) def peek_frame(self) -> Optional[np.ndarray]: """ Peek at next frame without advancing position Returns: Frame as numpy array or None if end of video """ current_pos = self.current_frame_idx frame = self.read_frame() if frame is not None: # Reset position self.seek(current_pos) return frame def read_frame_at(self, frame_idx: int) -> Optional[np.ndarray]: """ Read frame at specific index without changing current position Args: frame_idx: Frame index to read Returns: Frame as numpy array or None if invalid index """ current_pos = self.current_frame_idx if self.seek(frame_idx): frame = self.read_frame() # Restore position self.seek(current_pos) return frame return None def get_frame_batch(self, start_idx: int, count: int) -> list[np.ndarray]: """ Read a batch of frames (for initial detection or correction) Args: start_idx: Starting frame index count: Number of frames to read Returns: List of frames """ current_pos = self.current_frame_idx frames = [] if self.seek(start_idx): for i in range(count): frame = self.read_frame() if frame is None: break frames.append(frame) # Restore position self.seek(current_pos) return frames def estimate_memory_per_frame(self) -> float: """ Estimate memory usage per frame in MB Returns: Estimated memory in MB """ # BGR format = 3 channels, uint8 = 1 byte per channel bytes_per_frame = self.width * self.height * 3 return bytes_per_frame / (1024 * 1024) def close(self) -> None: """Release video capture resources""" if self.cap is not None: self.cap.release() self.cap = None def __enter__(self): """Context manager support""" return self def __exit__(self, exc_type, exc_val, exc_tb): """Context manager cleanup""" self.close() def __del__(self): """Ensure cleanup on deletion""" self.close() def __len__(self) -> int: """Total number of frames""" return self.total_frames def __iter__(self): """Iterator support""" self.reset() return self def __next__(self) -> np.ndarray: """Iterator next frame""" frame = self.read_frame() if frame is None: raise StopIteration return frame