179 lines
6.4 KiB
Python
179 lines
6.4 KiB
Python
import cv2
|
|
import numpy as np
|
|
import time
|
|
from PySide6.QtCore import QObject, Signal, Slot, QTimer
|
|
from logging import getLogger
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
class VideoStreamWorker(QObject):
|
|
"""
|
|
Worker class responsible for reading frames from OpenCV VideoCapture.
|
|
Runs in a separate QThread.
|
|
"""
|
|
frame_ready = Signal(np.ndarray)
|
|
frame_time = Signal(float, float, int) # (start_time, end_time)
|
|
error_occurred = Signal(str)
|
|
started = Signal(float, tuple) # (fps, (width, height))
|
|
stopped = Signal()
|
|
source_changed = Signal(object) # Nowy sygnał dla zmiany źródła
|
|
_run = Signal() # Sygnał do uruchomienia pętli wątku
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._cap = None
|
|
self._source = 0 # Default to first camera
|
|
self._is_running = False
|
|
self._timer = QTimer()
|
|
self._timer.timeout.connect(self._process_frame)
|
|
self.source_changed.connect(self._on_source_changed) # Łączymy sygnał ze slotem
|
|
self._run.connect(self.run) # Łączymy sygnał ze slotem
|
|
|
|
@Slot(object)
|
|
def set_source(self, source):
|
|
"""
|
|
Sets the video source asynchronously.
|
|
source: int (camera index) or str (file path).
|
|
"""
|
|
self.source_changed.emit(source) # Emitujemy sygnał zamiast synchronicznej zmiany
|
|
|
|
@Slot(object)
|
|
def _on_source_changed(self, source):
|
|
"""
|
|
Slot obsługujący zmianę źródła w wątku roboczym.
|
|
"""
|
|
logger.debug(f"Changing video source to: {source}")
|
|
was_running = self._is_running
|
|
if was_running:
|
|
self.stop() # Zatrzymujemy synchronicznie w tym samym wątku
|
|
self._source = source
|
|
if was_running:
|
|
self.start() # Uruchamiamy ponownie w tym samym wątku
|
|
|
|
@Slot()
|
|
def start(self):
|
|
if self._is_running:
|
|
logger.warning("VideoStreamWorker is already running.")
|
|
return
|
|
|
|
if self._source is None:
|
|
logger.error("No video source specified.")
|
|
self.error_occurred.emit("No source specified.")
|
|
return
|
|
|
|
self._cap = cv2.VideoCapture(self._source)
|
|
if not self._cap.isOpened():
|
|
logger.error(f"Could not open source: {self._source}")
|
|
self.error_occurred.emit(f"Could not open source: {self._source}")
|
|
return
|
|
|
|
self._is_running = True
|
|
# Using a timer for consistent frame rate and to play nice with the event loop
|
|
# For files, we might want to calculate the interval from FPS.
|
|
fps = self._cap.get(cv2.CAP_PROP_FPS)
|
|
interval = int(600 / fps) if fps > 0 else 20
|
|
|
|
video_res = (int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
|
|
logger.debug(f"Video opened: {self._source} (fps: {fps}, width: {video_res[0]}, height: {video_res[1]})")
|
|
|
|
# self._timer.start(interval)
|
|
self._run.emit() # Uruchamiamy pętlę wątku za pomocą sygnału
|
|
self.started.emit(fps, video_res)
|
|
|
|
@Slot()
|
|
def stop(self):
|
|
logger.debug("Stopping VideoStreamWorker.")
|
|
self._is_running = False
|
|
self._timer.stop()
|
|
if self._cap:
|
|
self._cap.release()
|
|
self._cap = None
|
|
self.stopped.emit()
|
|
|
|
|
|
# def run(self):
|
|
# while self._is_running:
|
|
# self._process_frame()
|
|
# time.sleep(0.028) # Krótka przerwa, aby nie zablokować CPU
|
|
|
|
|
|
def run(self):
|
|
"""Główna pętla wątku roboczego."""
|
|
if self._source is None:
|
|
logger.warning("No video source provided")
|
|
return
|
|
|
|
self.cap = cv2.VideoCapture(self._source)
|
|
if not self.cap.isOpened():
|
|
logger.error(f"Failed to open video source: {self._source}")
|
|
return
|
|
|
|
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
|
|
if self.fps <= 0:
|
|
self.fps = 30.0
|
|
|
|
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
|
|
logger.debug(f"Video opened: {self._source} (fps: {self.fps}, size: {self.width}x{self.height})")
|
|
|
|
frame_interval = 1.0 / (self.fps + 1)
|
|
self.running = True
|
|
frame_emit_time = time.perf_counter()
|
|
read_start_time = 0.0
|
|
read_end_time = 0.0
|
|
frames_dropped = 0
|
|
while self.running:
|
|
next_frame_time = frame_emit_time + frame_interval
|
|
|
|
ret, frame = self.cap.read()
|
|
read_end_time = time.perf_counter()
|
|
self.frame_time.emit(read_start_time, read_end_time, frames_dropped)
|
|
if not ret:
|
|
logger.debug("End of video stream or read error")
|
|
break
|
|
|
|
current_time = time.perf_counter()
|
|
# self.metrics.last_cap_time = current_time - frame_emit_time
|
|
|
|
if current_time < next_frame_time:
|
|
sleep_time = next_frame_time - current_time
|
|
time.sleep(sleep_time)
|
|
else:
|
|
frames_dropped += 1
|
|
logger.debug(f"Frame drops counted: {frames_dropped}")
|
|
|
|
frame_emit_time = time.perf_counter()
|
|
read_start_time = time.perf_counter()
|
|
self.frame_ready.emit(frame)
|
|
|
|
def _process_frame(self):
|
|
if not self._is_running:
|
|
logger.debug("VideoStreamWorker is not running. Skipping frame processing.")
|
|
return
|
|
|
|
if self._cap is None or not self._cap.isOpened():
|
|
logger.error("VideoCapture is not initialized or opened.")
|
|
self.error_occurred.emit("VideoCapture not initialized.")
|
|
self.stop()
|
|
return
|
|
try:
|
|
read_start_time = time.perf_counter()
|
|
ret, frame = self._cap.read()
|
|
if ret:
|
|
self.frame_ready.emit(frame)
|
|
else:
|
|
# End of video file or lost connection
|
|
if isinstance(self._source, str):
|
|
self.stop()
|
|
else:
|
|
logger.error("Lost connection to camera.")
|
|
self.error_occurred.emit("Lost connection to camera.")
|
|
self.stop()
|
|
read_end_time = time.perf_counter()
|
|
self.frame_time.emit(read_start_time, read_end_time)
|
|
except Exception as e:
|
|
logger.error(f"Error reading frame: {str(e)}")
|
|
self.error_occurred.emit(f"Error reading frame: {str(e)}")
|
|
|