KNEO-Academy/src/models/video_thread.py
HuangMason320 17deba3bdb Refactor: Clean up codebase and improve documentation
- Remove unused files (model_controller.py, model_service.py, device_connection_popup.py)
- Clean up commented code in device_service.py, device_popup.py, config.py
- Update docstrings and comments across all modules
- Improve code organization and readability
2025-12-30 16:47:31 +08:00

157 lines
5.4 KiB
Python

"""
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())