""" video_thread.py - Video Capture Thread This module provides a QThread-based video capture worker for webcam streaming. It handles camera connection, frame capture, and automatic reconnection. """ import cv2 import time import threading from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage class VideoThread(QThread): """ Thread for capturing video frames from a webcam. Emits frames as QImage objects via Qt signals for display in the UI. Handles camera connection with timeout and automatic reconnection. Signals: change_pixmap_signal: Emitted with each new frame (QImage) camera_error_signal: Emitted when camera errors occur (str) """ change_pixmap_signal = pyqtSignal(QImage) camera_error_signal = pyqtSignal(str) # Camera error signal def __init__(self): """Initialize the VideoThread with default settings.""" super().__init__() self._run_flag = True self._camera_open_attempts = 0 self._max_attempts = 3 self._camera_timeout = 5 # Camera open timeout in seconds def _open_camera_with_timeout(self, camera_index, backend=None): """ Open camera with timeout mechanism. Args: camera_index (int): Index of the camera to open. backend: OpenCV video capture backend (e.g., cv2.CAP_DSHOW). Returns: cv2.VideoCapture: Opened camera object, or None if failed. """ cap = None open_success = False def try_open(): nonlocal cap, open_success try: if backend is not None: cap = cv2.VideoCapture(camera_index, backend) else: cap = cv2.VideoCapture(camera_index) open_success = cap is not None and cap.isOpened() except Exception as e: print(f"Exception while opening camera: {e}") open_success = False # Try to open camera in a separate thread thread = threading.Thread(target=try_open) thread.daemon = True thread.start() thread.join(timeout=self._camera_timeout) if thread.is_alive(): print(f"Camera open timeout ({self._camera_timeout} seconds)") return None if open_success: return cap else: if cap is not None: cap.release() return None def run(self): """ Main thread execution loop. Attempts to open the camera with multiple backends and retries. Captures frames and emits them as QImage signals. """ # Try multiple times to open camera while self._camera_open_attempts < self._max_attempts and self._run_flag: self._camera_open_attempts += 1 print(f"Attempting to open camera (attempt {self._camera_open_attempts}/{self._max_attempts})...") # Try DirectShow backend first (usually faster on Windows) cap = self._open_camera_with_timeout(0, cv2.CAP_DSHOW) if cap is None: print("Unable to open camera with DirectShow, trying default backend") cap = self._open_camera_with_timeout(0) if cap is None: print("Unable to open camera with any backend, retrying in 1 second...") time.sleep(1) continue # Set camera properties for better performance # Lower resolution for faster startup and higher frame rate cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) # Set buffer size to 1 to reduce latency cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Warm up camera by discarding first few frames for _ in range(5): cap.read() # Camera opened successfully, reset attempt count self._camera_open_attempts = 0 # Main capture loop while self._run_flag: ret, frame = cap.read() if ret: # Convert to RGB format frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) height, width, channel = frame.shape bytes_per_line = channel * width qt_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888) self.change_pixmap_signal.emit(qt_image) else: print("Unable to read camera frame, camera may be disconnected") break # Release camera resources cap.release() # If stopped by stop signal, don't retry if not self._run_flag: break print("Camera connection lost, attempting to reconnect...") if self._camera_open_attempts >= self._max_attempts: print("Maximum attempts reached, unable to open camera") def stop(self): """Stop the video capture thread.""" try: print("Stopping camera thread...") self._run_flag = False # Wait for thread to finish if self.isRunning(): self.wait() print("Camera thread stopped") except Exception as e: print(f"Error stopping camera thread: {e}") import traceback print(traceback.format_exc())