diff --git a/app/main_window.py b/app/main_window.py index c3ea035..623931c 100644 --- a/app/main_window.py +++ b/app/main_window.py @@ -80,9 +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(1000) # Update metrics every second + self.timer.start(500) # Update metrics every second def setup_ui(self): self.central_widget = QWidget() @@ -219,10 +220,12 @@ class MainWindow(QMainWindow): super().closeEvent(event) def update_frame(self, frame): - # self.video_label.setPixmap(QPixmap.fromImage(frame)) - pass + self.video_label.setPixmap(QPixmap.fromImage(frame)) + # pass - def update_metrics(self, metrics: FrameMetrics): + 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 @@ -231,4 +234,8 @@ class MainWindow(QMainWindow): for key, value in asdict( metrics).items(): metrics_text += f"{key}: {value}\n" - self.metrics_label.setPlainText(metrics_text) \ No newline at end of file + self.metrics_label.setPlainText(metrics_text) + + if self._counter >= 20: + self._counter = 0 + self.video_controller.reset_metrics() \ No newline at end of file diff --git a/app/video_stream/controller.py b/app/video_stream/controller.py index a2687f0..65c8398 100644 --- a/app/video_stream/controller.py +++ b/app/video_stream/controller.py @@ -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) \ No newline at end of file + self._metrics.update_metrics(start_time, end_time, frames_dropped) \ No newline at end of file diff --git a/app/video_stream/worker.py b/app/video_stream/worker.py index db300db..314d354 100644 --- a/app/video_stream/worker.py +++ b/app/video_stream/worker.py @@ -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.")