Compare commits
2 Commits
eaa7c75868
...
651c150e23
| Author | SHA1 | Date | |
|---|---|---|---|
| 651c150e23 | |||
| c38e71dec4 |
148
app/camera.py
148
app/camera.py
@@ -1,148 +0,0 @@
|
|||||||
from PySide6.QtCore import QThread, QTimer, QMutex, QWaitCondition, Signal, QObject, QElapsedTimer
|
|
||||||
import cv2
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class VideoMetrics:
|
|
||||||
frames_processed: int
|
|
||||||
frames_dropped: int
|
|
||||||
last_frame_time: float
|
|
||||||
last_cap_time: float
|
|
||||||
fps_average: float
|
|
||||||
fps_last_time: float
|
|
||||||
fps_frame_count: float
|
|
||||||
|
|
||||||
def update_fps(self) -> None:
|
|
||||||
fps_now = time.perf_counter()
|
|
||||||
elypsed = fps_now - self.fps_last_time
|
|
||||||
if elypsed < 1.0:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.fps_frame_count = self.frames_processed - self.fps_frame_count
|
|
||||||
self.fps_average = self.fps_frame_count / elypsed
|
|
||||||
self.fps_frame_count = self.frames_processed
|
|
||||||
self.fps_last_time = fps_now
|
|
||||||
|
|
||||||
|
|
||||||
class VideoStreamWorker(QObject):
|
|
||||||
# Emitowany z wątku roboczego - będzie przenoszony do głównego wątku przez VideoStream
|
|
||||||
_internal_frame = Signal(object)
|
|
||||||
|
|
||||||
def __init__(self, source):
|
|
||||||
super().__init__()
|
|
||||||
self.source = source
|
|
||||||
self.cap = None
|
|
||||||
self.fps = 30.0
|
|
||||||
self.width = 0
|
|
||||||
self.height = 0
|
|
||||||
self.running = False
|
|
||||||
self.metrics = VideoMetrics(
|
|
||||||
frames_processed=0,
|
|
||||||
frames_dropped=0,
|
|
||||||
last_frame_time=0.0,
|
|
||||||
last_cap_time=0.0,
|
|
||||||
fps_average=0.0,
|
|
||||||
fps_last_time=time.perf_counter(),
|
|
||||||
fps_frame_count=0.0
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(f"VideoStreamWorker initialized with source: {source}")
|
|
||||||
|
|
||||||
def set_source(self, source):
|
|
||||||
logger.debug(f"Setting new video source: {source}")
|
|
||||||
self.source = source
|
|
||||||
if self.running:
|
|
||||||
self.stop()
|
|
||||||
self.run()
|
|
||||||
|
|
||||||
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
|
|
||||||
self.running = True
|
|
||||||
frame_emit_time = time.perf_counter()
|
|
||||||
|
|
||||||
while self.running:
|
|
||||||
next_frame_time = frame_emit_time + frame_interval
|
|
||||||
|
|
||||||
ret, frame = self.cap.read()
|
|
||||||
|
|
||||||
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:
|
|
||||||
self.metrics.frames_dropped += 1
|
|
||||||
logger.debug(f"Frame drops counted: {self.metrics.frames_dropped}")
|
|
||||||
|
|
||||||
frame_emit_time = time.perf_counter()
|
|
||||||
self._internal_frame.emit(frame)
|
|
||||||
|
|
||||||
self.metrics.frames_processed += 1
|
|
||||||
self.metrics.update_fps()
|
|
||||||
|
|
||||||
self.cap.release()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.running = False
|
|
||||||
self.mutex.lock()
|
|
||||||
self.condition.wakeAll()
|
|
||||||
self.mutex.unlock()
|
|
||||||
|
|
||||||
|
|
||||||
class VideoStream(QObject):
|
|
||||||
"""Klasa fasadowa do użycia w głównym wątku GUI."""
|
|
||||||
frame_ready = Signal(object) # To będzie emitowane z głównego wątku
|
|
||||||
|
|
||||||
def __init__(self, source):
|
|
||||||
super().__init__()
|
|
||||||
self.worker = VideoStreamWorker(source)
|
|
||||||
self.worker_thread = QThread()
|
|
||||||
|
|
||||||
# Przenosimy workera do osobnego wątku
|
|
||||||
self.worker.moveToThread(self.worker_thread)
|
|
||||||
|
|
||||||
# Łączymy sygnały
|
|
||||||
self.worker._internal_frame.connect(self._on_frame)
|
|
||||||
self.worker_thread.started.connect(self.worker.run)
|
|
||||||
self.worker_thread.finished.connect(self.worker.deleteLater)
|
|
||||||
self.worker_thread.finished.connect(self.worker_thread.deleteLater)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self.worker_thread.start()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.worker.stop()
|
|
||||||
self.worker_thread.quit()
|
|
||||||
self.worker_thread.wait()
|
|
||||||
|
|
||||||
def _on_frame(self, frame):
|
|
||||||
"""Slot wywoływany w głównym wątku po otrzymaniu klatki."""
|
|
||||||
# Tutaj możesz dodać korekcję ekspozycji jeśli nie zrobiłeś tego w workerze
|
|
||||||
self.frame_ready.emit(frame)
|
|
||||||
@@ -2,10 +2,11 @@ from enum import Enum
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QMainWindow, QStyle, QToolButton, QWidget, QVBoxLayout, QPushButton
|
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QMainWindow, QStyle, QToolButton, QWidget, QVBoxLayout, QPushButton, QLabel
|
||||||
from PySide6.QtGui import QIcon
|
from PySide6.QtGui import QIcon, QPixmap
|
||||||
from PySide6.QtCore import Qt, QSize
|
from PySide6.QtCore import Qt, QSize
|
||||||
|
|
||||||
|
from .video_stream.controller import VideoStreamController, FrameMetrics
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class VideoMode(Enum):
|
class VideoMode(Enum):
|
||||||
@@ -70,13 +71,22 @@ class MainWindow(QMainWindow):
|
|||||||
self.video_mode = VideoMode.STREAMING
|
self.video_mode = VideoMode.STREAMING
|
||||||
logger.debug(f"Initial video mode: {self.video_mode}")
|
logger.debug(f"Initial video mode: {self.video_mode}")
|
||||||
|
|
||||||
|
|
||||||
|
self.video_controller = VideoStreamController()
|
||||||
|
self.video_controller.change_source(0) # Start with default camera
|
||||||
|
self.video_controller.image_ready.connect(self.update_frame)
|
||||||
|
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
|
self.video_controller.start()
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
self.central_widget = QWidget()
|
self.central_widget = QWidget()
|
||||||
self.setCentralWidget(self.central_widget)
|
self.setCentralWidget(self.central_widget)
|
||||||
self.central_widget.setStyleSheet("background-color: #001e1e;")
|
self.central_widget.setStyleSheet("background-color: #001e1e;")
|
||||||
|
|
||||||
|
self.video_label = QLabel(self.central_widget)
|
||||||
|
|
||||||
self.toolbar_widget = QWidget(self.central_widget)
|
self.toolbar_widget = QWidget(self.central_widget)
|
||||||
self.toolbar_widget.setMinimumWidth(400)
|
self.toolbar_widget.setMinimumWidth(400)
|
||||||
self.toolbar_widget.setObjectName("bottomToolbar")
|
self.toolbar_widget.setObjectName("bottomToolbar")
|
||||||
@@ -185,4 +195,12 @@ class MainWindow(QMainWindow):
|
|||||||
def pause_video(self):
|
def pause_video(self):
|
||||||
logger.debug("Pausing video")
|
logger.debug("Pausing video")
|
||||||
self.video_mode = VideoMode.STOPPED
|
self.video_mode = VideoMode.STOPPED
|
||||||
set_icon(self.action_button, "play")
|
set_icon(self.action_button, "play")
|
||||||
|
|
||||||
|
def closeEvent(self, event: Any) -> None:
|
||||||
|
logger.debug("Closing main window, cleaning up resources.")
|
||||||
|
self.video_controller.cleanup()
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def update_frame(self, frame):
|
||||||
|
self.video_label.setPixmap(QPixmap.fromImage(frame))
|
||||||
3
app/video_stream/__init__.py
Normal file
3
app/video_stream/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .controller import VideoStreamController
|
||||||
|
|
||||||
|
__all__ = ['VideoStreamController']
|
||||||
238
app/video_stream/controller.py
Normal file
238
app/video_stream/controller.py
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
from logging import getLogger
|
||||||
|
from collections import deque
|
||||||
|
from cv2_enumerate_cameras import enumerate_cameras
|
||||||
|
from PySide6.QtCore import QObject, QThread, Signal, Slot
|
||||||
|
from PySide6.QtGui import QImage
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from .worker import VideoStreamWorker
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FrameMetrics:
|
||||||
|
fr_start_delta_min: float = float('inf') # Minimum time between frame read starts
|
||||||
|
fr_start_delta_max: float = 0.0 # Maximum time between frame read starts
|
||||||
|
fr_start_delta_avg: float = 0.0 # Average time between frame read starts (calculated over a buffer of recent frames)
|
||||||
|
fr_time_min: float = float('inf') # Minimum time taken to read a frame
|
||||||
|
fr_time_max: float = 0.0 # Maximum time taken to read a frame
|
||||||
|
fr_time_avg: float = 0.0 # Average time taken to read a frame (calculated over a buffer of recent frames)
|
||||||
|
fps_average_1s: float = 0.0 # Average FPS calculated over the last 1 second (or a buffer of recent frames)
|
||||||
|
frame_count: int = 0 # Total number of frames processed
|
||||||
|
last_start_time: float = 0.0 # Timestamp of the last frame read start (used for calculating deltas)
|
||||||
|
buf_size: int = 30 # Size of the buffer for calculating average metrics
|
||||||
|
b_fr_start_delta: deque = field(init=False, repr=False) # Buffer for recent frame read start deltas (used for calculating average)
|
||||||
|
total_start_delta: float = 0.0 # Total accumulated frame read start delta (used for calculating average)
|
||||||
|
b_fr_time_delta: deque = field(init=False, repr=False) # Buffer for recent frame read time deltas (used for calculating average)
|
||||||
|
total_frame_time: float = 0.0 # Total accumulated frame read time delta (used for calculating average)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.b_fr_start_delta = deque(maxlen=self.buf_size)
|
||||||
|
self.b_fr_time_delta = deque(maxlen=self.buf_size)
|
||||||
|
|
||||||
|
def calc_start_delta_min_max_avg(self, start_time: float) -> None:
|
||||||
|
if self.last_start_time == 0.0:
|
||||||
|
self.last_start_time = start_time
|
||||||
|
return
|
||||||
|
|
||||||
|
start_delta = start_time - self.last_start_time
|
||||||
|
self.fr_start_delta_min = min(self.fr_start_delta_min, start_delta)
|
||||||
|
self.fr_start_delta_max = max(self.fr_start_delta_max, start_delta)
|
||||||
|
|
||||||
|
if len(self.b_fr_start_delta) == self.buf_size:
|
||||||
|
self.total_start_delta -= self.b_fr_start_delta[0]
|
||||||
|
|
||||||
|
self.b_fr_start_delta.append(start_delta)
|
||||||
|
self.total_start_delta += start_delta
|
||||||
|
self.last_start_time = start_time
|
||||||
|
|
||||||
|
self.fr_start_delta_avg = self.total_start_delta / len(self.b_fr_start_delta)
|
||||||
|
|
||||||
|
def calc_frame_time_min_max_avg(self, start_time: float, end_time: float) -> None:
|
||||||
|
fr_time = end_time - start_time
|
||||||
|
self.fr_time_min = min(self.fr_time_min, fr_time)
|
||||||
|
self.fr_time_max = max(self.fr_time_max, fr_time)
|
||||||
|
|
||||||
|
if len(self.b_fr_time_delta) == self.buf_size:
|
||||||
|
self.total_frame_time -= self.b_fr_time_delta[0]
|
||||||
|
|
||||||
|
self.b_fr_time_delta.append(fr_time)
|
||||||
|
self.total_frame_time += fr_time
|
||||||
|
|
||||||
|
self.fr_time_avg = self.total_frame_time / len(self.b_fr_time_delta)
|
||||||
|
|
||||||
|
def calc_fps(self) -> None:
|
||||||
|
if self.frame_count < self.buf_size:
|
||||||
|
return
|
||||||
|
|
||||||
|
# fps = (len(self.b_fr_start_delta)) / self.total_start_delta if self.total_start_delta > 0 else 0.0
|
||||||
|
# self.fps_average_1s = fps
|
||||||
|
|
||||||
|
self.fps_average_1s = 1.0 / self.fr_start_delta_avg if self.fr_start_delta_avg > 0 else 0.0
|
||||||
|
|
||||||
|
def update_metrics(self, start_time: float, end_time: float) -> None:
|
||||||
|
self.frame_count += 1
|
||||||
|
self.calc_start_delta_min_max_avg(start_time)
|
||||||
|
self.calc_frame_time_min_max_avg(start_time, end_time)
|
||||||
|
self.calc_fps()
|
||||||
|
|
||||||
|
def reset_metrics(self) -> None:
|
||||||
|
self.fr_start_delta_min = float('inf')
|
||||||
|
self.fr_start_delta_max = 0.0
|
||||||
|
self.fr_start_delta_avg = 0.0
|
||||||
|
self.fr_time_min = float('inf')
|
||||||
|
self.fr_time_max = 0.0
|
||||||
|
self.fr_time_avg = 0.0
|
||||||
|
self.fps_average_1s = 0.0
|
||||||
|
self.frame_count = 0
|
||||||
|
self.last_start_time = 0.0
|
||||||
|
self.b_fr_start_delta.clear()
|
||||||
|
self.total_start_delta = 0.0
|
||||||
|
self.b_fr_time_delta.clear()
|
||||||
|
self.total_frame_time = 0.0
|
||||||
|
|
||||||
|
def get_metrics(self) -> "FrameMetrics":
|
||||||
|
return FrameMetrics(
|
||||||
|
fr_start_delta_min= round(self.fr_start_delta_min * 1000, 3), # Convert to milliseconds
|
||||||
|
fr_start_delta_max= round(self.fr_start_delta_max * 1000, 3),
|
||||||
|
fr_start_delta_avg= round(self.fr_start_delta_avg * 1000, 3),
|
||||||
|
fr_time_min= round(self.fr_time_min * 1000, 3),
|
||||||
|
fr_time_max= round(self.fr_time_max * 1000, 3),
|
||||||
|
fr_time_avg= round(self.fr_time_avg * 1000, 3),
|
||||||
|
fps_average_1s= round(self.fps_average_1s, 3),
|
||||||
|
frame_count=self.frame_count
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoStreamController(QObject):
|
||||||
|
"""
|
||||||
|
Facade class managing the VideoStreamWorker and its QThread.
|
||||||
|
Converts raw frames to QImage for the UI and can route them to other processors.
|
||||||
|
"""
|
||||||
|
image_ready = Signal(QImage) # Signal for the UI
|
||||||
|
raw_frame_ready = Signal(np.ndarray) # Signal for other processing modules
|
||||||
|
error_occurred = Signal(str)
|
||||||
|
status_changed = Signal(bool)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._thread = QThread()
|
||||||
|
self._worker = VideoStreamWorker()
|
||||||
|
self._worker.moveToThread(self._thread)
|
||||||
|
self._metrics: FrameMetrics | None = None
|
||||||
|
|
||||||
|
# Connect internal signals
|
||||||
|
self._worker.frame_ready.connect(self._handle_frame)
|
||||||
|
self._worker.frame_time.connect(self._handle_frame_time)
|
||||||
|
self._worker.error_occurred.connect(self.error_occurred)
|
||||||
|
self._worker.started.connect(self.started)
|
||||||
|
self._worker.stopped.connect(self.stopped)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
self._thread.finished.connect(self._worker.stop)
|
||||||
|
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_available_cameras():
|
||||||
|
"""
|
||||||
|
Returns a list of available camera devices using cv2_enumerate_cameras.
|
||||||
|
Each item is an object with properties like 'index' and 'name'.
|
||||||
|
"""
|
||||||
|
return list(enumerate_cameras())
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def get_metrics(self) -> FrameMetrics | None:
|
||||||
|
"""
|
||||||
|
Returns the current frame metrics.
|
||||||
|
"""
|
||||||
|
return self._metrics.get_metrics() if self._metrics else None
|
||||||
|
|
||||||
|
@Slot(object)
|
||||||
|
def change_source(self, source):
|
||||||
|
"""
|
||||||
|
Change the video source (camera index or file path).
|
||||||
|
"""
|
||||||
|
self._worker.set_source(source)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def start(self):
|
||||||
|
logger.debug("Starting video stream worker thread.")
|
||||||
|
self._worker.start()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def stop(self):
|
||||||
|
logger.debug("Stopping video stream worker thread.")
|
||||||
|
self._worker.stop()
|
||||||
|
|
||||||
|
@Slot(float, tuple)
|
||||||
|
def started(self, fps: float, video_res: tuple):
|
||||||
|
logger.debug(f"Video stream worker started with FPS: {fps}, Resolution: {video_res[0]}x{video_res[1]}")
|
||||||
|
self._metrics = FrameMetrics()
|
||||||
|
self.status_changed.emit(True)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def stopped(self):
|
||||||
|
logger.debug("Video stream worker thread stopped.")
|
||||||
|
if self._metrics:
|
||||||
|
self._metrics.reset_metrics()
|
||||||
|
self.status_changed.emit(False)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""
|
||||||
|
Safely shuts down the worker and the thread.
|
||||||
|
Must be called before the application exits.
|
||||||
|
"""
|
||||||
|
logger.debug("Cleaning up video stream controller.")
|
||||||
|
self._worker.stop()
|
||||||
|
self._thread.quit()
|
||||||
|
if not self._thread.wait(2000): # Wait up to 2 seconds
|
||||||
|
self._thread.terminate()
|
||||||
|
self._thread.wait()
|
||||||
|
|
||||||
|
@Slot(np.ndarray)
|
||||||
|
def _handle_frame(self, frame):
|
||||||
|
"""
|
||||||
|
Internal handler for frames from the worker.
|
||||||
|
Handles conversion to QImage and routing.
|
||||||
|
"""
|
||||||
|
# 1. Emit raw frame for other processing (OCR, Detection, etc.)
|
||||||
|
self.raw_frame_ready.emit(frame)
|
||||||
|
|
||||||
|
# 2. Convert to QImage for UI
|
||||||
|
q_image = self._convert_to_qimage(frame)
|
||||||
|
self.image_ready.emit(q_image)
|
||||||
|
|
||||||
|
def _convert_to_qimage(self, frame):
|
||||||
|
"""
|
||||||
|
Converts a BGR numpy array to a RGB QImage.
|
||||||
|
"""
|
||||||
|
if frame is None:
|
||||||
|
return QImage()
|
||||||
|
|
||||||
|
# OpenCV uses BGR, PySide uses RGB
|
||||||
|
try:
|
||||||
|
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||||
|
height, width, channel = frame_rgb.shape
|
||||||
|
bytes_per_line = channel * width
|
||||||
|
|
||||||
|
return QImage(
|
||||||
|
frame_rgb.data,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
bytes_per_line,
|
||||||
|
QImage.Format.Format_RGB888
|
||||||
|
).copy() # .copy() ensures the QImage owns the data and is safe to pass across threads
|
||||||
|
except Exception as e:
|
||||||
|
self.error_occurred.emit(f"Image conversion error: {str(e)}")
|
||||||
|
return QImage()
|
||||||
|
|
||||||
|
@Slot(float, float)
|
||||||
|
def _handle_frame_time(self, start_time, end_time):
|
||||||
|
"""
|
||||||
|
Optional: Handle frame timing information for performance monitoring.
|
||||||
|
"""
|
||||||
|
if self._metrics:
|
||||||
|
self._metrics.update_metrics(start_time, end_time)
|
||||||
118
app/video_stream/worker.py
Normal file
118
app/video_stream/worker.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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) # (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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@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(1000 / fps) if fps > 0 else 30
|
||||||
|
|
||||||
|
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.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 _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)}")
|
||||||
|
|
||||||
Reference in New Issue
Block a user