from PySide6.QtCore import QObject, Signal, QThread, Qt, QMutex, QWaitCondition, QTimer from PySide6.QtGui import QImage, QPixmap import cv2 import time import numpy as np from collections import deque from typing import Dict, List, Optional import os import sys # Add parent directory to path for imports sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # Import utilities from utils.annotation_utils import ( draw_detections, draw_violations, draw_performance_metrics, resize_frame_for_display, convert_cv_to_qimage, convert_cv_to_pixmap ) class VideoController(QObject): frame_ready = Signal(object, object, object, dict) # QPixmap, detections, violations, metrics raw_frame_ready = Signal(np.ndarray, list, list, float) # frame, detections, violations, fps def __init__(self, model_manager=None): """ Initialize video controller. Args: model_manager: Model manager instance for detection and violation """ super().__init__() self.model_manager = model_manager self.source = 0 # Default camera source self._running = False self.frame_count = 0 self.start_time = 0 self.source_fps = 0 self.actual_fps = 0 self.processing_times = deque(maxlen=30) self.cap = None # VideoCapture object # Configure thread self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self._run) # Performance measurement self.mutex = QMutex() self.condition = QWaitCondition() self.performance_metrics = { 'FPS': 0.0, 'Detection (ms)': 0.0, 'Violation (ms)': 0.0, 'Total (ms)': 0.0 } # Setup render timer self.render_timer = QTimer() self.render_timer.timeout.connect(self._process_frame) # Frame buffer self.current_frame = None self.current_detections = [] self.current_violations = [] # Debug counter self.debug_counter = 0 def set_source(self, source): """Set video source (file path, camera index, or URL)""" print(f"DEBUG: VideoController.set_source called with: {source} (type: {type(source)})") was_running = self._running if self._running: self.stop() # Critical fix: Make sure source is properly set if source is None: print("WARNING: Received None source, defaulting to camera 0") self.source = 0 elif isinstance(source, str) and source.strip(): # Handle file paths - verify the file exists if os.path.exists(source): self.source = source print(f"DEBUG: VideoController source set to file: {self.source}") else: # Try to interpret as camera index or URL try: # If it's a digit string, convert to integer camera index if source.isdigit(): self.source = int(source) print(f"DEBUG: VideoController source set to camera index: {self.source}") else: # Treat as URL or special device string self.source = source print(f"DEBUG: VideoController source set to URL/device: {self.source}") except ValueError: print(f"WARNING: Could not interpret source: {source}, defaulting to camera 0") self.source = 0 elif isinstance(source, int): # Camera index self.source = source print(f"DEBUG: VideoController source set to camera index: {self.source}") else: print(f"WARNING: Unrecognized source type: {type(source)}, defaulting to camera 0") self.source = 0 # Get properties of the source (fps, dimensions, etc) self._get_source_properties() if was_running: self.start() def _get_source_properties(self): """Get properties of video source""" try: cap = cv2.VideoCapture(self.source) if cap.isOpened(): self.source_fps = cap.get(cv2.CAP_PROP_FPS) if self.source_fps <= 0: self.source_fps = 30.0 # Default if undetectable self.frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) self.frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) self.frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) cap.release() print(f"Video source: {self.frame_width}x{self.frame_height}, {self.source_fps} FPS") else: print("Failed to open video source") except Exception as e: print(f"Error getting source properties: {e}") def start(self): """Start video processing""" if not self._running: self._running = True self.start_time = time.time() self.frame_count = 0 self.debug_counter = 0 print("DEBUG: Starting video processing thread") # Start the processing thread if not self.thread.isRunning(): self.thread.start() # Start the render timer with a faster interval (16ms = ~60fps) self.render_timer.start(16) print("DEBUG: Render timer started") def stop(self): """Stop video processing""" if self._running: print("DEBUG: Stopping video processing") self._running = False self.render_timer.stop() # Properly terminate the thread self.thread.quit() if not self.thread.wait(3000): # Wait 3 seconds max self.thread.terminate() print("WARNING: Thread termination forced") # Close the capture if it exists if self.cap and self.cap.isOpened(): self.cap.release() self.cap = None # Clear the current frame self.mutex.lock() self.current_frame = None self.mutex.unlock() print("DEBUG: Video processing stopped") def capture_snapshot(self) -> np.ndarray: """Capture current frame""" if self.current_frame is not None: return self.current_frame.copy() return None def _run(self): """Main processing loop (runs in thread)""" try: # Print the source we're trying to open print(f"DEBUG: Opening video source: {self.source} (type: {type(self.source)})") # Initialize the capture self.cap = None # Handle different source types if isinstance(self.source, str) and os.path.exists(self.source): # It's a valid file path print(f"DEBUG: Opening video file: {self.source}") self.cap = cv2.VideoCapture(self.source) # Verify file opened successfully if not self.cap.isOpened(): print(f"ERROR: Could not open video file: {self.source}") return elif isinstance(self.source, int) or (isinstance(self.source, str) and self.source.isdigit()): # It's a camera index camera_idx = int(self.source) if isinstance(self.source, str) else self.source print(f"DEBUG: Opening camera: {camera_idx}") self.cap = cv2.VideoCapture(camera_idx) # Try a few times to open camera (sometimes takes a moment) retry_count = 0 while not self.cap.isOpened() and retry_count < 3: print(f"Camera not ready, retrying ({retry_count+1}/3)...") time.sleep(1) self.cap.release() self.cap = cv2.VideoCapture(camera_idx) retry_count += 1 if not self.cap.isOpened(): print(f"ERROR: Could not open camera {camera_idx} after {retry_count} attempts") return else: # Try as a string source (URL or device path) print(f"DEBUG: Opening source as string: {self.source}") self.cap = cv2.VideoCapture(str(self.source)) if not self.cap.isOpened(): print(f"ERROR: Could not open source: {self.source}") return # Check again to ensure capture is valid if not self.cap or not self.cap.isOpened(): print(f"ERROR: Could not open video source {self.source}") return # Configure frame timing based on source FPS frame_time = 1.0 / self.source_fps if self.source_fps > 0 else 0.033 prev_time = time.time() # Log successful opening print(f"SUCCESS: Video source opened: {self.source}") print(f"Source info - FPS: {self.source_fps}, Size: {self.frame_width}x{self.frame_height}") # Main processing loop while self._running and self.cap.isOpened(): ret, frame = self.cap.read() if not ret: print("End of video or read error") break # Detection and violation processing process_start = time.time() # Process detections detection_start = time.time() detections = [] if self.model_manager: detections = self.model_manager.detect(frame) detection_time = (time.time() - detection_start) * 1000 # Violation detection is disabled violation_start = time.time() violations = [] # if self.model_manager and detections: # violations = self.model_manager.detect_violations( # detections, frame, time.time() # ) violation_time = (time.time() - violation_start) * 1000 # Update tracking if available if self.model_manager: detections = self.model_manager.update_tracking(detections, frame) # Calculate timing metrics process_time = (time.time() - process_start) * 1000 self.processing_times.append(process_time) # Update FPS now = time.time() self.frame_count += 1 elapsed = now - self.start_time if elapsed > 0: self.actual_fps = self.frame_count / elapsed fps_smoothed = 1.0 / (now - prev_time) if now > prev_time else 0 prev_time = now # Update metrics self.performance_metrics = { 'FPS': f"{fps_smoothed:.1f}", 'Detection (ms)': f"{detection_time:.1f}", 'Violation (ms)': f"{violation_time:.1f}", 'Total (ms)': f"{process_time:.1f}" } # Store current frame data (thread-safe) self.mutex.lock() self.current_frame = frame.copy() self.current_detections = detections self.current_violations = violations self.mutex.unlock() # Signal for raw data subscribers self.raw_frame_ready.emit(frame.copy(), detections, violations, fps_smoothed) # Control processing rate for file sources if isinstance(self.source, str) and self.source_fps > 0: frame_duration = time.time() - process_start if frame_duration < frame_time: time.sleep(frame_time - frame_duration) if self.cap: self.cap.release() self.cap = None except Exception as e: print(f"Video processing error: {e}") import traceback traceback.print_exc() finally: self._running = False if self.cap and self.cap.isOpened(): self.cap.release() self.cap = None def _process_frame(self): """Process current frame for UI rendering (called by timer)""" if not self._running: return # Debug counter if hasattr(self, 'debug_counter'): self.debug_counter += 1 if self.debug_counter % 30 == 0: # Print every ~30 frames print(f"DEBUG: Frame processing iteration: {self.debug_counter}") # Get frame data safely self.mutex.lock() frame = self.current_frame.copy() if self.current_frame is not None else None detections = self.current_detections.copy() if hasattr(self, 'current_detections') and self.current_detections else [] violations = self.current_violations.copy() if hasattr(self, 'current_violations') and self.current_violations else [] metrics = self.performance_metrics.copy() self.mutex.unlock() if frame is None: print("DEBUG: _process_frame skipped - no frame available") return try: # Annotate frame annotated_frame = frame.copy() if detections: annotated_frame = draw_detections(annotated_frame, detections, True, True) # Draw metrics annotated_frame = draw_performance_metrics(annotated_frame, metrics) # Resize for display display_frame = resize_frame_for_display(annotated_frame) # Convert to QPixmap directly using a better approach rgb_image = cv2.cvtColor(display_frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w # Create QImage - critical: use .copy() to ensure data stays valid q_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888).copy() # Convert to pixmap pixmap = QPixmap.fromImage(q_image) # Emit signal with processed frame if not pixmap.isNull(): print(f"DEBUG: Emitting pixmap: {pixmap.width()}x{pixmap.height()}") self.frame_ready.emit(pixmap, detections, violations, metrics) else: print("ERROR: Created QPixmap is null") except Exception as e: print(f"ERROR in _process_frame: {e}") import traceback traceback.print_exc()