""" Camera input source for the Cluster4NPU inference pipeline. This module provides camera input capabilities with support for multiple cameras, resolution configuration, and seamless integration with the InferencePipeline. """ import cv2 import numpy as np import threading import time from typing import Optional, Callable, Tuple, Dict, Any from dataclasses import dataclass from abc import ABC, abstractmethod @dataclass class CameraConfig: """Configuration for camera input source.""" camera_index: int = 0 width: int = 640 height: int = 480 fps: int = 30 format: str = 'BGR' auto_exposure: bool = True brightness: float = 0.5 contrast: float = 0.5 saturation: float = 0.5 class DataSourceBase(ABC): """Abstract base class for data sources.""" @abstractmethod def start(self) -> bool: """Start the data source.""" pass @abstractmethod def stop(self) -> None: """Stop the data source.""" pass @abstractmethod def is_running(self) -> bool: """Check if the data source is running.""" pass @abstractmethod def get_frame(self) -> Optional[np.ndarray]: """Get the next frame from the source.""" pass class CameraSource(DataSourceBase): """ Camera input source for real-time video capture. Features: - Multiple camera index support - Resolution and FPS configuration - Format conversion (BGR → model input format) - Error handling for camera disconnection - Thread-safe frame capture """ def __init__(self, config: CameraConfig, frame_callback: Optional[Callable[[np.ndarray], None]] = None): """ Initialize camera source. Args: config: Camera configuration frame_callback: Optional callback for each captured frame """ self.config = config self.frame_callback = frame_callback # Camera capture object self.cap: Optional[cv2.VideoCapture] = None # Threading control self._capture_thread: Optional[threading.Thread] = None self._stop_event = threading.Event() self._frame_lock = threading.Lock() # Current frame storage self._current_frame: Optional[np.ndarray] = None self._frame_count = 0 self._fps_counter = 0 self._last_fps_time = time.time() self._actual_fps = 0.0 # Error handling self._connection_lost = False self._last_error: Optional[str] = None def start(self) -> bool: """ Start camera capture. Returns: bool: True if camera started successfully, False otherwise """ if self.is_running(): return True try: # Initialize camera self.cap = cv2.VideoCapture(self.config.camera_index) if not self.cap.isOpened(): self._last_error = f"Failed to open camera {self.config.camera_index}" return False # Configure camera properties self._configure_camera() # Test camera capture ret, frame = self.cap.read() if not ret or frame is None: self._last_error = "Failed to read initial frame from camera" self.cap.release() self.cap = None return False print(f"[CameraSource] Camera {self.config.camera_index} opened successfully") print(f"[CameraSource] Resolution: {self.config.width}x{self.config.height}, FPS: {self.config.fps}") # Start capture thread self._stop_event.clear() self._connection_lost = False self._capture_thread = threading.Thread(target=self._capture_loop, daemon=True) self._capture_thread.start() return True except Exception as e: self._last_error = f"Camera initialization error: {str(e)}" if self.cap: self.cap.release() self.cap = None return False def stop(self) -> None: """Stop camera capture.""" if not self.is_running(): return print("[CameraSource] Stopping camera capture...") # Signal stop self._stop_event.set() # Wait for capture thread to finish if self._capture_thread and self._capture_thread.is_alive(): self._capture_thread.join(timeout=2.0) # Release camera if self.cap: self.cap.release() self.cap = None # Clear current frame with self._frame_lock: self._current_frame = None print("[CameraSource] Camera capture stopped") def is_running(self) -> bool: """Check if camera is currently running.""" return (self.cap is not None and self.cap.isOpened() and self._capture_thread is not None and self._capture_thread.is_alive() and not self._stop_event.is_set()) def get_frame(self) -> Optional[np.ndarray]: """ Get the latest captured frame. Returns: Optional[np.ndarray]: Latest frame or None if no frame available """ with self._frame_lock: if self._current_frame is not None: return self._current_frame.copy() return None def get_stats(self) -> Dict[str, Any]: """ Get camera statistics. Returns: Dict[str, Any]: Statistics including FPS, frame count, etc. """ return { 'frame_count': self._frame_count, 'actual_fps': self._actual_fps, 'target_fps': self.config.fps, 'resolution': (self.config.width, self.config.height), 'camera_index': self.config.camera_index, 'connection_lost': self._connection_lost, 'last_error': self._last_error, 'is_running': self.is_running() } def _configure_camera(self) -> None: """Configure camera properties.""" if not self.cap: return # Set resolution self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.config.width) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.config.height) # Set FPS self.cap.set(cv2.CAP_PROP_FPS, self.config.fps) # Set other properties if hasattr(cv2, 'CAP_PROP_AUTO_EXPOSURE'): self.cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1 if self.config.auto_exposure else 0) self.cap.set(cv2.CAP_PROP_BRIGHTNESS, self.config.brightness) self.cap.set(cv2.CAP_PROP_CONTRAST, self.config.contrast) self.cap.set(cv2.CAP_PROP_SATURATION, self.config.saturation) # Verify actual settings actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) actual_fps = self.cap.get(cv2.CAP_PROP_FPS) print(f"[CameraSource] Actual resolution: {actual_width}x{actual_height}, FPS: {actual_fps}") def _capture_loop(self) -> None: """Main capture loop running in separate thread.""" print("[CameraSource] Capture loop started") frame_interval = 1.0 / self.config.fps last_capture_time = time.time() while not self._stop_event.is_set(): try: # Control frame rate current_time = time.time() time_since_last = current_time - last_capture_time if time_since_last < frame_interval: sleep_time = frame_interval - time_since_last time.sleep(sleep_time) continue last_capture_time = current_time # Capture frame if not self.cap or not self.cap.isOpened(): self._connection_lost = True break ret, frame = self.cap.read() if not ret or frame is None: print("[CameraSource] Failed to read frame from camera") self._connection_lost = True break # Update frame with self._frame_lock: self._current_frame = frame # Update statistics self._frame_count += 1 self._fps_counter += 1 # Calculate actual FPS if current_time - self._last_fps_time >= 1.0: self._actual_fps = self._fps_counter / (current_time - self._last_fps_time) self._fps_counter = 0 self._last_fps_time = current_time # Call frame callback if provided if self.frame_callback: try: self.frame_callback(frame) except Exception as e: print(f"[CameraSource] Frame callback error: {e}") except Exception as e: print(f"[CameraSource] Capture loop error: {e}") self._last_error = str(e) time.sleep(0.1) # Brief pause before retrying print("[CameraSource] Capture loop ended") def __enter__(self): """Context manager entry.""" if not self.start(): raise RuntimeError(f"Failed to start camera: {self._last_error}") return self def __exit__(self, exc_type, exc_val, exc_tb): """Context manager exit.""" self.stop() class CameraPipelineFeeder: """ Helper class to feed camera frames to InferencePipeline. This class bridges the CameraSource and InferencePipeline, handling frame format conversion and pipeline data feeding. """ def __init__(self, camera_source: CameraSource, pipeline, feed_rate: float = 30.0): """ Initialize camera pipeline feeder. Args: camera_source: CameraSource instance pipeline: InferencePipeline instance feed_rate: Rate at which to feed frames to pipeline (FPS) """ self.camera_source = camera_source self.pipeline = pipeline self.feed_rate = feed_rate # Threading control self._feed_thread: Optional[threading.Thread] = None self._stop_event = threading.Event() self._is_feeding = False # Statistics self._frames_fed = 0 self._last_feed_time = 0.0 def start_feeding(self) -> bool: """ Start feeding camera frames to pipeline. Returns: bool: True if feeding started successfully """ if self._is_feeding: return True if not self.camera_source.is_running(): print("[CameraPipelineFeeder] Camera is not running") return False print("[CameraPipelineFeeder] Starting frame feeding...") self._stop_event.clear() self._is_feeding = True self._feed_thread = threading.Thread(target=self._feed_loop, daemon=True) self._feed_thread.start() return True def stop_feeding(self) -> None: """Stop feeding frames to pipeline.""" if not self._is_feeding: return print("[CameraPipelineFeeder] Stopping frame feeding...") self._stop_event.set() self._is_feeding = False if self._feed_thread and self._feed_thread.is_alive(): self._feed_thread.join(timeout=2.0) print("[CameraPipelineFeeder] Frame feeding stopped") def _feed_loop(self) -> None: """Main feeding loop.""" feed_interval = 1.0 / self.feed_rate last_feed_time = time.time() while not self._stop_event.is_set(): try: current_time = time.time() # Control feed rate if current_time - last_feed_time < feed_interval: time.sleep(0.001) # Small sleep to prevent busy waiting continue # Get frame from camera frame = self.camera_source.get_frame() if frame is None: time.sleep(0.01) continue # Feed frame to pipeline try: from InferencePipeline import PipelineData pipeline_data = PipelineData( data=frame, metadata={ 'source': 'camera', 'camera_index': self.camera_source.config.camera_index, 'timestamp': current_time, 'frame_id': self._frames_fed } ) # Put data into pipeline self.pipeline.put_data(pipeline_data) self._frames_fed += 1 last_feed_time = current_time except Exception as e: print(f"[CameraPipelineFeeder] Error feeding frame to pipeline: {e}") time.sleep(0.1) except Exception as e: print(f"[CameraPipelineFeeder] Feed loop error: {e}") time.sleep(0.1) def get_stats(self) -> Dict[str, Any]: """Get feeding statistics.""" return { 'frames_fed': self._frames_fed, 'feed_rate': self.feed_rate, 'is_feeding': self._is_feeding, 'camera_stats': self.camera_source.get_stats() } def list_available_cameras() -> Dict[int, Dict[str, Any]]: """ List all available camera devices. Returns: Dict[int, Dict[str, Any]]: Dictionary of camera index to camera info """ cameras = {} for i in range(10): # Check first 10 camera indices cap = cv2.VideoCapture(i) if cap.isOpened(): # Get camera properties 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) cameras[i] = { 'index': i, 'width': width, 'height': height, 'fps': fps, 'backend': cap.getBackendName() if hasattr(cap, 'getBackendName') else 'Unknown' } cap.release() return cameras # Example usage and testing if __name__ == "__main__": # List available cameras print("Available cameras:") cameras = list_available_cameras() for idx, info in cameras.items(): print(f" Camera {idx}: {info['width']}x{info['height']} @ {info['fps']} FPS ({info['backend']})") if not cameras: print("No cameras found!") exit(1) # Test camera capture config = CameraConfig( camera_index=0, width=640, height=480, fps=30 ) def frame_callback(frame): print(f"Frame captured: {frame.shape}") camera = CameraSource(config, frame_callback) try: if camera.start(): print("Camera started successfully") # Capture for 5 seconds time.sleep(5) # Print statistics stats = camera.get_stats() print(f"Statistics: {stats}") else: print("Failed to start camera") finally: camera.stop()