Compare commits

...

2 Commits

Author SHA1 Message Date
6f784b4e40 test sleep zamiast qtimer 2026-05-11 14:39:53 +02:00
f317e71899 test metrics 2026-05-10 23:54:11 +02:00
3 changed files with 122 additions and 13 deletions

View File

@@ -1,10 +1,11 @@
from dataclasses import asdict
from enum import Enum
from typing import Any
import logging
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QMainWindow, QStyle, QToolButton, QWidget, QVBoxLayout, QPushButton, QLabel
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QMainWindow, QStyle, QToolButton, QWidget, QVBoxLayout, QPushButton, QLabel, QTextEdit
from PySide6.QtGui import QIcon, QPixmap
from PySide6.QtCore import Qt, QSize
from PySide6.QtCore import QTimer, Qt, QSize
from .video_stream.controller import VideoStreamController, FrameMetrics
logger = logging.getLogger(__name__)
@@ -79,6 +80,10 @@ class MainWindow(QMainWindow):
self.setup_ui()
self.video_controller.start()
self._counter = 0
self.timer = QTimer()
self.timer.timeout.connect(self.update_metrics)
self.timer.start(500) # Update metrics every second
def setup_ui(self):
self.central_widget = QWidget()
@@ -86,6 +91,14 @@ class MainWindow(QMainWindow):
self.central_widget.setStyleSheet("background-color: #001e1e;")
self.video_label = QLabel(self.central_widget)
self.video_label.setText("Video Stream")
self.video_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.video_label.setStyleSheet("color: white; font-size: 24px; background-color: #001e1e;")
self.metrics_label = QTextEdit(self.central_widget)
self.metrics_label.setReadOnly(True)
# self.metrics_label.setFixedWidth(300)
self.metrics_label.setStyleSheet("background-color: rgba(20, 20, 20, 0.5); color: white; font-size: 14px; border: none; padding: 8px;")
self.toolbar_widget = QWidget(self.central_widget)
self.toolbar_widget.setMinimumWidth(400)
@@ -133,6 +146,10 @@ class MainWindow(QMainWindow):
def resizeEvent(self, event: Any) -> None:
super().resizeEvent(event)
self.video_label.setGeometry(self.central_widget.rect())
panel_width = min(300, max(280, self.central_widget.width() // 3))
self.metrics_label.setGeometry(18, 18, panel_width, 500)
self.toolbar_widget.adjustSize()
toolbar_size = self.toolbar_widget.sizeHint()
@@ -203,4 +220,22 @@ class MainWindow(QMainWindow):
super().closeEvent(event)
def update_frame(self, frame):
self.video_label.setPixmap(QPixmap.fromImage(frame))
self.video_label.setPixmap(QPixmap.fromImage(frame))
# pass
def update_metrics(self):
self._counter += 1
metrics = self.video_controller.get_metrics()
if metrics is None:
self.metrics_label.setPlainText("No metrics available")
return
metrics_text = ""
for key, value in asdict( metrics).items():
metrics_text += f"{key}: {value}\n"
self.metrics_label.setPlainText(metrics_text)
if self._counter >= 20:
self._counter = 0
self.video_controller.reset_metrics()

View File

@@ -27,6 +27,7 @@ class FrameMetrics:
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)
frame_drops: int = 0 # Total number of frames dropped (if frame read time exceeds expected frame interval)
def __post_init__(self):
self.b_fr_start_delta = deque(maxlen=self.buf_size)
@@ -72,11 +73,12 @@ class FrameMetrics:
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:
def update_metrics(self, start_time: float, end_time: float, frames_dropped: int) -> 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()
self.frame_drops = frames_dropped
def reset_metrics(self) -> None:
self.fr_start_delta_min = float('inf')
@@ -92,7 +94,8 @@ class FrameMetrics:
self.total_start_delta = 0.0
self.b_fr_time_delta.clear()
self.total_frame_time = 0.0
self.frame_drops = 0
def get_metrics(self) -> "FrameMetrics":
return FrameMetrics(
fr_start_delta_min= round(self.fr_start_delta_min * 1000, 3), # Convert to milliseconds
@@ -102,7 +105,11 @@ class FrameMetrics:
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
frame_count=self.frame_count,
last_start_time=self.last_start_time,
buf_size=self.buf_size,
total_frame_time=round(self.total_frame_time * 1000, 3),
total_start_delta=round(self.total_start_delta * 1000, 3)
)
@@ -150,6 +157,13 @@ class VideoStreamController(QObject):
"""
return self._metrics.get_metrics() if self._metrics else None
def reset_metrics(self):
"""
Resets the frame metrics to initial state.
"""
if self._metrics:
self._metrics.reset_metrics()
@Slot(object)
def change_source(self, source):
"""
@@ -170,7 +184,7 @@ class VideoStreamController(QObject):
@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._metrics = FrameMetrics(buf_size=int(fps))
self.status_changed.emit(True)
@Slot()
@@ -229,10 +243,10 @@ class VideoStreamController(QObject):
self.error_occurred.emit(f"Image conversion error: {str(e)}")
return QImage()
@Slot(float, float)
def _handle_frame_time(self, start_time, end_time):
@Slot(float, float, int)
def _handle_frame_time(self, start_time, end_time, frames_dropped):
"""
Optional: Handle frame timing information for performance monitoring.
"""
if self._metrics:
self._metrics.update_metrics(start_time, end_time)
self._metrics.update_metrics(start_time, end_time, frames_dropped)

View File

@@ -12,11 +12,12 @@ class VideoStreamWorker(QObject):
Runs in a separate QThread.
"""
frame_ready = Signal(np.ndarray)
frame_time = Signal(float, float) # (start_time, end_time)
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__()
@@ -26,6 +27,7 @@ class VideoStreamWorker(QObject):
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):
@@ -69,12 +71,13 @@ class VideoStreamWorker(QObject):
# 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
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._timer.start(interval)
self._run.emit() # Uruchamiamy pętlę wątku za pomocą sygnału
self.started.emit(fps, video_res)
@Slot()
@@ -87,6 +90,63 @@ class VideoStreamWorker(QObject):
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.")