feat: Add video playback functionality and inference support
- Introduced VideoPlayer class to handle local video playback, emitting frames via frame_ready signal. - Updated MainWindow to switch between camera and video sources, integrating video playback controls. - Enhanced AppMenuBar with options to open video files and manage inference models. - Implemented BboxOverlay for displaying detection results on video frames. - Added InferenceManager to manage YOLO inference in a separate process, with error handling and restart logic. - Created tests for BboxOverlay and InferenceManager to ensure functionality and robustness. - Updated pyproject.toml to include optional dependencies for inference support.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Menu bar — camera, video format and debug controls."""
|
||||
"""Menu bar — File, Camera, Video format, Image, Model and Debug controls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -6,9 +6,10 @@ import logging
|
||||
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtGui import QAction, QActionGroup
|
||||
from PySide6.QtWidgets import QMenuBar, QWidget
|
||||
from PySide6.QtWidgets import QFileDialog, QMenuBar, QWidget
|
||||
|
||||
from app.camera.camera_enumerator import CameraFormat, CameraInfo
|
||||
from app.config import MODEL_FILE_EXTENSIONS, VIDEO_FILE_EXTENSIONS
|
||||
from app.logging_setup import set_console_level
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -19,17 +20,32 @@ class AppMenuBar(QMenuBar):
|
||||
Application menu bar.
|
||||
|
||||
Signals:
|
||||
camera_selected(CameraInfo) — user picked a camera
|
||||
format_selected(CameraFormat) — user picked a full format (res+fps+pixel)
|
||||
reconnect_requested() — user hit Reconnect
|
||||
overlay_toggled(bool) — overlay show/hide
|
||||
log_toggled(bool) — console logging on/off
|
||||
camera_settings_requested() — user opened Image Settings dialog
|
||||
video_file_selected(str) — user picked a video file path
|
||||
video_closed() — user chose to close video and return to camera
|
||||
model_file_selected(str) — user picked a .pt model file path
|
||||
inference_toggled(bool) — user toggled inference on/off
|
||||
camera_selected(CameraInfo)
|
||||
format_selected(CameraFormat)
|
||||
reconnect_requested()
|
||||
overlay_toggled(bool)
|
||||
log_toggled(bool)
|
||||
camera_settings_requested()
|
||||
"""
|
||||
|
||||
# File / video
|
||||
video_file_selected = Signal(str)
|
||||
video_closed = Signal()
|
||||
|
||||
# Model / inference
|
||||
model_file_selected = Signal(str)
|
||||
inference_toggled = Signal(bool)
|
||||
|
||||
# Camera
|
||||
camera_selected = Signal(object) # CameraInfo
|
||||
format_selected = Signal(object) # CameraFormat
|
||||
reconnect_requested = Signal()
|
||||
|
||||
# View / debug
|
||||
overlay_toggled = Signal(bool)
|
||||
log_toggled = Signal(bool)
|
||||
camera_settings_requested = Signal()
|
||||
@@ -48,7 +64,6 @@ class AppMenuBar(QMenuBar):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def populate_cameras(self, cameras: list[CameraInfo]) -> None:
|
||||
"""Populate the Camera menu with discovered devices."""
|
||||
self._cameras = cameras
|
||||
menu = self._camera_menu
|
||||
|
||||
@@ -71,7 +86,6 @@ class AppMenuBar(QMenuBar):
|
||||
self._camera_group.actions()[0].setChecked(True)
|
||||
|
||||
def populate_formats(self, camera_info: CameraInfo) -> None:
|
||||
"""Populate the Resolution submenu with full format entries."""
|
||||
self._populate_format_menu(camera_info)
|
||||
|
||||
def set_active_camera(self, camera_info: CameraInfo) -> None:
|
||||
@@ -83,7 +97,6 @@ class AppMenuBar(QMenuBar):
|
||||
return
|
||||
|
||||
def set_active_format(self, fmt: CameraFormat) -> None:
|
||||
"""Mark the given format as checked in the Resolution menu."""
|
||||
if self._format_group is None:
|
||||
return
|
||||
for action in self._format_group.actions():
|
||||
@@ -98,34 +111,80 @@ class AppMenuBar(QMenuBar):
|
||||
return
|
||||
|
||||
def set_log_file_path(self, path: str) -> None:
|
||||
"""Display the log file path as a disabled menu item in Debug menu."""
|
||||
display = path if len(path) <= 60 else "\u2026" + path[-57:]
|
||||
self._log_file_action.setText(f"Log: {display}")
|
||||
self._log_file_action.setToolTip(path)
|
||||
|
||||
def set_video_source_active(self, is_video: bool) -> None:
|
||||
"""Update File menu state when source switches between camera and video."""
|
||||
self._close_video_action.setEnabled(is_video)
|
||||
|
||||
def set_inference_available(self, available: bool) -> None:
|
||||
"""Enable/disable the inference toggle (requires model to be loaded)."""
|
||||
self._inference_toggle_action.setEnabled(available)
|
||||
|
||||
def set_inference_checked(self, checked: bool) -> None:
|
||||
self._inference_toggle_action.setChecked(checked)
|
||||
|
||||
def set_model_label(self, name: str) -> None:
|
||||
"""Show loaded model name as disabled info item."""
|
||||
self._model_info_action.setText(f"Model: {name}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Menu construction
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_menus(self) -> None:
|
||||
# Camera menu
|
||||
# --- File menu ---
|
||||
file_menu = self.addMenu("File")
|
||||
|
||||
open_video_action = QAction("Open Video\u2026", self)
|
||||
open_video_action.triggered.connect(self._on_open_video)
|
||||
file_menu.addAction(open_video_action)
|
||||
|
||||
self._close_video_action = QAction("Close Video", self)
|
||||
self._close_video_action.setEnabled(False)
|
||||
self._close_video_action.triggered.connect(self.video_closed)
|
||||
file_menu.addAction(self._close_video_action)
|
||||
|
||||
# --- Camera menu ---
|
||||
self._camera_menu = self.addMenu("Camera")
|
||||
self._cam_separator = self._camera_menu.addSeparator()
|
||||
self._reconnect_action = QAction("Reconnect", self)
|
||||
self._reconnect_action.triggered.connect(self.reconnect_requested)
|
||||
self._camera_menu.addAction(self._reconnect_action)
|
||||
|
||||
# Video menu
|
||||
# --- Video menu ---
|
||||
self._video_menu = self.addMenu("Video")
|
||||
self._res_menu = self._video_menu.addMenu("Resolution")
|
||||
|
||||
# Image menu (camera controls)
|
||||
# --- Image menu ---
|
||||
self._image_menu = self.addMenu("Image")
|
||||
self._settings_action = QAction("Camera Settings\u2026", self)
|
||||
self._settings_action.triggered.connect(self.camera_settings_requested)
|
||||
self._image_menu.addAction(self._settings_action)
|
||||
|
||||
# Debug menu
|
||||
# --- Model menu ---
|
||||
model_menu = self.addMenu("Model")
|
||||
|
||||
load_model_action = QAction("Load Model\u2026", self)
|
||||
load_model_action.triggered.connect(self._on_load_model)
|
||||
model_menu.addAction(load_model_action)
|
||||
|
||||
self._inference_toggle_action = QAction("Enable Inference", self)
|
||||
self._inference_toggle_action.setCheckable(True)
|
||||
self._inference_toggle_action.setChecked(False)
|
||||
self._inference_toggle_action.setEnabled(False) # enabled after model loaded
|
||||
self._inference_toggle_action.toggled.connect(self.inference_toggled)
|
||||
model_menu.addAction(self._inference_toggle_action)
|
||||
|
||||
model_menu.addSeparator()
|
||||
|
||||
self._model_info_action = QAction("Model: (none)", self)
|
||||
self._model_info_action.setEnabled(False)
|
||||
model_menu.addAction(self._model_info_action)
|
||||
|
||||
# --- Debug menu ---
|
||||
debug_menu = self.addMenu("Debug")
|
||||
|
||||
self._overlay_action = QAction("Show Overlay", self)
|
||||
@@ -147,7 +206,6 @@ class AppMenuBar(QMenuBar):
|
||||
debug_menu.addAction(self._log_file_action)
|
||||
|
||||
def _populate_format_menu(self, camera_info: CameraInfo) -> None:
|
||||
"""Build Resolution submenu: one action per unique (W, H, FPS, pixel_format)."""
|
||||
self._res_menu.clear()
|
||||
self._format_group = QActionGroup(self)
|
||||
self._format_group.setExclusive(True)
|
||||
@@ -173,6 +231,28 @@ class AppMenuBar(QMenuBar):
|
||||
# Slots
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _on_open_video(self) -> None:
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self.parentWidget(),
|
||||
"Open Video File",
|
||||
"",
|
||||
VIDEO_FILE_EXTENSIONS,
|
||||
)
|
||||
if path:
|
||||
logger.debug("Video file selected: %s", path)
|
||||
self.video_file_selected.emit(path)
|
||||
|
||||
def _on_load_model(self) -> None:
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self.parentWidget(),
|
||||
"Load YOLO Model",
|
||||
"",
|
||||
MODEL_FILE_EXTENSIONS,
|
||||
)
|
||||
if path:
|
||||
logger.debug("Model file selected: %s", path)
|
||||
self.model_file_selected.emit(path)
|
||||
|
||||
def _on_camera_action(self) -> None:
|
||||
action = self.sender()
|
||||
if action is None:
|
||||
|
||||
Reference in New Issue
Block a user