Compare commits

19 Commits
main ... sonnet

Author SHA1 Message Date
60eb59fc32 test pyuvc 2026-05-18 16:09:23 +02:00
ae0b7784ac requirements 2026-05-14 16:10:37 +02:00
b13c468df9 test on mac 2026-05-14 16:08:17 +02:00
6c401b62bb feat: Enhance inference management with device tracking and telemetry updates 2026-05-13 22:39:08 +02:00
83346dc985 feat: Add detection count tracking and display in the UI 2026-05-13 22:08:13 +02:00
3b8f7eb3d4 feat: Improve frame conversion strategy and logging in InferenceManager 2026-05-13 21:47:43 +02:00
e9b474b1ed 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.
2026-05-13 21:30:13 +02:00
ac51498b7a doc: add note about last changes 2026-05-13 19:46:06 +02:00
cdeac53555 feat: Add Camera Settings dialog for UVC controls and Qt parameters
- Implemented CameraSettingsDialog to manage UVC and Qt camera controls.
- Integrated UVC parameter sliders and auto controls for brightness, contrast, saturation, hue, sharpness, gamma, white balance, backlight compensation, and exposure.
- Added functionality to change white balance and exposure settings via Qt controls.
- Updated MainWindow to open CameraSettingsDialog and manage UVC controller lifecycle.
- Enhanced AppMenuBar to include a Camera Settings option.
- Created tests for UVC controller abstraction layer and parameter info.
- Documented camera specifications and supported features in new markdown files.
2026-05-13 19:19:39 +02:00
d62416db4e feat: implement logging setup and CSV telemetry logging for performance metrics 2026-05-12 22:15:50 +02:00
aec286c5ec feat: enhance telemetry metrics with target FPS tracking and logging 2026-05-12 21:49:27 +02:00
b238f0d9b4 feat: add MVP application documentation with project structure and component descriptions 2026-05-12 21:49:12 +02:00
4cc4f4bf6c feat: implement overlay architecture with IOverlayLayer interface and telemetry overlay 2026-05-12 21:10:37 +02:00
74a5dcd057 feat: implement VideoWidget for rendering camera frames and metrics overlay 2026-05-12 20:43:38 +02:00
22e52d5f5a feat: add test overlay for debugging in main window 2026-05-12 20:24:35 +02:00
ece4e1cd6e feat: restructure overlay and video widget integration for improved rendering 2026-05-12 20:06:37 +02:00
03d3332b35 fix: correct import statement for QVideoWidget in main_window.py 2026-05-12 19:52:59 +02:00
cd7f196b25 feat: implement core functionality for camera preview application
- Add FrameDispatcher for distributing QVideoFrames to subscribers
- Implement TelemetryCollector to measure video pipeline performance metrics
- Create MainWindow as the main application interface with video rendering
- Develop AppMenuBar for camera selection, resolution, and FPS settings
- Establish overlay system for displaying telemetry metrics
- Set up project structure and configuration files
- Add unit tests for FrameDispatcher and TelemetryCollector
2026-05-12 19:49:53 +02:00
65b98c352d new future - play video 2026-05-12 19:35:04 +02:00
96 changed files with 15054 additions and 1 deletions

9
.gitignore vendored
View File

@@ -12,6 +12,9 @@ __pycache__/
venv/
env/
# Log files
# Local/runtime data
captures/photos/*
captures/videos/*
@@ -29,4 +32,8 @@ captures/videos/*
runs/
*.onnx
*.engine
*.log
*.log
!logs/*.log

0
app/__init__.py Normal file
View File

0
app/camera/__init__.py Normal file
View File

View File

@@ -0,0 +1,132 @@
"""Camera enumeration — discovers available video input devices."""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from PySide6.QtMultimedia import QCameraDevice, QMediaDevices, QVideoFrameFormat
logger = logging.getLogger(__name__)
# Map Qt pixel format enum → human-readable string
_PIXEL_FORMAT_NAMES: dict[QVideoFrameFormat.PixelFormat, str] = {
QVideoFrameFormat.PixelFormat.Format_BGRA8888: "BGRA",
QVideoFrameFormat.PixelFormat.Format_BGRX8888: "BGRX",
QVideoFrameFormat.PixelFormat.Format_ABGR8888: "ABGR",
QVideoFrameFormat.PixelFormat.Format_ARGB8888: "ARGB",
QVideoFrameFormat.PixelFormat.Format_RGBA8888: "RGBA",
QVideoFrameFormat.PixelFormat.Format_RGBX8888: "RGBX",
QVideoFrameFormat.PixelFormat.Format_YUV420P: "YUV420P",
QVideoFrameFormat.PixelFormat.Format_YUV422P: "YUV422P",
QVideoFrameFormat.PixelFormat.Format_NV12: "NV12",
QVideoFrameFormat.PixelFormat.Format_NV21: "NV21",
QVideoFrameFormat.PixelFormat.Format_UYVY: "UYVY",
QVideoFrameFormat.PixelFormat.Format_YUYV: "YUY2",
QVideoFrameFormat.PixelFormat.Format_Jpeg: "MJPG",
QVideoFrameFormat.PixelFormat.Format_Y8: "GRAY8",
QVideoFrameFormat.PixelFormat.Format_Y16: "GRAY16",
}
def pixel_format_name(fmt: QVideoFrameFormat.PixelFormat) -> str:
"""Return a short human-readable name for a Qt pixel format."""
return _PIXEL_FORMAT_NAMES.get(fmt, f"fmt_{fmt.value if hasattr(fmt, 'value') else fmt}")
@dataclass
class CameraFormat:
"""One supported video format entry for a camera device."""
width: int
height: int
max_fps: float
pixel_format: str # e.g. "MJPG", "YUY2", "NV12", "BGRA"
def __str__(self) -> str:
return f"{self.pixel_format:6s} {self.width}x{self.height} @ {self.max_fps:.1f} fps"
@dataclass
class CameraInfo:
"""Lightweight descriptor of a detected camera."""
device: QCameraDevice
name: str
id: str
formats: list[CameraFormat] = field(default_factory=list)
def __str__(self) -> str:
return f"{self.name} [{self.id}]"
class CameraEnumerator:
"""Discovers available video input devices via QMediaDevices."""
@staticmethod
def list_cameras() -> list[CameraInfo]:
"""Return all available camera devices with their supported formats."""
devices = QMediaDevices.videoInputs()
cameras: list[CameraInfo] = []
for device in devices:
formats: list[CameraFormat] = []
for qfmt in device.videoFormats():
res = qfmt.resolution()
formats.append(CameraFormat(
width=res.width(),
height=res.height(),
max_fps=qfmt.maxFrameRate(),
pixel_format=pixel_format_name(qfmt.pixelFormat()),
))
# deduplicate; sort: largest area first, then fps descending
seen: set[tuple[int, int, float, str]] = set()
unique: list[CameraFormat] = []
for f in sorted(
formats,
key=lambda x: (x.width * x.height, x.max_fps),
reverse=True,
):
key = (f.width, f.height, f.max_fps, f.pixel_format)
if key not in seen:
seen.add(key)
unique.append(f)
cam_id = CameraEnumerator._device_id(device)
cameras.append(CameraInfo(
device=device,
name=device.description(),
id=cam_id,
formats=unique,
))
logger.info("Cameras found: %d", len(cameras))
for idx, cam in enumerate(cameras):
logger.info(" [%d] %s (id: %s)", idx, cam.name, cam.id)
for fmt in cam.formats:
logger.info(" %s", fmt)
return cameras
@staticmethod
def default_camera() -> CameraInfo | None:
"""Return the system default camera, or None if no camera is available."""
device = QMediaDevices.defaultVideoInput()
if device.isNull():
return None
default_id = CameraEnumerator._device_id(device)
for cam in CameraEnumerator.list_cameras():
if cam.id == default_id:
return cam
cameras = CameraEnumerator.list_cameras()
return cameras[0] if cameras else None
@staticmethod
def _device_id(device: QCameraDevice) -> str:
raw = device.id()
if hasattr(raw, "toStdString"):
return raw.toStdString()
return raw.data().decode("utf-8", errors="replace")

View File

@@ -0,0 +1,229 @@
"""Camera Service — manages QCamera lifecycle and frame acquisition."""
from __future__ import annotations
import logging
import platform
from PySide6.QtCore import QObject, Signal
from PySide6.QtMultimedia import (
QCamera,
QMediaCaptureSession,
QVideoFrame,
QVideoSink,
)
from app.camera.camera_enumerator import CameraFormat, CameraInfo, pixel_format_name
logger = logging.getLogger(__name__)
class CameraService(QObject):
"""
Manages camera initialisation, configuration and frame delivery.
Emits:
frame_ready(QVideoFrame) — new frame available from the camera
camera_started() — camera successfully opened and streaming
camera_stopped() — camera stopped (clean shutdown)
camera_error(str) — camera error description
format_changed(float) — actual FPS after format was applied
"""
frame_ready = Signal(QVideoFrame)
camera_started = Signal()
camera_stopped = Signal()
camera_error = Signal(str)
format_changed = Signal(float)
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
self._camera: QCamera | None = None
self._session = QMediaCaptureSession(self)
self._sink = QVideoSink(self)
self._current_info: CameraInfo | None = None
self._session_logged: bool = False
# Desired format — applied on every (re)start
self._desired_fmt: CameraFormat | None = None
self._desired_fmt = CameraFormat(width=1280, height=720, max_fps=30, pixel_format="NV12")
self._session.setVideoSink(self._sink)
self._sink.videoFrameChanged.connect(self._on_frame)
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def start(self, camera_info: CameraInfo) -> None:
"""Start streaming from the given camera device."""
self._stop_camera()
self._current_info = camera_info
self._camera = QCamera(camera_info.device, self)
self._camera.errorOccurred.connect(self._on_error)
self._camera.activeChanged.connect(self._on_active_changed)
self._session.setCamera(self._camera)
self._apply_format()
self._camera.start()
logger.info("Camera start requested: %s", camera_info.name)
def stop(self) -> None:
"""Stop the current camera and forget the device."""
self._stop_camera()
self._current_info = None
def reconnect(self) -> None:
"""Restart the current camera (e.g. after an error or disconnect)."""
if self._current_info is not None:
logger.info("Reconnecting camera: %s", self._current_info.name)
self.start(self._current_info)
else:
logger.warning("Reconnect requested but no camera was previously started")
def set_format(self, fmt: CameraFormat) -> None:
"""
Request a specific video format (resolution + fps + pixel format).
Restarts the camera to apply the change reliably.
"""
self._desired_fmt = fmt
if self._current_info is not None:
logger.info(
"Format change: %dx%d @ %.4g fps (%s) — restarting camera",
fmt.width, fmt.height, fmt.max_fps, fmt.pixel_format,
)
self.start(self._current_info)
@property
def is_active(self) -> bool:
return self._camera is not None and self._camera.isActive()
@property
def current_camera(self) -> CameraInfo | None:
return self._current_info
@property
def qt_camera(self) -> QCamera | None:
"""Expose underlying QCamera for settings dialog."""
return self._camera
def video_sink(self) -> QVideoSink:
return self._sink
def capture_session(self) -> QMediaCaptureSession:
return self._session
# ------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------
def _stop_camera(self) -> None:
if self._camera is not None:
self._camera.stop()
self._camera.errorOccurred.disconnect()
self._camera.activeChanged.disconnect()
self._camera = None
logger.debug("Camera stopped (internal)")
def _apply_format(self) -> None:
"""Select and apply the best matching QCameraFormat before start()."""
if self._camera is None or self._current_info is None:
return
best = None
best_score = -1
desired = self._desired_fmt
for qfmt in self._current_info.device.videoFormats():
res = qfmt.resolution()
w, h = res.width(), res.height()
f = qfmt.maxFrameRate()
pf = pixel_format_name(qfmt.pixelFormat())
if desired is not None:
# Exact match preferred
exact_res = int(w == desired.width and h == desired.height) * 1000
exact_fps = int(abs(f - desired.max_fps) < 0.5) * 100
exact_pf = int(pf == desired.pixel_format) * 500
area_diff = abs(w * h - desired.width * desired.height)
score = exact_res + exact_fps + exact_pf - area_diff
else:
# No preference — pick largest area, then highest fps
score = w * h + int(f)
if score > best_score:
best_score = score
best = qfmt
if best is not None:
self._camera.setCameraFormat(best)
res = best.resolution()
pf = pixel_format_name(best.pixelFormat())
logger.info(
"Camera format requested: %s %dx%d @ %.4g fps",
pf, res.width(), res.height(), best.maxFrameRate(),
)
def _log_actual_format(self) -> None:
"""Log the format the camera actually started with and emit format_changed."""
if self._camera is None:
return
fmt = self._camera.cameraFormat()
res = fmt.resolution()
actual_fps = fmt.maxFrameRate()
pf = pixel_format_name(fmt.pixelFormat())
logger.info(
"Camera format ACTUAL: %s %dx%d @ %.4g fps",
pf, res.width(), res.height(), actual_fps,
)
if self._desired_fmt is not None:
d = self._desired_fmt
if abs(actual_fps - d.max_fps) > 0.5:
logger.warning(
"Requested %.4g fps but camera delivering %.4g fps",
d.max_fps, actual_fps,
)
if res.width() != d.width or res.height() != d.height:
logger.warning(
"Requested %dx%d but camera delivering %dx%d",
d.width, d.height, res.width(), res.height(),
)
self.format_changed.emit(actual_fps)
def _log_qt_backend(self) -> None:
"""Log the Qt multimedia backend in use (once per session)."""
try:
system = platform.system()
backend = {
"Darwin": "AVFoundation",
"Windows": "Media Foundation / DirectShow",
"Linux": "GStreamer / V4L2",
}.get(system, system)
logger.info("Qt multimedia backend: %s", backend)
except Exception:
pass
def _on_frame(self, frame: QVideoFrame) -> None:
if frame.isValid():
self.frame_ready.emit(frame)
def _on_error(self, error: QCamera.Error, error_string: str) -> None:
logger.error("Camera error %s: %s", error, error_string)
self.camera_error.emit(error_string)
def _on_active_changed(self, active: bool) -> None:
if active:
name = self._current_info.name if self._current_info else "?"
if not self._session_logged:
self._log_qt_backend()
self._session_logged = True
logger.info("Camera active: %s", name)
self._log_actual_format()
self.camera_started.emit()
else:
logger.info("Camera inactive")
self.camera_stopped.emit()

View File

@@ -0,0 +1,48 @@
"""UVC camera controls — platform-aware factory."""
from __future__ import annotations
import logging
import platform
from app.camera.uvc.base import UvcControllerBase, UvcParam, UvcParamInfo
logger = logging.getLogger(__name__)
def make_uvc_controller(device_name: str) -> UvcControllerBase:
"""
Return the best available UVC controller for the current platform.
Falls back to NullUvcController if the required native library is absent.
"""
system = platform.system()
if system == "Windows":
try:
from app.camera.uvc.windows import WindowsUvcController # noqa: PLC0415
ctrl = WindowsUvcController(device_name)
logger.info("UVC: Windows controller (duvc-ctl) loaded for '%s'", device_name)
return ctrl
except ImportError:
logger.warning(
"UVC: duvc-ctl not installed — install with: pip install duvc-ctl"
)
elif system == "Darwin":
try:
from app.camera.uvc.macos import MacUvcController # noqa: PLC0415
ctrl = MacUvcController(device_name)
logger.info("UVC: macOS controller loaded for '%s'", device_name)
return ctrl
except ImportError:
logger.warning(
"UVC: pyuvc not installed — UVC controls unavailable on macOS"
)
else:
logger.warning("UVC: platform '%s' not supported — controls unavailable", system)
from app.camera.uvc.stub import NullUvcController # noqa: PLC0415
return NullUvcController()
__all__ = ["make_uvc_controller", "UvcControllerBase", "UvcParam", "UvcParamInfo"]

77
app/camera/uvc/base.py Normal file
View File

@@ -0,0 +1,77 @@
"""UVC controller abstract base class."""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum, auto
class UvcParam(Enum):
"""UVC video proc amp controls supported by the ELP camera."""
BRIGHTNESS = auto()
CONTRAST = auto()
SATURATION = auto()
HUE = auto()
SHARPNESS = auto()
GAMMA = auto()
WHITE_BALANCE = auto()
BACKLIGHT_COMPENSATION = auto()
EXPOSURE = auto()
@dataclass
class UvcParamInfo:
"""Range and current value of a single UVC control."""
param: UvcParam
supported: bool
minimum: int = 0
maximum: int = 100
default: int = 50
current: int = 50
step: int = 1
auto_supported: bool = False
auto_enabled: bool = False
class UvcControllerBase(ABC):
"""
Abstract interface for platform-specific UVC camera controls.
All set_* methods are best-effort: if a control is unsupported the
call should silently do nothing (log at DEBUG level).
"""
@abstractmethod
def open(self, device_name: str) -> bool:
"""Open connection to the named camera device. Returns True on success."""
@abstractmethod
def close(self) -> None:
"""Release the camera device handle."""
@abstractmethod
def is_open(self) -> bool:
"""Return True if the device is currently open."""
@abstractmethod
def get_param_info(self, param: UvcParam) -> UvcParamInfo:
"""Return range + current value for the given control."""
@abstractmethod
def get_all_params(self) -> list[UvcParamInfo]:
"""Return info for all known UVC controls."""
@abstractmethod
def set_value(self, param: UvcParam, value: int) -> bool:
"""
Set a control to an integer value.
Returns True if the call succeeded.
"""
@abstractmethod
def set_auto(self, param: UvcParam, enabled: bool) -> bool:
"""
Enable/disable auto mode for a control (e.g. auto white balance).
Returns True if supported and the call succeeded.
"""

172
app/camera/uvc/macos.py Normal file
View File

@@ -0,0 +1,172 @@
"""macOS UVC controller — backed by pyuvc (libuvc bindings)."""
from __future__ import annotations
import logging
from app.camera.uvc.base import UvcControllerBase, UvcParam, UvcParamInfo
from app.camera.uvc.stub import NullUvcController
logger = logging.getLogger(__name__)
# pyuvc provides access to UVC controls via libuvc.
# Install: pip install pyuvc (requires libusb + libjpeg-turbo via brew)
#
# If pyuvc is not installed, fall back silently to NullUvcController.
try:
import uvc # type: ignore[import-untyped]
_PYUVC_AVAILABLE = True
except ImportError:
_PYUVC_AVAILABLE = False
logger.debug("pyuvc not available — macOS UVC controls disabled")
# Map UvcParam → pyuvc control name string
_CONTROL_NAME: dict[UvcParam, str] = {
UvcParam.BRIGHTNESS: "Brightness",
UvcParam.CONTRAST: "Contrast",
UvcParam.SATURATION: "Saturation",
UvcParam.HUE: "Hue",
UvcParam.SHARPNESS: "Sharpness",
UvcParam.GAMMA: "Gamma",
UvcParam.WHITE_BALANCE: "White Balance temperature",
UvcParam.BACKLIGHT_COMPENSATION: "Backlight Compensation",
UvcParam.EXPOSURE: "Absolute Exposure Time",
}
class MacUvcController(UvcControllerBase):
"""
UVC camera controls on macOS via pyuvc / libuvc.
pyuvc: https://github.com/pupil-labs/pyuvc
Install: pip install pyuvc (requires: brew install libusb jpeg-turbo)
"""
def __init__(self, device_name: str) -> None:
if not _PYUVC_AVAILABLE:
raise ImportError("pyuvc not installed")
self._device_name = device_name
self._cap: object | None = None
self._controls: dict[str, object] = {}
# ------------------------------------------------------------------
# Lifecycle
# ------------------------------------------------------------------
def open(self, device_name: str) -> bool:
self.close()
if not _PYUVC_AVAILABLE:
return False
try:
device_list = uvc.device_list()
target = None
for d in device_list:
if device_name.lower() in d.get("name", "").lower():
target = d
break
if target is None and device_list:
target = device_list[0]
logger.warning(
"UVC: camera '%s' not found by name, using '%s'",
device_name, target.get("name"),
)
if target is None:
logger.warning("UVC: no UVC devices found on macOS")
return False
self._cap = uvc.Capture(target["uid"])
# Index controls by name for fast lookup
self._controls = {c.display_name: c for c in self._cap.controls}
logger.info(
"UVC: opened '%s', controls: %s",
target.get("name"), list(self._controls.keys()),
)
return True
except Exception as exc:
logger.warning("UVC macOS open failed: %s", exc)
self._cap = None
return False
def close(self) -> None:
if self._cap is not None:
try:
self._cap.close()
except Exception:
pass
self._cap = None
self._controls.clear()
def is_open(self) -> bool:
return self._cap is not None
# ------------------------------------------------------------------
# Query
# ------------------------------------------------------------------
def get_param_info(self, param: UvcParam) -> UvcParamInfo:
ctrl_name = _CONTROL_NAME.get(param)
if ctrl_name is None or ctrl_name not in self._controls:
return UvcParamInfo(param=param, supported=False)
try:
ctrl = self._controls[ctrl_name]
auto_supported = hasattr(ctrl, "auto_mode")
auto_enabled = bool(ctrl.auto_mode) if auto_supported else False
return UvcParamInfo(
param=param,
supported=True,
minimum=int(ctrl.min_val),
maximum=int(ctrl.max_val),
default=int(ctrl.def_val),
current=int(ctrl.value),
step=int(getattr(ctrl, "step", 1)),
auto_supported=auto_supported,
auto_enabled=auto_enabled,
)
except Exception as exc:
logger.debug("UVC get_param_info(%s): %s", param.name, exc)
return UvcParamInfo(param=param, supported=False)
def get_all_params(self) -> list[UvcParamInfo]:
return [self.get_param_info(p) for p in UvcParam]
# ------------------------------------------------------------------
# Set
# ------------------------------------------------------------------
def set_value(self, param: UvcParam, value: int) -> bool:
ctrl_name = _CONTROL_NAME.get(param)
if ctrl_name is None or ctrl_name not in self._controls:
return False
try:
self._controls[ctrl_name].value = value
logger.debug("UVC set %s = %d", param.name, value)
return True
except Exception as exc:
logger.warning("UVC set_value(%s, %d): %s", param.name, value, exc)
return False
def set_auto(self, param: UvcParam, enabled: bool) -> bool:
ctrl_name = _CONTROL_NAME.get(param)
if ctrl_name is None or ctrl_name not in self._controls:
return False
try:
ctrl = self._controls[ctrl_name]
if hasattr(ctrl, "auto_mode"):
ctrl.auto_mode = enabled
logger.debug("UVC auto %s = %s", param.name, enabled)
return True
return False
except Exception as exc:
logger.warning("UVC set_auto(%s, %s): %s", param.name, enabled, exc)
return False
def make_mac_uvc_controller(device_name: str) -> UvcControllerBase:
"""Factory: returns MacUvcController or NullUvcController if pyuvc absent."""
if not _PYUVC_AVAILABLE:
return NullUvcController()
ctrl = MacUvcController(device_name)
if ctrl.open(device_name):
return ctrl
return NullUvcController()

43
app/camera/uvc/stub.py Normal file
View File

@@ -0,0 +1,43 @@
"""Null (no-op) UVC controller — used when no native library is available."""
from __future__ import annotations
import logging
from app.camera.uvc.base import UvcControllerBase, UvcParam, UvcParamInfo
logger = logging.getLogger(__name__)
class NullUvcController(UvcControllerBase):
"""
Fallback controller that reports all controls as unsupported.
Used on platforms where no UVC library is installed or when device
enumeration fails.
"""
def open(self, device_name: str) -> bool: # noqa: ARG002
return False
def close(self) -> None:
pass
def is_open(self) -> bool:
return False
def get_param_info(self, param: UvcParam) -> UvcParamInfo:
return UvcParamInfo(param=param, supported=False)
def get_all_params(self) -> list[UvcParamInfo]:
return [UvcParamInfo(param=p, supported=False) for p in UvcParam]
def set_value(self, param: UvcParam, value: int) -> bool: # noqa: ARG002
logger.debug("NullUvcController: set_value ignored (%s=%d)", param.name, value)
return False
def set_auto(self, param: UvcParam, enabled: bool) -> bool: # noqa: ARG002
logger.debug(
"NullUvcController: set_auto ignored (%s, auto=%s)", param.name, enabled
)
return False

175
app/camera/uvc/windows.py Normal file
View File

@@ -0,0 +1,175 @@
"""Windows UVC controller — backed by duvc-ctl (DirectShow)."""
from __future__ import annotations
import logging
import duvc_ctl as duvc # type: ignore[import-untyped] # noqa: PLC0415 (lazy import)
from app.camera.uvc.base import UvcControllerBase, UvcParam, UvcParamInfo
logger = logging.getLogger(__name__)
# Map our UvcParam → duvc_ctl VidProp string name
_VIDPROP: dict[UvcParam, str] = {
UvcParam.BRIGHTNESS: "brightness",
UvcParam.CONTRAST: "contrast",
UvcParam.SATURATION: "saturation",
UvcParam.HUE: "hue",
UvcParam.SHARPNESS: "sharpness",
UvcParam.GAMMA: "gamma",
UvcParam.WHITE_BALANCE: "white_balance",
UvcParam.BACKLIGHT_COMPENSATION: "backlight_compensation",
}
# Map UvcParam → duvc_ctl CamProp string for camera-side controls
_CAMPROP: dict[UvcParam, str] = {
UvcParam.EXPOSURE: "exposure",
}
class WindowsUvcController(UvcControllerBase):
"""
UVC camera controls on Windows via duvc-ctl (DirectShow).
duvc-ctl docs: https://allanhanan.github.io/duvc-ctl/
"""
def __init__(self, device_name: str) -> None:
self._device_name = device_name
self._cam: duvc.CameraController | None = None
self._supported: dict[UvcParam, bool] = {}
# ------------------------------------------------------------------
# Lifecycle
# ------------------------------------------------------------------
def open(self, device_name: str) -> bool:
self.close()
try:
devices = duvc.list_devices()
target = None
for d in devices:
if device_name.lower() in d.name.lower() or d.name.lower() in device_name.lower():
target = d
break
if target is None and devices:
target = devices[0] # fallback to first device
logger.warning(
"UVC: camera '%s' not found by name, using '%s'",
device_name, target.name,
)
if target is None:
logger.warning("UVC: no devices found on Windows")
return False
self._cam = duvc.CameraController(target)
self._cam.__enter__()
self._refresh_supported()
logger.info("UVC: opened device '%s'", target.name)
return True
except Exception as exc:
logger.warning("UVC: failed to open device: %s", exc)
self._cam = None
return False
def close(self) -> None:
if self._cam is not None:
try:
self._cam.__exit__(None, None, None)
except Exception:
pass
self._cam = None
self._supported.clear()
def is_open(self) -> bool:
return self._cam is not None
# ------------------------------------------------------------------
# Query
# ------------------------------------------------------------------
def get_param_info(self, param: UvcParam) -> UvcParamInfo:
if not self.is_open() or not self._supported.get(param, False):
return UvcParamInfo(param=param, supported=False)
try:
prop_name = _VIDPROP.get(param) or _CAMPROP.get(param)
if prop_name is None:
return UvcParamInfo(param=param, supported=False)
rng = self._cam.get_property_range(prop_name)
current = getattr(self._cam, prop_name, rng.get("default", 0))
auto_supported = param in (UvcParam.WHITE_BALANCE, UvcParam.EXPOSURE)
return UvcParamInfo(
param=param,
supported=True,
minimum=int(rng.get("min", 0)),
maximum=int(rng.get("max", 100)),
default=int(rng.get("default", 50)),
current=int(current),
step=int(rng.get("step", 1)),
auto_supported=auto_supported,
)
except Exception as exc:
logger.debug("UVC get_param_info(%s): %s", param.name, exc)
return UvcParamInfo(param=param, supported=False)
def get_all_params(self) -> list[UvcParamInfo]:
return [self.get_param_info(p) for p in UvcParam]
# ------------------------------------------------------------------
# Set
# ------------------------------------------------------------------
def set_value(self, param: UvcParam, value: int) -> bool:
if not self.is_open():
return False
try:
prop_name = _VIDPROP.get(param) or _CAMPROP.get(param)
if prop_name is None:
return False
setattr(self._cam, prop_name, value)
logger.debug("UVC set %s = %d", param.name, value)
return True
except Exception as exc:
logger.warning("UVC set_value(%s, %d): %s", param.name, value, exc)
return False
def set_auto(self, param: UvcParam, enabled: bool) -> bool:
if not self.is_open():
return False
try:
if param == UvcParam.WHITE_BALANCE:
prop = "white_balance"
elif param == UvcParam.EXPOSURE:
prop = "exposure"
else:
return False
mode = duvc.CamMode.AUTO if enabled else duvc.CamMode.MANUAL
self._cam.set_camera_property(prop, mode=mode)
logger.debug("UVC auto %s = %s", param.name, enabled)
return True
except Exception as exc:
logger.warning("UVC set_auto(%s, %s): %s", param.name, enabled, exc)
return False
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _refresh_supported(self) -> None:
if self._cam is None:
return
try:
info = self._cam.get_supported_properties()
vid = set(info.get("video", []))
cam = set(info.get("camera", []))
for param in UvcParam:
prop = _VIDPROP.get(param) or _CAMPROP.get(param, "")
self._supported[param] = prop in vid or prop in cam
except Exception as exc:
logger.debug("UVC _refresh_supported: %s", exc)
for param in UvcParam:
self._supported[param] = False

46
app/config.py Normal file
View File

@@ -0,0 +1,46 @@
"""Application-wide constants and default settings."""
from pathlib import Path
APP_NAME = "Duck Preview"
APP_VERSION = "0.1.0"
# Default camera settings
DEFAULT_FPS = 30
DEFAULT_WIDTH = 1280
DEFAULT_HEIGHT = 720
# Telemetry
TELEMETRY_UPDATE_INTERVAL_MS = 500 # how often the metrics snapshot is refreshed
# Overlay
OVERLAY_BG_COLOR = (0, 0, 0, 160) # RGBA
OVERLAY_TEXT_COLOR = (255, 255, 255, 255)
OVERLAY_FONT_SIZE = 13
OVERLAY_PADDING = 10
OVERLAY_MARGIN = 10
# Frame dispatcher
DISPATCHER_MAX_QUEUE_SIZE = 2 # max pending frames per slow subscriber before drop
# Logging
LOG_DIR = Path("logs") # relative to CWD (project root)
MAX_LOG_FILES = 20 # oldest sessions are deleted when exceeded
TELEMETRY_CSV_INTERVAL_S = 5.0 # how often a CSV row is written (seconds)
# Inference worker
INFERENCE_WORKER_TIMEOUT_S = 10.0 # seconds without response before watchdog fires
INFERENCE_MAX_RESTARTS = 3 # max auto-restart attempts before giving up
INFERENCE_POLL_INTERVAL_MS = 50 # how often GUI thread polls output queue (ms)
INFERENCE_WATCHDOG_INTERVAL_MS = 2000 # how often watchdog checks process health (ms)
# BBox overlay
BBOX_COLOR = (0, 220, 60, 255) # RGBA — vivid green
BBOX_LABEL_BG_COLOR = (0, 220, 60, 200) # RGBA — semi-transparent green for label bg
BBOX_LABEL_TEXT_COLOR = (0, 0, 0, 255) # RGBA — black text on green bg
BBOX_LINE_WIDTH = 2
BBOX_FONT_SIZE = 11
# Video file source
VIDEO_FILE_EXTENSIONS = "Video Files (*.mp4 *.avi *.mov *.mkv *.m4v *.webm)"
MODEL_FILE_EXTENSIONS = "YOLO Model (*.pt *.pth)"

View File

View File

@@ -0,0 +1,154 @@
"""BboxOverlay — draws YOLO detection bounding boxes on the camera view."""
from __future__ import annotations
import logging
from typing import NamedTuple
from PySide6.QtCore import QRect, QSize, Qt, Slot
from PySide6.QtGui import QColor, QFont, QPainter, QPen
from app.config import (
BBOX_COLOR,
BBOX_FONT_SIZE,
BBOX_LABEL_BG_COLOR,
BBOX_LABEL_TEXT_COLOR,
BBOX_LINE_WIDTH,
)
from app.overlay.overlay_layer import IOverlayLayer
logger = logging.getLogger(__name__)
class Detection(NamedTuple):
"""
A single object detection result.
Coordinates (x1, y1, x2, y2) are in pixels of the *source frame*
(i.e. the frame that was submitted to inference). BboxOverlay maps
them to the letterboxed video_rect before drawing.
"""
x1: float
y1: float
x2: float
y2: float
conf: float
label: str
class BboxOverlay(IOverlayLayer):
"""
Overlay layer that renders detection bounding boxes.
Usage:
overlay = BboxOverlay()
camera_view.add_overlay_layer(overlay)
inference_manager.detections_ready.connect(overlay.on_detections)
Thread safety:
on_detections() is called from the GUI thread (via Qt signal).
paint() is also called from the GUI thread (paintEvent).
No locks required.
"""
def __init__(self) -> None:
super().__init__()
self._detections: list[Detection] = []
self._source_size: QSize = QSize(0, 0)
self._pen = QPen(QColor(*BBOX_COLOR))
self._pen.setWidth(BBOX_LINE_WIDTH)
self._pen.setJoinStyle(Qt.PenJoinStyle.MiterJoin)
self._font = QFont("Monospace")
self._font.setStyleHint(QFont.StyleHint.TypeWriter)
self._font.setPointSize(BBOX_FONT_SIZE)
self._font.setBold(True)
self._box_color = QColor(*BBOX_COLOR)
self._bg_color = QColor(*BBOX_LABEL_BG_COLOR)
self._text_color = QColor(*BBOX_LABEL_TEXT_COLOR)
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
@Slot(object, object)
def on_detections(
self,
detections: list[Detection],
source_size: tuple[int, int],
) -> None:
"""
Receive detection results from InferenceManager.
Args:
detections: List of Detection namedtuples (pixel coords).
source_size: (width, height) of the frame that was inferred.
"""
self._detections = detections
self._source_size = QSize(*source_size)
def clear(self) -> None:
"""Remove all currently displayed detections."""
self._detections = []
# ------------------------------------------------------------------
# IOverlayLayer implementation
# ------------------------------------------------------------------
def paint(self, painter: QPainter, video_rect: QRect) -> None:
if not self._detections:
return
if self._source_size.isEmpty():
return
src_w = self._source_size.width()
src_h = self._source_size.height()
vr = video_rect
# Scale factors: source-pixel → video_rect-pixel
scale_x = vr.width() / src_w
scale_y = vr.height() / src_h
painter.setFont(self._font)
fm = painter.fontMetrics()
for det in self._detections:
# Map to widget coordinates
wx1 = vr.x() + int(det.x1 * scale_x)
wy1 = vr.y() + int(det.y1 * scale_y)
wx2 = vr.x() + int(det.x2 * scale_x)
wy2 = vr.y() + int(det.y2 * scale_y)
box_rect = QRect(wx1, wy1, wx2 - wx1, wy2 - wy1)
# Draw bounding box
painter.setPen(self._pen)
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawRect(box_rect)
# Label text: "label 0.87"
label_text = f"{det.label} {det.conf:.2f}"
text_w = fm.horizontalAdvance(label_text) + 6
text_h = fm.height() + 2
# Position label above box, clamped to video_rect
lx = wx1
ly = wy1 - text_h
if ly < vr.top():
ly = wy1 # draw inside box if no room above
label_bg = QRect(lx, ly, text_w, text_h)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(self._bg_color)
painter.drawRect(label_bg)
painter.setPen(QPen(self._text_color))
painter.drawText(
lx + 3,
ly + fm.ascent() + 1,
label_text,
)

219
app/inference/worker.py Normal file
View File

@@ -0,0 +1,219 @@
"""YOLO inference worker — runs in a separate process.
This module contains only plain functions (no Qt, no PySide6) so it can
safely be imported and executed in a child process via multiprocessing.
IPC protocol
------------
input_queue receives : FramePacket (frame_id, raw_bytes, width, height, channels)
output_queue sends : WorkerReadyPacket (device) — once after model load
: ResultPacket (frame_id, detections, width, height, elapsed_ms)
: None — on fatal load failure
stop_event : multiprocessing.Event — set by parent to request clean exit
Detection format (namedtuple-compatible plain tuple):
(x1, y1, x2, y2, conf, label) — all floats/str, x/y in source-frame pixels
"""
from __future__ import annotations
import logging
import platform
import sys
from multiprocessing import Event, Queue
from queue import Empty
from typing import NamedTuple
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Data structures shared between worker and manager
# ---------------------------------------------------------------------------
class FramePacket(NamedTuple):
frame_id: int
raw_bytes: bytes # RGB bytes, row-major, shape = (height, width, channels)
width: int
height: int
channels: int # always 3 (RGB)
class WorkerReadyPacket(NamedTuple):
"""
Sent once by the worker right after the model is loaded.
Carries the device string so the GUI can display it.
"""
device: str # e.g. "cpu", "mps"
class ResultPacket(NamedTuple):
frame_id: int
detections: list # list of (x1, y1, x2, y2, conf, label) tuples
width: int # source frame width (for overlay scaling)
height: int # source frame height
elapsed_ms: float = 0.0 # inference wall-clock time in milliseconds
# ---------------------------------------------------------------------------
# Worker entry point
# ---------------------------------------------------------------------------
def run_worker(
model_path: str,
input_queue: Queue,
output_queue: Queue,
stop_event: Event,
log_level: int = logging.WARNING,
) -> None:
"""
Main loop of the inference worker process.
Loads the YOLO model once, sends WorkerReadyPacket, then processes
frames from input_queue until stop_event is set.
Results are posted to output_queue.
This function is designed to be the target of multiprocessing.Process.
It must NOT import PySide6 or any Qt module.
"""
_configure_worker_logging(log_level)
logger.info("Inference worker starting (pid=%d)", _getpid())
# Select device once — never changes during the lifetime of this process
device = _select_device()
try:
model = _load_model(model_path, device)
except Exception as exc:
logger.error("Failed to load model '%s': %s", model_path, exc)
# Signal failure by putting None — manager treats it as error
output_queue.put(None)
return
logger.info("Model loaded: %s device=%s", model_path, device)
# Notify GUI thread of the device being used
output_queue.put(WorkerReadyPacket(device=device))
while not stop_event.is_set():
try:
packet: FramePacket = input_queue.get(timeout=0.1)
except Empty:
continue
except Exception as exc:
logger.error("Error reading input queue: %s", exc)
break
try:
result = _infer(model, packet, device)
output_queue.put(result)
except Exception as exc:
logger.error("Inference error (frame %d): %s", packet.frame_id, exc)
# Put empty result so manager knows we're still alive
output_queue.put(ResultPacket(
frame_id=packet.frame_id,
detections=[],
width=packet.width,
height=packet.height,
))
logger.info("Inference worker stopping")
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _load_model(model_path: str, device: str):
"""Load YOLO model and warm up on the selected device."""
from ultralytics import YOLO # noqa: PLC0415
logger.info("Loading YOLO model on device='%s'", device)
model = YOLO(model_path)
# Warm up — run on a tiny dummy to JIT-compile kernels
try:
import numpy as np # noqa: PLC0415
dummy = np.zeros((64, 64, 3), dtype=np.uint8)
model(dummy, device=device, verbose=False)
except Exception as exc:
logger.warning("Warm-up failed (non-fatal): %s", exc)
return model
def _select_device() -> str:
"""
Choose the best available inference device.
Priority:
- macOS → "mps" if torch.backends.mps.is_available(), else "cpu"
- others → "cpu"
Called once at worker startup — not per frame.
"""
system = platform.system()
if system == "Darwin":
try:
import torch # noqa: PLC0415
if torch.backends.mps.is_available():
logger.info("MPS (Metal) available — using GPU")
return "mps"
except Exception:
pass
logger.info("MPS not available — using CPU")
return "cpu"
def _infer(model, packet: FramePacket, device: str) -> ResultPacket:
"""Run model on one frame, return ResultPacket with elapsed_ms."""
import time # noqa: PLC0415
import numpy as np # noqa: PLC0415
frame_np = np.frombuffer(packet.raw_bytes, dtype=np.uint8).reshape(
(packet.height, packet.width, packet.channels)
)
t0 = time.perf_counter()
results = model(frame_np, device=device, verbose=False)
elapsed_ms = (time.perf_counter() - t0) * 1000.0
detections = []
for r in results:
if r.boxes is None:
continue
boxes = r.boxes
for i in range(len(boxes)):
xyxy = boxes.xyxy[i].tolist() # [x1, y1, x2, y2] in source pixels
conf = float(boxes.conf[i])
cls_idx = int(boxes.cls[i])
label = (
r.names[cls_idx]
if r.names and cls_idx in r.names
else str(cls_idx)
)
detections.append((
float(xyxy[0]), float(xyxy[1]),
float(xyxy[2]), float(xyxy[3]),
conf, label,
))
return ResultPacket(
frame_id=packet.frame_id,
detections=detections,
width=packet.width,
height=packet.height,
elapsed_ms=elapsed_ms,
)
def _configure_worker_logging(level: int) -> None:
logging.basicConfig(
level=level,
format="[worker %(process)d] %(levelname)s %(name)s: %(message)s",
stream=sys.stderr,
)
def _getpid() -> int:
import os # noqa: PLC0415
return os.getpid()

View File

@@ -0,0 +1,401 @@
"""InferenceManager — orchestrates the YOLO worker process from the GUI thread.
Responsibilities:
- Start / stop the worker process
- Submit frames (with drop-if-busy logic)
- Poll result queue via QTimer (never blocks the GUI thread)
- Watch process health via QTimer (auto-restart on crash)
- Emit Qt signals with results for BboxOverlay and TelemetryCollector
"""
from __future__ import annotations
import collections
import logging
import multiprocessing
import time
from pathlib import Path
from PySide6.QtCore import QObject, QTimer, Signal, Slot
from PySide6.QtMultimedia import QVideoFrame
from app.config import (
INFERENCE_MAX_RESTARTS,
INFERENCE_POLL_INTERVAL_MS,
INFERENCE_WATCHDOG_INTERVAL_MS,
INFERENCE_WORKER_TIMEOUT_S,
)
from app.inference.bbox_overlay import Detection
from app.inference.worker import FramePacket, ResultPacket, WorkerReadyPacket, run_worker
logger = logging.getLogger(__name__)
# Number of recent inference times to average for the overlay display
_ELAPSED_WINDOW = 10
class InferenceManager(QObject):
"""
Manages the YOLO worker subprocess.
Signals:
detections_ready(detections, source_size)
Emitted in the GUI thread when a result arrives.
detections : list[Detection]
source_size : tuple[int, int] — (width, height) of inferred frame
detection_count_updated(int)
Total number of frames on which at least one detection occurred.
inference_stats_updated(device, avg_ms)
Emitted after every result packet.
device : str — e.g. "cpu", "mps"
avg_ms : float — rolling average of inference time (last 10 frames)
inference_device_changed(str)
Emitted once when the worker reports its device after model load.
inference_started() — worker is up and model is loaded
inference_stopped() — worker has exited cleanly
inference_error(str) — fatal error (max restarts exceeded)
"""
detections_ready = Signal(object, object) # list[Detection], tuple[int,int]
detection_count_updated = Signal(int) # total frames with detections so far
inference_stats_updated = Signal(str, float) # device, avg_elapsed_ms
inference_device_changed = Signal(str) # emitted once on WorkerReadyPacket
inference_started = Signal()
inference_stopped = Signal()
inference_error = Signal(str)
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
self._model_path: str | None = None
self._process: multiprocessing.Process | None = None
self._input_queue: multiprocessing.Queue | None = None
self._output_queue: multiprocessing.Queue | None = None
self._stop_event: multiprocessing.Event | None = None
# Drop-if-busy flag — True while worker is processing a frame
self._busy: bool = False
self._frame_id: int = 0
# Restart tracking
self._restart_count: int = 0
self._last_result_time: float = 0.0
# Paused flag — inference can be suspended without stopping the process
self._paused: bool = False
# Detection counter — frames on which at least one detection occurred
self._detection_frame_count: int = 0
# Device reported by the worker after model load
self._current_device: str = "cpu"
# Rolling window of recent elapsed_ms values for averaging
self._elapsed_window: collections.deque[float] = collections.deque(
maxlen=_ELAPSED_WINDOW
)
# QTimers (GUI thread)
self._poll_timer = QTimer(self)
self._poll_timer.setInterval(INFERENCE_POLL_INTERVAL_MS)
self._poll_timer.timeout.connect(self._poll_output)
self._watchdog_timer = QTimer(self)
self._watchdog_timer.setInterval(INFERENCE_WATCHDOG_INTERVAL_MS)
self._watchdog_timer.timeout.connect(self._watchdog_check)
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def start(self, model_path: str) -> None:
"""Load model and start the worker process."""
if not Path(model_path).exists():
msg = f"Model file not found: {model_path}"
logger.error(msg)
self.inference_error.emit(msg)
return
self._stop_worker()
self._model_path = model_path
self._restart_count = 0
self._paused = False
self._detection_frame_count = 0
self._elapsed_window.clear()
self._current_device = "cpu"
self._start_worker()
def stop(self) -> None:
"""Stop the worker process and reset state."""
self._stop_worker()
self._model_path = None
self._restart_count = 0
self._paused = False
def pause(self) -> None:
"""Suspend frame submission without stopping the process."""
self._paused = True
logger.debug("InferenceManager: paused")
def resume(self) -> None:
"""Resume frame submission."""
self._paused = False
logger.debug("InferenceManager: resumed")
@property
def is_running(self) -> bool:
return self._process is not None and self._process.is_alive()
@property
def is_paused(self) -> bool:
return self._paused
@property
def model_path(self) -> str | None:
return self._model_path
@property
def current_device(self) -> str:
return self._current_device
@Slot(QVideoFrame)
def submit_frame(self, frame: QVideoFrame) -> None:
"""
Attempt to submit a frame for inference.
Drops the frame silently if:
- manager is not running
- manager is paused
- worker is still busy with previous frame (drop_if_busy)
Frame conversion strategy:
Use QVideoFrame.toImage() → QImage.Format_RGB32 → bits().
This handles all pixel formats (NV12, YUV420P, BGRA, MJPG, etc.)
because Qt decodes them internally. The cost is a CPU colour-space
conversion, but it only happens when the worker is idle (drop_if_busy).
"""
if not self.is_running or self._paused or self._busy:
return
if not frame.isValid():
return
# Convert frame to RGB via Qt's built-in decoder.
# toImage() handles NV12, YUV420P, MJPG, BGRA — any pixel format.
image = frame.toImage()
if image.isNull():
logger.warning("InferenceManager: toImage() returned null")
return
width = image.width()
height = image.height()
# Ensure we have packed RGB32 (BGRX on little-endian, 4 bytes/pixel)
from PySide6.QtGui import QImage # noqa: PLC0415
if image.format() != QImage.Format.Format_RGB32:
image = image.convertToFormat(QImage.Format.Format_RGB32)
# Extract RGB bytes (drop alpha/padding channel)
try:
import numpy as np # noqa: PLC0415
# bits() returns BGRX (B G R 0xFF) for Format_RGB32
ptr = image.bits()
arr = np.frombuffer(ptr, dtype=np.uint8).reshape((height, width, 4))
# Swap B↔R and drop X → RGB
rgb = arr[:, :, [2, 1, 0]].copy()
raw = rgb.tobytes()
except Exception as exc:
logger.warning("InferenceManager: frame conversion failed: %s", exc)
return
channels = 3
self._frame_id += 1
packet = FramePacket(
frame_id=self._frame_id,
raw_bytes=raw,
width=width,
height=height,
channels=channels,
)
try:
self._input_queue.put_nowait(packet)
self._busy = True
except Exception as exc:
logger.warning("InferenceManager: could not enqueue frame: %s", exc)
# ------------------------------------------------------------------
# Private — worker lifecycle
# ------------------------------------------------------------------
def _start_worker(self) -> None:
ctx = multiprocessing.get_context("spawn")
self._input_queue = ctx.Queue(maxsize=1)
self._output_queue = ctx.Queue(maxsize=4)
self._stop_event = ctx.Event()
self._process = ctx.Process(
target=run_worker,
args=(
self._model_path,
self._input_queue,
self._output_queue,
self._stop_event,
logging.WARNING,
),
daemon=True,
name="inference-worker",
)
self._process.start()
self._busy = False
self._last_result_time = time.monotonic()
self._poll_timer.start()
self._watchdog_timer.start()
logger.info(
"Inference worker started (pid=%d, model=%s)",
self._process.pid, self._model_path,
)
self.inference_started.emit()
def _stop_worker(self) -> None:
self._poll_timer.stop()
self._watchdog_timer.stop()
if self._stop_event is not None:
self._stop_event.set()
if self._process is not None:
self._process.join(timeout=3.0)
if self._process.is_alive():
logger.warning("Worker did not stop cleanly — terminating")
self._process.terminate()
self._process.join(timeout=2.0)
self._process = None
self._input_queue = None
self._output_queue = None
self._stop_event = None
self._busy = False
logger.info("Inference worker stopped")
self.inference_stopped.emit()
# ------------------------------------------------------------------
# Private — timers
# ------------------------------------------------------------------
@Slot()
def _poll_output(self) -> None:
"""Drain the output queue (called every INFERENCE_POLL_INTERVAL_MS ms)."""
if self._output_queue is None:
return
try:
while True:
item = self._output_queue.get_nowait()
if item is None:
# Worker signalled a fatal load error
logger.error("Worker reported model load failure")
self._handle_crash("Model failed to load in worker process")
return
# ----------------------------------------------------------
# WorkerReadyPacket — sent once after model load
# ----------------------------------------------------------
if isinstance(item, WorkerReadyPacket):
self._current_device = item.device
logger.info("Inference device: %s", item.device)
self.inference_device_changed.emit(item.device)
continue
# ----------------------------------------------------------
# ResultPacket — regular inference result
# ----------------------------------------------------------
packet: ResultPacket = item
self._busy = False
self._last_result_time = time.monotonic()
# Update rolling average of elapsed time
self._elapsed_window.append(packet.elapsed_ms)
avg_ms = sum(self._elapsed_window) / len(self._elapsed_window)
detections = [
Detection(x1, y1, x2, y2, conf, label)
for x1, y1, x2, y2, conf, label in packet.detections
]
source_size = (packet.width, packet.height)
if detections:
self._detection_frame_count += 1
conf_summary = ", ".join(
f"{d.label} {d.conf:.2f}" for d in detections
)
logger.info(
"frame %d: %d detection(s) in %.1f ms — %s",
packet.frame_id,
len(detections),
packet.elapsed_ms,
conf_summary,
)
self.detection_count_updated.emit(self._detection_frame_count)
# Always emit stats so overlay stays current
self.inference_stats_updated.emit(self._current_device, avg_ms)
self.detections_ready.emit(detections, source_size)
except Exception:
# Empty queue — normal
pass
@Slot()
def _watchdog_check(self) -> None:
"""Detect crashed or hung worker process."""
if self._process is None:
return
# Process died unexpectedly
if not self._process.is_alive():
exit_code = self._process.exitcode
logger.error("Worker process died (exitcode=%s)", exit_code)
self._handle_crash(f"Worker process exited with code {exit_code}")
return
# Worker alive but hasn't responded for too long (hung during inference)
if self._busy:
elapsed = time.monotonic() - self._last_result_time
if elapsed > INFERENCE_WORKER_TIMEOUT_S:
logger.error(
"Worker timeout: no response for %.1f s — restarting", elapsed
)
self._process.terminate()
self._process.join(timeout=2.0)
self._handle_crash("Worker timed out (hung during inference)")
def _handle_crash(self, reason: str) -> None:
"""Decide whether to auto-restart or give up."""
self._poll_timer.stop()
self._watchdog_timer.stop()
self._process = None
self._busy = False
if self._restart_count < INFERENCE_MAX_RESTARTS:
self._restart_count += 1
logger.warning(
"Auto-restarting worker (attempt %d/%d): %s",
self._restart_count, INFERENCE_MAX_RESTARTS, reason,
)
self._start_worker()
else:
msg = (
f"Inference worker failed after {INFERENCE_MAX_RESTARTS} restarts. "
f"Last error: {reason}"
)
logger.error(msg)
self.inference_error.emit(msg)

118
app/logging_setup.py Normal file
View File

@@ -0,0 +1,118 @@
"""Logging initialisation — file + console handlers with session isolation."""
from __future__ import annotations
import logging
import platform
import sys
from pathlib import Path
from app.config import APP_NAME, APP_VERSION, MAX_LOG_FILES
_LOG_FORMAT = "%(asctime)s.%(msecs)03d [%(levelname)-7s] %(name)s: %(message)s"
_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
_console_handler: logging.StreamHandler | None = None
def setup_logging(log_dir: Path, session_id: str) -> Path:
"""
Configure the root logger for a new session.
Creates:
<log_dir>/duck-preview_<session_id>.log — DEBUG level, all messages
console StreamHandler — WARNING by default
Prunes oldest log files when count exceeds MAX_LOG_FILES.
Args:
log_dir: Directory where log files are stored.
session_id: Timestamp string used as filename suffix (e.g. "2026-05-12_14-30-00").
Returns:
Path to the created log file.
"""
global _console_handler
log_dir.mkdir(parents=True, exist_ok=True)
log_path = log_dir / f"duck-preview_{session_id}.log"
root = logging.getLogger()
root.setLevel(logging.DEBUG) # handlers filter individually
formatter = logging.Formatter(_LOG_FORMAT, datefmt=_DATE_FORMAT)
# --- File handler — always DEBUG ---
file_handler = logging.FileHandler(log_path, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
root.addHandler(file_handler)
# --- Console handler — WARNING by default, toggled by Debug menu ---
_console_handler = logging.StreamHandler(sys.stdout)
_console_handler.setLevel(logging.WARNING)
_console_handler.setFormatter(formatter)
root.addHandler(_console_handler)
# Write session header to file
_write_session_header(log_path, session_id)
# Prune old log files
_prune_old_logs(log_dir, log_path)
return log_path
def set_console_level(debug: bool) -> None:
"""Toggle console handler between DEBUG and WARNING (called from Debug menu)."""
if _console_handler is not None:
_console_handler.setLevel(logging.DEBUG if debug else logging.WARNING)
def _write_session_header(log_path: Path, session_id: str) -> None:
"""Write a human-readable header block at the top of the log file."""
try:
import PySide6
pyside_version = PySide6.__version__
except Exception:
pyside_version = "unknown"
try:
import psutil
mem = psutil.virtual_memory()
cpu_count = psutil.cpu_count(logical=True)
mem_total_gb = mem.total / (1024 ** 3)
hw_info = f"{cpu_count} logical CPUs, {mem_total_gb:.1f} GB RAM"
except Exception:
hw_info = "unknown"
lines = [
"=" * 72,
f" {APP_NAME} {APP_VERSION}",
f" Session: {session_id}",
"=" * 72,
f" Platform : {platform.platform()}",
f" Python : {sys.version.split()[0]}",
f" PySide6 : {pyside_version}",
f" Hardware : {hw_info}",
f" Log file : {log_path.resolve()}",
"=" * 72,
"",
]
with log_path.open("w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
def _prune_old_logs(log_dir: Path, current: Path) -> None:
"""Delete oldest .log files if total count exceeds MAX_LOG_FILES."""
log_files = sorted(
[p for p in log_dir.glob("duck-preview_*.log") if p != current],
key=lambda p: p.stat().st_mtime,
)
excess = len(log_files) - (MAX_LOG_FILES - 1)
for path in log_files[:excess]:
try:
path.unlink()
logging.getLogger(__name__).debug("Pruned old log: %s", path.name)
except OSError:
pass

41
app/main.py Normal file
View File

@@ -0,0 +1,41 @@
"""Application entry point."""
from __future__ import annotations
import sys
from datetime import datetime
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication
from app.config import APP_NAME, LOG_DIR
from app.logging_setup import setup_logging
def main() -> None:
session_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
log_path = setup_logging(LOG_DIR, session_id)
# Import after logging is ready so module-level loggers work correctly
import logging # noqa: PLC0415
from app.ui.main_window import MainWindow # noqa: PLC0415
logger = logging.getLogger(__name__)
logger.info("Application starting (session: %s)", session_id)
app = QApplication(sys.argv)
app.setApplicationName(APP_NAME)
app.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
window = MainWindow(log_path=log_path)
window.show()
logger.info("Application shutting down")
sys.exit(app.exec())
if __name__ == "__main__":
main()

0
app/overlay/__init__.py Normal file
View File

View File

@@ -0,0 +1,54 @@
"""IOverlayLayer — interface for pluggable overlay layers drawn on CameraView."""
from __future__ import annotations
from abc import ABC, abstractmethod
from PySide6.QtCore import QRect
from PySide6.QtGui import QPainter
class IOverlayLayer(ABC):
"""
Interface for a single overlay layer drawn over the camera frame.
Each layer receives the active QPainter and the video rect (the letterboxed
area where the camera image was drawn) so it can position elements relative
to the actual video content if needed.
To add a new overlay (e.g. YOLO bboxes):
1. Subclass IOverlayLayer.
2. Implement paint().
3. Register with CameraView.add_overlay_layer().
No Qt subclassing is required — layers are plain Python objects.
"""
@property
def name(self) -> str:
"""Human-readable identifier used in menus / debug output."""
return type(self).__name__
@property
def visible(self) -> bool:
"""Whether this layer should be drawn."""
return self._visible
@visible.setter
def visible(self, value: bool) -> None:
self._visible = value
def __init__(self) -> None:
self._visible: bool = True
@abstractmethod
def paint(self, painter: QPainter, video_rect: QRect) -> None:
"""
Draw this layer.
Args:
painter: Active QPainter on the CameraView widget.
Caller saves/restores painter state around each layer.
video_rect: The QRect where the camera image was drawn (letterboxed).
Use this to position overlays relative to the video image.
"""

View File

@@ -0,0 +1,117 @@
"""TelemetryOverlay — draws the performance metrics box on the camera view."""
from __future__ import annotations
from PySide6.QtCore import QRect, Qt, Slot
from PySide6.QtGui import QColor, QFont, QPainter, QPen
from app.config import (
OVERLAY_BG_COLOR,
OVERLAY_FONT_SIZE,
OVERLAY_MARGIN,
OVERLAY_PADDING,
OVERLAY_TEXT_COLOR,
)
from app.overlay.overlay_layer import IOverlayLayer
from app.telemetry.telemetry_collector import TelemetrySnapshot
class TelemetryOverlay(IOverlayLayer):
"""
Renders a semi-transparent metrics box in the top-left corner.
Usage:
overlay = TelemetryOverlay()
camera_view.add_overlay_layer(overlay)
telemetry_collector.metrics_updated.connect(overlay.on_metrics_updated)
Display format:
FPS req 60.0 ← what was requested from camera
FPS got 30.2 ← what camera actually delivered
Frame 33.1 ms
Drop 0
CPU sys 14.8 % ← normalised by cpu_count (matches Task Manager)
CPU core 118.4 % ← per single core (can exceed 100%)
Mem 68 MB
Inf.dev mps ← inference device (only when model loaded)
Inf.time 87 ms ← rolling average of model() call time
"""
def __init__(self) -> None:
super().__init__()
self._snapshot: TelemetrySnapshot | None = None
self._font = QFont("Monospace")
self._font.setStyleHint(QFont.StyleHint.TypeWriter)
self._font.setPointSize(OVERLAY_FONT_SIZE)
self._font.setBold(False)
@Slot(object)
def on_metrics_updated(self, snapshot: TelemetrySnapshot) -> None:
"""Receive a new snapshot from TelemetryCollector."""
self._snapshot = snapshot
# ------------------------------------------------------------------
# IOverlayLayer
# ------------------------------------------------------------------
def paint(self, painter: QPainter, video_rect: QRect) -> None:
if self._snapshot is None:
return
lines = self._format_lines(self._snapshot)
if not lines:
return
painter.setFont(self._font)
fm = painter.fontMetrics()
line_height = fm.height()
max_width = max(fm.horizontalAdvance(line) for line in lines)
box_w = max_width + OVERLAY_PADDING * 2
box_h = line_height * len(lines) + OVERLAY_PADDING * 2
x = video_rect.left() + OVERLAY_MARGIN
y = video_rect.top() + OVERLAY_MARGIN
# Background
painter.setBrush(QColor(*OVERLAY_BG_COLOR))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawRoundedRect(QRect(x, y, box_w, box_h), 6, 6)
# Text
painter.setPen(QPen(QColor(*OVERLAY_TEXT_COLOR)))
text_x = x + OVERLAY_PADDING
text_y = y + OVERLAY_PADDING + fm.ascent()
for line in lines:
painter.drawText(text_x, text_y, line)
text_y += line_height
# ------------------------------------------------------------------
# Private
# ------------------------------------------------------------------
@staticmethod
def _format_lines(snap: TelemetrySnapshot) -> list[str]:
lines: list[str] = []
# FPS — show target if known, then actual
if snap.target_fps is not None:
lines.append(f"FPS req {snap.target_fps:>6.1f}")
lines.append(f"FPS got {snap.fps:>6.1f}")
lines.append(f"Frame {snap.frame_time_ms:>6.1f} ms")
lines.append(f"Drop {snap.dropped_frames:>6d}")
lines.append(f"CPU sys {snap.cpu_percent_sys:>5.1f} %")
lines.append(f"CPU core {snap.cpu_percent_core:>5.1f} %")
if snap.memory_mb is not None:
lines.append(f"Mem {snap.memory_mb:>5.0f} MB")
if snap.inference_device is not None:
lines.append(f"Inf.dev {snap.inference_device:>6s}")
if snap.inference_time_ms is not None:
lines.append(f"Inf.time {snap.inference_time_ms:>5.0f} ms")
return lines

0
app/pipeline/__init__.py Normal file
View File

View File

@@ -0,0 +1,111 @@
"""Frame Dispatcher — distributes QVideoFrames to registered subscribers."""
from __future__ import annotations
import logging
import time
from collections.abc import Callable
from dataclasses import dataclass, field
from PySide6.QtCore import QObject, Slot
from PySide6.QtMultimedia import QVideoFrame
logger = logging.getLogger(__name__)
FrameCallback = Callable[[QVideoFrame], None]
@dataclass
class _Subscriber:
callback: FrameCallback
drop_if_busy: bool = True
_busy: bool = field(default=False, init=False, repr=False)
class FrameDispatcher(QObject):
"""
Receives frames from CameraService and fans them out to all subscribers.
Each subscriber is a callable (QVideoFrame) -> None.
Subscribers that set drop_if_busy=True will skip a frame if they are still
processing the previous one (non-blocking). Subscribers with drop_if_busy=False
always receive every frame.
All dispatch happens in the GUI thread (Qt signal/slot), so subscribers
must NOT perform heavy work directly — they should queue to a worker thread
if processing is needed.
"""
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
self._subscribers: list[_Subscriber] = []
self._frame_count: int = 0
self._last_dispatch_time: float = 0.0
# ------------------------------------------------------------------
# Subscription API
# ------------------------------------------------------------------
def subscribe(self, callback: FrameCallback, *, drop_if_busy: bool = True) -> None:
"""Register a frame callback.
Args:
callback: Callable that receives QVideoFrame.
drop_if_busy: When True, frame is skipped if subscriber is still
marked busy from last call (default True).
"""
for sub in self._subscribers:
if sub.callback is callback:
logger.warning("Subscriber %r already registered", callback)
return
self._subscribers.append(_Subscriber(callback=callback, drop_if_busy=drop_if_busy))
logger.debug("Subscriber added: %r (drop_if_busy=%s)", callback, drop_if_busy)
def unsubscribe(self, callback: FrameCallback) -> None:
"""Remove a previously registered callback."""
before = len(self._subscribers)
self._subscribers = [s for s in self._subscribers if s.callback is not callback]
if len(self._subscribers) < before:
logger.debug("Subscriber removed: %r", callback)
else:
logger.debug("Subscriber not found for removal: %r", callback)
def subscriber_count(self) -> int:
return len(self._subscribers)
# ------------------------------------------------------------------
# Frame intake — connect CameraService.frame_ready to this slot
# ------------------------------------------------------------------
@Slot(QVideoFrame)
def dispatch(self, frame: QVideoFrame) -> None:
"""Distribute the frame to all registered subscribers."""
self._frame_count += 1
now = time.perf_counter()
self._last_dispatch_time = now
for sub in self._subscribers:
if sub.drop_if_busy and sub._busy:
logger.debug("Dropping frame for busy subscriber %r", sub.callback)
continue
sub._busy = True
try:
sub.callback(frame)
except Exception:
logger.exception("Error in frame subscriber %r", sub.callback)
finally:
sub._busy = False
# ------------------------------------------------------------------
# Stats
# ------------------------------------------------------------------
@property
def frame_count(self) -> int:
return self._frame_count
@property
def last_dispatch_time(self) -> float:
"""perf_counter timestamp of the last dispatched frame."""
return self._last_dispatch_time

View File

104
app/telemetry/csv_logger.py Normal file
View File

@@ -0,0 +1,104 @@
"""CsvTelemetryLogger — writes telemetry snapshots to a CSV file with throttling.
Design decisions:
- Does NOT use the logging module — writes directly via csv.writer so the file
is readable independently of the text log level.
- Flushes after every row so the file is intact even on crash or force-quit.
- Throttle: only one row per TELEMETRY_CSV_INTERVAL_S seconds, even if
metrics_updated fires every 500 ms. This keeps the file manageable for
long sessions (8 h @ 5 s interval = 5 760 rows).
"""
from __future__ import annotations
import csv
import time
from datetime import datetime
from pathlib import Path
from app.config import TELEMETRY_CSV_INTERVAL_S
from app.telemetry.telemetry_collector import TelemetrySnapshot
_CSV_HEADER = [
"timestamp",
"fps_got",
"fps_req",
"frame_time_ms",
"dropped_frames",
"cpu_sys_pct",
"cpu_core_pct",
"mem_mb",
]
class CsvTelemetryLogger:
"""
Receives TelemetrySnapshot objects and writes throttled rows to a CSV file.
Usage:
logger = CsvTelemetryLogger(path)
telemetry_collector.metrics_updated.connect(logger.on_metrics_updated)
# call logger.close() on application exit
"""
def __init__(
self,
path: Path,
interval_s: float = TELEMETRY_CSV_INTERVAL_S,
) -> None:
self._interval_s = interval_s
self._last_write_time: float = 0.0
self._rows_written: int = 0
path.parent.mkdir(parents=True, exist_ok=True)
self._file = path.open("w", newline="", encoding="utf-8")
self._writer = csv.writer(self._file)
self._writer.writerow(_CSV_HEADER)
self._file.flush()
# ------------------------------------------------------------------
# Slot — connect to TelemetryCollector.metrics_updated
# ------------------------------------------------------------------
def on_metrics_updated(self, snapshot: TelemetrySnapshot) -> None:
"""Write a row if the throttle interval has elapsed."""
now = time.monotonic()
if now - self._last_write_time < self._interval_s:
return
self._last_write_time = now
self._write_row(snapshot)
# ------------------------------------------------------------------
# Lifecycle
# ------------------------------------------------------------------
def close(self) -> None:
"""Flush and close the CSV file. Call on application shutdown."""
try:
self._file.flush()
self._file.close()
except OSError:
pass
@property
def rows_written(self) -> int:
return self._rows_written
# ------------------------------------------------------------------
# Private
# ------------------------------------------------------------------
def _write_row(self, snap: TelemetrySnapshot) -> None:
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3] # HH:MM:SS.mmm
self._writer.writerow([
ts,
f"{snap.fps:.1f}",
f"{snap.target_fps:.1f}" if snap.target_fps is not None else "",
f"{snap.frame_time_ms:.2f}",
snap.dropped_frames,
f"{snap.cpu_percent_sys:.1f}",
f"{snap.cpu_percent_core:.1f}",
f"{snap.memory_mb:.1f}" if snap.memory_mb is not None else "",
])
self._file.flush()
self._rows_written += 1

View File

@@ -0,0 +1,215 @@
"""Telemetry Collector — measures video pipeline performance metrics."""
from __future__ import annotations
import time
from collections import deque
from dataclasses import dataclass
import psutil
from PySide6.QtCore import QObject, QTimer, Signal
from PySide6.QtMultimedia import QVideoFrame
from app.config import TELEMETRY_UPDATE_INTERVAL_MS
@dataclass
class TelemetrySnapshot:
"""Immutable snapshot of current performance metrics."""
fps: float # actual frames received in the last second
target_fps: float | None # FPS requested from the camera (None = unknown)
frame_time_ms: float # average inter-frame time in ms
dropped_frames: int # cumulative dropped frames detected
cpu_percent_sys: float # process CPU as % of total system capacity
# (divided by cpu_count) — matches Task Manager
cpu_percent_core: float # process CPU per single core — can exceed 100%
memory_mb: float | None # process private working set in MB
timestamp: float # time.perf_counter() when snapshot was taken
# Inference fields — None when inference is disabled / model not loaded
inference_device: str | None = None # e.g. "cpu", "mps"
inference_time_ms: float | None = None # rolling average of model() call time
class TelemetryCollector(QObject):
"""
Frame subscriber that measures pipeline performance.
Connect to FrameDispatcher:
dispatcher.subscribe(collector.on_frame, drop_if_busy=False)
Receive target FPS updates from CameraService:
camera_service.format_changed.connect(collector.set_target_fps)
Listen to metrics updates:
collector.metrics_updated.connect(my_slot)
"""
metrics_updated = Signal(object) # emits TelemetrySnapshot
def __init__(
self,
update_interval_ms: int = TELEMETRY_UPDATE_INTERVAL_MS,
parent: QObject | None = None,
) -> None:
super().__init__(parent)
self._update_interval_ms = update_interval_ms
self._target_fps: float | None = None
# frame timing ring-buffer (last 120 samples)
self._frame_times: deque[float] = deque(maxlen=120)
self._last_frame_time: float = 0.0
self._total_frames: int = 0
self._dropped_frames: int = 0
# FPS window — count frames in the last second
self._fps_window: deque[float] = deque()
self._fps_window_size_s: float = 1.0
# psutil — initialise baseline so first real reading is non-zero
self._process = psutil.Process()
self._process.cpu_percent() # first call always returns 0.0; discard
self._cpu_count: int = max(psutil.cpu_count(logical=True) or 1, 1)
# Inference stats (updated externally via set_inference_stats)
self._inference_device: str | None = None
self._inference_time_ms: float | None = None
# periodic snapshot timer
self._timer = QTimer(self)
self._timer.setInterval(update_interval_ms)
self._timer.timeout.connect(self._emit_snapshot)
self._timer.start()
self._latest: TelemetrySnapshot = self._make_empty_snapshot()
# ------------------------------------------------------------------
# Configuration
# ------------------------------------------------------------------
def set_target_fps(self, fps: float | None) -> None:
"""Record the FPS that was requested from the camera."""
self._target_fps = fps
def set_inference_stats(self, device: str, avg_ms: float) -> None:
"""Update inference device and average inference time (called from MainWindow)."""
self._inference_device: str | None = device
self._inference_time_ms: float | None = avg_ms
def clear_inference_stats(self) -> None:
"""Clear inference stats when inference is disabled."""
self._inference_device = None
self._inference_time_ms = None
# ------------------------------------------------------------------
# Frame subscriber callback
# ------------------------------------------------------------------
def on_frame(self, frame: QVideoFrame) -> None:
"""Called by FrameDispatcher for every frame. Must be fast."""
now = time.perf_counter()
if self._last_frame_time > 0:
delta = now - self._last_frame_time
self._frame_times.append(delta)
# drop detection: gap > 2.5× rolling average
if len(self._frame_times) >= 5:
avg = sum(self._frame_times) / len(self._frame_times)
if delta > avg * 2.5:
self._dropped_frames += 1
self._last_frame_time = now
self._total_frames += 1
self._fps_window.append(now)
cutoff = now - self._fps_window_size_s
while self._fps_window and self._fps_window[0] < cutoff:
self._fps_window.popleft()
# ------------------------------------------------------------------
# Snapshot
# ------------------------------------------------------------------
def latest_snapshot(self) -> TelemetrySnapshot:
return self._latest
def reset_counters(self) -> None:
"""Reset cumulative counters (e.g. after camera switch)."""
self._frame_times.clear()
self._fps_window.clear()
self._last_frame_time = 0.0
self._total_frames = 0
self._dropped_frames = 0
# ------------------------------------------------------------------
# Internal
# ------------------------------------------------------------------
def _emit_snapshot(self) -> None:
snapshot = self._compute_snapshot()
self._latest = snapshot
self.metrics_updated.emit(snapshot)
def _compute_snapshot(self) -> TelemetrySnapshot:
now = time.perf_counter()
# FPS — prune stale entries before counting
cutoff = now - self._fps_window_size_s
while self._fps_window and self._fps_window[0] < cutoff:
self._fps_window.popleft()
fps = float(len(self._fps_window))
# average frame time
avg_frame_time_ms = (
(sum(self._frame_times) / len(self._frame_times)) * 1000.0
if self._frame_times
else 0.0
)
# CPU — per-core reading, then derive system-normalised value
try:
cpu_core = self._process.cpu_percent()
except Exception:
cpu_core = 0.0
cpu_sys = cpu_core / self._cpu_count
# Memory — private working set (Windows) or RSS (macOS/Linux)
try:
mem_info = self._process.memory_info()
mem_bytes = getattr(mem_info, "wset", None) or mem_info.rss
mem_mb: float | None = mem_bytes / (1024 * 1024)
except Exception:
mem_mb = None
return TelemetrySnapshot(
fps=round(fps, 1),
target_fps=self._target_fps,
frame_time_ms=round(avg_frame_time_ms, 2),
dropped_frames=self._dropped_frames,
cpu_percent_sys=round(cpu_sys, 1),
cpu_percent_core=round(cpu_core, 1),
memory_mb=round(mem_mb, 1) if mem_mb is not None else None,
timestamp=now,
inference_device=self._inference_device,
inference_time_ms=(
round(self._inference_time_ms, 1)
if self._inference_time_ms is not None
else None
),
)
def _make_empty_snapshot(self) -> TelemetrySnapshot:
return TelemetrySnapshot(
fps=0.0,
target_fps=self._target_fps,
frame_time_ms=0.0,
dropped_frames=0,
cpu_percent_sys=0.0,
cpu_percent_core=0.0,
memory_mb=None,
timestamp=time.perf_counter(),
inference_device=None,
inference_time_ms=None,
)

0
app/ui/__init__.py Normal file
View File

View File

@@ -0,0 +1,310 @@
"""Camera Settings dialog — sliders for UVC controls + Qt WhiteBalance/Exposure."""
from __future__ import annotations
import logging
from PySide6.QtCore import Qt
from PySide6.QtMultimedia import QCamera
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
QFormLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QScrollArea,
QSlider,
QSpinBox,
QVBoxLayout,
QWidget,
)
from app.camera.uvc.base import UvcControllerBase, UvcParam
logger = logging.getLogger(__name__)
# Human-readable labels for each UVC parameter
_PARAM_LABELS: dict[UvcParam, str] = {
UvcParam.BRIGHTNESS: "Brightness",
UvcParam.CONTRAST: "Contrast",
UvcParam.SATURATION: "Saturation",
UvcParam.HUE: "Hue",
UvcParam.SHARPNESS: "Sharpness",
UvcParam.GAMMA: "Gamma",
UvcParam.WHITE_BALANCE: "White Balance (K)",
UvcParam.BACKLIGHT_COMPENSATION: "Backlight Compensation",
UvcParam.EXPOSURE: "Exposure Time",
}
# Qt WhiteBalance modes shown in the combo
_WB_MODES: list[tuple[str, QCamera.WhiteBalanceMode]] = [
("Auto", QCamera.WhiteBalanceMode.WhiteBalanceAuto),
("Sunlight", QCamera.WhiteBalanceMode.WhiteBalanceSunlight),
("Cloudy", QCamera.WhiteBalanceMode.WhiteBalanceCloudy),
("Shade", QCamera.WhiteBalanceMode.WhiteBalanceShade),
("Tungsten", QCamera.WhiteBalanceMode.WhiteBalanceTungsten),
("Fluorescent", QCamera.WhiteBalanceMode.WhiteBalanceFluorescent),
("Flash", QCamera.WhiteBalanceMode.WhiteBalanceFlash),
("Sunset", QCamera.WhiteBalanceMode.WhiteBalanceSunset),
("Manual (K)", QCamera.WhiteBalanceMode.WhiteBalanceManual),
]
_EXPOSURE_MODES: list[tuple[str, QCamera.ExposureMode]] = [
("Auto", QCamera.ExposureMode.ExposureAuto),
("Manual", QCamera.ExposureMode.ExposureManual),
]
class CameraSettingsDialog(QDialog):
"""
Modal dialog for camera image controls.
Sections:
• Qt controls — WhiteBalance mode + colour temperature, Exposure mode + time
• UVC controls — sliders for Brightness, Contrast, Saturation, Hue,
Sharpness, Gamma, White Balance (manual K), Backlight,
Exposure (if UVC controller is open)
Changes are applied live to the camera.
"""
def __init__(
self,
camera: QCamera,
uvc: UvcControllerBase,
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
self.setWindowTitle("Camera Settings")
self.setMinimumWidth(440)
self._camera = camera
self._uvc = uvc
outer = QVBoxLayout(self)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
outer.addWidget(scroll)
content = QWidget()
scroll.setWidget(content)
layout = QVBoxLayout(content)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
layout.addWidget(self._build_qt_group())
uvc_group = self._build_uvc_group()
if uvc_group is not None:
layout.addWidget(uvc_group)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
buttons.rejected.connect(self.accept)
outer.addWidget(buttons)
# ------------------------------------------------------------------
# Qt controls section
# ------------------------------------------------------------------
def _build_qt_group(self) -> QGroupBox:
group = QGroupBox("Qt Camera Controls")
form = QFormLayout(group)
# White Balance mode
self._wb_combo = QComboBox()
for label, mode in _WB_MODES:
self._wb_combo.addItem(label, mode)
current_wb = self._camera.whiteBalanceMode()
for i, (_, mode) in enumerate(_WB_MODES):
if mode == current_wb:
self._wb_combo.setCurrentIndex(i)
break
self._wb_combo.currentIndexChanged.connect(self._on_wb_mode_changed)
form.addRow("White Balance:", self._wb_combo)
# Colour temperature (manual only)
self._temp_label = QLabel("Colour Temp (K):")
self._temp_spin = QSpinBox()
self._temp_spin.setRange(2000, 10000)
self._temp_spin.setSingleStep(100)
current_temp = self._camera.colorTemperature()
self._temp_spin.setValue(current_temp if current_temp >= 2000 else 5500)
self._temp_spin.valueChanged.connect(self._on_color_temp_changed)
form.addRow(self._temp_label, self._temp_spin)
self._update_temp_visibility()
form.addRow(QLabel("")) # spacer
# Exposure mode
self._exp_combo = QComboBox()
for label, mode in _EXPOSURE_MODES:
self._exp_combo.addItem(label, mode)
current_exp = self._camera.exposureMode()
for i, (_, mode) in enumerate(_EXPOSURE_MODES):
if mode == current_exp:
self._exp_combo.setCurrentIndex(i)
break
self._exp_combo.currentIndexChanged.connect(self._on_exp_mode_changed)
form.addRow("Exposure Mode:", self._exp_combo)
# Manual exposure time
self._exp_label = QLabel("Exposure Time (s):")
self._exp_spin = QDoubleSpinBox()
exp_min = self._camera.minimumExposureTime()
exp_max = self._camera.maximumExposureTime()
self._exp_spin.setRange(
exp_min if exp_min > 0 else 1e-6,
exp_max if exp_max > 0 else 1.0,
)
self._exp_spin.setDecimals(6)
self._exp_spin.setSingleStep(0.001)
manual_t = self._camera.manualExposureTime()
self._exp_spin.setValue(manual_t if manual_t > 0 else 0.033)
self._exp_spin.valueChanged.connect(self._on_exp_time_changed)
form.addRow(self._exp_label, self._exp_spin)
self._update_exp_visibility()
return group
# ------------------------------------------------------------------
# UVC controls section
# ------------------------------------------------------------------
def _build_uvc_group(self) -> QGroupBox | None:
params = self._uvc.get_all_params()
supported = [p for p in params if p.supported]
group = QGroupBox("UVC Camera Controls")
form = QFormLayout(group)
if not supported:
note = QLabel(
"UVC controls not available.\n"
"Install duvc-ctl (Windows) or pyuvc (macOS) to enable."
)
note.setEnabled(False)
form.addRow(note)
return group
self._uvc_sliders: dict[UvcParam, QSlider] = {}
self._uvc_spins: dict[UvcParam, QSpinBox] = {}
self._uvc_auto_boxes: dict[UvcParam, QCheckBox] = {}
for info in params:
label = _PARAM_LABELS.get(info.param, info.param.name.title())
if not info.supported:
lbl = QLabel("Not supported")
lbl.setEnabled(False)
form.addRow(f"{label}:", lbl)
continue
row_widget = QWidget()
row_layout = QHBoxLayout(row_widget)
row_layout.setContentsMargins(0, 0, 0, 0)
slider = QSlider(Qt.Orientation.Horizontal)
slider.setRange(info.minimum, info.maximum)
slider.setValue(info.current)
slider.setSingleStep(max(1, info.step))
spin = QSpinBox()
spin.setRange(info.minimum, info.maximum)
spin.setValue(info.current)
spin.setFixedWidth(75)
# Keep slider ↔ spin in sync
slider.valueChanged.connect(spin.setValue)
spin.valueChanged.connect(slider.setValue)
slider.valueChanged.connect(
lambda v, p=info.param: self._on_uvc_value(p, v)
)
row_layout.addWidget(slider)
row_layout.addWidget(spin)
if info.auto_supported:
auto_box = QCheckBox("Auto")
auto_box.setChecked(info.auto_enabled)
auto_box.toggled.connect(
lambda checked, p=info.param: self._on_uvc_auto(p, checked)
)
row_layout.addWidget(auto_box)
self._uvc_auto_boxes[info.param] = auto_box
# Init enabled state
slider.setEnabled(not info.auto_enabled)
spin.setEnabled(not info.auto_enabled)
self._uvc_sliders[info.param] = slider
self._uvc_spins[info.param] = spin
form.addRow(f"{label}:", row_widget)
return group
# ------------------------------------------------------------------
# Qt control slots
# ------------------------------------------------------------------
def _on_wb_mode_changed(self, index: int) -> None:
mode: QCamera.WhiteBalanceMode = self._wb_combo.itemData(index)
if self._camera.isWhiteBalanceModeSupported(mode):
self._camera.setWhiteBalanceMode(mode)
logger.debug("WB mode: %s", mode)
else:
logger.debug("WB mode %s not supported by this camera", mode)
self._update_temp_visibility()
def _on_color_temp_changed(self, value: int) -> None:
self._camera.setColorTemperature(value)
logger.debug("Colour temp: %d K", value)
def _on_exp_mode_changed(self, index: int) -> None:
mode: QCamera.ExposureMode = self._exp_combo.itemData(index)
if self._camera.isExposureModeSupported(mode):
self._camera.setExposureMode(mode)
logger.debug("Exposure mode: %s", mode)
self._update_exp_visibility()
def _on_exp_time_changed(self, value: float) -> None:
self._camera.setManualExposureTime(value)
logger.debug("Exposure time: %.6f s", value)
def _update_temp_visibility(self) -> None:
manual = (
self._wb_combo.currentData()
== QCamera.WhiteBalanceMode.WhiteBalanceManual
)
self._temp_label.setVisible(manual)
self._temp_spin.setVisible(manual)
def _update_exp_visibility(self) -> None:
manual = (
self._exp_combo.currentData()
== QCamera.ExposureMode.ExposureManual
)
self._exp_label.setVisible(manual)
self._exp_spin.setVisible(manual)
# ------------------------------------------------------------------
# UVC control slots
# ------------------------------------------------------------------
def _on_uvc_value(self, param: UvcParam, value: int) -> None:
self._uvc.set_value(param, value)
def _on_uvc_auto(self, param: UvcParam, enabled: bool) -> None:
self._uvc.set_auto(param, enabled)
slider = self._uvc_sliders.get(param)
spin = self._uvc_spins.get(param)
if slider:
slider.setEnabled(not enabled)
if spin:
spin.setEnabled(not enabled)

144
app/ui/camera_view.py Normal file
View File

@@ -0,0 +1,144 @@
"""CameraView — QWidget that renders camera frames and composites overlay layers.
Responsibilities:
- Receive QVideoFrame, convert to QImage, schedule repaint.
- In paintEvent: draw the frame (letterboxed) then call paint() on each
registered IOverlayLayer in order.
- Know nothing about what specific overlays draw — that is entirely up to
each IOverlayLayer implementation.
Adding a new overlay (e.g. YOLO bboxes):
layer = YoloBboxOverlay()
camera_view.add_overlay_layer(layer)
yolo_processor.results_ready.connect(layer.on_results)
No modification to CameraView is needed.
"""
from __future__ import annotations
import logging
from PySide6.QtCore import QRect, Qt, Slot
from PySide6.QtGui import QImage, QPainter
from PySide6.QtMultimedia import QVideoFrame
from PySide6.QtWidgets import QWidget
from app.overlay.overlay_layer import IOverlayLayer
logger = logging.getLogger(__name__)
class CameraView(QWidget):
"""
Camera preview widget.
Frame pipeline:
CameraService.frame_ready(QVideoFrame)
→ CameraView.on_frame() — convert to QImage, schedule update()
→ paintEvent() — draw image + all overlay layers
"""
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setStyleSheet("background: black;")
self._current_image: QImage | None = None
self._video_rect: QRect = QRect()
self._overlay_layers: list[IOverlayLayer] = []
# ------------------------------------------------------------------
# Overlay layer registry
# ------------------------------------------------------------------
def add_overlay_layer(self, layer: IOverlayLayer) -> None:
"""Register an overlay layer. Layers are painted in registration order."""
if layer not in self._overlay_layers:
self._overlay_layers.append(layer)
logger.debug("Overlay layer added: %s", layer.name)
def remove_overlay_layer(self, layer: IOverlayLayer) -> None:
"""Unregister an overlay layer."""
try:
self._overlay_layers.remove(layer)
logger.debug("Overlay layer removed: %s", layer.name)
except ValueError:
pass
def set_all_overlays_visible(self, visible: bool) -> None:
"""Show or hide all registered overlay layers at once."""
for layer in self._overlay_layers:
layer.visible = visible
self.update()
# ------------------------------------------------------------------
# Frame input
# ------------------------------------------------------------------
@Slot(QVideoFrame)
def on_frame(self, frame: QVideoFrame) -> None:
"""Receive a camera frame, convert to QImage, schedule repaint."""
if not frame.isValid():
return
image = frame.toImage()
if image.isNull():
return
# Convert to Format_RGB32 — QPainter's fastest path on all platforms
if image.format() != QImage.Format.Format_RGB32:
image = image.convertToFormat(QImage.Format.Format_RGB32)
self._current_image = image
self.update()
# ------------------------------------------------------------------
# Qt paint
# ------------------------------------------------------------------
def paintEvent(self, event) -> None: # noqa: N802
painter = QPainter(self)
# 1. Background
painter.fillRect(self.rect(), Qt.GlobalColor.black)
# 2. Camera frame — letterboxed
if self._current_image is not None:
img = self._current_image
self._video_rect = _letterbox_rect(
img.width(), img.height(), self.width(), self.height()
)
painter.drawImage(self._video_rect, img)
else:
self._video_rect = self.rect()
# 3. Overlay layers — each gets a clean painter state
for layer in self._overlay_layers:
if not layer.visible:
continue
painter.save()
try:
layer.paint(painter, self._video_rect)
except Exception:
logger.exception("Error painting overlay layer %s", layer.name)
finally:
painter.restore()
painter.end()
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _letterbox_rect(img_w: int, img_h: int, widget_w: int, widget_h: int) -> QRect:
"""Return the largest rect that fits the image while preserving aspect ratio."""
if img_w <= 0 or img_h <= 0:
return QRect(0, 0, widget_w, widget_h)
scale = min(widget_w / img_w, widget_h / img_h)
draw_w = int(img_w * scale)
draw_h = int(img_h * scale)
x = (widget_w - draw_w) // 2
y = (widget_h - draw_h) // 2
return QRect(x, y, draw_w, draw_h)

377
app/ui/main_window.py Normal file
View File

@@ -0,0 +1,377 @@
"""Main application window."""
from __future__ import annotations
import logging
from pathlib import Path
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QLabel, QMainWindow, QMessageBox, QSizePolicy, QStatusBar
from app.camera.camera_enumerator import CameraEnumerator, CameraFormat, CameraInfo
from app.camera.camera_service import CameraService
from app.camera.uvc import make_uvc_controller
from app.camera.uvc.base import UvcControllerBase
from app.camera.uvc.stub import NullUvcController
from app.config import APP_NAME, APP_VERSION
from app.inference.bbox_overlay import BboxOverlay
from app.inference.worker_manager import InferenceManager
from app.overlay.telemetry_overlay import TelemetryOverlay
from app.pipeline.frame_dispatcher import FrameDispatcher
from app.telemetry.csv_logger import CsvTelemetryLogger
from app.telemetry.telemetry_collector import TelemetryCollector
from app.ui.camera_settings_dialog import CameraSettingsDialog
from app.ui.camera_view import CameraView
from app.ui.menu_bar import AppMenuBar
from app.video.video_player import VideoPlayer
logger = logging.getLogger(__name__)
class MainWindow(QMainWindow):
"""
Top-level application window.
Frame source (exclusive):
• CameraService — live camera (default)
• VideoPlayer — local video file
Inference pipeline (optional):
InferenceManager runs YOLO in a separate process.
Frames submitted via FrameDispatcher subscriber (drop_if_busy).
Results displayed by BboxOverlay.
Signal flow:
[CameraService | VideoPlayer].frame_ready(QVideoFrame)
→ FrameDispatcher.dispatch
→ CameraView.on_frame (render)
→ TelemetryCollector.on_frame (metrics)
→ TelemetryOverlay (HUD)
→ CsvTelemetryLogger (CSV)
→ InferenceManager.submit_frame (drop_if_busy, optional)
→ [worker process] YOLO
→ BboxOverlay.on_detections (draw boxes)
"""
def __init__(self, log_path: Path | None = None) -> None:
super().__init__()
self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
self.setMinimumSize(640, 480)
self.resize(1280, 720)
# --- Core pipeline ---
self._camera_service = CameraService(self)
self._video_player = VideoPlayer(self)
self._dispatcher = FrameDispatcher(self)
self._telemetry = TelemetryCollector(parent=self)
self._inference = InferenceManager(self)
# Track which source is active
self._video_source_active: bool = False
self._current_camera: CameraInfo | None = None
# --- UVC ---
self._uvc: UvcControllerBase = NullUvcController()
# --- CSV logger ---
self._csv_logger: CsvTelemetryLogger | None = None
if log_path is not None:
csv_path = log_path.with_suffix(".csv")
self._csv_logger = CsvTelemetryLogger(csv_path)
logger.info("Telemetry CSV: %s", csv_path.resolve())
# --- Camera view ---
self._camera_view = CameraView(self)
self._camera_view.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
)
self.setCentralWidget(self._camera_view)
# --- Overlay layers ---
self._telemetry_overlay = TelemetryOverlay()
self._bbox_overlay = BboxOverlay()
self._camera_view.add_overlay_layer(self._telemetry_overlay)
self._camera_view.add_overlay_layer(self._bbox_overlay)
self._bbox_overlay.visible = False # hidden until inference enabled
# --- Menu bar ---
self._menu = AppMenuBar(self)
self.setMenuBar(self._menu)
if log_path is not None:
self._menu.set_log_file_path(str(log_path.resolve()))
# --- Status bar ---
self._status_bar = QStatusBar(self)
self.setStatusBar(self._status_bar)
self._status_label = QLabel("Initialising\u2026")
self._status_bar.addWidget(self._status_label, stretch=1)
# Detection counter — right-aligned permanent widget
self._detection_label = QLabel("")
self._detection_label.setVisible(False)
self._status_bar.addPermanentWidget(self._detection_label)
# --- Wire signals ---
self._wire_signals()
QTimer.singleShot(0, self._initialise_cameras)
# ------------------------------------------------------------------
# Initialisation
# ------------------------------------------------------------------
def _initialise_cameras(self) -> None:
cameras = CameraEnumerator.list_cameras()
if not cameras:
self._status_label.setText("No cameras found")
logger.warning("No cameras detected")
return
self._menu.populate_cameras(cameras)
default = CameraEnumerator.default_camera()
start_cam = default if default is not None else cameras[0]
self._menu.populate_formats(start_cam)
self._start_camera(start_cam)
def _start_camera(self, cam: CameraInfo) -> None:
self._current_camera = cam
self._telemetry.reset_counters()
self._camera_service.start(cam)
self._menu.set_active_camera(cam)
self._status_label.setText(f"Opening: {cam.name}")
self._open_uvc(cam)
def _open_uvc(self, cam: CameraInfo) -> None:
if self._uvc.is_open():
self._uvc.close()
ctrl = make_uvc_controller(cam.name)
if not ctrl.is_open():
ctrl.open(cam.name)
self._uvc = ctrl
# ------------------------------------------------------------------
# Signal wiring
# ------------------------------------------------------------------
def _wire_signals(self) -> None:
# ---- Active source → dispatcher ----
# (connected dynamically in _switch_to_camera / _switch_to_video)
self._camera_service.frame_ready.connect(self._dispatcher.dispatch)
# ---- Dispatcher fans out to all consumers ----
self._dispatcher.subscribe(self._camera_view.on_frame, drop_if_busy=True)
self._dispatcher.subscribe(self._telemetry.on_frame, drop_if_busy=False)
# InferenceManager subscriber added/removed dynamically on toggle
# ---- Telemetry ----
self._telemetry.metrics_updated.connect(
self._telemetry_overlay.on_metrics_updated
)
if self._csv_logger is not None:
self._telemetry.metrics_updated.connect(self._csv_logger.on_metrics_updated)
self._camera_service.format_changed.connect(self._telemetry.set_target_fps)
# ---- Camera service status ----
self._camera_service.camera_started.connect(self._on_camera_started)
self._camera_service.camera_stopped.connect(self._on_camera_stopped)
self._camera_service.camera_error.connect(self._on_camera_error)
# ---- Video player status ----
self._video_player.playback_started.connect(self._on_playback_started)
self._video_player.playback_stopped.connect(self._on_playback_stopped)
self._video_player.playback_error.connect(self._on_playback_error)
# ---- InferenceManager ----
self._inference.detections_ready.connect(self._bbox_overlay.on_detections)
self._inference.detection_count_updated.connect(self._on_detection_count_updated)
self._inference.inference_stats_updated.connect(self._on_inference_stats_updated)
self._inference.inference_started.connect(self._on_inference_started)
self._inference.inference_stopped.connect(self._on_inference_stopped)
self._inference.inference_error.connect(self._on_inference_error)
# ---- Menu ----
self._menu.camera_selected.connect(self._on_camera_selected)
self._menu.format_selected.connect(self._on_format_selected)
self._menu.reconnect_requested.connect(self._camera_service.reconnect)
self._menu.overlay_toggled.connect(self._camera_view.set_all_overlays_visible)
self._menu.camera_settings_requested.connect(self._on_settings_requested)
self._menu.video_file_selected.connect(self._on_video_selected)
self._menu.video_closed.connect(self._on_video_closed)
self._menu.model_file_selected.connect(self._on_model_selected)
self._menu.inference_toggled.connect(self._on_inference_toggled)
# ------------------------------------------------------------------
# Source switching
# ------------------------------------------------------------------
def _switch_to_camera(self) -> None:
"""Disconnect VideoPlayer, connect CameraService to dispatcher."""
try:
self._video_player.frame_ready.disconnect(self._dispatcher.dispatch)
except RuntimeError:
pass
self._camera_service.frame_ready.connect(self._dispatcher.dispatch)
self._video_source_active = False
self._menu.set_video_source_active(False)
def _switch_to_video(self) -> None:
"""Disconnect CameraService, connect VideoPlayer to dispatcher."""
try:
self._camera_service.frame_ready.disconnect(self._dispatcher.dispatch)
except RuntimeError:
pass
self._video_player.frame_ready.connect(self._dispatcher.dispatch)
self._video_source_active = True
self._menu.set_video_source_active(True)
# ------------------------------------------------------------------
# Camera status slots
# ------------------------------------------------------------------
def _on_camera_started(self) -> None:
cam = self._camera_service.current_camera
name = cam.name if cam else "Unknown"
self._status_label.setText(f"Streaming: {name}")
logger.info("Camera streaming: %s", name)
def _on_camera_stopped(self) -> None:
self._status_label.setText("Camera stopped")
def _on_camera_error(self, message: str) -> None:
self._status_label.setText(f"Error: {message}")
logger.error("Camera error: %s", message)
# ------------------------------------------------------------------
# Video player slots
# ------------------------------------------------------------------
def _on_playback_started(self) -> None:
path = self._video_player.current_path or ""
name = Path(path).name if path else "video"
self._status_label.setText(f"Playing: {name}")
def _on_playback_stopped(self) -> None:
self._status_label.setText("Playback finished")
def _on_playback_error(self, message: str) -> None:
self._status_label.setText(f"Video error: {message}")
logger.error(message)
# ------------------------------------------------------------------
# Inference slots
# ------------------------------------------------------------------
def _on_inference_started(self) -> None:
self._status_label.setText("Inference running")
self._menu.set_inference_checked(True)
def _on_detection_count_updated(self, count: int) -> None:
self._detection_label.setText(f"Detections: {count} frames")
def _on_inference_stats_updated(self, device: str, avg_ms: float) -> None:
self._telemetry.set_inference_stats(device, avg_ms)
def _on_inference_stopped(self) -> None:
self._bbox_overlay.clear()
def _on_inference_error(self, message: str) -> None:
logger.error("Inference: %s", message)
self._menu.set_inference_available(False)
self._menu.set_inference_checked(False)
self._bbox_overlay.visible = False
self._detection_label.setVisible(False)
self._telemetry.clear_inference_stats()
QMessageBox.critical(self, "Inference Error", message)
# ------------------------------------------------------------------
# Menu action slots
# ------------------------------------------------------------------
def _on_camera_selected(self, cam: CameraInfo) -> None:
if self._video_source_active:
self._video_player.stop()
self._switch_to_camera()
self._start_camera(cam)
def _on_format_selected(self, fmt: CameraFormat) -> None:
logger.info(
"Format selected via menu: %dx%d @ %.4g fps (%s)",
fmt.width, fmt.height, fmt.max_fps, fmt.pixel_format,
)
self._camera_service.set_format(fmt)
def _on_settings_requested(self) -> None:
qt_cam = self._camera_service.qt_camera
if qt_cam is None:
logger.warning("Settings requested but no camera is active")
return
dlg = CameraSettingsDialog(qt_cam, self._uvc, parent=self)
dlg.exec()
def _on_video_selected(self, path: str) -> None:
"""Switch source to video file."""
self._camera_service.stop()
self._switch_to_video()
self._video_player.play(path)
logger.info("Video source: %s", path)
def _on_video_closed(self) -> None:
"""Return to camera source."""
self._video_player.stop()
self._switch_to_camera()
if self._current_camera is not None:
self._start_camera(self._current_camera)
logger.info("Returned to camera source")
def _on_model_selected(self, path: str) -> None:
"""Load YOLO model into inference manager."""
name = Path(path).name
logger.info("Loading model: %s", path)
self._status_label.setText(f"Loading model: {name}\u2026")
self._inference.start(path)
self._menu.set_model_label(name)
self._menu.set_inference_available(True)
self._menu.set_inference_checked(False) # user must explicitly enable
def _on_inference_toggled(self, enabled: bool) -> None:
if enabled:
if not self._inference.is_running:
# shouldn't happen but be safe
logger.warning("Inference toggle on but manager not running")
self._menu.set_inference_checked(False)
return
self._inference.resume()
self._dispatcher.subscribe(
self._inference.submit_frame, drop_if_busy=True
)
self._bbox_overlay.visible = True
self._detection_label.setText("Detections: 0 frames")
self._detection_label.setVisible(True)
self._status_label.setText("Inference enabled")
logger.info("Inference enabled")
else:
self._inference.pause()
self._dispatcher.unsubscribe(self._inference.submit_frame)
self._bbox_overlay.clear()
self._bbox_overlay.visible = False
self._detection_label.setVisible(False)
self._telemetry.clear_inference_stats()
self._status_label.setText("Inference disabled")
logger.info("Inference disabled")
# ------------------------------------------------------------------
# Qt overrides
# ------------------------------------------------------------------
def closeEvent(self, event) -> None: # noqa: N802
self._inference.stop()
self._camera_service.stop()
self._video_player.stop()
if self._uvc.is_open():
self._uvc.close()
if self._csv_logger is not None:
logger.info(
"CSV telemetry: %d rows written", self._csv_logger.rows_written
)
self._csv_logger.close()
super().closeEvent(event)

278
app/ui/menu_bar.py Normal file
View File

@@ -0,0 +1,278 @@
"""Menu bar — File, Camera, Video format, Image, Model and Debug controls."""
from __future__ import annotations
import logging
from PySide6.QtCore import Signal
from PySide6.QtGui import QAction, QActionGroup
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__)
class AppMenuBar(QMenuBar):
"""
Application menu bar.
Signals:
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()
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self._camera_group: QActionGroup | None = None
self._format_group: QActionGroup | None = None
self._cameras: list[CameraInfo] = []
self._build_menus()
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def populate_cameras(self, cameras: list[CameraInfo]) -> None:
self._cameras = cameras
menu = self._camera_menu
for action in list(menu.actions()):
if action not in (self._reconnect_action, self._cam_separator):
menu.removeAction(action)
self._camera_group = QActionGroup(self)
self._camera_group.setExclusive(True)
for cam in cameras:
action = QAction(cam.name, self)
action.setCheckable(True)
action.setData(cam)
self._camera_group.addAction(action)
menu.insertAction(self._cam_separator, action)
action.triggered.connect(self._on_camera_action)
if cameras:
self._camera_group.actions()[0].setChecked(True)
def populate_formats(self, camera_info: CameraInfo) -> None:
self._populate_format_menu(camera_info)
def set_active_camera(self, camera_info: CameraInfo) -> None:
if self._camera_group is None:
return
for action in self._camera_group.actions():
if action.data() is camera_info:
action.setChecked(True)
return
def set_active_format(self, fmt: CameraFormat) -> None:
if self._format_group is None:
return
for action in self._format_group.actions():
f: CameraFormat = action.data()
if (
f.width == fmt.width
and f.height == fmt.height
and abs(f.max_fps - fmt.max_fps) < 0.5
and f.pixel_format == fmt.pixel_format
):
action.setChecked(True)
return
def set_log_file_path(self, path: str) -> None:
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:
# --- 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 ---
self._video_menu = self.addMenu("Video")
self._res_menu = self._video_menu.addMenu("Resolution")
# --- 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)
# --- 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)
self._overlay_action.setCheckable(True)
self._overlay_action.setChecked(True)
self._overlay_action.toggled.connect(self.overlay_toggled)
debug_menu.addAction(self._overlay_action)
self._log_action = QAction("Console Logging", self)
self._log_action.setCheckable(True)
self._log_action.setChecked(False)
self._log_action.toggled.connect(self._on_log_toggled)
debug_menu.addAction(self._log_action)
debug_menu.addSeparator()
self._log_file_action = QAction("Log: (not started)", self)
self._log_file_action.setEnabled(False)
debug_menu.addAction(self._log_file_action)
def _populate_format_menu(self, camera_info: CameraInfo) -> None:
self._res_menu.clear()
self._format_group = QActionGroup(self)
self._format_group.setExclusive(True)
for fmt in camera_info.formats:
label = (
f"{fmt.width}\u00d7{fmt.height}"
f" @ {fmt.max_fps:.4g}fps"
f" ({fmt.pixel_format})"
)
action = QAction(label, self)
action.setCheckable(True)
action.setData(fmt)
self._format_group.addAction(action)
self._res_menu.addAction(action)
action.triggered.connect(self._on_format_action)
actions = self._format_group.actions()
if actions:
actions[0].setChecked(True)
# ------------------------------------------------------------------
# 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:
return
cam: CameraInfo = action.data()
logger.debug("Camera selected: %s", cam.name)
self.camera_selected.emit(cam)
self._populate_format_menu(cam)
def _on_format_action(self) -> None:
action = self.sender()
if action is None:
return
fmt: CameraFormat = action.data()
logger.debug(
"Format selected: %dx%d @ %.4g fps (%s)",
fmt.width, fmt.height, fmt.max_fps, fmt.pixel_format,
)
self.format_selected.emit(fmt)
def _on_log_toggled(self, enabled: bool) -> None:
set_console_level(enabled)
self.log_toggled.emit(enabled)

0
app/video/__init__.py Normal file
View File

117
app/video/video_player.py Normal file
View File

@@ -0,0 +1,117 @@
"""VideoPlayer — plays a local video file and delivers frames via frame_ready signal.
The public interface mirrors CameraService so MainWindow can treat both
interchangeably: both emit frame_ready(QVideoFrame).
"""
from __future__ import annotations
import logging
from pathlib import Path
from PySide6.QtCore import QObject, QUrl, Signal, Slot
from PySide6.QtMultimedia import (
QMediaPlayer,
QVideoFrame,
QVideoSink,
)
logger = logging.getLogger(__name__)
class VideoPlayer(QObject):
"""
Wraps QMediaPlayer + QVideoSink to replay a local video file.
Signal flow (identical interface to CameraService):
VideoPlayer.frame_ready(QVideoFrame) → FrameDispatcher
Notes:
- Playback is real-time (1×) — no seek/pause in this version.
- At end-of-file: emits playback_stopped() and stops.
- On any error: emits playback_error(str) then playback_stopped().
"""
frame_ready = Signal(QVideoFrame)
playback_started = Signal()
playback_stopped = Signal()
playback_error = Signal(str)
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
self._player = QMediaPlayer(self)
self._sink = QVideoSink(self)
self._player.setVideoSink(self._sink)
self._sink.videoFrameChanged.connect(self._on_frame)
self._player.playbackStateChanged.connect(self._on_playback_state_changed)
self._player.errorOccurred.connect(self._on_error)
self._current_path: str | None = None
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def play(self, path: str) -> None:
"""Open and start playing a video file."""
self.stop()
p = Path(path)
if not p.exists():
msg = f"Video file not found: {path}"
logger.error(msg)
self.playback_error.emit(msg)
return
self._current_path = path
url = QUrl.fromLocalFile(str(p.resolve()))
self._player.setSource(url)
self._player.play()
logger.info("VideoPlayer: starting playback of '%s'", p.name)
def stop(self) -> None:
"""Stop playback and clear source."""
if self._player.playbackState() != QMediaPlayer.PlaybackState.StoppedState:
self._player.stop()
self._player.setSource(QUrl())
self._current_path = None
@property
def is_playing(self) -> bool:
return (
self._player.playbackState()
== QMediaPlayer.PlaybackState.PlayingState
)
@property
def current_path(self) -> str | None:
return self._current_path
# ------------------------------------------------------------------
# Private slots
# ------------------------------------------------------------------
@Slot(QVideoFrame)
def _on_frame(self, frame: QVideoFrame) -> None:
if frame.isValid():
self.frame_ready.emit(frame)
@Slot(QMediaPlayer.PlaybackState)
def _on_playback_state_changed(self, state: QMediaPlayer.PlaybackState) -> None:
if state == QMediaPlayer.PlaybackState.PlayingState:
logger.info("VideoPlayer: playing")
self.playback_started.emit()
elif state == QMediaPlayer.PlaybackState.StoppedState:
logger.info("VideoPlayer: stopped")
self.playback_stopped.emit()
@Slot(QMediaPlayer.Error, str)
def _on_error(self, error: QMediaPlayer.Error, error_string: str) -> None:
if error == QMediaPlayer.Error.NoError:
return
msg = f"VideoPlayer error: {error_string}"
logger.error(msg)
self.playback_error.emit(msg)
self.playback_stopped.emit()

729
logs/camera_debug.log Normal file
View File

@@ -0,0 +1,729 @@
2026-05-13 15:33:20,198 [INFO] ==========================================
2026-05-13 15:33:20,198 [INFO] VIDEO DEVICES ENUMERATION
2026-05-13 15:33:20,198 [INFO] ==========================================
2026-05-13 15:33:20,198 [INFO] ------------------------------------------
2026-05-13 15:33:20,198 [INFO] KAMERA #0
2026-05-13 15:33:20,198 [INFO] ------------------------------------------
2026-05-13 15:33:20,198 [INFO] Description: HD USB CAMERA
2026-05-13 15:33:20,199 [INFO] ID: 0x1420000032e40317
2026-05-13 15:33:20,199 [INFO] Is default: True
2026-05-13 15:33:20,200 [INFO] Liczba formatów: 11
2026-05-13 15:33:20,200 [INFO]
2026-05-13 15:33:20,200 [INFO] FORMAT #0
2026-05-13 15:33:20,200 [INFO] Resolution: 320x240
2026-05-13 15:33:20,200 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:33:20,200 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:33:20,200 [INFO] Pixel format name: NV12
2026-05-13 15:33:20,201 [INFO]
2026-05-13 15:33:20,201 [INFO] FORMAT #1
2026-05-13 15:33:20,201 [INFO] Resolution: 640x480
2026-05-13 15:33:20,201 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:33:20,201 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:33:20,201 [INFO] Pixel format name: NV12
2026-05-13 15:33:20,201 [INFO]
2026-05-13 15:33:20,202 [INFO] FORMAT #2
2026-05-13 15:33:20,202 [INFO] Resolution: 800x600
2026-05-13 15:33:20,202 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:33:20,202 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:33:20,202 [INFO] Pixel format name: NV12
2026-05-13 15:33:20,202 [INFO]
2026-05-13 15:33:20,203 [INFO] FORMAT #3
2026-05-13 15:33:20,203 [INFO] Resolution: 1024x768
2026-05-13 15:33:20,204 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:33:20,209 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:33:20,210 [INFO] Pixel format name: NV12
2026-05-13 15:33:20,211 [INFO]
2026-05-13 15:33:20,211 [INFO] FORMAT #4
2026-05-13 15:33:20,211 [INFO] Resolution: 1280x720
2026-05-13 15:33:20,211 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:33:20,211 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:33:20,211 [INFO] Pixel format name: NV12
2026-05-13 15:33:20,211 [INFO]
2026-05-13 15:33:20,211 [INFO] FORMAT #5
2026-05-13 15:33:20,212 [INFO] Resolution: 1280x960
2026-05-13 15:33:20,212 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:33:20,212 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:33:20,212 [INFO] Pixel format name: NV12
2026-05-13 15:33:20,212 [INFO]
2026-05-13 15:33:20,212 [INFO] FORMAT #6
2026-05-13 15:33:20,212 [INFO] Resolution: 1600x1200
2026-05-13 15:33:20,212 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:33:20,212 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:33:20,212 [INFO] Pixel format name: NV12
2026-05-13 15:33:20,213 [INFO]
2026-05-13 15:33:20,213 [INFO] FORMAT #7
2026-05-13 15:33:20,213 [INFO] Resolution: 1920x1080
2026-05-13 15:33:20,214 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:33:20,214 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:33:20,214 [INFO] Pixel format name: NV12
2026-05-13 15:33:20,214 [INFO]
2026-05-13 15:33:20,214 [INFO] FORMAT #8
2026-05-13 15:33:20,214 [INFO] Resolution: 2048x1536
2026-05-13 15:33:20,215 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:33:20,215 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:33:20,215 [INFO] Pixel format name: NV12
2026-05-13 15:33:20,215 [INFO]
2026-05-13 15:33:20,215 [INFO] FORMAT #9
2026-05-13 15:33:20,215 [INFO] Resolution: 2592x1944
2026-05-13 15:33:20,215 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:33:20,215 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:33:20,216 [INFO] Pixel format name: NV12
2026-05-13 15:33:20,216 [INFO]
2026-05-13 15:33:20,216 [INFO] FORMAT #10
2026-05-13 15:33:20,217 [INFO] Resolution: 3840x2160
2026-05-13 15:33:20,217 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:33:20,217 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:33:20,218 [INFO] Pixel format name: NV12
2026-05-13 15:33:20,218 [INFO] Wybrano kamerę: HD USB CAMERA
2026-05-13 15:37:15,875 [INFO] ==========================================
2026-05-13 15:37:15,875 [INFO] VIDEO DEVICES ENUMERATION
2026-05-13 15:37:15,875 [INFO] ==========================================
2026-05-13 15:37:15,875 [INFO] ------------------------------------------
2026-05-13 15:37:15,875 [INFO] KAMERA #0
2026-05-13 15:37:15,875 [INFO] ------------------------------------------
2026-05-13 15:37:15,875 [INFO] Description: HD USB CAMERA
2026-05-13 15:37:15,876 [INFO] ID: 0x1420000032e40317
2026-05-13 15:37:15,876 [INFO] Is default: True
2026-05-13 15:37:15,876 [INFO] Liczba formatów: 11
2026-05-13 15:37:15,877 [INFO]
2026-05-13 15:37:15,877 [INFO] FORMAT #0
2026-05-13 15:37:15,877 [INFO] Resolution: 320x240
2026-05-13 15:37:15,877 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:37:15,877 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:37:15,877 [INFO] Pixel format name: NV12
2026-05-13 15:37:15,877 [INFO]
2026-05-13 15:37:15,877 [INFO] FORMAT #1
2026-05-13 15:37:15,877 [INFO] Resolution: 640x480
2026-05-13 15:37:15,877 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:37:15,877 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:37:15,878 [INFO] Pixel format name: NV12
2026-05-13 15:37:15,878 [INFO]
2026-05-13 15:37:15,878 [INFO] FORMAT #2
2026-05-13 15:37:15,878 [INFO] Resolution: 800x600
2026-05-13 15:37:15,878 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:37:15,878 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:37:15,878 [INFO] Pixel format name: NV12
2026-05-13 15:37:15,878 [INFO]
2026-05-13 15:37:15,878 [INFO] FORMAT #3
2026-05-13 15:37:15,878 [INFO] Resolution: 1024x768
2026-05-13 15:37:15,878 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:37:15,878 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:37:15,878 [INFO] Pixel format name: NV12
2026-05-13 15:37:15,878 [INFO]
2026-05-13 15:37:15,879 [INFO] FORMAT #4
2026-05-13 15:37:15,879 [INFO] Resolution: 1280x720
2026-05-13 15:37:15,879 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:37:15,879 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:37:15,879 [INFO] Pixel format name: NV12
2026-05-13 15:37:15,879 [INFO]
2026-05-13 15:37:15,879 [INFO] FORMAT #5
2026-05-13 15:37:15,879 [INFO] Resolution: 1280x960
2026-05-13 15:37:15,879 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:37:15,880 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:37:15,880 [INFO] Pixel format name: NV12
2026-05-13 15:37:15,880 [INFO]
2026-05-13 15:37:15,880 [INFO] FORMAT #6
2026-05-13 15:37:15,881 [INFO] Resolution: 1600x1200
2026-05-13 15:37:15,881 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:37:15,881 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:37:15,881 [INFO] Pixel format name: NV12
2026-05-13 15:37:15,881 [INFO]
2026-05-13 15:37:15,881 [INFO] FORMAT #7
2026-05-13 15:37:15,881 [INFO] Resolution: 1920x1080
2026-05-13 15:37:15,881 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:37:15,881 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:37:15,881 [INFO] Pixel format name: NV12
2026-05-13 15:37:15,881 [INFO]
2026-05-13 15:37:15,881 [INFO] FORMAT #8
2026-05-13 15:37:15,881 [INFO] Resolution: 2048x1536
2026-05-13 15:37:15,882 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:37:15,882 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:37:15,882 [INFO] Pixel format name: NV12
2026-05-13 15:37:15,882 [INFO]
2026-05-13 15:37:15,882 [INFO] FORMAT #9
2026-05-13 15:37:15,883 [INFO] Resolution: 2592x1944
2026-05-13 15:37:15,883 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:37:15,884 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:37:15,884 [INFO] Pixel format name: NV12
2026-05-13 15:37:15,884 [INFO]
2026-05-13 15:37:15,884 [INFO] FORMAT #10
2026-05-13 15:37:15,884 [INFO] Resolution: 3840x2160
2026-05-13 15:37:15,884 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:37:15,884 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:37:15,884 [INFO] Pixel format name: NV12
2026-05-13 15:37:15,884 [INFO] Wybrano kamerę: HD USB CAMERA
2026-05-13 15:37:15,933 [INFO] Próba ustawienia formatu: 1280x720 @ 30 FPS
2026-05-13 15:37:15,933 [DEBUG] Sprawdzam format -> 320x240 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:37:15,933 [DEBUG] Sprawdzam format -> 640x480 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:37:15,933 [DEBUG] Sprawdzam format -> 800x600 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:37:15,933 [DEBUG] Sprawdzam format -> 1024x768 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:37:15,933 [DEBUG] Sprawdzam format -> 1280x720 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:37:15,934 [INFO] Ustawiono format kamery:
2026-05-13 15:37:15,934 [INFO] Resolution: 1280x720
2026-05-13 15:37:15,934 [INFO] FPS range: 5.0 - 30.000030517578125
2026-05-13 15:37:15,934 [INFO] Pixel format: PixelFormat.Format_NV12
2026-05-13 15:37:15,934 [INFO]
2026-05-13 15:37:15,934 [INFO] ==========================================
2026-05-13 15:37:15,934 [INFO] QCAMERA RUNTIME CAPABILITIES
2026-05-13 15:37:15,934 [INFO] ==========================================
2026-05-13 15:37:15,934 [INFO] Camera active: False
2026-05-13 15:37:15,934 [INFO] focusMode: FocusMode.FocusModeAuto
2026-05-13 15:37:15,935 [INFO] exposureMode: ExposureMode.ExposureAuto
2026-05-13 15:37:15,935 [INFO] whiteBalanceMode: WhiteBalanceMode.WhiteBalanceAuto
2026-05-13 15:37:15,935 [INFO] flashMode: FlashMode.FlashOff
2026-05-13 15:37:15,935 [INFO] torchMode: TorchMode.TorchOff
2026-05-13 15:37:15,935 [INFO]
2026-05-13 15:37:15,935 [INFO] QT MULTIMEDIA INFO
2026-05-13 15:37:15,935 [INFO] Backend zależy od platformy:
2026-05-13 15:37:15,935 [INFO] - Windows -> MediaFoundation
2026-05-13 15:37:15,935 [INFO] - Linux -> GStreamer / PipeWire / V4L2
2026-05-13 15:37:15,935 [INFO] - macOS -> AVFoundation
2026-05-13 15:37:16,105 [INFO] Camera active changed: True
2026-05-13 15:37:16,106 [INFO] Kamera uruchomiona.
2026-05-13 15:38:36,435 [INFO] Zamykanie aplikacji.
2026-05-13 15:38:36,485 [INFO] Camera active changed: False
2026-05-13 15:39:07,696 [INFO] ==========================================
2026-05-13 15:39:07,696 [INFO] VIDEO DEVICES ENUMERATION
2026-05-13 15:39:07,696 [INFO] ==========================================
2026-05-13 15:39:07,697 [INFO] ------------------------------------------
2026-05-13 15:39:07,697 [INFO] KAMERA #0
2026-05-13 15:39:07,697 [INFO] ------------------------------------------
2026-05-13 15:39:07,697 [INFO] Description: HD USB CAMERA
2026-05-13 15:39:07,698 [INFO] ID: 0x1420000032e40317
2026-05-13 15:39:07,698 [INFO] Is default: True
2026-05-13 15:39:07,698 [INFO] Liczba formatów: 11
2026-05-13 15:39:07,698 [INFO]
2026-05-13 15:39:07,698 [INFO] FORMAT #0
2026-05-13 15:39:07,699 [INFO] Resolution: 320x240
2026-05-13 15:39:07,699 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:39:07,699 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:39:07,700 [INFO] Pixel format name: NV12
2026-05-13 15:39:07,700 [INFO]
2026-05-13 15:39:07,700 [INFO] FORMAT #1
2026-05-13 15:39:07,700 [INFO] Resolution: 640x480
2026-05-13 15:39:07,700 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:39:07,700 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:39:07,701 [INFO] Pixel format name: NV12
2026-05-13 15:39:07,701 [INFO]
2026-05-13 15:39:07,701 [INFO] FORMAT #2
2026-05-13 15:39:07,701 [INFO] Resolution: 800x600
2026-05-13 15:39:07,701 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:39:07,701 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:39:07,701 [INFO] Pixel format name: NV12
2026-05-13 15:39:07,702 [INFO]
2026-05-13 15:39:07,702 [INFO] FORMAT #3
2026-05-13 15:39:07,702 [INFO] Resolution: 1024x768
2026-05-13 15:39:07,702 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:39:07,702 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:39:07,702 [INFO] Pixel format name: NV12
2026-05-13 15:39:07,702 [INFO]
2026-05-13 15:39:07,702 [INFO] FORMAT #4
2026-05-13 15:39:07,702 [INFO] Resolution: 1280x720
2026-05-13 15:39:07,702 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:39:07,702 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:39:07,703 [INFO] Pixel format name: NV12
2026-05-13 15:39:07,703 [INFO]
2026-05-13 15:39:07,703 [INFO] FORMAT #5
2026-05-13 15:39:07,703 [INFO] Resolution: 1280x960
2026-05-13 15:39:07,703 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:39:07,703 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:39:07,703 [INFO] Pixel format name: NV12
2026-05-13 15:39:07,704 [INFO]
2026-05-13 15:39:07,704 [INFO] FORMAT #6
2026-05-13 15:39:07,704 [INFO] Resolution: 1600x1200
2026-05-13 15:39:07,704 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:39:07,704 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:39:07,704 [INFO] Pixel format name: NV12
2026-05-13 15:39:07,704 [INFO]
2026-05-13 15:39:07,704 [INFO] FORMAT #7
2026-05-13 15:39:07,704 [INFO] Resolution: 1920x1080
2026-05-13 15:39:07,704 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:39:07,704 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:39:07,705 [INFO] Pixel format name: NV12
2026-05-13 15:39:07,705 [INFO]
2026-05-13 15:39:07,705 [INFO] FORMAT #8
2026-05-13 15:39:07,705 [INFO] Resolution: 2048x1536
2026-05-13 15:39:07,705 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:39:07,705 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:39:07,705 [INFO] Pixel format name: NV12
2026-05-13 15:39:07,705 [INFO]
2026-05-13 15:39:07,705 [INFO] FORMAT #9
2026-05-13 15:39:07,705 [INFO] Resolution: 2592x1944
2026-05-13 15:39:07,706 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:39:07,706 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:39:07,707 [INFO] Pixel format name: NV12
2026-05-13 15:39:07,707 [INFO]
2026-05-13 15:39:07,707 [INFO] FORMAT #10
2026-05-13 15:39:07,708 [INFO] Resolution: 3840x2160
2026-05-13 15:39:07,708 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:39:07,708 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:39:07,708 [INFO] Pixel format name: NV12
2026-05-13 15:39:07,708 [INFO] Wybrano kamerę: HD USB CAMERA
2026-05-13 15:39:07,748 [INFO] Próba ustawienia formatu: 1920x1080 @ 30 FPS
2026-05-13 15:39:07,748 [DEBUG] Sprawdzam format -> 320x240 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:39:07,748 [DEBUG] Sprawdzam format -> 640x480 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:39:07,749 [DEBUG] Sprawdzam format -> 800x600 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:39:07,749 [DEBUG] Sprawdzam format -> 1024x768 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:39:07,749 [DEBUG] Sprawdzam format -> 1280x720 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:39:07,749 [DEBUG] Sprawdzam format -> 1280x960 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:39:07,749 [DEBUG] Sprawdzam format -> 1600x1200 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:39:07,749 [DEBUG] Sprawdzam format -> 1920x1080 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:39:07,750 [INFO] Ustawiono format kamery:
2026-05-13 15:39:07,750 [INFO] Resolution: 1920x1080
2026-05-13 15:39:07,750 [INFO] FPS range: 5.0 - 30.000030517578125
2026-05-13 15:39:07,750 [INFO] Pixel format: PixelFormat.Format_NV12
2026-05-13 15:39:07,750 [INFO]
2026-05-13 15:39:07,751 [INFO] ==========================================
2026-05-13 15:39:07,751 [INFO] QCAMERA RUNTIME CAPABILITIES
2026-05-13 15:39:07,751 [INFO] ==========================================
2026-05-13 15:39:07,751 [INFO] Camera active: False
2026-05-13 15:39:07,751 [INFO] focusMode: FocusMode.FocusModeAuto
2026-05-13 15:39:07,751 [INFO] exposureMode: ExposureMode.ExposureAuto
2026-05-13 15:39:07,752 [INFO] whiteBalanceMode: WhiteBalanceMode.WhiteBalanceAuto
2026-05-13 15:39:07,752 [INFO] flashMode: FlashMode.FlashOff
2026-05-13 15:39:07,752 [INFO] torchMode: TorchMode.TorchOff
2026-05-13 15:39:07,752 [INFO]
2026-05-13 15:39:07,753 [INFO] QT MULTIMEDIA INFO
2026-05-13 15:39:07,753 [INFO] Backend zależy od platformy:
2026-05-13 15:39:07,753 [INFO] - Windows -> MediaFoundation
2026-05-13 15:39:07,753 [INFO] - Linux -> GStreamer / PipeWire / V4L2
2026-05-13 15:39:07,753 [INFO] - macOS -> AVFoundation
2026-05-13 15:39:07,910 [INFO] Camera active changed: True
2026-05-13 15:39:07,911 [INFO] Kamera uruchomiona.
2026-05-13 15:39:27,466 [INFO] Zamykanie aplikacji.
2026-05-13 15:39:27,491 [INFO] Camera active changed: False
2026-05-13 15:40:23,621 [INFO] ==========================================
2026-05-13 15:40:23,622 [INFO] VIDEO DEVICES ENUMERATION
2026-05-13 15:40:23,622 [INFO] ==========================================
2026-05-13 15:40:23,625 [INFO] ------------------------------------------
2026-05-13 15:40:23,626 [INFO] KAMERA #0
2026-05-13 15:40:23,626 [INFO] ------------------------------------------
2026-05-13 15:40:23,626 [INFO] Description: HD USB CAMERA
2026-05-13 15:40:23,627 [INFO] ID: 0x1420000032e40317
2026-05-13 15:40:23,627 [INFO] Is default: True
2026-05-13 15:40:23,627 [INFO] Liczba formatów: 11
2026-05-13 15:40:23,627 [INFO]
2026-05-13 15:40:23,628 [INFO] FORMAT #0
2026-05-13 15:40:23,628 [INFO] Resolution: 320x240
2026-05-13 15:40:23,628 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:23,629 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:23,629 [INFO] Pixel format name: NV12
2026-05-13 15:40:23,629 [INFO]
2026-05-13 15:40:23,629 [INFO] FORMAT #1
2026-05-13 15:40:23,629 [INFO] Resolution: 640x480
2026-05-13 15:40:23,629 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:23,629 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:23,682 [INFO] Pixel format name: NV12
2026-05-13 15:40:23,682 [INFO]
2026-05-13 15:40:23,682 [INFO] FORMAT #2
2026-05-13 15:40:23,682 [INFO] Resolution: 800x600
2026-05-13 15:40:23,682 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:23,682 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:23,683 [INFO] Pixel format name: NV12
2026-05-13 15:40:23,683 [INFO]
2026-05-13 15:40:23,683 [INFO] FORMAT #3
2026-05-13 15:40:23,683 [INFO] Resolution: 1024x768
2026-05-13 15:40:23,683 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:23,684 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:23,684 [INFO] Pixel format name: NV12
2026-05-13 15:40:23,684 [INFO]
2026-05-13 15:40:23,684 [INFO] FORMAT #4
2026-05-13 15:40:23,684 [INFO] Resolution: 1280x720
2026-05-13 15:40:23,684 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:23,684 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:23,685 [INFO] Pixel format name: NV12
2026-05-13 15:40:23,685 [INFO]
2026-05-13 15:40:23,685 [INFO] FORMAT #5
2026-05-13 15:40:23,685 [INFO] Resolution: 1280x960
2026-05-13 15:40:23,685 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:23,685 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:23,685 [INFO] Pixel format name: NV12
2026-05-13 15:40:23,685 [INFO]
2026-05-13 15:40:23,686 [INFO] FORMAT #6
2026-05-13 15:40:23,686 [INFO] Resolution: 1600x1200
2026-05-13 15:40:23,686 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:23,686 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:23,687 [INFO] Pixel format name: NV12
2026-05-13 15:40:23,687 [INFO]
2026-05-13 15:40:23,687 [INFO] FORMAT #7
2026-05-13 15:40:23,687 [INFO] Resolution: 1920x1080
2026-05-13 15:40:23,687 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:23,688 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:23,688 [INFO] Pixel format name: NV12
2026-05-13 15:40:23,688 [INFO]
2026-05-13 15:40:23,688 [INFO] FORMAT #8
2026-05-13 15:40:23,689 [INFO] Resolution: 2048x1536
2026-05-13 15:40:23,689 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:23,689 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:23,689 [INFO] Pixel format name: NV12
2026-05-13 15:40:23,689 [INFO]
2026-05-13 15:40:23,689 [INFO] FORMAT #9
2026-05-13 15:40:23,690 [INFO] Resolution: 2592x1944
2026-05-13 15:40:23,690 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:23,690 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:23,690 [INFO] Pixel format name: NV12
2026-05-13 15:40:23,690 [INFO]
2026-05-13 15:40:23,690 [INFO] FORMAT #10
2026-05-13 15:40:23,690 [INFO] Resolution: 3840x2160
2026-05-13 15:40:23,690 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:23,691 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:23,691 [INFO] Pixel format name: NV12
2026-05-13 15:40:23,691 [INFO] Wybrano kamerę: HD USB CAMERA
2026-05-13 15:40:23,721 [INFO] Próba ustawienia formatu: 1920x1080 @ 30 FPS
2026-05-13 15:40:23,721 [DEBUG] Sprawdzam format -> 320x240 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:23,721 [DEBUG] Sprawdzam format -> 640x480 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:23,721 [DEBUG] Sprawdzam format -> 800x600 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:23,722 [DEBUG] Sprawdzam format -> 1024x768 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:23,722 [DEBUG] Sprawdzam format -> 1280x720 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:23,722 [DEBUG] Sprawdzam format -> 1280x960 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:23,722 [DEBUG] Sprawdzam format -> 1600x1200 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:23,722 [DEBUG] Sprawdzam format -> 1920x1080 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:23,722 [INFO] Ustawiono format kamery:
2026-05-13 15:40:23,723 [INFO] Resolution: 1920x1080
2026-05-13 15:40:23,724 [INFO] FPS range: 5.0 - 30.000030517578125
2026-05-13 15:40:23,725 [INFO] Pixel format: PixelFormat.Format_NV12
2026-05-13 15:40:23,725 [INFO]
2026-05-13 15:40:23,725 [INFO] ==========================================
2026-05-13 15:40:23,726 [INFO] QCAMERA RUNTIME CAPABILITIES
2026-05-13 15:40:23,726 [INFO] ==========================================
2026-05-13 15:40:23,726 [INFO] Camera active: False
2026-05-13 15:40:23,726 [INFO] focusMode: FocusMode.FocusModeAuto
2026-05-13 15:40:23,726 [INFO] exposureMode: ExposureMode.ExposureAuto
2026-05-13 15:40:23,726 [INFO] whiteBalanceMode: WhiteBalanceMode.WhiteBalanceAuto
2026-05-13 15:40:23,726 [INFO] flashMode: FlashMode.FlashOff
2026-05-13 15:40:23,726 [INFO] torchMode: TorchMode.TorchOff
2026-05-13 15:40:23,726 [INFO]
2026-05-13 15:40:23,727 [INFO] QT MULTIMEDIA INFO
2026-05-13 15:40:23,727 [INFO] Backend zależy od platformy:
2026-05-13 15:40:23,727 [INFO] - Windows -> MediaFoundation
2026-05-13 15:40:23,727 [INFO] - Linux -> GStreamer / PipeWire / V4L2
2026-05-13 15:40:23,727 [INFO] - macOS -> AVFoundation
2026-05-13 15:40:23,935 [INFO] Camera active changed: True
2026-05-13 15:40:23,936 [INFO] Kamera uruchomiona.
2026-05-13 15:40:33,738 [INFO] Zamykanie aplikacji.
2026-05-13 15:40:33,763 [INFO] Camera active changed: False
2026-05-13 15:40:48,145 [INFO] ==========================================
2026-05-13 15:40:48,146 [INFO] VIDEO DEVICES ENUMERATION
2026-05-13 15:40:48,146 [INFO] ==========================================
2026-05-13 15:40:48,146 [INFO] ------------------------------------------
2026-05-13 15:40:48,146 [INFO] KAMERA #0
2026-05-13 15:40:48,146 [INFO] ------------------------------------------
2026-05-13 15:40:48,146 [INFO] Description: HD USB CAMERA
2026-05-13 15:40:48,147 [INFO] ID: 0x1420000032e40317
2026-05-13 15:40:48,147 [INFO] Is default: True
2026-05-13 15:40:48,147 [INFO] Liczba formatów: 11
2026-05-13 15:40:48,147 [INFO]
2026-05-13 15:40:48,147 [INFO] FORMAT #0
2026-05-13 15:40:48,147 [INFO] Resolution: 320x240
2026-05-13 15:40:48,147 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:48,148 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:48,148 [INFO] Pixel format name: NV12
2026-05-13 15:40:48,148 [INFO]
2026-05-13 15:40:48,148 [INFO] FORMAT #1
2026-05-13 15:40:48,148 [INFO] Resolution: 640x480
2026-05-13 15:40:48,148 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:48,148 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:48,148 [INFO] Pixel format name: NV12
2026-05-13 15:40:48,148 [INFO]
2026-05-13 15:40:48,148 [INFO] FORMAT #2
2026-05-13 15:40:48,148 [INFO] Resolution: 800x600
2026-05-13 15:40:48,148 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:48,148 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:48,148 [INFO] Pixel format name: NV12
2026-05-13 15:40:48,149 [INFO]
2026-05-13 15:40:48,149 [INFO] FORMAT #3
2026-05-13 15:40:48,149 [INFO] Resolution: 1024x768
2026-05-13 15:40:48,149 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:48,149 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:48,149 [INFO] Pixel format name: NV12
2026-05-13 15:40:48,149 [INFO]
2026-05-13 15:40:48,149 [INFO] FORMAT #4
2026-05-13 15:40:48,150 [INFO] Resolution: 1280x720
2026-05-13 15:40:48,150 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:48,150 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:48,150 [INFO] Pixel format name: NV12
2026-05-13 15:40:48,152 [INFO]
2026-05-13 15:40:48,152 [INFO] FORMAT #5
2026-05-13 15:40:48,153 [INFO] Resolution: 1280x960
2026-05-13 15:40:48,153 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:48,154 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:48,154 [INFO] Pixel format name: NV12
2026-05-13 15:40:48,155 [INFO]
2026-05-13 15:40:48,155 [INFO] FORMAT #6
2026-05-13 15:40:48,155 [INFO] Resolution: 1600x1200
2026-05-13 15:40:48,155 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:48,155 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:48,155 [INFO] Pixel format name: NV12
2026-05-13 15:40:48,155 [INFO]
2026-05-13 15:40:48,155 [INFO] FORMAT #7
2026-05-13 15:40:48,155 [INFO] Resolution: 1920x1080
2026-05-13 15:40:48,155 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:48,155 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:48,155 [INFO] Pixel format name: NV12
2026-05-13 15:40:48,155 [INFO]
2026-05-13 15:40:48,155 [INFO] FORMAT #8
2026-05-13 15:40:48,155 [INFO] Resolution: 2048x1536
2026-05-13 15:40:48,155 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:48,155 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:48,156 [INFO] Pixel format name: NV12
2026-05-13 15:40:48,156 [INFO]
2026-05-13 15:40:48,156 [INFO] FORMAT #9
2026-05-13 15:40:48,156 [INFO] Resolution: 2592x1944
2026-05-13 15:40:48,156 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:48,157 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:48,157 [INFO] Pixel format name: NV12
2026-05-13 15:40:48,157 [INFO]
2026-05-13 15:40:48,158 [INFO] FORMAT #10
2026-05-13 15:40:48,158 [INFO] Resolution: 3840x2160
2026-05-13 15:40:48,158 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:40:48,159 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:40:48,159 [INFO] Pixel format name: NV12
2026-05-13 15:40:48,159 [INFO] Wybrano kamerę: HD USB CAMERA
2026-05-13 15:40:48,196 [INFO] Próba ustawienia formatu: 1280x720 @ 30 FPS
2026-05-13 15:40:48,196 [DEBUG] Sprawdzam format -> 320x240 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:48,196 [DEBUG] Sprawdzam format -> 640x480 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:48,196 [DEBUG] Sprawdzam format -> 800x600 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:48,196 [DEBUG] Sprawdzam format -> 1024x768 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:48,196 [DEBUG] Sprawdzam format -> 1280x720 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:40:48,197 [INFO] Ustawiono format kamery:
2026-05-13 15:40:48,197 [INFO] Resolution: 1280x720
2026-05-13 15:40:48,197 [INFO] FPS range: 5.0 - 30.000030517578125
2026-05-13 15:40:48,197 [INFO] Pixel format: PixelFormat.Format_NV12
2026-05-13 15:40:48,197 [INFO]
2026-05-13 15:40:48,197 [INFO] ==========================================
2026-05-13 15:40:48,197 [INFO] QCAMERA RUNTIME CAPABILITIES
2026-05-13 15:40:48,197 [INFO] ==========================================
2026-05-13 15:40:48,197 [INFO] Camera active: False
2026-05-13 15:40:48,197 [INFO] focusMode: FocusMode.FocusModeAuto
2026-05-13 15:40:48,197 [INFO] exposureMode: ExposureMode.ExposureAuto
2026-05-13 15:40:48,197 [INFO] whiteBalanceMode: WhiteBalanceMode.WhiteBalanceAuto
2026-05-13 15:40:48,197 [INFO] flashMode: FlashMode.FlashOff
2026-05-13 15:40:48,197 [INFO] torchMode: TorchMode.TorchOff
2026-05-13 15:40:48,198 [INFO]
2026-05-13 15:40:48,198 [INFO] QT MULTIMEDIA INFO
2026-05-13 15:40:48,198 [INFO] Backend zależy od platformy:
2026-05-13 15:40:48,198 [INFO] - Windows -> MediaFoundation
2026-05-13 15:40:48,198 [INFO] - Linux -> GStreamer / PipeWire / V4L2
2026-05-13 15:40:48,198 [INFO] - macOS -> AVFoundation
2026-05-13 15:40:48,348 [INFO] Camera active changed: True
2026-05-13 15:40:48,348 [INFO] Kamera uruchomiona.
2026-05-13 15:40:53,956 [INFO] Zamykanie aplikacji.
2026-05-13 15:40:53,982 [INFO] Camera active changed: False
2026-05-13 15:45:01,588 [INFO] ==========================================
2026-05-13 15:45:01,588 [INFO] VIDEO DEVICES ENUMERATION
2026-05-13 15:45:01,588 [INFO] ==========================================
2026-05-13 15:45:01,588 [INFO] ------------------------------------------
2026-05-13 15:45:01,588 [INFO] KAMERA #0
2026-05-13 15:45:01,588 [INFO] ------------------------------------------
2026-05-13 15:45:01,588 [INFO] Description: HD USB CAMERA
2026-05-13 15:45:01,589 [INFO] ID: 0x1420000032e40317
2026-05-13 15:45:01,589 [INFO] Is default: True
2026-05-13 15:45:01,589 [INFO] Liczba formatów: 11
2026-05-13 15:45:01,589 [INFO]
2026-05-13 15:45:01,589 [INFO] FORMAT #0
2026-05-13 15:45:01,590 [INFO] Resolution: 320x240
2026-05-13 15:45:01,590 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:01,590 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:01,590 [INFO] Pixel format name: NV12
2026-05-13 15:45:01,590 [INFO]
2026-05-13 15:45:01,590 [INFO] FORMAT #1
2026-05-13 15:45:01,590 [INFO] Resolution: 640x480
2026-05-13 15:45:01,590 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:01,590 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:01,590 [INFO] Pixel format name: NV12
2026-05-13 15:45:01,590 [INFO]
2026-05-13 15:45:01,590 [INFO] FORMAT #2
2026-05-13 15:45:01,591 [INFO] Resolution: 800x600
2026-05-13 15:45:01,591 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:01,591 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:01,591 [INFO] Pixel format name: NV12
2026-05-13 15:45:01,591 [INFO]
2026-05-13 15:45:01,591 [INFO] FORMAT #3
2026-05-13 15:45:01,591 [INFO] Resolution: 1024x768
2026-05-13 15:45:01,591 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:01,591 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:01,591 [INFO] Pixel format name: NV12
2026-05-13 15:45:01,591 [INFO]
2026-05-13 15:45:01,591 [INFO] FORMAT #4
2026-05-13 15:45:01,592 [INFO] Resolution: 1280x720
2026-05-13 15:45:01,592 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:01,592 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:01,592 [INFO] Pixel format name: NV12
2026-05-13 15:45:01,592 [INFO]
2026-05-13 15:45:01,592 [INFO] FORMAT #5
2026-05-13 15:45:01,592 [INFO] Resolution: 1280x960
2026-05-13 15:45:01,592 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:01,592 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:01,593 [INFO] Pixel format name: NV12
2026-05-13 15:45:01,593 [INFO]
2026-05-13 15:45:01,593 [INFO] FORMAT #6
2026-05-13 15:45:01,593 [INFO] Resolution: 1600x1200
2026-05-13 15:45:01,593 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:01,594 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:01,594 [INFO] Pixel format name: NV12
2026-05-13 15:45:01,594 [INFO]
2026-05-13 15:45:01,594 [INFO] FORMAT #7
2026-05-13 15:45:01,594 [INFO] Resolution: 1920x1080
2026-05-13 15:45:01,594 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:01,594 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:01,594 [INFO] Pixel format name: NV12
2026-05-13 15:45:01,594 [INFO]
2026-05-13 15:45:01,594 [INFO] FORMAT #8
2026-05-13 15:45:01,594 [INFO] Resolution: 2048x1536
2026-05-13 15:45:01,594 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:01,594 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:01,595 [INFO] Pixel format name: NV12
2026-05-13 15:45:01,595 [INFO]
2026-05-13 15:45:01,595 [INFO] FORMAT #9
2026-05-13 15:45:01,596 [INFO] Resolution: 2592x1944
2026-05-13 15:45:01,596 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:01,596 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:01,596 [INFO] Pixel format name: NV12
2026-05-13 15:45:01,596 [INFO]
2026-05-13 15:45:01,596 [INFO] FORMAT #10
2026-05-13 15:45:01,597 [INFO] Resolution: 3840x2160
2026-05-13 15:45:01,597 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:01,597 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:01,597 [INFO] Pixel format name: NV12
2026-05-13 15:45:01,597 [INFO] Wybrano kamerę: HD USB CAMERA
2026-05-13 15:45:01,638 [INFO] Próba ustawienia formatu: 1280x720 @ 30 FPS
2026-05-13 15:45:01,638 [DEBUG] Sprawdzam format -> 320x240 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:45:01,638 [DEBUG] Sprawdzam format -> 640x480 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:45:01,638 [DEBUG] Sprawdzam format -> 800x600 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:45:01,638 [DEBUG] Sprawdzam format -> 1024x768 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:45:01,638 [DEBUG] Sprawdzam format -> 1280x720 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:45:01,638 [INFO] Ustawiono format kamery:
2026-05-13 15:45:01,638 [INFO] Resolution: 1280x720
2026-05-13 15:45:01,639 [INFO] FPS range: 5.0 - 30.000030517578125
2026-05-13 15:45:01,639 [INFO] Pixel format: PixelFormat.Format_NV12
2026-05-13 15:45:01,639 [INFO]
2026-05-13 15:45:01,639 [INFO] ==========================================
2026-05-13 15:45:01,639 [INFO] QCAMERA RUNTIME CAPABILITIES
2026-05-13 15:45:01,639 [INFO] ==========================================
2026-05-13 15:45:01,639 [INFO] Camera active: False
2026-05-13 15:45:01,639 [INFO] focusMode: FocusMode.FocusModeAuto
2026-05-13 15:45:01,640 [INFO] exposureMode: ExposureMode.ExposureAuto
2026-05-13 15:45:01,640 [INFO] whiteBalanceMode: WhiteBalanceMode.WhiteBalanceAuto
2026-05-13 15:45:01,640 [INFO] flashMode: FlashMode.FlashOff
2026-05-13 15:45:01,640 [INFO] torchMode: TorchMode.TorchOff
2026-05-13 15:45:01,640 [INFO]
2026-05-13 15:45:01,640 [INFO] QT MULTIMEDIA INFO
2026-05-13 15:45:01,640 [INFO] Backend zależy od platformy:
2026-05-13 15:45:01,641 [INFO] - Windows -> MediaFoundation
2026-05-13 15:45:01,641 [INFO] - Linux -> GStreamer / PipeWire / V4L2
2026-05-13 15:45:01,641 [INFO] - macOS -> AVFoundation
2026-05-13 15:45:01,787 [INFO] Camera active changed: True
2026-05-13 15:45:01,787 [INFO] Kamera uruchomiona.
2026-05-13 15:45:04,515 [INFO] Zamykanie aplikacji.
2026-05-13 15:45:04,516 [INFO] Stopping camera...
2026-05-13 15:45:04,542 [INFO] Camera active changed: False
2026-05-13 15:45:04,542 [INFO] Detaching camera from capture session...
2026-05-13 15:45:25,073 [INFO] ==========================================
2026-05-13 15:45:25,074 [INFO] VIDEO DEVICES ENUMERATION
2026-05-13 15:45:25,074 [INFO] ==========================================
2026-05-13 15:45:25,074 [INFO] ------------------------------------------
2026-05-13 15:45:25,074 [INFO] KAMERA #0
2026-05-13 15:45:25,074 [INFO] ------------------------------------------
2026-05-13 15:45:25,074 [INFO] Description: HD USB CAMERA
2026-05-13 15:45:25,075 [INFO] ID: 0x1420000032e40317
2026-05-13 15:45:25,075 [INFO] Is default: True
2026-05-13 15:45:25,075 [INFO] Liczba formatów: 11
2026-05-13 15:45:25,075 [INFO]
2026-05-13 15:45:25,075 [INFO] FORMAT #0
2026-05-13 15:45:25,075 [INFO] Resolution: 320x240
2026-05-13 15:45:25,075 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:25,075 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:25,075 [INFO] Pixel format name: NV12
2026-05-13 15:45:25,076 [INFO]
2026-05-13 15:45:25,076 [INFO] FORMAT #1
2026-05-13 15:45:25,076 [INFO] Resolution: 640x480
2026-05-13 15:45:25,076 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:25,076 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:25,076 [INFO] Pixel format name: NV12
2026-05-13 15:45:25,076 [INFO]
2026-05-13 15:45:25,076 [INFO] FORMAT #2
2026-05-13 15:45:25,076 [INFO] Resolution: 800x600
2026-05-13 15:45:25,076 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:25,076 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:25,076 [INFO] Pixel format name: NV12
2026-05-13 15:45:25,077 [INFO]
2026-05-13 15:45:25,077 [INFO] FORMAT #3
2026-05-13 15:45:25,077 [INFO] Resolution: 1024x768
2026-05-13 15:45:25,077 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:25,077 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:25,077 [INFO] Pixel format name: NV12
2026-05-13 15:45:25,077 [INFO]
2026-05-13 15:45:25,077 [INFO] FORMAT #4
2026-05-13 15:45:25,077 [INFO] Resolution: 1280x720
2026-05-13 15:45:25,078 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:25,078 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:25,078 [INFO] Pixel format name: NV12
2026-05-13 15:45:25,078 [INFO]
2026-05-13 15:45:25,078 [INFO] FORMAT #5
2026-05-13 15:45:25,078 [INFO] Resolution: 1280x960
2026-05-13 15:45:25,079 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:25,079 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:25,080 [INFO] Pixel format name: NV12
2026-05-13 15:45:25,080 [INFO]
2026-05-13 15:45:25,080 [INFO] FORMAT #6
2026-05-13 15:45:25,081 [INFO] Resolution: 1600x1200
2026-05-13 15:45:25,081 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:25,081 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:25,081 [INFO] Pixel format name: NV12
2026-05-13 15:45:25,082 [INFO]
2026-05-13 15:45:25,082 [INFO] FORMAT #7
2026-05-13 15:45:25,082 [INFO] Resolution: 1920x1080
2026-05-13 15:45:25,082 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:25,082 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:25,082 [INFO] Pixel format name: NV12
2026-05-13 15:45:25,083 [INFO]
2026-05-13 15:45:25,083 [INFO] FORMAT #8
2026-05-13 15:45:25,083 [INFO] Resolution: 2048x1536
2026-05-13 15:45:25,084 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:25,084 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:25,084 [INFO] Pixel format name: NV12
2026-05-13 15:45:25,084 [INFO]
2026-05-13 15:45:25,084 [INFO] FORMAT #9
2026-05-13 15:45:25,086 [INFO] Resolution: 2592x1944
2026-05-13 15:45:25,087 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:25,088 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:25,088 [INFO] Pixel format name: NV12
2026-05-13 15:45:25,088 [INFO]
2026-05-13 15:45:25,088 [INFO] FORMAT #10
2026-05-13 15:45:25,088 [INFO] Resolution: 3840x2160
2026-05-13 15:45:25,088 [INFO] FPS min/max: 5.0 / 30.000030517578125
2026-05-13 15:45:25,088 [INFO] Pixel format enum: PixelFormat.Format_NV12
2026-05-13 15:45:25,089 [INFO] Pixel format name: NV12
2026-05-13 15:45:25,090 [INFO] Wybrano kamerę: HD USB CAMERA
2026-05-13 15:45:25,135 [INFO] Próba ustawienia formatu: 1280x720 @ 30 FPS
2026-05-13 15:45:25,135 [DEBUG] Sprawdzam format -> 320x240 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:45:25,136 [DEBUG] Sprawdzam format -> 640x480 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:45:25,136 [DEBUG] Sprawdzam format -> 800x600 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:45:25,136 [DEBUG] Sprawdzam format -> 1024x768 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:45:25,136 [DEBUG] Sprawdzam format -> 1280x720 FPS:5.0-30.000030517578125 PIX:PixelFormat.Format_NV12
2026-05-13 15:45:25,137 [INFO] Ustawiono format kamery:
2026-05-13 15:45:25,137 [INFO] Resolution: 1280x720
2026-05-13 15:45:25,137 [INFO] FPS range: 5.0 - 30.000030517578125
2026-05-13 15:45:25,137 [INFO] Pixel format: PixelFormat.Format_NV12
2026-05-13 15:45:25,138 [INFO]
2026-05-13 15:45:25,138 [INFO] ==========================================
2026-05-13 15:45:25,138 [INFO] QCAMERA RUNTIME CAPABILITIES
2026-05-13 15:45:25,138 [INFO] ==========================================
2026-05-13 15:45:25,138 [INFO] Camera active: False
2026-05-13 15:45:25,138 [INFO] focusMode: FocusMode.FocusModeAuto
2026-05-13 15:45:25,138 [INFO] exposureMode: ExposureMode.ExposureAuto
2026-05-13 15:45:25,138 [INFO] whiteBalanceMode: WhiteBalanceMode.WhiteBalanceAuto
2026-05-13 15:45:25,138 [INFO] flashMode: FlashMode.FlashOff
2026-05-13 15:45:25,138 [INFO] torchMode: TorchMode.TorchOff
2026-05-13 15:45:25,138 [INFO]
2026-05-13 15:45:25,138 [INFO] QT MULTIMEDIA INFO
2026-05-13 15:45:25,138 [INFO] Backend zależy od platformy:
2026-05-13 15:45:25,138 [INFO] - Windows -> MediaFoundation
2026-05-13 15:45:25,139 [INFO] - Linux -> GStreamer / PipeWire / V4L2
2026-05-13 15:45:25,139 [INFO] - macOS -> AVFoundation
2026-05-13 15:45:25,306 [INFO] Camera active changed: True
2026-05-13 15:45:25,307 [INFO] Kamera uruchomiona.
2026-05-13 15:45:44,233 [INFO] Zamykanie aplikacji.
2026-05-13 15:45:44,233 [INFO] Stopping camera...
2026-05-13 15:45:44,279 [INFO] Camera active changed: False
2026-05-13 15:45:44,279 [INFO] Detaching camera from capture session...

View File

@@ -0,0 +1,17 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
07:36:44.615,0.0,30.0,0.00,0,13.1,52.4,81.0
07:36:49.644,29.0,30.0,26.86,4,7.2,28.7,110.4
07:36:55.125,28.0,30.0,33.57,4,6.8,27.0,112.2
07:37:00.638,31.0,30.0,37.24,7,12.4,49.8,156.7
07:37:06.126,31.0,30.0,33.28,7,14.2,56.7,157.2
07:37:11.143,31.0,30.0,34.84,10,14.0,56.0,169.4
07:37:16.625,31.0,30.0,32.20,11,11.7,46.9,173.1
07:37:22.067,31.0,30.0,37.43,14,11.8,47.3,162.7
07:37:27.126,31.0,30.0,33.00,14,12.0,48.0,162.3
07:37:32.137,31.0,30.0,33.05,14,13.3,53.2,162.1
07:37:37.142,0.0,30.0,35.33,17,0.0,0.1,159.1
07:37:42.626,0.0,30.0,35.33,17,0.0,0.1,159.1
07:37:48.126,0.0,30.0,35.33,17,1.4,5.7,158.8
07:37:53.626,0.0,30.0,35.33,17,0.6,2.3,158.7
07:37:58.891,28.0,30.0,204.02,18,26.2,104.8,416.0
07:38:14.113,28.0,30.0,41.66,18,26.2,104.9,788.4
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 07:36:44.615 0.0 30.0 0.00 0 13.1 52.4 81.0
3 07:36:49.644 29.0 30.0 26.86 4 7.2 28.7 110.4
4 07:36:55.125 28.0 30.0 33.57 4 6.8 27.0 112.2
5 07:37:00.638 31.0 30.0 37.24 7 12.4 49.8 156.7
6 07:37:06.126 31.0 30.0 33.28 7 14.2 56.7 157.2
7 07:37:11.143 31.0 30.0 34.84 10 14.0 56.0 169.4
8 07:37:16.625 31.0 30.0 32.20 11 11.7 46.9 173.1
9 07:37:22.067 31.0 30.0 37.43 14 11.8 47.3 162.7
10 07:37:27.126 31.0 30.0 33.00 14 12.0 48.0 162.3
11 07:37:32.137 31.0 30.0 33.05 14 13.3 53.2 162.1
12 07:37:37.142 0.0 30.0 35.33 17 0.0 0.1 159.1
13 07:37:42.626 0.0 30.0 35.33 17 0.0 0.1 159.1
14 07:37:48.126 0.0 30.0 35.33 17 1.4 5.7 158.8
15 07:37:53.626 0.0 30.0 35.33 17 0.6 2.3 158.7
16 07:37:58.891 28.0 30.0 204.02 18 26.2 104.8 416.0
17 07:38:14.113 28.0 30.0 41.66 18 26.2 104.9 788.4

View File

@@ -0,0 +1,13 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
07:40:42.390,0.0,30.0,0.00,0,12.8,51.2,76.6
07:40:47.624,29.0,30.0,33.26,0,7.2,28.6,106.3
07:40:52.625,30.0,30.0,33.27,0,8.3,33.1,106.3
07:40:58.123,29.0,30.0,33.26,0,7.2,28.8,106.5
07:41:03.123,30.0,30.0,33.28,0,7.2,28.9,106.5
07:41:08.124,29.0,30.0,33.27,0,7.2,28.8,106.5
07:41:13.624,30.0,30.0,33.27,0,7.2,28.8,106.5
07:41:18.629,30.0,30.0,33.28,0,7.6,30.3,106.6
07:41:24.124,30.0,30.0,33.27,0,7.5,29.8,107.0
07:41:29.124,31.0,30.0,33.28,0,7.3,29.1,107.0
07:41:34.624,30.0,30.0,33.27,0,7.2,28.8,107.1
07:41:39.624,31.0,30.0,33.29,0,7.6,30.5,107.1
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 07:40:42.390 0.0 30.0 0.00 0 12.8 51.2 76.6
3 07:40:47.624 29.0 30.0 33.26 0 7.2 28.6 106.3
4 07:40:52.625 30.0 30.0 33.27 0 8.3 33.1 106.3
5 07:40:58.123 29.0 30.0 33.26 0 7.2 28.8 106.5
6 07:41:03.123 30.0 30.0 33.28 0 7.2 28.9 106.5
7 07:41:08.124 29.0 30.0 33.27 0 7.2 28.8 106.5
8 07:41:13.624 30.0 30.0 33.27 0 7.2 28.8 106.5
9 07:41:18.629 30.0 30.0 33.28 0 7.6 30.3 106.6
10 07:41:24.124 30.0 30.0 33.27 0 7.5 29.8 107.0
11 07:41:29.124 31.0 30.0 33.28 0 7.3 29.1 107.0
12 07:41:34.624 30.0 30.0 33.27 0 7.2 28.8 107.1
13 07:41:39.624 31.0 30.0 33.29 0 7.6 30.5 107.1

View File

@@ -0,0 +1,9 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
07:41:53.769,0.0,30.0,0.00,0,12.5,49.9,80.7
07:41:59.124,0.0,30.0,0.00,0,0.4,1.6,81.1
07:42:04.625,31.0,30.0,32.41,0,9.3,37.4,115.7
07:42:09.723,27.0,30.0,34.23,2,11.3,45.2,114.1
07:42:15.124,0.0,30.0,34.23,2,0.0,0.1,114.0
07:42:20.124,0.0,30.0,34.23,2,0.1,0.4,113.6
07:42:25.623,0.0,30.0,34.23,2,0.5,2.0,114.0
07:42:30.624,0.0,30.0,34.23,2,0.0,0.1,113.4
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 07:41:53.769 0.0 30.0 0.00 0 12.5 49.9 80.7
3 07:41:59.124 0.0 30.0 0.00 0 0.4 1.6 81.1
4 07:42:04.625 31.0 30.0 32.41 0 9.3 37.4 115.7
5 07:42:09.723 27.0 30.0 34.23 2 11.3 45.2 114.1
6 07:42:15.124 0.0 30.0 34.23 2 0.0 0.1 114.0
7 07:42:20.124 0.0 30.0 34.23 2 0.1 0.4 113.6
8 07:42:25.623 0.0 30.0 34.23 2 0.5 2.0 114.0
9 07:42:30.624 0.0 30.0 34.23 2 0.0 0.1 113.4

View File

@@ -0,0 +1,62 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
08:04:51.194,0.0,30.0,0.00,0,13.5,54.0,81.3
08:04:56.614,28.0,30.0,33.57,0,9.3,37.4,114.0
08:05:01.616,30.0,30.0,33.55,1,9.1,36.5,116.2
08:05:07.115,21.0,30.0,37.40,4,8.0,31.9,116.7
08:05:12.614,30.0,30.0,33.26,4,7.5,29.8,116.4
08:05:17.622,31.0,30.0,33.29,4,9.0,36.0,116.1
08:05:23.114,30.0,30.0,33.26,4,8.2,33.0,116.9
08:05:28.124,31.0,30.0,37.47,7,12.9,51.5,157.6
08:05:33.614,30.0,30.0,33.31,7,13.4,53.5,157.5
08:05:38.621,31.0,30.0,33.27,7,12.8,51.2,157.3
08:05:44.114,30.0,30.0,33.00,7,12.3,49.3,157.4
08:05:49.614,31.0,30.0,32.98,7,12.5,50.1,156.5
08:05:54.627,31.0,30.0,33.02,7,13.8,55.0,156.4
08:06:00.113,30.0,30.0,32.96,7,12.6,50.5,159.5
08:06:05.118,31.0,30.0,32.99,7,12.3,49.2,159.5
08:06:10.624,30.0,30.0,33.23,7,11.9,47.6,159.5
08:06:16.113,30.0,30.0,33.26,7,10.8,43.4,159.5
08:06:21.125,30.0,30.0,33.30,7,11.7,46.9,159.5
08:06:26.614,29.0,30.0,33.17,7,12.1,48.5,159.5
08:06:31.617,30.0,30.0,33.30,7,12.1,48.5,159.5
08:06:37.127,31.0,30.0,33.27,7,12.8,51.1,159.5
08:06:42.614,30.0,30.0,33.28,7,12.2,48.6,159.5
08:06:47.622,31.0,30.0,33.24,7,12.6,50.5,159.5
08:06:53.113,30.0,30.0,33.27,7,11.8,47.4,159.5
08:06:58.114,31.0,30.0,33.24,7,12.6,50.4,159.5
08:07:03.613,30.0,30.0,33.31,7,14.5,57.9,159.6
08:07:09.113,31.0,30.0,33.26,7,12.3,49.2,159.6
08:07:14.121,31.0,30.0,33.55,7,12.0,48.0,159.6
08:07:19.613,30.0,30.0,33.27,7,12.6,50.2,159.6
08:07:24.616,31.0,30.0,33.29,7,12.3,49.1,159.6
08:07:30.125,31.0,30.0,33.27,7,12.7,50.6,159.6
08:07:35.613,30.0,30.0,33.27,7,12.1,48.4,159.6
08:07:40.620,31.0,30.0,33.27,7,13.4,53.7,159.6
08:07:46.113,29.0,30.0,33.55,7,12.8,51.0,159.7
08:07:51.113,31.0,30.0,33.27,7,13.6,54.3,159.7
08:07:56.113,30.0,30.0,33.27,7,12.9,51.6,160.3
08:08:01.130,30.0,30.0,33.39,7,13.4,53.6,160.3
08:08:06.624,31.0,30.0,33.26,8,12.9,51.6,160.6
08:08:12.113,30.0,30.0,33.02,8,12.0,48.0,160.3
08:08:17.613,33.0,30.0,33.24,9,9.7,38.9,166.3
08:08:22.617,30.0,30.0,34.03,10,10.6,42.5,159.9
08:08:28.113,28.0,30.0,34.88,13,8.3,33.1,159.9
08:08:33.121,28.0,30.0,33.73,15,10.8,43.4,159.9
08:08:38.613,29.0,30.0,33.26,15,9.3,37.3,159.9
08:08:44.112,30.0,30.0,34.38,17,9.9,39.5,159.9
08:08:49.122,30.0,30.0,33.30,17,9.6,38.5,159.9
08:08:54.612,29.0,30.0,35.23,20,12.0,47.9,159.9
08:08:59.621,30.0,30.0,33.28,20,12.1,48.3,159.9
08:09:05.112,30.0,30.0,33.28,20,12.0,48.1,159.9
08:09:10.116,31.0,30.0,33.30,20,12.0,48.1,159.9
08:09:15.627,31.0,30.0,33.28,20,12.5,50.1,159.9
08:09:21.113,30.0,30.0,33.27,20,12.2,48.7,159.9
08:09:26.119,31.0,30.0,33.27,20,12.6,50.5,159.9
08:09:31.612,30.0,30.0,33.31,20,12.6,50.4,160.1
08:09:36.613,31.0,30.0,33.23,20,12.8,51.2,160.9
08:09:41.613,0.0,30.0,34.86,22,1.4,5.8,154.5
08:09:47.112,0.0,30.0,34.86,22,1.4,5.6,154.2
08:09:52.113,0.0,30.0,34.86,22,1.2,5.0,154.1
08:09:57.612,0.0,30.0,34.86,22,1.0,4.1,153.7
08:10:02.613,0.0,30.0,34.86,22,0.8,3.3,153.9
08:10:08.226,0.0,30.0,34.86,22,5.9,23.6,155.1
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 08:04:51.194 0.0 30.0 0.00 0 13.5 54.0 81.3
3 08:04:56.614 28.0 30.0 33.57 0 9.3 37.4 114.0
4 08:05:01.616 30.0 30.0 33.55 1 9.1 36.5 116.2
5 08:05:07.115 21.0 30.0 37.40 4 8.0 31.9 116.7
6 08:05:12.614 30.0 30.0 33.26 4 7.5 29.8 116.4
7 08:05:17.622 31.0 30.0 33.29 4 9.0 36.0 116.1
8 08:05:23.114 30.0 30.0 33.26 4 8.2 33.0 116.9
9 08:05:28.124 31.0 30.0 37.47 7 12.9 51.5 157.6
10 08:05:33.614 30.0 30.0 33.31 7 13.4 53.5 157.5
11 08:05:38.621 31.0 30.0 33.27 7 12.8 51.2 157.3
12 08:05:44.114 30.0 30.0 33.00 7 12.3 49.3 157.4
13 08:05:49.614 31.0 30.0 32.98 7 12.5 50.1 156.5
14 08:05:54.627 31.0 30.0 33.02 7 13.8 55.0 156.4
15 08:06:00.113 30.0 30.0 32.96 7 12.6 50.5 159.5
16 08:06:05.118 31.0 30.0 32.99 7 12.3 49.2 159.5
17 08:06:10.624 30.0 30.0 33.23 7 11.9 47.6 159.5
18 08:06:16.113 30.0 30.0 33.26 7 10.8 43.4 159.5
19 08:06:21.125 30.0 30.0 33.30 7 11.7 46.9 159.5
20 08:06:26.614 29.0 30.0 33.17 7 12.1 48.5 159.5
21 08:06:31.617 30.0 30.0 33.30 7 12.1 48.5 159.5
22 08:06:37.127 31.0 30.0 33.27 7 12.8 51.1 159.5
23 08:06:42.614 30.0 30.0 33.28 7 12.2 48.6 159.5
24 08:06:47.622 31.0 30.0 33.24 7 12.6 50.5 159.5
25 08:06:53.113 30.0 30.0 33.27 7 11.8 47.4 159.5
26 08:06:58.114 31.0 30.0 33.24 7 12.6 50.4 159.5
27 08:07:03.613 30.0 30.0 33.31 7 14.5 57.9 159.6
28 08:07:09.113 31.0 30.0 33.26 7 12.3 49.2 159.6
29 08:07:14.121 31.0 30.0 33.55 7 12.0 48.0 159.6
30 08:07:19.613 30.0 30.0 33.27 7 12.6 50.2 159.6
31 08:07:24.616 31.0 30.0 33.29 7 12.3 49.1 159.6
32 08:07:30.125 31.0 30.0 33.27 7 12.7 50.6 159.6
33 08:07:35.613 30.0 30.0 33.27 7 12.1 48.4 159.6
34 08:07:40.620 31.0 30.0 33.27 7 13.4 53.7 159.6
35 08:07:46.113 29.0 30.0 33.55 7 12.8 51.0 159.7
36 08:07:51.113 31.0 30.0 33.27 7 13.6 54.3 159.7
37 08:07:56.113 30.0 30.0 33.27 7 12.9 51.6 160.3
38 08:08:01.130 30.0 30.0 33.39 7 13.4 53.6 160.3
39 08:08:06.624 31.0 30.0 33.26 8 12.9 51.6 160.6
40 08:08:12.113 30.0 30.0 33.02 8 12.0 48.0 160.3
41 08:08:17.613 33.0 30.0 33.24 9 9.7 38.9 166.3
42 08:08:22.617 30.0 30.0 34.03 10 10.6 42.5 159.9
43 08:08:28.113 28.0 30.0 34.88 13 8.3 33.1 159.9
44 08:08:33.121 28.0 30.0 33.73 15 10.8 43.4 159.9
45 08:08:38.613 29.0 30.0 33.26 15 9.3 37.3 159.9
46 08:08:44.112 30.0 30.0 34.38 17 9.9 39.5 159.9
47 08:08:49.122 30.0 30.0 33.30 17 9.6 38.5 159.9
48 08:08:54.612 29.0 30.0 35.23 20 12.0 47.9 159.9
49 08:08:59.621 30.0 30.0 33.28 20 12.1 48.3 159.9
50 08:09:05.112 30.0 30.0 33.28 20 12.0 48.1 159.9
51 08:09:10.116 31.0 30.0 33.30 20 12.0 48.1 159.9
52 08:09:15.627 31.0 30.0 33.28 20 12.5 50.1 159.9
53 08:09:21.113 30.0 30.0 33.27 20 12.2 48.7 159.9
54 08:09:26.119 31.0 30.0 33.27 20 12.6 50.5 159.9
55 08:09:31.612 30.0 30.0 33.31 20 12.6 50.4 160.1
56 08:09:36.613 31.0 30.0 33.23 20 12.8 51.2 160.9
57 08:09:41.613 0.0 30.0 34.86 22 1.4 5.8 154.5
58 08:09:47.112 0.0 30.0 34.86 22 1.4 5.6 154.2
59 08:09:52.113 0.0 30.0 34.86 22 1.2 5.0 154.1
60 08:09:57.612 0.0 30.0 34.86 22 1.0 4.1 153.7
61 08:10:02.613 0.0 30.0 34.86 22 0.8 3.3 153.9
62 08:10:08.226 0.0 30.0 34.86 22 5.9 23.6 155.1

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
15:39:40.185,0.0,30.0,0.00,0,13.8,55.1,82.0
15:39:45.534,0.0,30.0,0.00,0,0.0,0.1,82.2
15:39:51.035,0.0,30.0,0.00,0,0.0,0.1,82.1
15:39:56.035,0.0,30.0,0.00,0,0.0,0.1,82.4
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 15:39:40.185 0.0 30.0 0.00 0 13.8 55.1 82.0
3 15:39:45.534 0.0 30.0 0.00 0 0.0 0.1 82.2
4 15:39:51.035 0.0 30.0 0.00 0 0.0 0.1 82.1
5 15:39:56.035 0.0 30.0 0.00 0 0.0 0.1 82.4

View File

@@ -0,0 +1,3 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
15:40:09.108,0.0,30.0,0.00,0,14.7,58.7,80.6
15:40:14.534,25.0,30.0,39.89,0,6.5,26.1,110.1
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 15:40:09.108 0.0 30.0 0.00 0 14.7 58.7 80.6
3 15:40:14.534 25.0 30.0 39.89 0 6.5 26.1 110.1

View File

@@ -0,0 +1,20 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
07:31:32.392,0.0,30.0,0.00,0,14.1,56.3,99.3
07:31:37.600,8.0,30.0,111.99,2,9.8,39.3,304.1
07:31:42.630,11.0,30.0,116.35,6,22.9,91.5,304.1
07:31:48.121,7.0,30.0,125.33,8,7.3,29.1,304.4
07:31:53.601,4.0,30.0,152.61,12,2.8,11.0,304.4
07:31:58.601,5.0,30.0,151.74,14,8.8,35.4,306.6
07:32:03.673,2.0,30.0,163.66,18,7.4,29.5,308.9
07:32:08.902,7.0,30.0,171.27,18,19.6,78.2,467.9
07:32:14.102,5.0,30.0,163.81,21,6.2,24.9,455.7
07:32:19.646,7.0,30.0,197.26,27,7.0,28.1,455.1
07:32:25.101,2.0,30.0,211.44,31,4.7,18.9,455.1
07:32:30.101,2.0,30.0,228.48,32,4.8,19.0,455.1
07:32:35.102,3.0,30.0,242.35,33,5.2,20.6,455.1
07:32:40.601,5.0,30.0,260.70,33,5.2,20.8,455.1
07:32:45.602,6.0,30.0,273.24,34,7.5,29.8,455.1
07:32:51.102,7.0,30.0,254.06,34,4.7,18.6,455.1
07:32:56.602,4.0,30.0,244.86,35,2.6,10.5,455.2
07:33:01.622,4.0,30.0,249.15,36,4.7,18.8,455.2
07:33:06.631,6.0,30.0,248.25,39,12.9,51.7,455.2
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 07:31:32.392 0.0 30.0 0.00 0 14.1 56.3 99.3
3 07:31:37.600 8.0 30.0 111.99 2 9.8 39.3 304.1
4 07:31:42.630 11.0 30.0 116.35 6 22.9 91.5 304.1
5 07:31:48.121 7.0 30.0 125.33 8 7.3 29.1 304.4
6 07:31:53.601 4.0 30.0 152.61 12 2.8 11.0 304.4
7 07:31:58.601 5.0 30.0 151.74 14 8.8 35.4 306.6
8 07:32:03.673 2.0 30.0 163.66 18 7.4 29.5 308.9
9 07:32:08.902 7.0 30.0 171.27 18 19.6 78.2 467.9
10 07:32:14.102 5.0 30.0 163.81 21 6.2 24.9 455.7
11 07:32:19.646 7.0 30.0 197.26 27 7.0 28.1 455.1
12 07:32:25.101 2.0 30.0 211.44 31 4.7 18.9 455.1
13 07:32:30.101 2.0 30.0 228.48 32 4.8 19.0 455.1
14 07:32:35.102 3.0 30.0 242.35 33 5.2 20.6 455.1
15 07:32:40.601 5.0 30.0 260.70 33 5.2 20.8 455.1
16 07:32:45.602 6.0 30.0 273.24 34 7.5 29.8 455.1
17 07:32:51.102 7.0 30.0 254.06 34 4.7 18.6 455.1
18 07:32:56.602 4.0 30.0 244.86 35 2.6 10.5 455.2
19 07:33:01.622 4.0 30.0 249.15 36 4.7 18.8 455.2
20 07:33:06.631 6.0 30.0 248.25 39 12.9 51.7 455.2

View File

@@ -0,0 +1,6 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
07:43:07.984,0.0,30.0,0.00,0,17.6,70.2,99.1
07:43:13.112,0.0,30.0,0.00,0,0.1,0.5,101.0
07:43:18.612,0.0,30.0,0.00,0,0.9,3.7,102.2
07:43:23.815,0.0,30.0,0.00,0,9.5,37.9,106.9
07:43:29.112,0.0,30.0,0.00,0,1.7,6.9,106.9
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 07:43:07.984 0.0 30.0 0.00 0 17.6 70.2 99.1
3 07:43:13.112 0.0 30.0 0.00 0 0.1 0.5 101.0
4 07:43:18.612 0.0 30.0 0.00 0 0.9 3.7 102.2
5 07:43:23.815 0.0 30.0 0.00 0 9.5 37.9 106.9
6 07:43:29.112 0.0 30.0 0.00 0 1.7 6.9 106.9

View File

@@ -0,0 +1,2 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
07:43:54.723,0.0,30.0,0.00,0,15.1,60.3,98.9
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 07:43:54.723 0.0 30.0 0.00 0 15.1 60.3 98.9

View File

@@ -0,0 +1,6 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
07:48:54.766,0.0,30.0,0.00,0,17.6,70.6,98.7
07:49:00.119,0.0,30.0,0.00,0,1.4,5.7,99.4
07:49:05.120,0.0,30.0,0.00,0,1.6,6.4,103.0
07:49:10.620,0.0,30.0,0.00,0,1.0,4.0,103.1
07:49:15.620,0.0,30.0,0.00,0,1.4,5.8,113.1
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 07:48:54.766 0.0 30.0 0.00 0 17.6 70.6 98.7
3 07:49:00.119 0.0 30.0 0.00 0 1.4 5.7 99.4
4 07:49:05.120 0.0 30.0 0.00 0 1.6 6.4 103.0
5 07:49:10.620 0.0 30.0 0.00 0 1.0 4.0 103.1
6 07:49:15.620 0.0 30.0 0.00 0 1.4 5.8 113.1

View File

@@ -0,0 +1,3 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
07:49:20.673,0.0,30.0,0.00,0,18.9,75.6,98.8
07:49:26.121,0.0,30.0,0.00,0,0.0,0.1,99.4
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 07:49:20.673 0.0 30.0 0.00 0 18.9 75.6 98.8
3 07:49:26.121 0.0 30.0 0.00 0 0.0 0.1 99.4

View File

@@ -0,0 +1,2 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
07:49:37.200,0.0,30.0,0.00,0,18.3,73.2,98.8
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 07:49:37.200 0.0 30.0 0.00 0 18.3 73.2 98.8

View File

@@ -0,0 +1,28 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
07:49:50.847,0.0,30.0,0.00,0,18.2,72.9,99.1
07:49:56.120,0.0,30.0,0.00,0,1.4,5.7,99.6
07:50:01.121,0.0,30.0,0.00,0,0.0,0.1,103.3
07:50:06.622,0.0,30.0,0.00,0,0.1,0.3,103.5
07:50:12.121,26.0,30.0,40.02,0,8.6,34.4,135.3
07:50:17.135,25.0,30.0,45.12,3,12.0,48.1,175.3
07:50:22.621,25.0,30.0,40.44,4,10.3,41.1,179.6
07:50:27.751,25.0,30.0,45.58,7,6.5,26.0,169.8
07:50:33.129,26.0,30.0,40.43,7,8.1,32.5,169.4
07:50:38.622,0.0,30.0,41.90,9,0.1,0.4,167.7
07:50:44.122,0.0,30.0,41.90,9,1.0,4.0,167.2
07:50:49.124,25.0,30.0,146.22,10,7.7,30.8,169.4
07:50:54.294,24.0,30.0,40.99,11,9.3,37.1,173.4
07:50:59.621,25.0,30.0,40.43,12,6.8,27.3,173.6
07:51:04.622,25.0,30.0,40.43,12,6.8,27.1,173.5
07:51:09.622,26.0,30.0,40.41,12,7.1,28.4,173.5
07:51:15.126,26.0,30.0,40.43,13,5.2,20.7,172.1
07:51:20.622,25.0,30.0,40.43,13,4.8,19.2,172.1
07:51:25.622,26.0,30.0,40.42,13,5.2,21.0,172.1
07:51:31.122,25.0,30.0,40.44,13,4.9,19.7,172.1
07:51:36.622,25.0,30.0,40.42,13,5.0,20.1,172.1
07:51:41.625,26.0,30.0,40.41,13,5.6,22.5,172.1
07:51:47.122,25.0,30.0,40.43,14,5.2,20.9,172.1
07:51:52.124,25.0,30.0,40.43,14,4.9,19.5,172.1
07:51:57.623,25.0,30.0,40.43,14,5.2,20.9,172.1
07:52:02.675,24.0,30.0,40.93,15,5.5,22.2,172.1
07:52:08.494,25.0,30.0,40.68,17,11.4,45.7,193.1
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 07:49:50.847 0.0 30.0 0.00 0 18.2 72.9 99.1
3 07:49:56.120 0.0 30.0 0.00 0 1.4 5.7 99.6
4 07:50:01.121 0.0 30.0 0.00 0 0.0 0.1 103.3
5 07:50:06.622 0.0 30.0 0.00 0 0.1 0.3 103.5
6 07:50:12.121 26.0 30.0 40.02 0 8.6 34.4 135.3
7 07:50:17.135 25.0 30.0 45.12 3 12.0 48.1 175.3
8 07:50:22.621 25.0 30.0 40.44 4 10.3 41.1 179.6
9 07:50:27.751 25.0 30.0 45.58 7 6.5 26.0 169.8
10 07:50:33.129 26.0 30.0 40.43 7 8.1 32.5 169.4
11 07:50:38.622 0.0 30.0 41.90 9 0.1 0.4 167.7
12 07:50:44.122 0.0 30.0 41.90 9 1.0 4.0 167.2
13 07:50:49.124 25.0 30.0 146.22 10 7.7 30.8 169.4
14 07:50:54.294 24.0 30.0 40.99 11 9.3 37.1 173.4
15 07:50:59.621 25.0 30.0 40.43 12 6.8 27.3 173.6
16 07:51:04.622 25.0 30.0 40.43 12 6.8 27.1 173.5
17 07:51:09.622 26.0 30.0 40.41 12 7.1 28.4 173.5
18 07:51:15.126 26.0 30.0 40.43 13 5.2 20.7 172.1
19 07:51:20.622 25.0 30.0 40.43 13 4.8 19.2 172.1
20 07:51:25.622 26.0 30.0 40.42 13 5.2 21.0 172.1
21 07:51:31.122 25.0 30.0 40.44 13 4.9 19.7 172.1
22 07:51:36.622 25.0 30.0 40.42 13 5.0 20.1 172.1
23 07:51:41.625 26.0 30.0 40.41 13 5.6 22.5 172.1
24 07:51:47.122 25.0 30.0 40.43 14 5.2 20.9 172.1
25 07:51:52.124 25.0 30.0 40.43 14 4.9 19.5 172.1
26 07:51:57.623 25.0 30.0 40.43 14 5.2 20.9 172.1
27 07:52:02.675 24.0 30.0 40.93 15 5.5 22.2 172.1
28 07:52:08.494 25.0 30.0 40.68 17 11.4 45.7 193.1

View File

@@ -0,0 +1,7 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
08:10:09.669,0.0,30.0,0.00,0,18.9,75.5,98.7
08:10:15.132,0.0,30.0,0.00,0,0.0,0.1,98.8
08:10:20.132,0.0,30.0,0.00,0,1.0,4.1,101.5
08:10:25.631,0.0,30.0,0.00,0,0.0,0.1,103.6
08:10:30.632,0.0,30.0,0.00,0,0.0,0.1,103.2
08:10:36.133,0.0,30.0,0.00,0,0.0,0.1,102.8
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 08:10:09.669 0.0 30.0 0.00 0 18.9 75.5 98.7
3 08:10:15.132 0.0 30.0 0.00 0 0.0 0.1 98.8
4 08:10:20.132 0.0 30.0 0.00 0 1.0 4.1 101.5
5 08:10:25.631 0.0 30.0 0.00 0 0.0 0.1 103.6
6 08:10:30.632 0.0 30.0 0.00 0 0.0 0.1 103.2
7 08:10:36.133 0.0 30.0 0.00 0 0.0 0.1 102.8

View File

@@ -0,0 +1,22 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
08:10:55.056,0.0,30.0,0.00,0,16.1,64.5,99.1
08:11:00.133,0.0,30.0,0.00,0,0.0,0.1,99.3
08:11:05.133,0.0,30.0,0.00,0,0.0,0.1,99.3
08:11:10.633,0.0,30.0,0.00,0,1.5,6.1,101.9
08:11:15.637,19.0,30.0,38.85,0,6.2,24.8,134.8
08:11:21.133,24.0,30.0,40.41,0,6.8,27.4,132.0
08:11:26.633,24.0,30.0,40.43,0,5.9,23.6,131.9
08:11:32.136,25.0,30.0,40.44,0,7.8,31.0,132.0
08:11:37.632,24.0,30.0,40.67,0,6.2,24.7,130.8
08:11:42.633,25.0,30.0,40.43,0,6.0,23.8,130.8
08:11:48.134,24.0,30.0,40.75,0,7.6,30.3,130.9
08:11:53.914,25.0,30.0,42.35,2,7.2,28.9,163.2
08:11:59.134,25.0,30.0,40.62,3,7.6,30.5,136.8
08:12:04.633,24.0,30.0,40.70,4,6.3,25.4,137.2
08:12:10.134,24.0,30.0,40.73,4,5.4,21.6,137.2
08:12:15.134,25.0,30.0,40.70,4,4.2,16.9,137.1
08:12:20.634,24.0,30.0,40.70,4,4.3,17.2,137.0
08:12:25.634,25.0,30.0,40.70,4,4.4,17.5,137.0
08:12:31.136,25.0,30.0,40.41,4,4.6,18.5,137.0
08:12:36.634,24.0,30.0,40.70,4,4.4,17.6,137.0
08:12:41.634,25.0,30.0,40.45,4,4.3,17.3,137.0
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 08:10:55.056 0.0 30.0 0.00 0 16.1 64.5 99.1
3 08:11:00.133 0.0 30.0 0.00 0 0.0 0.1 99.3
4 08:11:05.133 0.0 30.0 0.00 0 0.0 0.1 99.3
5 08:11:10.633 0.0 30.0 0.00 0 1.5 6.1 101.9
6 08:11:15.637 19.0 30.0 38.85 0 6.2 24.8 134.8
7 08:11:21.133 24.0 30.0 40.41 0 6.8 27.4 132.0
8 08:11:26.633 24.0 30.0 40.43 0 5.9 23.6 131.9
9 08:11:32.136 25.0 30.0 40.44 0 7.8 31.0 132.0
10 08:11:37.632 24.0 30.0 40.67 0 6.2 24.7 130.8
11 08:11:42.633 25.0 30.0 40.43 0 6.0 23.8 130.8
12 08:11:48.134 24.0 30.0 40.75 0 7.6 30.3 130.9
13 08:11:53.914 25.0 30.0 42.35 2 7.2 28.9 163.2
14 08:11:59.134 25.0 30.0 40.62 3 7.6 30.5 136.8
15 08:12:04.633 24.0 30.0 40.70 4 6.3 25.4 137.2
16 08:12:10.134 24.0 30.0 40.73 4 5.4 21.6 137.2
17 08:12:15.134 25.0 30.0 40.70 4 4.2 16.9 137.1
18 08:12:20.634 24.0 30.0 40.70 4 4.3 17.2 137.0
19 08:12:25.634 25.0 30.0 40.70 4 4.4 17.5 137.0
20 08:12:31.136 25.0 30.0 40.41 4 4.6 18.5 137.0
21 08:12:36.634 24.0 30.0 40.70 4 4.4 17.6 137.0
22 08:12:41.634 25.0 30.0 40.45 4 4.3 17.3 137.0

View File

@@ -0,0 +1,3 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
08:22:47.307,0.0,30.0,0.00,0,19.6,78.2,106.2
08:22:56.506,13.0,30.0,82.39,0,21.9,87.7,833.9
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 08:22:47.307 0.0 30.0 0.00 0 19.6 78.2 106.2
3 08:22:56.506 13.0 30.0 82.39 0 21.9 87.7 833.9

View File

@@ -0,0 +1,54 @@
========================================================================
Duck Preview 0.1.0
Session: 2026-05-14_08-22-46
========================================================================
Platform : macOS-13.7.8-x86_64-i386-64bit
Python : 3.12.9
PySide6 : 6.11.0
Hardware : 4 logical CPUs, 16.0 GB RAM
Log file : /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_08-22-46.log
========================================================================
2026-05-14 08:22:46.126 [DEBUG ] app.logging_setup: Pruned old log: duck-preview_2026-05-13_15-40-08.log
2026-05-14 08:22:46.206 [INFO ] __main__: Application starting (session: 2026-05-14_08-22-46)
2026-05-14 08:22:46.353 [INFO ] app.ui.main_window: Telemetry CSV: /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_08-22-46.csv
2026-05-14 08:22:46.353 [DEBUG ] app.ui.camera_view: Overlay layer added: TelemetryOverlay
2026-05-14 08:22:46.353 [DEBUG ] app.ui.camera_view: Overlay layer added: BboxOverlay
2026-05-14 08:22:46.538 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method CameraView.on_frame of <app.ui.camera_view.CameraView(0x7f93a1f9cf40) at 0x208aecc80>> (drop_if_busy=True)
2026-05-14 08:22:46.538 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method TelemetryCollector.on_frame of <app.telemetry.telemetry_collector.TelemetryCollector(0x7f93a1fa49a0) at 0x208aec100>> (drop_if_busy=False)
2026-05-14 08:22:46.545 [INFO ] __main__: Application shutting down
2026-05-14 08:22:46.796 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 08:22:46.797 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 08:22:46.798 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 08:22:46.798 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 08:22:46.799 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 08:22:46.799 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 08:22:46.799 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 08:22:46.799 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:22:46.799 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 08:22:46.799 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 08:22:46.799 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 08:22:46.799 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 08:22:46.799 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 08:22:46.799 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 08:22:46.799 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 08:22:46.837 [INFO ] app.camera.camera_service: Camera format requested: NV12 3840x2160 @ 30 fps
2026-05-14 08:22:47.043 [INFO ] app.camera.camera_service: Qt multimedia backend: AVFoundation
2026-05-14 08:22:47.044 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 08:22:47.044 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 3840x2160 @ 30 fps
2026-05-14 08:22:47.044 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 08:22:47.044 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 08:22:47.244 [DEBUG ] uvc: Running on macOS (Kernel release 22.6.0)
2026-05-14 08:22:47.244 [DEBUG ] uvc: Running on macOS Monterey or newer. Requires root privileges to detach kernel drivers.
2026-05-14 08:22:47.249 [INFO ] app.camera.uvc: UVC: macOS controller loaded for 'HD USB CAMERA'

View File

@@ -0,0 +1,5 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
08:26:41.655,0.0,30.0,0.00,0,18.4,73.5,106.7
08:26:46.658,0.0,30.0,0.00,0,0.0,0.1,106.8
08:26:52.158,0.0,30.0,0.00,0,0.0,0.1,106.8
08:26:57.158,0.0,30.0,0.00,0,2.6,10.5,110.4
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 08:26:41.655 0.0 30.0 0.00 0 18.4 73.5 106.7
3 08:26:46.658 0.0 30.0 0.00 0 0.0 0.1 106.8
4 08:26:52.158 0.0 30.0 0.00 0 0.0 0.1 106.8
5 08:26:57.158 0.0 30.0 0.00 0 2.6 10.5 110.4

View File

@@ -0,0 +1,61 @@
========================================================================
Duck Preview 0.1.0
Session: 2026-05-14_08-26-40
========================================================================
Platform : macOS-13.7.8-x86_64-i386-64bit
Python : 3.12.9
PySide6 : 6.11.0
Hardware : 4 logical CPUs, 16.0 GB RAM
Log file : /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_08-26-40.log
========================================================================
2026-05-14 08:26:40.289 [DEBUG ] app.logging_setup: Pruned old log: duck-preview_2026-05-14_07-31-30.log
2026-05-14 08:26:40.381 [INFO ] __main__: Application starting (session: 2026-05-14_08-26-40)
2026-05-14 08:26:40.542 [INFO ] app.ui.main_window: Telemetry CSV: /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_08-26-40.csv
2026-05-14 08:26:40.544 [DEBUG ] app.ui.camera_view: Overlay layer added: TelemetryOverlay
2026-05-14 08:26:40.544 [DEBUG ] app.ui.camera_view: Overlay layer added: BboxOverlay
2026-05-14 08:26:40.734 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method CameraView.on_frame of <app.ui.camera_view.CameraView(0x7fca8f5c81b0) at 0x2062eff00>> (drop_if_busy=True)
2026-05-14 08:26:40.734 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method TelemetryCollector.on_frame of <app.telemetry.telemetry_collector.TelemetryCollector(0x7fca8f3266c0) at 0x2063040c0>> (drop_if_busy=False)
2026-05-14 08:26:40.739 [INFO ] __main__: Application shutting down
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 08:26:41.035 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 08:26:41.036 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 08:26:41.036 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 08:26:41.036 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 08:26:41.036 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 08:26:41.036 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 08:26:41.036 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:26:41.036 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 08:26:41.036 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 08:26:41.036 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 08:26:41.037 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 08:26:41.037 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 08:26:41.037 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 08:26:41.037 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 08:26:41.072 [INFO ] app.camera.camera_service: Camera format requested: NV12 3840x2160 @ 30 fps
2026-05-14 08:26:41.227 [INFO ] app.camera.camera_service: Qt multimedia backend: AVFoundation
2026-05-14 08:26:41.227 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 08:26:41.228 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 3840x2160 @ 30 fps
2026-05-14 08:26:41.228 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 08:26:41.228 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 08:26:41.517 [DEBUG ] uvc: Running on macOS (Kernel release 22.6.0)
2026-05-14 08:26:41.517 [DEBUG ] uvc: Running on macOS Monterey or newer. Requires root privileges to detach kernel drivers.
2026-05-14 08:26:41.525 [INFO ] app.camera.uvc: UVC: macOS controller loaded for 'HD USB CAMERA'
2026-05-14 08:26:41.556 [DEBUG ] uvc: Found device that mached uid: 20:11
2026-05-14 08:26:41.568 [DEBUG ] uvc: Device info: {'name': 'HD USB CAMERA', 'manufacturer': '4K USB CAMERA', 'serialNumber': '01.00.00', 'idProduct': 791, 'idVendor': 13028, 'device_address': 11, 'bus_number': 20, 'uid': '20:11'}
2026-05-14 08:26:41.581 [WARNING] app.camera.uvc.macos: UVC macOS open failed: Could not open device. Error: Access denied
2026-05-14 08:26:59.636 [INFO ] app.inference.worker_manager: Inference worker stopped
2026-05-14 08:26:59.660 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 08:26:59.660 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 08:26:59.660 [INFO ] app.ui.main_window: CSV telemetry: 4 rows written

View File

@@ -0,0 +1,34 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
08:29:31.177,0.0,30.0,0.00,0,13.9,55.5,106.5
08:29:36.279,0.0,30.0,0.00,0,0.0,0.1,106.7
08:29:41.778,0.0,30.0,0.00,0,0.0,0.1,110.0
08:29:47.278,0.0,30.0,0.00,0,0.0,0.1,109.9
08:29:52.777,0.0,30.0,0.00,0,1.0,4.1,110.0
08:29:57.778,0.0,30.0,0.00,0,0.0,0.1,110.0
08:30:02.779,0.0,30.0,0.00,0,0.0,0.1,110.0
08:30:08.278,0.0,30.0,0.00,0,0.0,0.1,110.0
08:30:13.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:30:18.778,0.0,30.0,0.00,0,0.0,0.1,110.0
08:30:23.779,0.0,30.0,0.00,0,0.0,0.1,110.0
08:30:29.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:30:34.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:30:39.779,0.0,30.0,0.00,0,0.0,0.1,110.0
08:30:44.779,0.0,30.0,0.00,0,0.0,0.1,110.0
08:30:50.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:30:55.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:31:00.779,0.0,30.0,0.00,0,0.0,0.1,110.0
08:31:06.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:31:11.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:31:16.779,0.0,30.0,0.00,0,0.0,0.1,110.0
08:31:22.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:31:27.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:31:32.779,0.0,30.0,0.00,0,0.0,0.1,110.0
08:31:38.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:31:43.779,0.0,30.0,0.00,0,0.0,0.1,110.0
08:31:49.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:31:54.779,0.0,30.0,0.00,0,0.0,0.1,110.0
08:32:00.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:32:05.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:32:10.779,0.0,30.0,0.00,0,0.0,0.1,110.0
08:32:16.279,0.0,30.0,0.00,0,0.0,0.1,110.0
08:32:21.279,0.0,30.0,0.00,0,0.0,0.1,110.0
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 08:29:31.177 0.0 30.0 0.00 0 13.9 55.5 106.5
3 08:29:36.279 0.0 30.0 0.00 0 0.0 0.1 106.7
4 08:29:41.778 0.0 30.0 0.00 0 0.0 0.1 110.0
5 08:29:47.278 0.0 30.0 0.00 0 0.0 0.1 109.9
6 08:29:52.777 0.0 30.0 0.00 0 1.0 4.1 110.0
7 08:29:57.778 0.0 30.0 0.00 0 0.0 0.1 110.0
8 08:30:02.779 0.0 30.0 0.00 0 0.0 0.1 110.0
9 08:30:08.278 0.0 30.0 0.00 0 0.0 0.1 110.0
10 08:30:13.279 0.0 30.0 0.00 0 0.0 0.1 110.0
11 08:30:18.778 0.0 30.0 0.00 0 0.0 0.1 110.0
12 08:30:23.779 0.0 30.0 0.00 0 0.0 0.1 110.0
13 08:30:29.279 0.0 30.0 0.00 0 0.0 0.1 110.0
14 08:30:34.279 0.0 30.0 0.00 0 0.0 0.1 110.0
15 08:30:39.779 0.0 30.0 0.00 0 0.0 0.1 110.0
16 08:30:44.779 0.0 30.0 0.00 0 0.0 0.1 110.0
17 08:30:50.279 0.0 30.0 0.00 0 0.0 0.1 110.0
18 08:30:55.279 0.0 30.0 0.00 0 0.0 0.1 110.0
19 08:31:00.779 0.0 30.0 0.00 0 0.0 0.1 110.0
20 08:31:06.279 0.0 30.0 0.00 0 0.0 0.1 110.0
21 08:31:11.279 0.0 30.0 0.00 0 0.0 0.1 110.0
22 08:31:16.779 0.0 30.0 0.00 0 0.0 0.1 110.0
23 08:31:22.279 0.0 30.0 0.00 0 0.0 0.1 110.0
24 08:31:27.279 0.0 30.0 0.00 0 0.0 0.1 110.0
25 08:31:32.779 0.0 30.0 0.00 0 0.0 0.1 110.0
26 08:31:38.279 0.0 30.0 0.00 0 0.0 0.1 110.0
27 08:31:43.779 0.0 30.0 0.00 0 0.0 0.1 110.0
28 08:31:49.279 0.0 30.0 0.00 0 0.0 0.1 110.0
29 08:31:54.779 0.0 30.0 0.00 0 0.0 0.1 110.0
30 08:32:00.279 0.0 30.0 0.00 0 0.0 0.1 110.0
31 08:32:05.279 0.0 30.0 0.00 0 0.0 0.1 110.0
32 08:32:10.779 0.0 30.0 0.00 0 0.0 0.1 110.0
33 08:32:16.279 0.0 30.0 0.00 0 0.0 0.1 110.0
34 08:32:21.279 0.0 30.0 0.00 0 0.0 0.1 110.0

View File

@@ -0,0 +1,61 @@
========================================================================
Duck Preview 0.1.0
Session: 2026-05-14_08-29-28
========================================================================
Platform : macOS-13.7.8-x86_64-i386-64bit
Python : 3.12.9
PySide6 : 6.11.0
Hardware : 4 logical CPUs, 16.0 GB RAM
Log file : /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_08-29-28.log
========================================================================
2026-05-14 08:29:28.706 [DEBUG ] app.logging_setup: Pruned old log: duck-preview_2026-05-14_07-43-06.log
2026-05-14 08:29:28.976 [INFO ] __main__: Application starting (session: 2026-05-14_08-29-28)
2026-05-14 08:29:29.395 [INFO ] app.ui.main_window: Telemetry CSV: /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_08-29-28.csv
2026-05-14 08:29:29.399 [DEBUG ] app.ui.camera_view: Overlay layer added: TelemetryOverlay
2026-05-14 08:29:29.399 [DEBUG ] app.ui.camera_view: Overlay layer added: BboxOverlay
2026-05-14 08:29:29.787 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method CameraView.on_frame of <app.ui.camera_view.CameraView(0x7fbc9a83d070) at 0x205680a40>> (drop_if_busy=True)
2026-05-14 08:29:29.787 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method TelemetryCollector.on_frame of <app.telemetry.telemetry_collector.TelemetryCollector(0x7fbc98e77f70) at 0x200abfec0>> (drop_if_busy=False)
2026-05-14 08:29:29.800 [INFO ] __main__: Application shutting down
2026-05-14 08:29:30.254 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 08:29:30.255 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 08:29:30.256 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 08:29:30.256 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 08:29:30.256 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 08:29:30.256 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 08:29:30.256 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 08:29:30.256 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:29:30.256 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 08:29:30.256 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 08:29:30.256 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 08:29:30.257 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 08:29:30.257 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 08:29:30.257 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 08:29:30.257 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 08:29:30.298 [INFO ] app.camera.camera_service: Camera format requested: NV12 3840x2160 @ 30 fps
2026-05-14 08:29:30.493 [INFO ] app.camera.camera_service: Qt multimedia backend: AVFoundation
2026-05-14 08:29:30.493 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 08:29:30.494 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 3840x2160 @ 30 fps
2026-05-14 08:29:30.494 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 08:29:30.494 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 08:29:31.029 [DEBUG ] uvc: Running on macOS (Kernel release 22.6.0)
2026-05-14 08:29:31.029 [DEBUG ] uvc: Running on macOS Monterey or newer. Requires root privileges to detach kernel drivers.
2026-05-14 08:29:31.037 [INFO ] app.camera.uvc: UVC: macOS controller loaded for 'HD USB CAMERA'
2026-05-14 08:29:31.072 [DEBUG ] uvc: Found device that mached uid: 20:10
2026-05-14 08:29:31.084 [DEBUG ] uvc: Device info: {'name': 'HD USB CAMERA', 'manufacturer': '4K USB CAMERA', 'serialNumber': '01.00.00', 'idProduct': 791, 'idVendor': 13028, 'device_address': 10, 'bus_number': 20, 'uid': '20:10'}
2026-05-14 08:29:31.098 [WARNING] app.camera.uvc.macos: UVC macOS open failed: Could not open device. Error: Access denied
2026-05-14 08:32:26.267 [INFO ] app.inference.worker_manager: Inference worker stopped
2026-05-14 08:32:26.288 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 08:32:26.288 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 08:32:26.289 [INFO ] app.ui.main_window: CSV telemetry: 33 rows written

View File

@@ -0,0 +1,10 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
08:34:27.798,0.0,30.0,0.00,0,14.2,57.0,80.7
08:34:33.285,25.0,30.0,40.25,0,6.3,25.1,113.8
08:34:38.778,24.0,30.0,40.27,0,6.1,24.4,116.3
08:34:43.781,25.0,30.0,40.70,1,6.9,27.6,117.7
08:34:48.786,25.0,30.0,45.56,4,9.9,39.7,157.4
08:34:54.278,24.0,30.0,40.44,4,11.3,45.1,156.9
08:34:59.778,25.0,30.0,40.53,4,11.4,45.6,157.7
08:35:04.788,25.0,30.0,40.65,5,10.4,41.5,160.7
08:35:10.278,24.0,30.0,40.43,5,9.7,38.7,160.2
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 08:34:27.798 0.0 30.0 0.00 0 14.2 57.0 80.7
3 08:34:33.285 25.0 30.0 40.25 0 6.3 25.1 113.8
4 08:34:38.778 24.0 30.0 40.27 0 6.1 24.4 116.3
5 08:34:43.781 25.0 30.0 40.70 1 6.9 27.6 117.7
6 08:34:48.786 25.0 30.0 45.56 4 9.9 39.7 157.4
7 08:34:54.278 24.0 30.0 40.44 4 11.3 45.1 156.9
8 08:34:59.778 25.0 30.0 40.53 4 11.4 45.6 157.7
9 08:35:04.788 25.0 30.0 40.65 5 10.4 41.5 160.7
10 08:35:10.278 24.0 30.0 40.43 5 9.7 38.7 160.2

View File

@@ -0,0 +1,62 @@
========================================================================
Duck Preview 0.1.0
Session: 2026-05-14_08-34-26
========================================================================
Platform : macOS-13.7.8-x86_64-i386-64bit
Python : 3.12.9
PySide6 : 6.11.0
Hardware : 4 logical CPUs, 16.0 GB RAM
Log file : /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_08-34-26.log
========================================================================
2026-05-14 08:34:26.868 [DEBUG ] app.logging_setup: Pruned old log: duck-preview_2026-05-14_07-43-53.log
2026-05-14 08:34:26.922 [INFO ] __main__: Application starting (session: 2026-05-14_08-34-26)
2026-05-14 08:34:27.054 [INFO ] app.ui.main_window: Telemetry CSV: /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_08-34-26.csv
2026-05-14 08:34:27.055 [DEBUG ] app.ui.camera_view: Overlay layer added: TelemetryOverlay
2026-05-14 08:34:27.238 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method CameraView.on_frame of <app.ui.camera_view.CameraView(0x7f8c82231fb0) at 0x20959e400>> (drop_if_busy=True)
2026-05-14 08:34:27.238 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method TelemetryCollector.on_frame of <app.telemetry.telemetry_collector.TelemetryCollector(0x7f8c8176ec80) at 0x20959eb00>> (drop_if_busy=False)
2026-05-14 08:34:27.242 [INFO ] __main__: Application shutting down
2026-05-14 08:34:27.465 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 08:34:27.465 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 08:34:27.465 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 08:34:27.466 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 08:34:27.466 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 08:34:27.466 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:34:27.466 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 08:34:27.466 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 08:34:27.466 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 08:34:27.466 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 08:34:27.466 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 08:34:27.466 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 08:34:27.466 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 08:34:27.467 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 08:34:27.468 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 08:34:27.502 [INFO ] app.camera.camera_service: Camera format requested: NV12 1280x720 @ 30.0 fps
2026-05-14 08:34:27.725 [INFO ] app.camera.camera_service: Qt multimedia backend: AVFoundation
2026-05-14 08:34:27.725 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 08:34:27.725 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1280x720 @ 30.0 fps
2026-05-14 08:34:27.725 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 08:34:27.726 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 08:34:44.493 [DEBUG ] app.ui.menu_bar: Resolution selected: 1920x1080
2026-05-14 08:34:44.493 [INFO ] app.camera.camera_service: Resolution change: 1920x1080 — restarting camera
2026-05-14 08:34:44.543 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 08:34:44.543 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 08:34:44.569 [INFO ] app.camera.camera_service: Camera format requested: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:34:44.658 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 08:34:44.658 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:34:44.658 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 08:34:44.658 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 08:35:14.675 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 08:35:14.675 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 08:35:14.675 [INFO ] app.ui.main_window: CSV telemetry: 9 rows written

View File

@@ -0,0 +1,3 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
08:40:02.523,0.0,30.0,0.00,0,19.9,79.7,104.8
08:40:07.778,0.0,30.0,0.00,0,0.0,0.1,104.9
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 08:40:02.523 0.0 30.0 0.00 0 19.9 79.7 104.8
3 08:40:07.778 0.0 30.0 0.00 0 0.0 0.1 104.9

View File

@@ -0,0 +1,59 @@
========================================================================
Duck Preview 0.1.0
Session: 2026-05-14_08-40-01
========================================================================
Platform : macOS-13.7.8-x86_64-i386-64bit
Python : 3.12.9
PySide6 : 6.11.0
Hardware : 4 logical CPUs, 16.0 GB RAM
Log file : /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_08-40-01.log
========================================================================
2026-05-14 08:40:01.323 [DEBUG ] app.logging_setup: Pruned old log: duck-preview_2026-05-14_07-48-53.log
2026-05-14 08:40:01.384 [INFO ] __main__: Application starting (session: 2026-05-14_08-40-01)
2026-05-14 08:40:01.532 [INFO ] app.ui.main_window: Telemetry CSV: /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_08-40-01.csv
2026-05-14 08:40:01.532 [DEBUG ] app.ui.camera_view: Overlay layer added: TelemetryOverlay
2026-05-14 08:40:01.724 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method CameraView.on_frame of <app.ui.camera_view.CameraView(0x7f9a95074610) at 0x20799de80>> (drop_if_busy=True)
2026-05-14 08:40:01.724 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method TelemetryCollector.on_frame of <app.telemetry.telemetry_collector.TelemetryCollector(0x7f9a95073260) at 0x20799e580>> (drop_if_busy=False)
2026-05-14 08:40:01.730 [INFO ] __main__: Application shutting down
2026-05-14 08:40:01.968 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 08:40:01.968 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 08:40:01.968 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 08:40:01.968 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 08:40:01.969 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 08:40:01.969 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:40:01.969 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 08:40:01.969 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 08:40:01.969 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 08:40:01.969 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 08:40:01.969 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 08:40:01.969 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 08:40:01.969 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 08:40:01.970 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 08:40:01.970 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 08:40:01.970 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 08:40:01.970 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 08:40:01.970 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 08:40:01.970 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 08:40:01.970 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 08:40:01.970 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 08:40:01.970 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 08:40:01.971 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 08:40:01.971 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 08:40:01.971 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 08:40:01.971 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 08:40:02.006 [INFO ] app.camera.camera_service: Camera format requested: NV12 3840x2160 @ 30 fps
2026-05-14 08:40:02.231 [INFO ] app.camera.camera_service: Qt multimedia backend: AVFoundation
2026-05-14 08:40:02.231 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 08:40:02.231 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 3840x2160 @ 30 fps
2026-05-14 08:40:02.231 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 08:40:02.232 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 08:40:02.407 [DEBUG ] uvc: Running on macOS (Kernel release 22.6.0)
2026-05-14 08:40:02.407 [DEBUG ] uvc: Running on macOS Monterey or newer. Requires root privileges to detach kernel drivers.
2026-05-14 08:40:02.413 [INFO ] app.camera.uvc: UVC: macOS controller loaded for 'HD USB CAMERA'
2026-05-14 08:40:02.443 [DEBUG ] uvc: Found device that mached uid: 20:10
2026-05-14 08:40:02.455 [DEBUG ] uvc: Device info: {'name': 'HD USB CAMERA', 'manufacturer': '4K USB CAMERA', 'serialNumber': '01.00.00', 'idProduct': 791, 'idVendor': 13028, 'device_address': 10, 'bus_number': 20, 'uid': '20:10'}
2026-05-14 08:40:02.468 [WARNING] app.camera.uvc.macos: UVC macOS open failed: Could not open device. Error: Access denied
2026-05-14 08:40:11.300 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 08:40:11.300 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 08:40:11.300 [INFO ] app.ui.main_window: CSV telemetry: 2 rows written

View File

@@ -0,0 +1,5 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
09:05:00.079,0.0,30.0,0.00,0,15.1,60.3,82.1
09:05:05.402,0.0,30.0,0.00,0,0.0,0.1,82.2
09:05:10.402,0.0,30.0,0.00,0,0.1,0.6,84.5
09:05:15.403,0.0,30.0,0.00,0,0.2,0.8,84.5
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 09:05:00.079 0.0 30.0 0.00 0 15.1 60.3 82.1
3 09:05:05.402 0.0 30.0 0.00 0 0.0 0.1 82.2
4 09:05:10.402 0.0 30.0 0.00 0 0.1 0.6 84.5
5 09:05:15.403 0.0 30.0 0.00 0 0.2 0.8 84.5

View File

@@ -0,0 +1,57 @@
========================================================================
Duck Preview 0.1.0
Session: 2026-05-14_09-04-58
========================================================================
Platform : macOS-13.7.8-x86_64-i386-64bit
Python : 3.12.9
PySide6 : 6.11.1
Hardware : 4 logical CPUs, 16.0 GB RAM
Log file : /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_09-04-58.log
========================================================================
2026-05-14 09:04:58.962 [DEBUG ] app.logging_setup: Pruned old log: duck-preview_2026-05-14_07-49-19.log
2026-05-14 09:04:59.048 [INFO ] __main__: Application starting (session: 2026-05-14_09-04-58)
2026-05-14 09:04:59.235 [INFO ] app.ui.main_window: Telemetry CSV: /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_09-04-58.csv
2026-05-14 09:04:59.236 [DEBUG ] app.ui.camera_view: Overlay layer added: TelemetryOverlay
2026-05-14 09:04:59.236 [DEBUG ] app.ui.camera_view: Overlay layer added: BboxOverlay
2026-05-14 09:04:59.552 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method CameraView.on_frame of <app.ui.camera_view.CameraView(0x7f9feeec75f0) at 0x2042c4040>> (drop_if_busy=True)
2026-05-14 09:04:59.552 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method TelemetryCollector.on_frame of <app.telemetry.telemetry_collector.TelemetryCollector(0x7f9feeecfa10) at 0x2042bb840>> (drop_if_busy=False)
2026-05-14 09:04:59.558 [INFO ] __main__: Application shutting down
2026-05-14 09:04:59.813 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 09:04:59.814 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 09:04:59.814 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 09:04:59.814 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 09:04:59.814 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 09:04:59.814 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 09:04:59.814 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 09:04:59.814 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 09:04:59.814 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 09:04:59.815 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 09:04:59.815 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 09:04:59.815 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 09:04:59.815 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 09:04:59.815 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 09:04:59.816 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 09:04:59.864 [INFO ] app.camera.camera_service: Camera format requested: NV12 3840x2160 @ 30 fps
2026-05-14 09:05:00.011 [INFO ] app.camera.camera_service: Qt multimedia backend: AVFoundation
2026-05-14 09:05:00.011 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:05:00.011 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 3840x2160 @ 30 fps
2026-05-14 09:05:00.011 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:05:00.011 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:05:00.012 [DEBUG ] app.camera.uvc.macos: pyuvc not available — macOS UVC controls disabled
2026-05-14 09:05:00.012 [WARNING] app.camera.uvc: UVC: pyuvc not installed — UVC controls unavailable on macOS
2026-05-14 09:05:20.524 [INFO ] app.inference.worker_manager: Inference worker stopped
2026-05-14 09:05:20.549 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:05:20.550 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:05:20.550 [INFO ] app.ui.main_window: CSV telemetry: 4 rows written

View File

@@ -0,0 +1,12 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
09:06:42.042,0.0,30.0,0.00,0,13.4,53.8,81.8
09:06:47.434,25.0,30.0,40.45,0,7.8,31.3,112.3
09:06:52.905,28.0,30.0,38.49,2,7.5,30.0,118.9
09:06:57.905,24.0,30.0,35.18,2,7.1,28.4,118.6
09:07:03.434,21.0,30.0,42.03,4,8.7,34.7,116.5
09:07:08.904,25.0,30.0,40.42,5,10.9,43.5,159.0
09:07:13.905,25.0,30.0,40.43,5,9.9,39.6,158.8
09:07:18.906,25.0,30.0,40.56,5,12.8,51.1,158.9
09:07:23.910,25.0,30.0,40.60,5,10.1,40.4,157.6
09:07:29.406,24.0,30.0,40.42,5,9.8,39.2,157.1
09:07:34.407,25.0,30.0,40.68,5,10.1,40.3,157.1
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 09:06:42.042 0.0 30.0 0.00 0 13.4 53.8 81.8
3 09:06:47.434 25.0 30.0 40.45 0 7.8 31.3 112.3
4 09:06:52.905 28.0 30.0 38.49 2 7.5 30.0 118.9
5 09:06:57.905 24.0 30.0 35.18 2 7.1 28.4 118.6
6 09:07:03.434 21.0 30.0 42.03 4 8.7 34.7 116.5
7 09:07:08.904 25.0 30.0 40.42 5 10.9 43.5 159.0
8 09:07:13.905 25.0 30.0 40.43 5 9.9 39.6 158.8
9 09:07:18.906 25.0 30.0 40.56 5 12.8 51.1 158.9
10 09:07:23.910 25.0 30.0 40.60 5 10.1 40.4 157.6
11 09:07:29.406 24.0 30.0 40.42 5 9.8 39.2 157.1
12 09:07:34.407 25.0 30.0 40.68 5 10.1 40.3 157.1

View File

@@ -0,0 +1,67 @@
========================================================================
Duck Preview 0.1.0
Session: 2026-05-14_09-06-41
========================================================================
Platform : macOS-13.7.8-x86_64-i386-64bit
Python : 3.12.9
PySide6 : 6.11.1
Hardware : 4 logical CPUs, 16.0 GB RAM
Log file : /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_09-06-41.log
========================================================================
2026-05-14 09:06:41.050 [DEBUG ] app.logging_setup: Pruned old log: duck-preview_2026-05-14_07-49-36.log
2026-05-14 09:06:41.119 [INFO ] __main__: Application starting (session: 2026-05-14_09-06-41)
2026-05-14 09:06:41.276 [INFO ] app.ui.main_window: Telemetry CSV: /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_09-06-41.csv
2026-05-14 09:06:41.277 [DEBUG ] app.ui.camera_view: Overlay layer added: TelemetryOverlay
2026-05-14 09:06:41.277 [DEBUG ] app.ui.camera_view: Overlay layer added: BboxOverlay
2026-05-14 09:06:41.478 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method CameraView.on_frame of <app.ui.camera_view.CameraView(0x7fa3d85b0590) at 0x1feeffec0>> (drop_if_busy=True)
2026-05-14 09:06:41.478 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method TelemetryCollector.on_frame of <app.telemetry.telemetry_collector.TelemetryCollector(0x7fa3d85ada80) at 0x1feeff2c0>> (drop_if_busy=False)
2026-05-14 09:06:41.482 [INFO ] __main__: Application shutting down
2026-05-14 09:06:41.754 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 09:06:41.754 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 09:06:41.754 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 09:06:41.754 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 09:06:41.754 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 09:06:41.755 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 09:06:41.755 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 09:06:41.755 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 09:06:41.755 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 09:06:41.755 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 09:06:41.755 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 09:06:41.755 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 09:06:41.755 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 09:06:41.756 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 09:06:41.757 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 09:06:41.757 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 09:06:41.757 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 09:06:41.757 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 09:06:41.758 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 09:06:41.758 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 09:06:41.758 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 09:06:41.758 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 09:06:41.758 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 09:06:41.758 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 09:06:41.758 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 09:06:41.758 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 09:06:41.786 [INFO ] app.camera.camera_service: Camera format requested: NV12 1280x720 @ 30 fps
2026-05-14 09:06:41.973 [INFO ] app.camera.camera_service: Qt multimedia backend: AVFoundation
2026-05-14 09:06:41.973 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:06:41.973 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1280x720 @ 30 fps
2026-05-14 09:06:41.973 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:06:41.973 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:06:41.974 [DEBUG ] app.camera.uvc.macos: pyuvc not available — macOS UVC controls disabled
2026-05-14 09:06:41.975 [WARNING] app.camera.uvc: UVC: pyuvc not installed — UVC controls unavailable on macOS
2026-05-14 09:07:03.257 [DEBUG ] app.ui.menu_bar: Format selected: 1920x1080 @ 30 fps (NV12)
2026-05-14 09:07:03.258 [INFO ] app.ui.main_window: Format selected via menu: 1920x1080 @ 30 fps (NV12)
2026-05-14 09:07:03.258 [INFO ] app.camera.camera_service: Format change: 1920x1080 @ 30 fps (NV12) — restarting camera
2026-05-14 09:07:03.304 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:07:03.304 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:07:03.334 [INFO ] app.camera.camera_service: Camera format requested: NV12 1920x1080 @ 30 fps
2026-05-14 09:07:03.416 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:07:03.417 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1920x1080 @ 30 fps
2026-05-14 09:07:03.417 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:07:03.417 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:07:35.646 [INFO ] app.inference.worker_manager: Inference worker stopped
2026-05-14 09:07:35.713 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:07:35.714 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:07:35.714 [INFO ] app.ui.main_window: CSV telemetry: 11 rows written

View File

@@ -0,0 +1,9 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
09:36:27.985,0.0,30.0,0.00,0,14.2,57.0,81.8
09:36:32.988,30.0,30.0,33.30,0,9.4,37.8,111.0
09:36:38.208,33.0,30.0,35.65,3,8.1,32.5,159.8
09:36:43.489,30.0,30.0,33.77,5,9.9,39.5,119.8
09:36:48.489,31.0,30.0,33.29,6,7.8,31.3,120.1
09:36:53.988,30.0,30.0,33.25,6,7.6,30.4,120.1
09:36:58.996,26.0,30.0,33.36,7,14.8,59.3,150.2
09:37:04.493,31.0,30.0,33.26,9,10.1,40.5,143.8
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 09:36:27.985 0.0 30.0 0.00 0 14.2 57.0 81.8
3 09:36:32.988 30.0 30.0 33.30 0 9.4 37.8 111.0
4 09:36:38.208 33.0 30.0 35.65 3 8.1 32.5 159.8
5 09:36:43.489 30.0 30.0 33.77 5 9.9 39.5 119.8
6 09:36:48.489 31.0 30.0 33.29 6 7.8 31.3 120.1
7 09:36:53.988 30.0 30.0 33.25 6 7.6 30.4 120.1
8 09:36:58.996 26.0 30.0 33.36 7 14.8 59.3 150.2
9 09:37:04.493 31.0 30.0 33.26 9 10.1 40.5 143.8

View File

@@ -0,0 +1,74 @@
========================================================================
Duck Preview 0.1.0
Session: 2026-05-14_09-36-27
========================================================================
Platform : macOS-13.7.8-x86_64-i386-64bit
Python : 3.12.9
PySide6 : 6.11.1
Hardware : 4 logical CPUs, 16.0 GB RAM
Log file : /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_09-36-27.log
========================================================================
2026-05-14 09:36:27.061 [DEBUG ] app.logging_setup: Pruned old log: duck-preview_2026-05-14_07-49-49.log
2026-05-14 09:36:27.127 [INFO ] __main__: Application starting (session: 2026-05-14_09-36-27)
2026-05-14 09:36:27.279 [INFO ] app.ui.main_window: Telemetry CSV: /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_09-36-27.csv
2026-05-14 09:36:27.280 [DEBUG ] app.ui.camera_view: Overlay layer added: TelemetryOverlay
2026-05-14 09:36:27.280 [DEBUG ] app.ui.camera_view: Overlay layer added: BboxOverlay
2026-05-14 09:36:27.456 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method CameraView.on_frame of <app.ui.camera_view.CameraView(0x7f7a748690e0) at 0x2092e8c00>> (drop_if_busy=True)
2026-05-14 09:36:27.456 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method TelemetryCollector.on_frame of <app.telemetry.telemetry_collector.TelemetryCollector(0x7f7a74866290) at 0x2092e8080>> (drop_if_busy=False)
2026-05-14 09:36:27.461 [INFO ] __main__: Application shutting down
2026-05-14 09:36:27.696 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 09:36:27.696 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 09:36:27.696 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 09:36:27.696 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 09:36:27.696 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 09:36:27.696 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 09:36:27.696 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 09:36:27.696 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 09:36:27.696 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 09:36:27.696 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 09:36:27.697 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 09:36:27.697 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 09:36:27.697 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 09:36:27.698 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 09:36:27.698 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 09:36:27.698 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 09:36:27.698 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 09:36:27.698 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 09:36:27.698 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 09:36:27.698 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 09:36:27.698 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 09:36:27.699 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 09:36:27.699 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 09:36:27.699 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 09:36:27.699 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 09:36:27.699 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 09:36:27.735 [INFO ] app.camera.camera_service: Camera format requested: NV12 1280x720 @ 30 fps
2026-05-14 09:36:27.908 [INFO ] app.camera.camera_service: Qt multimedia backend: AVFoundation
2026-05-14 09:36:27.908 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:36:27.908 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1280x720 @ 30 fps
2026-05-14 09:36:27.909 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:36:27.909 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:36:27.909 [DEBUG ] app.camera.uvc.macos: pyuvc not available — macOS UVC controls disabled
2026-05-14 09:36:27.910 [WARNING] app.camera.uvc: UVC: pyuvc not installed — UVC controls unavailable on macOS
2026-05-14 09:36:43.640 [DEBUG ] app.ui.menu_bar: Model file selected: /Users/rafalkaczka/Repos/duck-preview/models/best_v1.pt
2026-05-14 09:36:43.641 [INFO ] app.ui.main_window: Loading model: /Users/rafalkaczka/Repos/duck-preview/models/best_v1.pt
2026-05-14 09:36:43.641 [INFO ] app.inference.worker_manager: Inference worker stopped
2026-05-14 09:36:43.708 [INFO ] app.inference.worker_manager: Inference worker started (pid=3571, model=/Users/rafalkaczka/Repos/duck-preview/models/best_v1.pt)
2026-05-14 09:36:43.709 [DEBUG ] app.inference.worker_manager: InferenceManager: resumed
2026-05-14 09:36:43.710 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7f7a74867720) at 0x2092e8440>> (drop_if_busy=True)
2026-05-14 09:36:43.713 [INFO ] app.ui.main_window: Inference enabled
2026-05-14 09:36:43.714 [DEBUG ] app.inference.worker_manager: InferenceManager: paused
2026-05-14 09:36:43.714 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber not found for removal: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7f7a74867720) at 0x2092e8440>>
2026-05-14 09:36:43.715 [INFO ] app.ui.main_window: Inference disabled
2026-05-14 09:36:49.389 [INFO ] app.inference.worker_manager: Inference device: cpu
2026-05-14 09:36:58.844 [DEBUG ] app.inference.worker_manager: InferenceManager: resumed
2026-05-14 09:36:58.844 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7f7a74867720) at 0x2092e8440>> (drop_if_busy=True)
2026-05-14 09:36:58.845 [INFO ] app.ui.main_window: Inference enabled
2026-05-14 09:37:03.253 [DEBUG ] app.inference.worker_manager: InferenceManager: paused
2026-05-14 09:37:03.254 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber not found for removal: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7f7a74867720) at 0x2092e8440>>
2026-05-14 09:37:03.254 [INFO ] app.ui.main_window: Inference disabled
2026-05-14 09:37:05.808 [INFO ] app.inference.worker_manager: Inference worker stopped
2026-05-14 09:37:05.867 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:37:05.867 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:37:05.867 [INFO ] app.ui.main_window: CSV telemetry: 8 rows written

View File

@@ -0,0 +1,72 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
09:40:55.663,0.0,30.0,0.00,0,14.3,57.1,81.8
09:41:01.763,34.0,30.0,35.67,2,8.4,33.6,160.3
09:41:06.996,32.0,30.0,33.22,3,7.9,31.5,119.6
09:41:12.002,31.0,30.0,33.27,4,10.7,42.9,131.2
09:41:17.496,30.0,30.0,33.27,4,8.4,33.7,120.7
09:41:22.501,31.0,30.0,33.31,6,12.2,48.6,151.0
09:41:27.995,30.0,30.0,33.31,6,11.4,45.8,151.1
09:41:32.998,31.0,30.0,33.30,6,12.1,48.5,151.0
09:41:38.507,31.0,30.0,33.25,6,11.7,46.7,151.1
09:41:43.997,30.0,30.0,33.28,6,11.2,44.6,150.3
09:41:49.496,25.0,30.0,36.81,6,9.9,39.7,150.3
09:41:54.498,24.0,30.0,40.73,6,10.0,40.0,150.3
09:41:59.997,25.0,30.0,40.66,6,10.1,40.3,150.4
09:42:05.497,25.0,30.0,40.68,6,9.8,39.0,150.5
09:42:10.498,24.0,30.0,40.71,6,9.8,39.0,150.5
09:42:15.499,25.0,30.0,40.46,6,9.2,36.8,150.5
09:42:20.998,24.0,30.0,41.21,7,11.8,47.4,150.5
09:42:26.497,24.0,30.0,40.41,7,9.8,39.2,150.5
09:42:31.509,24.0,30.0,42.09,8,10.1,40.4,150.5
09:42:36.998,23.0,30.0,42.63,8,9.4,37.7,150.6
09:42:42.001,24.0,30.0,42.59,9,8.9,35.7,150.6
09:42:47.498,23.0,30.0,42.66,11,8.7,34.7,150.6
09:42:52.499,26.0,30.0,43.72,12,10.0,39.9,150.6
09:42:58.010,24.0,30.0,43.20,13,9.3,37.1,150.6
09:43:03.500,19.0,30.0,42.91,14,9.2,37.0,150.6
09:43:08.504,22.0,30.0,44.86,15,8.6,34.3,150.6
09:43:14.007,22.0,30.0,44.23,17,9.2,36.6,150.6
09:43:19.500,24.0,30.0,43.53,18,10.1,40.2,150.6
09:43:24.514,20.0,30.0,49.00,23,8.7,34.7,151.8
09:43:30.000,24.0,30.0,44.75,24,6.5,26.0,152.0
09:43:35.000,25.0,30.0,42.36,25,7.0,28.0,151.7
09:43:40.504,25.0,30.0,41.79,25,6.6,26.3,153.1
09:43:46.000,23.0,30.0,46.75,29,6.5,26.0,152.2
09:43:51.003,25.0,30.0,41.55,30,8.0,32.0,152.3
09:43:56.501,0.0,30.0,41.92,31,0.5,2.0,150.6
09:44:02.001,0.0,30.0,41.92,31,0.1,0.6,150.6
09:44:07.501,0.0,30.0,41.92,31,0.2,0.8,150.2
09:44:12.501,0.0,30.0,41.92,31,0.5,2.0,148.8
09:44:18.004,7.0,30.0,251.99,32,7.9,31.5,189.3
09:44:23.512,30.0,30.0,33.28,32,11.9,47.7,189.4
09:44:29.001,29.0,30.0,33.29,32,12.7,50.9,188.9
09:44:34.007,30.0,30.0,33.30,32,12.2,48.9,189.0
09:44:39.502,29.0,30.0,33.28,32,13.7,54.9,189.7
09:44:44.502,30.0,30.0,35.66,35,8.0,32.0,186.5
09:44:49.509,31.0,30.0,33.27,35,8.3,33.2,186.4
09:44:55.001,30.0,30.0,33.02,35,8.8,35.2,186.2
09:45:00.010,31.0,30.0,33.04,36,11.6,46.5,187.7
09:45:05.503,30.0,30.0,33.53,37,12.2,48.6,187.7
09:45:10.507,31.0,30.0,33.00,37,11.1,44.4,187.6
09:45:16.002,22.0,30.0,38.49,38,8.2,33.0,187.6
09:45:21.002,25.0,30.0,41.53,38,10.0,40.0,186.7
09:45:26.003,25.0,30.0,41.01,38,9.7,38.8,186.7
09:45:31.004,24.0,30.0,42.05,38,9.8,39.4,186.7
09:45:36.509,25.0,30.0,43.42,39,12.2,48.6,186.7
09:45:42.003,23.0,30.0,42.60,40,10.8,43.0,186.7
09:45:47.007,25.0,30.0,43.21,41,10.7,42.6,186.7
09:45:52.504,21.0,30.0,47.30,45,8.2,33.0,186.7
09:45:57.504,20.0,30.0,50.86,49,8.6,34.5,186.7
09:46:02.504,22.0,30.0,47.30,52,8.0,32.1,186.7
09:46:08.004,17.0,30.0,48.15,58,10.0,40.0,187.1
09:46:13.504,27.0,30.0,39.61,60,7.7,30.8,187.1
09:46:19.013,30.0,30.0,34.41,62,10.1,40.3,186.9
09:46:24.504,17.0,30.0,40.21,69,4.5,18.2,189.9
09:46:29.511,29.0,30.0,34.40,69,12.3,49.4,190.0
09:46:34.520,30.0,30.0,39.79,73,12.2,48.9,220.8
09:46:40.005,28.0,30.0,34.64,75,13.5,54.1,220.8
09:46:45.014,29.0,30.0,35.78,80,11.8,47.4,220.8
09:46:50.506,0.0,30.0,35.36,82,0.1,0.6,212.2
09:46:56.006,0.0,30.0,35.36,82,0.4,1.6,212.1
09:47:01.506,0.0,30.0,35.36,82,1.6,6.2,212.2
09:47:07.013,30.0,30.0,34.06,83,12.8,51.4,215.5
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 09:40:55.663 0.0 30.0 0.00 0 14.3 57.1 81.8
3 09:41:01.763 34.0 30.0 35.67 2 8.4 33.6 160.3
4 09:41:06.996 32.0 30.0 33.22 3 7.9 31.5 119.6
5 09:41:12.002 31.0 30.0 33.27 4 10.7 42.9 131.2
6 09:41:17.496 30.0 30.0 33.27 4 8.4 33.7 120.7
7 09:41:22.501 31.0 30.0 33.31 6 12.2 48.6 151.0
8 09:41:27.995 30.0 30.0 33.31 6 11.4 45.8 151.1
9 09:41:32.998 31.0 30.0 33.30 6 12.1 48.5 151.0
10 09:41:38.507 31.0 30.0 33.25 6 11.7 46.7 151.1
11 09:41:43.997 30.0 30.0 33.28 6 11.2 44.6 150.3
12 09:41:49.496 25.0 30.0 36.81 6 9.9 39.7 150.3
13 09:41:54.498 24.0 30.0 40.73 6 10.0 40.0 150.3
14 09:41:59.997 25.0 30.0 40.66 6 10.1 40.3 150.4
15 09:42:05.497 25.0 30.0 40.68 6 9.8 39.0 150.5
16 09:42:10.498 24.0 30.0 40.71 6 9.8 39.0 150.5
17 09:42:15.499 25.0 30.0 40.46 6 9.2 36.8 150.5
18 09:42:20.998 24.0 30.0 41.21 7 11.8 47.4 150.5
19 09:42:26.497 24.0 30.0 40.41 7 9.8 39.2 150.5
20 09:42:31.509 24.0 30.0 42.09 8 10.1 40.4 150.5
21 09:42:36.998 23.0 30.0 42.63 8 9.4 37.7 150.6
22 09:42:42.001 24.0 30.0 42.59 9 8.9 35.7 150.6
23 09:42:47.498 23.0 30.0 42.66 11 8.7 34.7 150.6
24 09:42:52.499 26.0 30.0 43.72 12 10.0 39.9 150.6
25 09:42:58.010 24.0 30.0 43.20 13 9.3 37.1 150.6
26 09:43:03.500 19.0 30.0 42.91 14 9.2 37.0 150.6
27 09:43:08.504 22.0 30.0 44.86 15 8.6 34.3 150.6
28 09:43:14.007 22.0 30.0 44.23 17 9.2 36.6 150.6
29 09:43:19.500 24.0 30.0 43.53 18 10.1 40.2 150.6
30 09:43:24.514 20.0 30.0 49.00 23 8.7 34.7 151.8
31 09:43:30.000 24.0 30.0 44.75 24 6.5 26.0 152.0
32 09:43:35.000 25.0 30.0 42.36 25 7.0 28.0 151.7
33 09:43:40.504 25.0 30.0 41.79 25 6.6 26.3 153.1
34 09:43:46.000 23.0 30.0 46.75 29 6.5 26.0 152.2
35 09:43:51.003 25.0 30.0 41.55 30 8.0 32.0 152.3
36 09:43:56.501 0.0 30.0 41.92 31 0.5 2.0 150.6
37 09:44:02.001 0.0 30.0 41.92 31 0.1 0.6 150.6
38 09:44:07.501 0.0 30.0 41.92 31 0.2 0.8 150.2
39 09:44:12.501 0.0 30.0 41.92 31 0.5 2.0 148.8
40 09:44:18.004 7.0 30.0 251.99 32 7.9 31.5 189.3
41 09:44:23.512 30.0 30.0 33.28 32 11.9 47.7 189.4
42 09:44:29.001 29.0 30.0 33.29 32 12.7 50.9 188.9
43 09:44:34.007 30.0 30.0 33.30 32 12.2 48.9 189.0
44 09:44:39.502 29.0 30.0 33.28 32 13.7 54.9 189.7
45 09:44:44.502 30.0 30.0 35.66 35 8.0 32.0 186.5
46 09:44:49.509 31.0 30.0 33.27 35 8.3 33.2 186.4
47 09:44:55.001 30.0 30.0 33.02 35 8.8 35.2 186.2
48 09:45:00.010 31.0 30.0 33.04 36 11.6 46.5 187.7
49 09:45:05.503 30.0 30.0 33.53 37 12.2 48.6 187.7
50 09:45:10.507 31.0 30.0 33.00 37 11.1 44.4 187.6
51 09:45:16.002 22.0 30.0 38.49 38 8.2 33.0 187.6
52 09:45:21.002 25.0 30.0 41.53 38 10.0 40.0 186.7
53 09:45:26.003 25.0 30.0 41.01 38 9.7 38.8 186.7
54 09:45:31.004 24.0 30.0 42.05 38 9.8 39.4 186.7
55 09:45:36.509 25.0 30.0 43.42 39 12.2 48.6 186.7
56 09:45:42.003 23.0 30.0 42.60 40 10.8 43.0 186.7
57 09:45:47.007 25.0 30.0 43.21 41 10.7 42.6 186.7
58 09:45:52.504 21.0 30.0 47.30 45 8.2 33.0 186.7
59 09:45:57.504 20.0 30.0 50.86 49 8.6 34.5 186.7
60 09:46:02.504 22.0 30.0 47.30 52 8.0 32.1 186.7
61 09:46:08.004 17.0 30.0 48.15 58 10.0 40.0 187.1
62 09:46:13.504 27.0 30.0 39.61 60 7.7 30.8 187.1
63 09:46:19.013 30.0 30.0 34.41 62 10.1 40.3 186.9
64 09:46:24.504 17.0 30.0 40.21 69 4.5 18.2 189.9
65 09:46:29.511 29.0 30.0 34.40 69 12.3 49.4 190.0
66 09:46:34.520 30.0 30.0 39.79 73 12.2 48.9 220.8
67 09:46:40.005 28.0 30.0 34.64 75 13.5 54.1 220.8
68 09:46:45.014 29.0 30.0 35.78 80 11.8 47.4 220.8
69 09:46:50.506 0.0 30.0 35.36 82 0.1 0.6 212.2
70 09:46:56.006 0.0 30.0 35.36 82 0.4 1.6 212.1
71 09:47:01.506 0.0 30.0 35.36 82 1.6 6.2 212.2
72 09:47:07.013 30.0 30.0 34.06 83 12.8 51.4 215.5

View File

@@ -0,0 +1,487 @@
========================================================================
Duck Preview 0.1.0
Session: 2026-05-14_09-40-54
========================================================================
Platform : macOS-13.7.8-x86_64-i386-64bit
Python : 3.12.9
PySide6 : 6.11.1
Hardware : 4 logical CPUs, 16.0 GB RAM
Log file : /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_09-40-54.log
========================================================================
2026-05-14 09:40:54.717 [DEBUG ] app.logging_setup: Pruned old log: duck-preview_2026-05-14_08-10-08.log
2026-05-14 09:40:54.793 [INFO ] __main__: Application starting (session: 2026-05-14_09-40-54)
2026-05-14 09:40:54.934 [INFO ] app.ui.main_window: Telemetry CSV: /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_09-40-54.csv
2026-05-14 09:40:54.934 [DEBUG ] app.ui.camera_view: Overlay layer added: TelemetryOverlay
2026-05-14 09:40:54.934 [DEBUG ] app.ui.camera_view: Overlay layer added: BboxOverlay
2026-05-14 09:40:55.131 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method CameraView.on_frame of <app.ui.camera_view.CameraView(0x7fc86adfdd30) at 0x1faf1c9c0>> (drop_if_busy=True)
2026-05-14 09:40:55.131 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method TelemetryCollector.on_frame of <app.telemetry.telemetry_collector.TelemetryCollector(0x7fc86adfae50) at 0x1faeffe00>> (drop_if_busy=False)
2026-05-14 09:40:55.137 [INFO ] __main__: Application shutting down
2026-05-14 09:40:55.368 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 09:40:55.369 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 09:40:55.370 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 09:40:55.370 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 09:40:55.370 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 09:40:55.370 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 09:40:55.370 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 09:40:55.370 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 09:40:55.370 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 09:40:55.370 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 09:40:55.370 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 09:40:55.370 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 09:40:55.370 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 09:40:55.371 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 09:40:55.371 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 09:40:55.405 [INFO ] app.camera.camera_service: Camera format requested: NV12 1280x720 @ 30 fps
2026-05-14 09:40:55.581 [INFO ] app.camera.camera_service: Qt multimedia backend: AVFoundation
2026-05-14 09:40:55.581 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:40:55.581 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1280x720 @ 30 fps
2026-05-14 09:40:55.581 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:40:55.581 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:40:55.582 [DEBUG ] app.camera.uvc.macos: pyuvc not available — macOS UVC controls disabled
2026-05-14 09:40:55.583 [WARNING] app.camera.uvc: UVC: pyuvc not installed — UVC controls unavailable on macOS
2026-05-14 09:41:05.949 [DEBUG ] app.ui.menu_bar: Model file selected: /Users/rafalkaczka/Repos/duck-preview/models/best_v1.pt
2026-05-14 09:41:05.949 [INFO ] app.ui.main_window: Loading model: /Users/rafalkaczka/Repos/duck-preview/models/best_v1.pt
2026-05-14 09:41:05.950 [INFO ] app.inference.worker_manager: Inference worker stopped
2026-05-14 09:41:05.975 [INFO ] app.inference.worker_manager: Inference worker started (pid=3786, model=/Users/rafalkaczka/Repos/duck-preview/models/best_v1.pt)
2026-05-14 09:41:05.976 [DEBUG ] app.inference.worker_manager: InferenceManager: resumed
2026-05-14 09:41:05.977 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7fc86adfc2f0) at 0x1faeffac0>> (drop_if_busy=True)
2026-05-14 09:41:05.978 [INFO ] app.ui.main_window: Inference enabled
2026-05-14 09:41:05.979 [DEBUG ] app.inference.worker_manager: InferenceManager: paused
2026-05-14 09:41:05.979 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber not found for removal: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7fc86adfc2f0) at 0x1faeffac0>>
2026-05-14 09:41:05.980 [INFO ] app.ui.main_window: Inference disabled
2026-05-14 09:41:11.884 [INFO ] app.inference.worker_manager: Inference device: cpu
2026-05-14 09:41:19.091 [DEBUG ] app.inference.worker_manager: InferenceManager: resumed
2026-05-14 09:41:19.092 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7fc86adfc2f0) at 0x1faeffac0>> (drop_if_busy=True)
2026-05-14 09:41:19.092 [INFO ] app.ui.main_window: Inference enabled
2026-05-14 09:41:55.401 [INFO ] app.inference.worker_manager: frame 177: 1 detection(s) in 130.5 ms — etykieta 0.32
2026-05-14 09:41:58.047 [INFO ] app.inference.worker_manager: frame 189: 1 detection(s) in 127.1 ms — etykieta 0.27
2026-05-14 09:41:58.304 [INFO ] app.inference.worker_manager: frame 190: 1 detection(s) in 155.3 ms — etykieta 0.25
2026-05-14 09:41:58.547 [INFO ] app.inference.worker_manager: frame 191: 1 detection(s) in 134.4 ms — etykieta 0.26
2026-05-14 09:41:59.146 [INFO ] app.inference.worker_manager: frame 194: 1 detection(s) in 144.5 ms — etykieta 0.34
2026-05-14 09:41:59.347 [INFO ] app.inference.worker_manager: frame 195: 1 detection(s) in 132.8 ms — etykieta 0.29
2026-05-14 09:41:59.546 [INFO ] app.inference.worker_manager: frame 196: 1 detection(s) in 140.1 ms — etykieta 0.27
2026-05-14 09:42:00.796 [INFO ] app.inference.worker_manager: frame 202: 1 detection(s) in 154.7 ms — etykieta 0.26
2026-05-14 09:42:01.047 [INFO ] app.inference.worker_manager: frame 203: 1 detection(s) in 150.3 ms — etykieta 0.29
2026-05-14 09:42:01.247 [INFO ] app.inference.worker_manager: frame 204: 1 detection(s) in 131.5 ms — etykieta 0.27
2026-05-14 09:42:01.447 [INFO ] app.inference.worker_manager: frame 205: 1 detection(s) in 141.3 ms — etykieta 0.30
2026-05-14 09:42:01.648 [INFO ] app.inference.worker_manager: frame 206: 1 detection(s) in 131.5 ms — etykieta 0.29
2026-05-14 09:42:03.597 [INFO ] app.inference.worker_manager: frame 215: 1 detection(s) in 133.8 ms — etykieta 0.30
2026-05-14 09:42:06.098 [INFO ] app.inference.worker_manager: frame 227: 1 detection(s) in 125.3 ms — etykieta 0.27
2026-05-14 09:42:06.998 [INFO ] app.inference.worker_manager: frame 231: 1 detection(s) in 150.9 ms — etykieta 0.26
2026-05-14 09:42:07.448 [INFO ] app.inference.worker_manager: frame 233: 1 detection(s) in 139.3 ms — etykieta 0.25
2026-05-14 09:42:08.247 [INFO ] app.inference.worker_manager: frame 237: 1 detection(s) in 153.6 ms — etykieta 0.25
2026-05-14 09:42:08.448 [INFO ] app.inference.worker_manager: frame 238: 1 detection(s) in 143.1 ms — etykieta 0.25
2026-05-14 09:42:12.698 [INFO ] app.inference.worker_manager: frame 258: 1 detection(s) in 130.6 ms — etykieta 0.25
2026-05-14 09:42:17.197 [INFO ] app.inference.worker_manager: frame 279: 1 detection(s) in 138.9 ms — etykieta 0.26
2026-05-14 09:42:17.448 [INFO ] app.inference.worker_manager: frame 280: 1 detection(s) in 156.1 ms — etykieta 0.25
2026-05-14 09:42:17.848 [INFO ] app.inference.worker_manager: frame 282: 1 detection(s) in 125.4 ms — etykieta 0.26
2026-05-14 09:42:19.447 [INFO ] app.inference.worker_manager: frame 289: 1 detection(s) in 129.9 ms — etykieta 0.25
2026-05-14 09:42:20.697 [INFO ] app.inference.worker_manager: frame 294: 1 detection(s) in 159.0 ms — etykieta 0.26
2026-05-14 09:42:21.147 [INFO ] app.inference.worker_manager: frame 296: 1 detection(s) in 142.0 ms — etykieta 0.25
2026-05-14 09:42:21.450 [INFO ] app.inference.worker_manager: frame 297: 1 detection(s) in 221.0 ms — etykieta 0.26
2026-05-14 09:42:21.900 [INFO ] app.inference.worker_manager: frame 299: 1 detection(s) in 132.7 ms — etykieta 0.26
2026-05-14 09:42:22.998 [INFO ] app.inference.worker_manager: frame 303: 1 detection(s) in 171.4 ms — etykieta 0.25
2026-05-14 09:42:25.048 [INFO ] app.inference.worker_manager: frame 312: 1 detection(s) in 144.5 ms — etykieta 0.26
2026-05-14 09:42:25.848 [INFO ] app.inference.worker_manager: frame 316: 1 detection(s) in 150.7 ms — etykieta 0.25
2026-05-14 09:42:26.254 [INFO ] app.inference.worker_manager: frame 318: 1 detection(s) in 131.9 ms — etykieta 0.25
2026-05-14 09:42:26.498 [INFO ] app.inference.worker_manager: frame 319: 1 detection(s) in 147.4 ms — etykieta 0.25
2026-05-14 09:42:26.698 [INFO ] app.inference.worker_manager: frame 320: 1 detection(s) in 127.9 ms — etykieta 0.26
2026-05-14 09:42:28.747 [INFO ] app.inference.worker_manager: frame 329: 1 detection(s) in 142.2 ms — etykieta 0.25
2026-05-14 09:42:30.198 [INFO ] app.inference.worker_manager: frame 336: 1 detection(s) in 134.9 ms — etykieta 0.25
2026-05-14 09:42:30.398 [INFO ] app.inference.worker_manager: frame 337: 1 detection(s) in 146.0 ms — etykieta 0.25
2026-05-14 09:42:31.702 [INFO ] app.inference.worker_manager: frame 343: 1 detection(s) in 169.5 ms — etykieta 0.26
2026-05-14 09:42:31.947 [INFO ] app.inference.worker_manager: frame 344: 1 detection(s) in 147.4 ms — etykieta 0.26
2026-05-14 09:42:32.348 [INFO ] app.inference.worker_manager: frame 346: 1 detection(s) in 131.3 ms — etykieta 0.26
2026-05-14 09:42:32.798 [INFO ] app.inference.worker_manager: frame 348: 1 detection(s) in 174.2 ms — etykieta 0.25
2026-05-14 09:42:33.052 [INFO ] app.inference.worker_manager: frame 349: 1 detection(s) in 134.8 ms — etykieta 0.25
2026-05-14 09:42:33.448 [INFO ] app.inference.worker_manager: frame 351: 1 detection(s) in 122.5 ms — etykieta 0.25
2026-05-14 09:42:33.652 [INFO ] app.inference.worker_manager: frame 352: 1 detection(s) in 133.6 ms — etykieta 0.26
2026-05-14 09:42:34.802 [INFO ] app.inference.worker_manager: frame 357: 1 detection(s) in 163.6 ms — etykieta 0.25
2026-05-14 09:42:35.005 [INFO ] app.inference.worker_manager: frame 358: 1 detection(s) in 140.0 ms — etykieta 0.25
2026-05-14 09:42:35.398 [INFO ] app.inference.worker_manager: frame 360: 1 detection(s) in 119.2 ms — etykieta 0.25
2026-05-14 09:42:36.798 [INFO ] app.inference.worker_manager: frame 366: 1 detection(s) in 149.7 ms — etykieta 0.25
2026-05-14 09:42:37.147 [INFO ] app.inference.worker_manager: frame 368: 1 detection(s) in 114.2 ms — etykieta 0.25
2026-05-14 09:42:37.348 [INFO ] app.inference.worker_manager: frame 369: 1 detection(s) in 133.7 ms — etykieta 0.25
2026-05-14 09:42:38.005 [INFO ] app.inference.worker_manager: frame 372: 1 detection(s) in 177.3 ms — etykieta 0.26
2026-05-14 09:42:38.248 [INFO ] app.inference.worker_manager: frame 373: 1 detection(s) in 145.1 ms — etykieta 0.25
2026-05-14 09:42:39.097 [INFO ] app.inference.worker_manager: frame 377: 1 detection(s) in 140.6 ms — etykieta 0.25
2026-05-14 09:42:39.298 [INFO ] app.inference.worker_manager: frame 378: 1 detection(s) in 138.4 ms — etykieta 0.25
2026-05-14 09:42:39.498 [INFO ] app.inference.worker_manager: frame 379: 1 detection(s) in 131.1 ms — etykieta 0.25
2026-05-14 09:42:41.204 [INFO ] app.inference.worker_manager: frame 387: 1 detection(s) in 134.2 ms — etykieta 0.25
2026-05-14 09:42:41.648 [INFO ] app.inference.worker_manager: frame 389: 1 detection(s) in 135.2 ms — etykieta 0.25
2026-05-14 09:42:43.054 [INFO ] app.inference.worker_manager: frame 395: 1 detection(s) in 140.6 ms — etykieta 0.26
2026-05-14 09:42:44.506 [INFO ] app.inference.worker_manager: frame 401: 1 detection(s) in 153.4 ms — etykieta 0.25
2026-05-14 09:42:45.149 [INFO ] app.inference.worker_manager: frame 404: 1 detection(s) in 145.6 ms — etykieta 0.25
2026-05-14 09:42:45.748 [INFO ] app.inference.worker_manager: frame 407: 1 detection(s) in 141.4 ms — etykieta 0.25
2026-05-14 09:42:47.098 [INFO ] app.inference.worker_manager: frame 413: 1 detection(s) in 152.0 ms — etykieta 0.26
2026-05-14 09:42:47.598 [INFO ] app.inference.worker_manager: frame 415: 1 detection(s) in 137.3 ms — etykieta 0.25
2026-05-14 09:42:49.399 [INFO ] app.inference.worker_manager: frame 423: 1 detection(s) in 132.9 ms — etykieta 0.25
2026-05-14 09:42:49.653 [INFO ] app.inference.worker_manager: frame 424: 1 detection(s) in 159.0 ms — etykieta 0.25
2026-05-14 09:42:50.099 [INFO ] app.inference.worker_manager: frame 426: 1 detection(s) in 132.2 ms — etykieta 0.25
2026-05-14 09:42:50.299 [INFO ] app.inference.worker_manager: frame 427: 1 detection(s) in 140.7 ms — etykieta 0.25
2026-05-14 09:42:50.498 [INFO ] app.inference.worker_manager: frame 428: 1 detection(s) in 127.4 ms — etykieta 0.25
2026-05-14 09:42:50.899 [INFO ] app.inference.worker_manager: frame 430: 1 detection(s) in 123.1 ms — etykieta 0.25
2026-05-14 09:42:51.348 [INFO ] app.inference.worker_manager: frame 432: 1 detection(s) in 131.5 ms — etykieta 0.25
2026-05-14 09:42:51.548 [INFO ] app.inference.worker_manager: frame 433: 1 detection(s) in 157.0 ms — etykieta 0.25
2026-05-14 09:42:51.749 [INFO ] app.inference.worker_manager: frame 434: 1 detection(s) in 133.0 ms — etykieta 0.25
2026-05-14 09:42:52.149 [INFO ] app.inference.worker_manager: frame 436: 1 detection(s) in 129.5 ms — etykieta 0.26
2026-05-14 09:42:52.798 [INFO ] app.inference.worker_manager: frame 439: 1 detection(s) in 157.5 ms — etykieta 0.25
2026-05-14 09:42:52.999 [INFO ] app.inference.worker_manager: frame 440: 1 detection(s) in 130.9 ms — etykieta 0.25
2026-05-14 09:42:53.598 [INFO ] app.inference.worker_manager: frame 443: 1 detection(s) in 143.9 ms — etykieta 0.25
2026-05-14 09:42:54.107 [INFO ] app.inference.worker_manager: frame 445: 1 detection(s) in 131.4 ms — etykieta 0.25
2026-05-14 09:42:54.349 [INFO ] app.inference.worker_manager: frame 446: 1 detection(s) in 134.8 ms — etykieta 0.26
2026-05-14 09:42:54.749 [INFO ] app.inference.worker_manager: frame 448: 1 detection(s) in 132.7 ms — etykieta 0.26
2026-05-14 09:42:54.949 [INFO ] app.inference.worker_manager: frame 449: 1 detection(s) in 128.9 ms — etykieta 0.25
2026-05-14 09:42:55.148 [INFO ] app.inference.worker_manager: frame 450: 1 detection(s) in 127.0 ms — etykieta 0.26
2026-05-14 09:42:56.299 [INFO ] app.inference.worker_manager: frame 455: 1 detection(s) in 130.1 ms — etykieta 0.25
2026-05-14 09:42:56.548 [INFO ] app.inference.worker_manager: frame 456: 1 detection(s) in 166.9 ms — etykieta 0.25
2026-05-14 09:42:56.999 [INFO ] app.inference.worker_manager: frame 458: 1 detection(s) in 133.3 ms — etykieta 0.25
2026-05-14 09:42:57.848 [INFO ] app.inference.worker_manager: frame 462: 1 detection(s) in 152.3 ms — etykieta 0.25
2026-05-14 09:42:58.099 [INFO ] app.inference.worker_manager: frame 463: 1 detection(s) in 129.3 ms — etykieta 0.25
2026-05-14 09:42:58.498 [INFO ] app.inference.worker_manager: frame 465: 1 detection(s) in 130.9 ms — etykieta 0.25
2026-05-14 09:42:58.699 [INFO ] app.inference.worker_manager: frame 466: 1 detection(s) in 129.1 ms — etykieta 0.26
2026-05-14 09:42:58.899 [INFO ] app.inference.worker_manager: frame 467: 1 detection(s) in 138.9 ms — etykieta 0.25
2026-05-14 09:42:59.159 [INFO ] app.inference.worker_manager: frame 468: 1 detection(s) in 171.8 ms — etykieta 0.25
2026-05-14 09:42:59.756 [INFO ] app.inference.worker_manager: frame 471: 1 detection(s) in 147.4 ms — etykieta 0.25
2026-05-14 09:43:00.149 [INFO ] app.inference.worker_manager: frame 473: 1 detection(s) in 133.2 ms — etykieta 0.25
2026-05-14 09:43:00.599 [INFO ] app.inference.worker_manager: frame 475: 1 detection(s) in 130.3 ms — etykieta 0.25
2026-05-14 09:43:00.799 [INFO ] app.inference.worker_manager: frame 476: 1 detection(s) in 133.8 ms — etykieta 0.26
2026-05-14 09:43:01.405 [INFO ] app.inference.worker_manager: frame 479: 1 detection(s) in 142.6 ms — etykieta 0.25
2026-05-14 09:43:01.649 [INFO ] app.inference.worker_manager: frame 480: 1 detection(s) in 135.4 ms — etykieta 0.25
2026-05-14 09:43:02.049 [INFO ] app.inference.worker_manager: frame 482: 1 detection(s) in 124.0 ms — etykieta 0.26
2026-05-14 09:43:02.449 [INFO ] app.inference.worker_manager: frame 484: 1 detection(s) in 129.8 ms — etykieta 0.25
2026-05-14 09:43:03.099 [INFO ] app.inference.worker_manager: frame 487: 1 detection(s) in 153.4 ms — etykieta 0.26
2026-05-14 09:43:04.149 [INFO ] app.inference.worker_manager: frame 492: 1 detection(s) in 152.3 ms — etykieta 0.25
2026-05-14 09:43:04.449 [INFO ] app.inference.worker_manager: frame 493: 1 detection(s) in 199.4 ms — etykieta 0.26
2026-05-14 09:43:05.299 [INFO ] app.inference.worker_manager: frame 497: 1 detection(s) in 147.0 ms — etykieta 0.25
2026-05-14 09:43:05.549 [INFO ] app.inference.worker_manager: frame 498: 1 detection(s) in 134.8 ms — etykieta 0.25
2026-05-14 09:43:07.903 [INFO ] app.inference.worker_manager: frame 509: 1 detection(s) in 135.8 ms — etykieta 0.25
2026-05-14 09:43:08.548 [INFO ] app.inference.worker_manager: frame 512: 1 detection(s) in 161.0 ms — etykieta 0.25
2026-05-14 09:43:09.459 [INFO ] app.inference.worker_manager: frame 516: 1 detection(s) in 167.6 ms — etykieta 0.25
2026-05-14 09:43:09.999 [INFO ] app.inference.worker_manager: frame 518: 1 detection(s) in 154.3 ms — etykieta 0.26
2026-05-14 09:43:11.649 [INFO ] app.inference.worker_manager: frame 526: 1 detection(s) in 147.5 ms — etykieta 0.25
2026-05-14 09:43:11.899 [INFO ] app.inference.worker_manager: frame 527: 1 detection(s) in 185.0 ms — etykieta 0.26
2026-05-14 09:43:12.149 [INFO ] app.inference.worker_manager: frame 528: 1 detection(s) in 145.8 ms — etykieta 0.26
2026-05-14 09:43:13.598 [INFO ] app.inference.worker_manager: frame 534: 1 detection(s) in 151.8 ms — etykieta 0.25
2026-05-14 09:43:14.007 [INFO ] app.inference.worker_manager: frame 536: 1 detection(s) in 134.8 ms — etykieta 0.26
2026-05-14 09:43:15.149 [INFO ] app.inference.worker_manager: frame 541: 1 detection(s) in 160.2 ms — etykieta 0.25
2026-05-14 09:43:15.558 [INFO ] app.inference.worker_manager: frame 543: 1 detection(s) in 145.8 ms — etykieta 0.25
2026-05-14 09:43:16.299 [INFO ] app.inference.worker_manager: frame 546: 1 detection(s) in 150.9 ms — etykieta 0.25
2026-05-14 09:43:16.549 [INFO ] app.inference.worker_manager: frame 547: 1 detection(s) in 170.1 ms — etykieta 0.26
2026-05-14 09:43:16.949 [INFO ] app.inference.worker_manager: frame 549: 1 detection(s) in 142.2 ms — etykieta 0.26
2026-05-14 09:43:17.150 [INFO ] app.inference.worker_manager: frame 550: 1 detection(s) in 141.3 ms — etykieta 0.25
2026-05-14 09:43:17.750 [INFO ] app.inference.worker_manager: frame 553: 1 detection(s) in 155.2 ms — etykieta 0.25
2026-05-14 09:43:18.000 [INFO ] app.inference.worker_manager: frame 554: 1 detection(s) in 137.7 ms — etykieta 0.25
2026-05-14 09:43:19.099 [INFO ] app.inference.worker_manager: frame 559: 1 detection(s) in 172.7 ms — etykieta 0.25
2026-05-14 09:43:19.599 [INFO ] app.inference.worker_manager: frame 561: 1 detection(s) in 142.8 ms — etykieta 0.25
2026-05-14 09:43:19.799 [INFO ] app.inference.worker_manager: frame 562: 1 detection(s) in 136.7 ms — etykieta 0.26
2026-05-14 09:43:20.099 [INFO ] app.inference.worker_manager: frame 563: 1 detection(s) in 123.7 ms — etykieta 0.25
2026-05-14 09:43:20.949 [INFO ] app.inference.worker_manager: frame 566: 1 detection(s) in 215.0 ms — etykieta 0.26
2026-05-14 09:43:21.401 [INFO ] app.inference.worker_manager: frame 568: 1 detection(s) in 155.2 ms — etykieta 0.26
2026-05-14 09:43:22.249 [INFO ] app.inference.worker_manager: frame 571: 1 detection(s) in 173.6 ms — etykieta 0.26
2026-05-14 09:43:23.449 [INFO ] app.inference.worker_manager: frame 576: 1 detection(s) in 157.7 ms — etykieta 0.26
2026-05-14 09:43:24.650 [INFO ] app.inference.worker_manager: frame 581: 1 detection(s) in 166.7 ms — etykieta 0.25
2026-05-14 09:43:25.099 [INFO ] app.inference.worker_manager: frame 583: 1 detection(s) in 137.3 ms — etykieta 0.26
2026-05-14 09:43:25.550 [INFO ] app.inference.worker_manager: frame 585: 1 detection(s) in 137.5 ms — etykieta 0.25
2026-05-14 09:43:26.200 [INFO ] app.inference.worker_manager: frame 588: 1 detection(s) in 146.6 ms — etykieta 0.26
2026-05-14 09:43:26.845 [DEBUG ] app.inference.worker_manager: InferenceManager: paused
2026-05-14 09:43:26.845 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber not found for removal: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7fc86adfc2f0) at 0x1faeffac0>>
2026-05-14 09:43:26.846 [INFO ] app.ui.main_window: Inference disabled
2026-05-14 09:43:42.400 [DEBUG ] app.ui.menu_bar: Format selected: 1280x720 @ 30 fps (NV12)
2026-05-14 09:43:42.400 [INFO ] app.ui.main_window: Format selected via menu: 1280x720 @ 30 fps (NV12)
2026-05-14 09:43:42.400 [INFO ] app.camera.camera_service: Format change: 1280x720 @ 30 fps (NV12) — restarting camera
2026-05-14 09:43:42.449 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:43:42.450 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:43:42.480 [INFO ] app.camera.camera_service: Camera format requested: NV12 1280x720 @ 30 fps
2026-05-14 09:43:42.565 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:43:42.565 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1280x720 @ 30 fps
2026-05-14 09:43:42.566 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:43:42.566 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:43:52.497 [DEBUG ] app.ui.menu_bar: Format selected: 1920x1080 @ 30 fps (NV12)
2026-05-14 09:43:52.497 [INFO ] app.ui.main_window: Format selected via menu: 1920x1080 @ 30 fps (NV12)
2026-05-14 09:43:52.498 [INFO ] app.camera.camera_service: Format change: 1920x1080 @ 30 fps (NV12) — restarting camera
2026-05-14 09:43:52.538 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:43:52.538 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:43:52.568 [INFO ] app.camera.camera_service: Camera format requested: NV12 1920x1080 @ 30 fps
2026-05-14 09:43:52.639 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:43:52.640 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1920x1080 @ 30 fps
2026-05-14 09:43:52.640 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:43:52.640 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:44:17.191 [DEBUG ] app.ui.menu_bar: Format selected: 1600x1200 @ 30 fps (NV12)
2026-05-14 09:44:17.191 [INFO ] app.ui.main_window: Format selected via menu: 1600x1200 @ 30 fps (NV12)
2026-05-14 09:44:17.191 [INFO ] app.camera.camera_service: Format change: 1600x1200 @ 30 fps (NV12) — restarting camera
2026-05-14 09:44:17.221 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:44:17.222 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:44:17.254 [INFO ] app.camera.camera_service: Camera format requested: NV12 1600x1200 @ 30 fps
2026-05-14 09:44:17.338 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:44:17.338 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1600x1200 @ 30 fps
2026-05-14 09:44:17.339 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:44:17.339 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:44:40.310 [DEBUG ] app.ui.menu_bar: Format selected: 1280x720 @ 30 fps (NV12)
2026-05-14 09:44:40.311 [INFO ] app.ui.main_window: Format selected via menu: 1280x720 @ 30 fps (NV12)
2026-05-14 09:44:40.311 [INFO ] app.camera.camera_service: Format change: 1280x720 @ 30 fps (NV12) — restarting camera
2026-05-14 09:44:40.363 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:44:40.364 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:44:40.392 [INFO ] app.camera.camera_service: Camera format requested: NV12 1280x720 @ 30 fps
2026-05-14 09:44:40.482 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:44:40.482 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1280x720 @ 30 fps
2026-05-14 09:44:40.483 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:44:40.483 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:44:57.525 [DEBUG ] app.inference.worker_manager: InferenceManager: resumed
2026-05-14 09:44:57.525 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7fc86adfc2f0) at 0x1faeffac0>> (drop_if_busy=True)
2026-05-14 09:44:57.525 [INFO ] app.ui.main_window: Inference enabled
2026-05-14 09:45:13.010 [INFO ] app.inference.worker_manager: frame 664: 1 detection(s) in 132.7 ms — etykieta 0.36
2026-05-14 09:45:13.253 [INFO ] app.inference.worker_manager: frame 665: 1 detection(s) in 157.1 ms — etykieta 0.46
2026-05-14 09:45:13.506 [INFO ] app.inference.worker_manager: frame 666: 1 detection(s) in 133.0 ms — etykieta 0.59
2026-05-14 09:45:13.707 [INFO ] app.inference.worker_manager: frame 667: 1 detection(s) in 134.0 ms — etykieta 0.55
2026-05-14 09:45:13.905 [INFO ] app.inference.worker_manager: frame 668: 1 detection(s) in 126.4 ms — etykieta 0.57
2026-05-14 09:45:14.153 [INFO ] app.inference.worker_manager: frame 669: 1 detection(s) in 171.8 ms — etykieta 0.53
2026-05-14 09:45:14.353 [INFO ] app.inference.worker_manager: frame 670: 1 detection(s) in 134.3 ms — etykieta 0.54
2026-05-14 09:45:14.559 [INFO ] app.inference.worker_manager: frame 671: 1 detection(s) in 139.9 ms — etykieta 0.55
2026-05-14 09:45:14.760 [INFO ] app.inference.worker_manager: frame 672: 1 detection(s) in 141.1 ms — etykieta 0.54
2026-05-14 09:45:14.958 [INFO ] app.inference.worker_manager: frame 673: 1 detection(s) in 134.1 ms — etykieta 0.55
2026-05-14 09:45:15.161 [INFO ] app.inference.worker_manager: frame 674: 1 detection(s) in 140.3 ms — etykieta 0.57
2026-05-14 09:45:15.357 [INFO ] app.inference.worker_manager: frame 675: 1 detection(s) in 137.1 ms — etykieta 0.55
2026-05-14 09:45:15.603 [INFO ] app.inference.worker_manager: frame 676: 1 detection(s) in 141.8 ms — etykieta 0.53
2026-05-14 09:45:15.803 [INFO ] app.inference.worker_manager: frame 677: 1 detection(s) in 140.9 ms — etykieta 0.58
2026-05-14 09:45:16.202 [INFO ] app.inference.worker_manager: frame 678: 1 detection(s) in 218.5 ms — etykieta 0.52
2026-05-14 09:45:16.408 [INFO ] app.inference.worker_manager: frame 679: 1 detection(s) in 131.6 ms — etykieta 0.51
2026-05-14 09:45:16.603 [INFO ] app.inference.worker_manager: frame 680: 1 detection(s) in 135.5 ms — etykieta 0.47
2026-05-14 09:45:16.853 [INFO ] app.inference.worker_manager: frame 681: 1 detection(s) in 144.0 ms — etykieta 0.46
2026-05-14 09:45:17.052 [INFO ] app.inference.worker_manager: frame 682: 1 detection(s) in 131.8 ms — etykieta 0.42
2026-05-14 09:45:17.252 [INFO ] app.inference.worker_manager: frame 683: 1 detection(s) in 135.9 ms — etykieta 0.46
2026-05-14 09:45:17.452 [INFO ] app.inference.worker_manager: frame 684: 1 detection(s) in 117.5 ms — etykieta 0.46
2026-05-14 09:45:17.653 [INFO ] app.inference.worker_manager: frame 685: 1 detection(s) in 133.5 ms — etykieta 0.45
2026-05-14 09:45:17.860 [INFO ] app.inference.worker_manager: frame 686: 1 detection(s) in 139.1 ms — etykieta 0.46
2026-05-14 09:45:18.102 [INFO ] app.inference.worker_manager: frame 687: 1 detection(s) in 122.8 ms — etykieta 0.52
2026-05-14 09:45:18.303 [INFO ] app.inference.worker_manager: frame 688: 1 detection(s) in 139.2 ms — etykieta 0.50
2026-05-14 09:45:18.502 [INFO ] app.inference.worker_manager: frame 689: 1 detection(s) in 134.1 ms — etykieta 0.50
2026-05-14 09:45:18.703 [INFO ] app.inference.worker_manager: frame 690: 1 detection(s) in 134.5 ms — etykieta 0.52
2026-05-14 09:45:18.902 [INFO ] app.inference.worker_manager: frame 691: 1 detection(s) in 133.6 ms — etykieta 0.52
2026-05-14 09:45:19.154 [INFO ] app.inference.worker_manager: frame 692: 1 detection(s) in 175.9 ms — etykieta 0.52
2026-05-14 09:45:19.353 [INFO ] app.inference.worker_manager: frame 693: 1 detection(s) in 145.5 ms — etykieta 0.54
2026-05-14 09:45:19.553 [INFO ] app.inference.worker_manager: frame 694: 1 detection(s) in 133.4 ms — etykieta 0.50
2026-05-14 09:45:19.752 [INFO ] app.inference.worker_manager: frame 695: 1 detection(s) in 134.3 ms — etykieta 0.51
2026-05-14 09:45:19.953 [INFO ] app.inference.worker_manager: frame 696: 1 detection(s) in 128.8 ms — etykieta 0.46
2026-05-14 09:45:20.153 [INFO ] app.inference.worker_manager: frame 697: 1 detection(s) in 134.9 ms — etykieta 0.50
2026-05-14 09:45:20.353 [INFO ] app.inference.worker_manager: frame 698: 1 detection(s) in 136.0 ms — etykieta 0.53
2026-05-14 09:45:20.602 [INFO ] app.inference.worker_manager: frame 699: 1 detection(s) in 144.3 ms — etykieta 0.58
2026-05-14 09:45:20.802 [INFO ] app.inference.worker_manager: frame 700: 1 detection(s) in 134.9 ms — etykieta 0.45
2026-05-14 09:45:21.003 [INFO ] app.inference.worker_manager: frame 701: 1 detection(s) in 137.4 ms — etykieta 0.53
2026-05-14 09:45:21.203 [INFO ] app.inference.worker_manager: frame 702: 1 detection(s) in 133.9 ms — etykieta 0.42
2026-05-14 09:45:21.452 [INFO ] app.inference.worker_manager: frame 703: 1 detection(s) in 174.4 ms — etykieta 0.36
2026-05-14 09:45:21.703 [INFO ] app.inference.worker_manager: frame 704: 1 detection(s) in 137.7 ms — etykieta 0.40
2026-05-14 09:45:21.903 [INFO ] app.inference.worker_manager: frame 705: 1 detection(s) in 133.5 ms — etykieta 0.33
2026-05-14 09:45:23.002 [INFO ] app.inference.worker_manager: frame 710: 1 detection(s) in 154.4 ms — etykieta 0.31
2026-05-14 09:45:23.202 [INFO ] app.inference.worker_manager: frame 711: 1 detection(s) in 156.7 ms — etykieta 0.42
2026-05-14 09:45:23.403 [INFO ] app.inference.worker_manager: frame 712: 1 detection(s) in 131.9 ms — etykieta 0.48
2026-05-14 09:45:23.604 [INFO ] app.inference.worker_manager: frame 713: 1 detection(s) in 147.7 ms — etykieta 0.50
2026-05-14 09:45:23.803 [INFO ] app.inference.worker_manager: frame 714: 1 detection(s) in 137.3 ms — etykieta 0.49
2026-05-14 09:45:24.003 [INFO ] app.inference.worker_manager: frame 715: 1 detection(s) in 138.4 ms — etykieta 0.41
2026-05-14 09:45:24.252 [INFO ] app.inference.worker_manager: frame 716: 1 detection(s) in 146.8 ms — etykieta 0.37
2026-05-14 09:45:24.453 [INFO ] app.inference.worker_manager: frame 717: 1 detection(s) in 138.7 ms — etykieta 0.40
2026-05-14 09:45:24.660 [INFO ] app.inference.worker_manager: frame 718: 1 detection(s) in 135.9 ms — etykieta 0.34
2026-05-14 09:45:24.859 [INFO ] app.inference.worker_manager: frame 719: 1 detection(s) in 140.9 ms — etykieta 0.41
2026-05-14 09:45:25.056 [INFO ] app.inference.worker_manager: frame 720: 1 detection(s) in 132.6 ms — etykieta 0.35
2026-05-14 09:45:25.253 [INFO ] app.inference.worker_manager: frame 721: 1 detection(s) in 132.9 ms — etykieta 0.35
2026-05-14 09:45:25.453 [INFO ] app.inference.worker_manager: frame 722: 1 detection(s) in 132.8 ms — etykieta 0.39
2026-05-14 09:45:25.653 [INFO ] app.inference.worker_manager: frame 723: 1 detection(s) in 142.4 ms — etykieta 0.44
2026-05-14 09:45:25.853 [INFO ] app.inference.worker_manager: frame 724: 1 detection(s) in 135.7 ms — etykieta 0.43
2026-05-14 09:45:26.108 [INFO ] app.inference.worker_manager: frame 725: 1 detection(s) in 139.4 ms — etykieta 0.42
2026-05-14 09:45:26.310 [INFO ] app.inference.worker_manager: frame 726: 1 detection(s) in 130.5 ms — etykieta 0.47
2026-05-14 09:45:26.509 [INFO ] app.inference.worker_manager: frame 727: 1 detection(s) in 134.7 ms — etykieta 0.54
2026-05-14 09:45:26.753 [INFO ] app.inference.worker_manager: frame 728: 1 detection(s) in 183.8 ms — etykieta 0.49
2026-05-14 09:45:27.003 [INFO ] app.inference.worker_manager: frame 729: 1 detection(s) in 143.6 ms — etykieta 0.48
2026-05-14 09:45:27.202 [INFO ] app.inference.worker_manager: frame 730: 1 detection(s) in 132.9 ms — etykieta 0.48
2026-05-14 09:45:27.404 [INFO ] app.inference.worker_manager: frame 731: 1 detection(s) in 134.2 ms — etykieta 0.48
2026-05-14 09:45:27.603 [INFO ] app.inference.worker_manager: frame 732: 1 detection(s) in 130.3 ms — etykieta 0.50
2026-05-14 09:45:27.803 [INFO ] app.inference.worker_manager: frame 733: 1 detection(s) in 142.8 ms — etykieta 0.51
2026-05-14 09:45:28.056 [INFO ] app.inference.worker_manager: frame 734: 1 detection(s) in 142.0 ms — etykieta 0.54
2026-05-14 09:45:28.258 [INFO ] app.inference.worker_manager: frame 735: 1 detection(s) in 134.9 ms — etykieta 0.57
2026-05-14 09:45:28.456 [INFO ] app.inference.worker_manager: frame 736: 1 detection(s) in 134.5 ms — etykieta 0.58
2026-05-14 09:45:28.652 [INFO ] app.inference.worker_manager: frame 737: 1 detection(s) in 127.4 ms — etykieta 0.53
2026-05-14 09:45:28.903 [INFO ] app.inference.worker_manager: frame 738: 1 detection(s) in 146.6 ms — etykieta 0.52
2026-05-14 09:45:29.153 [INFO ] app.inference.worker_manager: frame 739: 1 detection(s) in 141.2 ms — etykieta 0.55
2026-05-14 09:45:29.353 [INFO ] app.inference.worker_manager: frame 740: 1 detection(s) in 129.7 ms — etykieta 0.56
2026-05-14 09:45:29.603 [INFO ] app.inference.worker_manager: frame 741: 1 detection(s) in 132.4 ms — etykieta 0.55
2026-05-14 09:45:29.853 [INFO ] app.inference.worker_manager: frame 742: 1 detection(s) in 149.9 ms — etykieta 0.54
2026-05-14 09:45:30.053 [INFO ] app.inference.worker_manager: frame 743: 1 detection(s) in 123.0 ms — etykieta 0.54
2026-05-14 09:45:30.253 [INFO ] app.inference.worker_manager: frame 744: 1 detection(s) in 133.4 ms — etykieta 0.55
2026-05-14 09:45:30.453 [INFO ] app.inference.worker_manager: frame 745: 1 detection(s) in 132.5 ms — etykieta 0.56
2026-05-14 09:45:30.653 [INFO ] app.inference.worker_manager: frame 746: 1 detection(s) in 123.2 ms — etykieta 0.54
2026-05-14 09:45:30.853 [INFO ] app.inference.worker_manager: frame 747: 1 detection(s) in 137.2 ms — etykieta 0.55
2026-05-14 09:45:31.058 [INFO ] app.inference.worker_manager: frame 748: 1 detection(s) in 133.5 ms — etykieta 0.54
2026-05-14 09:45:31.259 [INFO ] app.inference.worker_manager: frame 749: 1 detection(s) in 130.6 ms — etykieta 0.54
2026-05-14 09:45:31.458 [INFO ] app.inference.worker_manager: frame 750: 1 detection(s) in 132.4 ms — etykieta 0.55
2026-05-14 09:45:31.655 [INFO ] app.inference.worker_manager: frame 751: 1 detection(s) in 120.5 ms — etykieta 0.54
2026-05-14 09:45:31.903 [INFO ] app.inference.worker_manager: frame 752: 1 detection(s) in 152.9 ms — etykieta 0.55
2026-05-14 09:45:32.153 [INFO ] app.inference.worker_manager: frame 753: 1 detection(s) in 150.7 ms — etykieta 0.53
2026-05-14 09:45:32.353 [INFO ] app.inference.worker_manager: frame 754: 1 detection(s) in 119.4 ms — etykieta 0.53
2026-05-14 09:45:32.553 [INFO ] app.inference.worker_manager: frame 755: 1 detection(s) in 119.4 ms — etykieta 0.55
2026-05-14 09:45:32.753 [INFO ] app.inference.worker_manager: frame 756: 1 detection(s) in 120.1 ms — etykieta 0.55
2026-05-14 09:45:32.953 [INFO ] app.inference.worker_manager: frame 757: 1 detection(s) in 134.7 ms — etykieta 0.54
2026-05-14 09:45:33.152 [INFO ] app.inference.worker_manager: frame 758: 1 detection(s) in 134.0 ms — etykieta 0.54
2026-05-14 09:45:33.403 [INFO ] app.inference.worker_manager: frame 759: 1 detection(s) in 133.8 ms — etykieta 0.53
2026-05-14 09:45:33.606 [INFO ] app.inference.worker_manager: frame 760: 1 detection(s) in 133.6 ms — etykieta 0.54
2026-05-14 09:45:33.902 [INFO ] app.inference.worker_manager: frame 761: 1 detection(s) in 139.7 ms — etykieta 0.55
2026-05-14 09:45:34.153 [INFO ] app.inference.worker_manager: frame 762: 1 detection(s) in 156.5 ms — etykieta 0.55
2026-05-14 09:45:34.361 [INFO ] app.inference.worker_manager: frame 763: 1 detection(s) in 135.3 ms — etykieta 0.53
2026-05-14 09:45:34.560 [INFO ] app.inference.worker_manager: frame 764: 1 detection(s) in 133.3 ms — etykieta 0.54
2026-05-14 09:45:34.857 [INFO ] app.inference.worker_manager: frame 765: 1 detection(s) in 133.8 ms — etykieta 0.55
2026-05-14 09:45:35.060 [INFO ] app.inference.worker_manager: frame 766: 1 detection(s) in 137.4 ms — etykieta 0.53
2026-05-14 09:45:35.255 [INFO ] app.inference.worker_manager: frame 767: 1 detection(s) in 128.9 ms — etykieta 0.54
2026-05-14 09:45:35.453 [INFO ] app.inference.worker_manager: frame 768: 1 detection(s) in 134.6 ms — etykieta 0.54
2026-05-14 09:45:35.652 [INFO ] app.inference.worker_manager: frame 769: 1 detection(s) in 123.0 ms — etykieta 0.56
2026-05-14 09:45:35.853 [INFO ] app.inference.worker_manager: frame 770: 1 detection(s) in 139.6 ms — etykieta 0.55
2026-05-14 09:45:36.054 [INFO ] app.inference.worker_manager: frame 771: 1 detection(s) in 140.4 ms — etykieta 0.55
2026-05-14 09:45:36.253 [INFO ] app.inference.worker_manager: frame 772: 1 detection(s) in 136.0 ms — etykieta 0.54
2026-05-14 09:45:36.453 [INFO ] app.inference.worker_manager: frame 773: 1 detection(s) in 138.8 ms — etykieta 0.54
2026-05-14 09:45:36.653 [INFO ] app.inference.worker_manager: frame 774: 1 detection(s) in 119.7 ms — etykieta 0.55
2026-05-14 09:45:36.853 [INFO ] app.inference.worker_manager: frame 775: 1 detection(s) in 149.1 ms — etykieta 0.55
2026-05-14 09:45:37.055 [INFO ] app.inference.worker_manager: frame 776: 1 detection(s) in 148.9 ms — etykieta 0.55
2026-05-14 09:45:37.303 [INFO ] app.inference.worker_manager: frame 777: 1 detection(s) in 193.4 ms — etykieta 0.55
2026-05-14 09:45:37.553 [INFO ] app.inference.worker_manager: frame 778: 1 detection(s) in 157.7 ms — etykieta 0.55
2026-05-14 09:45:37.761 [INFO ] app.inference.worker_manager: frame 779: 1 detection(s) in 148.5 ms — etykieta 0.54
2026-05-14 09:45:38.003 [INFO ] app.inference.worker_manager: frame 780: 1 detection(s) in 138.4 ms — etykieta 0.54
2026-05-14 09:45:38.203 [INFO ] app.inference.worker_manager: frame 781: 1 detection(s) in 135.8 ms — etykieta 0.55
2026-05-14 09:45:38.403 [INFO ] app.inference.worker_manager: frame 782: 1 detection(s) in 129.7 ms — etykieta 0.55
2026-05-14 09:45:38.602 [INFO ] app.inference.worker_manager: frame 783: 1 detection(s) in 131.1 ms — etykieta 0.55
2026-05-14 09:45:38.803 [INFO ] app.inference.worker_manager: frame 784: 1 detection(s) in 128.5 ms — etykieta 0.55
2026-05-14 09:45:39.004 [INFO ] app.inference.worker_manager: frame 785: 1 detection(s) in 135.2 ms — etykieta 0.54
2026-05-14 09:45:39.203 [INFO ] app.inference.worker_manager: frame 786: 1 detection(s) in 158.1 ms — etykieta 0.55
2026-05-14 09:45:39.402 [INFO ] app.inference.worker_manager: frame 787: 1 detection(s) in 136.4 ms — etykieta 0.55
2026-05-14 09:45:39.607 [INFO ] app.inference.worker_manager: frame 788: 1 detection(s) in 127.2 ms — etykieta 0.55
2026-05-14 09:45:39.809 [INFO ] app.inference.worker_manager: frame 789: 1 detection(s) in 132.1 ms — etykieta 0.55
2026-05-14 09:45:40.006 [INFO ] app.inference.worker_manager: frame 790: 1 detection(s) in 131.2 ms — etykieta 0.55
2026-05-14 09:45:40.204 [INFO ] app.inference.worker_manager: frame 791: 1 detection(s) in 122.7 ms — etykieta 0.55
2026-05-14 09:45:40.403 [INFO ] app.inference.worker_manager: frame 792: 1 detection(s) in 125.4 ms — etykieta 0.53
2026-05-14 09:45:40.603 [INFO ] app.inference.worker_manager: frame 793: 1 detection(s) in 136.4 ms — etykieta 0.56
2026-05-14 09:45:40.861 [INFO ] app.inference.worker_manager: frame 794: 1 detection(s) in 140.4 ms — etykieta 0.54
2026-05-14 09:45:41.053 [INFO ] app.inference.worker_manager: frame 795: 1 detection(s) in 129.7 ms — etykieta 0.55
2026-05-14 09:45:41.260 [INFO ] app.inference.worker_manager: frame 796: 1 detection(s) in 130.6 ms — etykieta 0.55
2026-05-14 09:45:41.453 [INFO ] app.inference.worker_manager: frame 797: 1 detection(s) in 125.6 ms — etykieta 0.54
2026-05-14 09:45:41.753 [INFO ] app.inference.worker_manager: frame 798: 1 detection(s) in 140.0 ms — etykieta 0.55
2026-05-14 09:45:41.953 [INFO ] app.inference.worker_manager: frame 799: 1 detection(s) in 131.8 ms — etykieta 0.55
2026-05-14 09:45:42.154 [INFO ] app.inference.worker_manager: frame 800: 1 detection(s) in 132.3 ms — etykieta 0.54
2026-05-14 09:45:42.453 [INFO ] app.inference.worker_manager: frame 801: 1 detection(s) in 174.1 ms — etykieta 0.54
2026-05-14 09:45:42.653 [INFO ] app.inference.worker_manager: frame 802: 1 detection(s) in 134.1 ms — etykieta 0.54
2026-05-14 09:45:42.852 [INFO ] app.inference.worker_manager: frame 803: 1 detection(s) in 134.6 ms — etykieta 0.55
2026-05-14 09:45:43.053 [INFO ] app.inference.worker_manager: frame 804: 1 detection(s) in 127.8 ms — etykieta 0.54
2026-05-14 09:45:43.254 [INFO ] app.inference.worker_manager: frame 805: 1 detection(s) in 130.3 ms — etykieta 0.55
2026-05-14 09:45:43.454 [INFO ] app.inference.worker_manager: frame 806: 1 detection(s) in 131.0 ms — etykieta 0.56
2026-05-14 09:45:43.653 [INFO ] app.inference.worker_manager: frame 807: 1 detection(s) in 132.9 ms — etykieta 0.54
2026-05-14 09:45:43.853 [INFO ] app.inference.worker_manager: frame 808: 1 detection(s) in 128.6 ms — etykieta 0.54
2026-05-14 09:45:44.053 [INFO ] app.inference.worker_manager: frame 809: 1 detection(s) in 142.0 ms — etykieta 0.55
2026-05-14 09:45:44.260 [INFO ] app.inference.worker_manager: frame 810: 1 detection(s) in 126.7 ms — etykieta 0.56
2026-05-14 09:45:44.503 [INFO ] app.inference.worker_manager: frame 811: 1 detection(s) in 149.8 ms — etykieta 0.55
2026-05-14 09:45:44.754 [INFO ] app.inference.worker_manager: frame 812: 1 detection(s) in 157.2 ms — etykieta 0.55
2026-05-14 09:45:44.956 [INFO ] app.inference.worker_manager: frame 813: 1 detection(s) in 136.7 ms — etykieta 0.56
2026-05-14 09:45:45.153 [INFO ] app.inference.worker_manager: frame 814: 1 detection(s) in 113.4 ms — etykieta 0.54
2026-05-14 09:45:45.353 [INFO ] app.inference.worker_manager: frame 815: 1 detection(s) in 135.8 ms — etykieta 0.55
2026-05-14 09:45:45.553 [INFO ] app.inference.worker_manager: frame 816: 1 detection(s) in 137.4 ms — etykieta 0.54
2026-05-14 09:45:45.753 [INFO ] app.inference.worker_manager: frame 817: 1 detection(s) in 147.1 ms — etykieta 0.56
2026-05-14 09:45:46.009 [INFO ] app.inference.worker_manager: frame 818: 1 detection(s) in 138.3 ms — etykieta 0.55
2026-05-14 09:45:46.210 [INFO ] app.inference.worker_manager: frame 819: 1 detection(s) in 130.0 ms — etykieta 0.55
2026-05-14 09:45:46.454 [INFO ] app.inference.worker_manager: frame 820: 1 detection(s) in 157.9 ms — etykieta 0.54
2026-05-14 09:45:46.653 [INFO ] app.inference.worker_manager: frame 821: 1 detection(s) in 156.2 ms — etykieta 0.54
2026-05-14 09:45:46.903 [INFO ] app.inference.worker_manager: frame 822: 1 detection(s) in 152.9 ms — etykieta 0.55
2026-05-14 09:45:47.153 [INFO ] app.inference.worker_manager: frame 823: 1 detection(s) in 151.0 ms — etykieta 0.55
2026-05-14 09:45:47.353 [INFO ] app.inference.worker_manager: frame 824: 1 detection(s) in 130.2 ms — etykieta 0.54
2026-05-14 09:45:47.553 [INFO ] app.inference.worker_manager: frame 825: 1 detection(s) in 161.9 ms — etykieta 0.55
2026-05-14 09:45:47.803 [INFO ] app.inference.worker_manager: frame 826: 1 detection(s) in 151.9 ms — etykieta 0.54
2026-05-14 09:45:48.004 [INFO ] app.inference.worker_manager: frame 827: 1 detection(s) in 131.1 ms — etykieta 0.55
2026-05-14 09:45:48.204 [INFO ] app.inference.worker_manager: frame 828: 1 detection(s) in 131.6 ms — etykieta 0.55
2026-05-14 09:45:48.403 [INFO ] app.inference.worker_manager: frame 829: 1 detection(s) in 121.7 ms — etykieta 0.55
2026-05-14 09:45:48.603 [INFO ] app.inference.worker_manager: frame 830: 1 detection(s) in 118.1 ms — etykieta 0.55
2026-05-14 09:45:48.753 [INFO ] app.inference.worker_manager: frame 831: 1 detection(s) in 113.2 ms — etykieta 0.54
2026-05-14 09:45:48.953 [INFO ] app.inference.worker_manager: frame 832: 1 detection(s) in 130.6 ms — etykieta 0.56
2026-05-14 09:45:49.203 [INFO ] app.inference.worker_manager: frame 833: 1 detection(s) in 158.7 ms — etykieta 0.54
2026-05-14 09:45:49.454 [INFO ] app.inference.worker_manager: frame 834: 1 detection(s) in 131.5 ms — etykieta 0.55
2026-05-14 09:45:49.654 [INFO ] app.inference.worker_manager: frame 835: 1 detection(s) in 135.1 ms — etykieta 0.55
2026-05-14 09:45:50.003 [INFO ] app.inference.worker_manager: frame 836: 1 detection(s) in 124.7 ms — etykieta 0.54
2026-05-14 09:45:50.203 [INFO ] app.inference.worker_manager: frame 837: 1 detection(s) in 131.8 ms — etykieta 0.56
2026-05-14 09:45:50.454 [INFO ] app.inference.worker_manager: frame 838: 1 detection(s) in 135.4 ms — etykieta 0.54
2026-05-14 09:45:50.653 [INFO ] app.inference.worker_manager: frame 839: 1 detection(s) in 142.8 ms — etykieta 0.55
2026-05-14 09:45:50.862 [INFO ] app.inference.worker_manager: frame 840: 1 detection(s) in 123.8 ms — etykieta 0.54
2026-05-14 09:45:51.053 [INFO ] app.inference.worker_manager: frame 841: 1 detection(s) in 123.8 ms — etykieta 0.54
2026-05-14 09:45:51.304 [INFO ] app.inference.worker_manager: frame 842: 1 detection(s) in 150.8 ms — etykieta 0.54
2026-05-14 09:45:51.504 [INFO ] app.inference.worker_manager: frame 843: 1 detection(s) in 141.5 ms — etykieta 0.54
2026-05-14 09:45:51.703 [INFO ] app.inference.worker_manager: frame 844: 1 detection(s) in 126.4 ms — etykieta 0.55
2026-05-14 09:45:51.904 [INFO ] app.inference.worker_manager: frame 845: 1 detection(s) in 150.7 ms — etykieta 0.55
2026-05-14 09:45:52.103 [INFO ] app.inference.worker_manager: frame 846: 1 detection(s) in 143.7 ms — etykieta 0.55
2026-05-14 09:45:52.303 [INFO ] app.inference.worker_manager: frame 847: 1 detection(s) in 130.6 ms — etykieta 0.55
2026-05-14 09:45:52.554 [INFO ] app.inference.worker_manager: frame 848: 1 detection(s) in 123.4 ms — etykieta 0.55
2026-05-14 09:45:52.810 [INFO ] app.inference.worker_manager: frame 849: 1 detection(s) in 177.1 ms — etykieta 0.54
2026-05-14 09:45:53.010 [INFO ] app.inference.worker_manager: frame 850: 1 detection(s) in 130.7 ms — etykieta 0.54
2026-05-14 09:45:53.206 [INFO ] app.inference.worker_manager: frame 851: 1 detection(s) in 121.3 ms — etykieta 0.54
2026-05-14 09:45:53.408 [INFO ] app.inference.worker_manager: frame 852: 1 detection(s) in 140.4 ms — etykieta 0.56
2026-05-14 09:45:53.654 [INFO ] app.inference.worker_manager: frame 853: 1 detection(s) in 134.5 ms — etykieta 0.54
2026-05-14 09:45:53.854 [INFO ] app.inference.worker_manager: frame 854: 1 detection(s) in 123.1 ms — etykieta 0.53
2026-05-14 09:45:54.103 [INFO ] app.inference.worker_manager: frame 855: 1 detection(s) in 137.7 ms — etykieta 0.54
2026-05-14 09:45:54.304 [INFO ] app.inference.worker_manager: frame 856: 1 detection(s) in 126.3 ms — etykieta 0.53
2026-05-14 09:45:54.505 [INFO ] app.inference.worker_manager: frame 857: 1 detection(s) in 122.2 ms — etykieta 0.54
2026-05-14 09:45:54.704 [INFO ] app.inference.worker_manager: frame 858: 1 detection(s) in 138.9 ms — etykieta 0.54
2026-05-14 09:45:54.904 [INFO ] app.inference.worker_manager: frame 859: 1 detection(s) in 113.3 ms — etykieta 0.54
2026-05-14 09:45:55.104 [INFO ] app.inference.worker_manager: frame 860: 1 detection(s) in 119.8 ms — etykieta 0.53
2026-05-14 09:45:55.254 [INFO ] app.inference.worker_manager: frame 861: 1 detection(s) in 111.9 ms — etykieta 0.53
2026-05-14 09:45:55.454 [INFO ] app.inference.worker_manager: frame 862: 1 detection(s) in 134.1 ms — etykieta 0.51
2026-05-14 09:45:55.654 [INFO ] app.inference.worker_manager: frame 863: 1 detection(s) in 121.8 ms — etykieta 0.53
2026-05-14 09:46:08.521 [DEBUG ] app.inference.worker_manager: InferenceManager: paused
2026-05-14 09:46:08.522 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber not found for removal: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7fc86adfc2f0) at 0x1faeffac0>>
2026-05-14 09:46:08.522 [INFO ] app.ui.main_window: Inference disabled
2026-05-14 09:46:23.807 [DEBUG ] app.ui.menu_bar: Format selected: 1600x1200 @ 30 fps (NV12)
2026-05-14 09:46:23.807 [INFO ] app.ui.main_window: Format selected via menu: 1600x1200 @ 30 fps (NV12)
2026-05-14 09:46:23.808 [INFO ] app.camera.camera_service: Format change: 1600x1200 @ 30 fps (NV12) — restarting camera
2026-05-14 09:46:23.859 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:46:23.860 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:46:23.886 [INFO ] app.camera.camera_service: Camera format requested: NV12 1600x1200 @ 30 fps
2026-05-14 09:46:23.965 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:46:23.966 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1600x1200 @ 30 fps
2026-05-14 09:46:23.966 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:46:23.966 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:46:32.338 [DEBUG ] app.ui.menu_bar: Format selected: 1920x1080 @ 30 fps (NV12)
2026-05-14 09:46:32.338 [INFO ] app.ui.main_window: Format selected via menu: 1920x1080 @ 30 fps (NV12)
2026-05-14 09:46:32.339 [INFO ] app.camera.camera_service: Format change: 1920x1080 @ 30 fps (NV12) — restarting camera
2026-05-14 09:46:32.387 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:46:32.387 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:46:32.416 [INFO ] app.camera.camera_service: Camera format requested: NV12 1920x1080 @ 30 fps
2026-05-14 09:46:32.496 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:46:32.496 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1920x1080 @ 30 fps
2026-05-14 09:46:32.496 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:46:32.496 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:46:46.600 [DEBUG ] app.ui.menu_bar: Format selected: 2048x1536 @ 30 fps (NV12)
2026-05-14 09:46:46.600 [INFO ] app.ui.main_window: Format selected via menu: 2048x1536 @ 30 fps (NV12)
2026-05-14 09:46:46.601 [INFO ] app.camera.camera_service: Format change: 2048x1536 @ 30 fps (NV12) — restarting camera
2026-05-14 09:46:46.648 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:46:46.648 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:46:46.675 [INFO ] app.camera.camera_service: Camera format requested: NV12 2048x1536 @ 30 fps
2026-05-14 09:46:46.762 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:46:46.762 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 2048x1536 @ 30 fps
2026-05-14 09:46:46.762 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:46:46.762 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:47:02.153 [DEBUG ] app.ui.menu_bar: Format selected: 1920x1080 @ 30 fps (NV12)
2026-05-14 09:47:02.153 [INFO ] app.ui.main_window: Format selected via menu: 1920x1080 @ 30 fps (NV12)
2026-05-14 09:47:02.154 [INFO ] app.camera.camera_service: Format change: 1920x1080 @ 30 fps (NV12) — restarting camera
2026-05-14 09:47:02.185 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:47:02.185 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:47:02.212 [INFO ] app.camera.camera_service: Camera format requested: NV12 1920x1080 @ 30 fps
2026-05-14 09:47:02.293 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 09:47:02.294 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1920x1080 @ 30 fps
2026-05-14 09:47:02.294 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 09:47:02.294 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 09:47:09.285 [INFO ] app.inference.worker_manager: Inference worker stopped
2026-05-14 09:47:09.340 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 09:47:09.340 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 09:47:09.341 [INFO ] app.ui.main_window: CSV telemetry: 71 rows written

View File

@@ -0,0 +1,92 @@
timestamp,fps_got,fps_req,frame_time_ms,dropped_frames,cpu_sys_pct,cpu_core_pct,mem_mb
15:33:53.869,0.0,30.0,0.00,0,14.8,59.1,82.1
15:33:58.976,25.0,30.0,39.88,0,6.6,26.5,112.2
15:34:04.476,25.0,30.0,31.29,2,11.6,46.4,119.2
15:34:09.484,26.0,30.0,40.44,2,8.1,32.4,120.4
15:34:14.976,25.0,30.0,40.42,3,6.8,27.1,120.7
15:34:19.977,0.0,30.0,41.75,5,0.2,0.7,119.8
15:34:24.977,0.0,30.0,41.75,5,1.6,6.6,119.5
15:34:30.477,24.0,30.0,133.59,6,7.2,28.6,121.5
15:34:35.477,0.0,30.0,134.55,7,0.1,0.6,119.7
15:34:40.478,0.0,30.0,134.55,7,0.2,1.0,119.6
15:34:45.485,25.0,30.0,138.94,8,11.1,44.4,159.5
15:34:50.977,25.0,30.0,45.09,10,11.1,44.4,190.3
15:34:56.478,25.0,30.0,40.44,10,12.1,48.2,190.3
15:35:01.479,25.0,30.0,40.42,10,10.4,41.7,189.5
15:35:06.978,25.0,30.0,40.43,10,11.9,47.5,189.4
15:35:12.481,26.0,30.0,40.43,10,10.8,43.1,188.0
15:35:17.689,24.0,30.0,40.93,11,13.8,55.4,212.4
15:35:22.982,26.0,30.0,40.38,12,16.3,65.1,216.4
15:35:28.479,25.0,30.0,40.44,12,14.8,59.3,216.3
15:35:33.480,25.0,30.0,40.45,12,16.4,65.6,216.3
15:35:38.987,26.0,30.0,40.39,12,15.8,63.4,215.9
15:35:43.995,25.0,30.0,40.34,12,15.7,62.6,215.9
15:35:49.486,26.0,30.0,40.44,12,16.0,64.1,215.9
15:35:54.496,25.0,30.0,40.72,12,15.2,61.0,215.9
15:35:59.979,25.0,30.0,40.44,12,14.6,58.3,216.0
15:36:04.991,25.0,30.0,40.63,12,14.8,59.3,216.0
15:36:10.480,25.0,30.0,40.40,12,15.1,60.3,216.0
15:36:15.521,25.0,30.0,40.45,12,16.0,63.9,216.0
15:36:21.039,25.0,30.0,41.30,13,16.0,64.1,216.0
15:36:26.480,25.0,30.0,41.75,14,13.8,55.1,216.0
15:36:31.490,24.0,30.0,42.30,15,16.4,65.6,216.0
15:36:36.981,23.0,30.0,42.40,15,14.6,58.2,216.0
15:36:42.481,22.0,30.0,45.87,18,15.4,61.5,216.1
15:36:47.482,24.0,30.0,41.77,19,15.6,62.4,216.1
15:36:52.484,23.0,30.0,44.59,22,14.9,59.6,216.1
15:36:57.996,21.0,30.0,44.59,23,11.7,46.7,216.1
15:37:03.507,23.0,30.0,44.64,24,15.0,60.0,216.1
15:37:08.981,21.0,30.0,45.61,27,15.7,62.9,216.1
15:37:14.000,23.0,30.0,47.88,28,14.1,56.2,216.1
15:37:19.483,19.0,30.0,49.01,31,12.2,48.8,216.1
15:37:24.983,16.0,30.0,52.48,36,12.5,49.9,216.1
15:37:30.497,16.0,30.0,56.05,39,10.4,41.5,216.1
15:37:35.982,18.0,30.0,57.24,43,11.7,46.6,216.1
15:37:40.982,20.0,30.0,53.84,43,12.8,51.3,216.1
15:37:45.983,17.0,30.0,62.73,46,11.3,45.1,216.1
15:37:51.486,18.0,30.0,61.56,49,10.4,41.7,216.1
15:37:56.983,13.0,30.0,65.21,54,9.0,36.0,216.1
15:38:01.984,18.0,30.0,66.29,56,13.2,52.7,216.1
15:38:06.986,13.0,30.0,72.05,60,9.4,37.5,216.1
15:38:12.483,15.0,30.0,71.79,62,9.5,38.0,216.1
15:38:17.495,17.0,30.0,72.88,66,10.2,40.9,216.1
15:38:23.038,17.0,30.0,68.84,69,13.8,55.4,216.1
15:38:28.541,10.0,30.0,79.95,75,8.9,35.6,216.1
15:38:33.992,16.0,30.0,81.68,76,12.8,51.4,216.1
15:38:39.492,17.0,30.0,68.20,77,10.9,43.7,216.1
15:38:44.984,17.0,30.0,68.20,80,9.7,38.9,216.1
15:38:49.999,15.0,30.0,70.99,85,9.7,38.7,216.1
15:38:55.496,13.0,30.0,76.36,86,8.9,35.8,216.1
15:39:00.986,8.0,30.0,78.92,88,6.0,24.1,216.1
15:39:05.996,10.0,30.0,83.36,91,9.8,39.0,216.1
15:39:11.492,10.0,30.0,91.48,96,5.0,20.0,216.1
15:39:16.985,14.0,30.0,96.91,101,10.4,41.6,216.1
15:39:21.986,9.0,30.0,84.60,101,6.2,24.7,216.1
15:39:26.991,15.0,30.0,78.74,107,10.6,42.5,216.1
15:39:32.486,8.0,30.0,89.75,111,9.1,36.4,216.1
15:39:37.523,11.0,30.0,86.73,113,8.3,33.2,216.1
15:39:42.987,12.0,30.0,96.83,117,8.6,34.4,216.1
15:39:47.998,11.0,30.0,99.02,119,6.0,23.9,216.1
15:39:53.486,10.0,30.0,86.08,121,6.8,27.1,216.1
15:39:58.986,17.0,30.0,83.31,123,13.0,52.0,216.1
15:40:03.987,11.0,30.0,86.83,127,9.5,37.9,216.1
15:40:08.987,9.0,30.0,92.66,130,8.8,35.3,216.1
15:40:14.487,12.0,30.0,92.42,131,9.2,36.6,216.1
15:40:19.488,11.0,30.0,94.93,134,9.0,36.0,216.1
15:40:24.987,11.0,30.0,92.65,136,9.4,37.5,216.1
15:40:30.487,13.0,30.0,91.07,139,12.1,48.2,216.1
15:40:35.488,11.0,30.0,90.47,142,6.5,25.8,216.1
15:40:40.512,13.0,30.0,96.50,145,7.5,30.2,216.1
15:40:45.988,7.0,30.0,102.85,147,2.9,11.7,216.1
15:40:51.008,9.0,30.0,101.70,150,6.6,26.4,216.1
15:40:56.019,13.0,30.0,98.46,154,6.4,25.7,216.1
15:41:01.488,11.0,30.0,90.95,158,5.4,21.5,216.9
15:41:06.989,18.0,30.0,70.04,158,9.3,37.2,216.8
15:41:12.489,20.0,30.0,48.11,158,7.8,31.2,216.7
15:41:17.502,21.0,30.0,47.28,161,9.4,37.7,216.7
15:41:22.989,23.0,30.0,43.47,164,10.9,43.6,217.0
15:41:28.490,23.0,30.0,47.93,168,5.9,23.6,211.3
15:41:33.492,25.0,30.0,41.81,169,6.5,26.1,211.3
15:41:39.004,11.0,30.0,45.64,172,8.9,35.6,214.7
15:41:44.490,25.0,30.0,41.52,172,10.1,40.3,214.7
15:41:49.497,26.0,30.0,41.00,172,11.2,44.7,213.8
1 timestamp fps_got fps_req frame_time_ms dropped_frames cpu_sys_pct cpu_core_pct mem_mb
2 15:33:53.869 0.0 30.0 0.00 0 14.8 59.1 82.1
3 15:33:58.976 25.0 30.0 39.88 0 6.6 26.5 112.2
4 15:34:04.476 25.0 30.0 31.29 2 11.6 46.4 119.2
5 15:34:09.484 26.0 30.0 40.44 2 8.1 32.4 120.4
6 15:34:14.976 25.0 30.0 40.42 3 6.8 27.1 120.7
7 15:34:19.977 0.0 30.0 41.75 5 0.2 0.7 119.8
8 15:34:24.977 0.0 30.0 41.75 5 1.6 6.6 119.5
9 15:34:30.477 24.0 30.0 133.59 6 7.2 28.6 121.5
10 15:34:35.477 0.0 30.0 134.55 7 0.1 0.6 119.7
11 15:34:40.478 0.0 30.0 134.55 7 0.2 1.0 119.6
12 15:34:45.485 25.0 30.0 138.94 8 11.1 44.4 159.5
13 15:34:50.977 25.0 30.0 45.09 10 11.1 44.4 190.3
14 15:34:56.478 25.0 30.0 40.44 10 12.1 48.2 190.3
15 15:35:01.479 25.0 30.0 40.42 10 10.4 41.7 189.5
16 15:35:06.978 25.0 30.0 40.43 10 11.9 47.5 189.4
17 15:35:12.481 26.0 30.0 40.43 10 10.8 43.1 188.0
18 15:35:17.689 24.0 30.0 40.93 11 13.8 55.4 212.4
19 15:35:22.982 26.0 30.0 40.38 12 16.3 65.1 216.4
20 15:35:28.479 25.0 30.0 40.44 12 14.8 59.3 216.3
21 15:35:33.480 25.0 30.0 40.45 12 16.4 65.6 216.3
22 15:35:38.987 26.0 30.0 40.39 12 15.8 63.4 215.9
23 15:35:43.995 25.0 30.0 40.34 12 15.7 62.6 215.9
24 15:35:49.486 26.0 30.0 40.44 12 16.0 64.1 215.9
25 15:35:54.496 25.0 30.0 40.72 12 15.2 61.0 215.9
26 15:35:59.979 25.0 30.0 40.44 12 14.6 58.3 216.0
27 15:36:04.991 25.0 30.0 40.63 12 14.8 59.3 216.0
28 15:36:10.480 25.0 30.0 40.40 12 15.1 60.3 216.0
29 15:36:15.521 25.0 30.0 40.45 12 16.0 63.9 216.0
30 15:36:21.039 25.0 30.0 41.30 13 16.0 64.1 216.0
31 15:36:26.480 25.0 30.0 41.75 14 13.8 55.1 216.0
32 15:36:31.490 24.0 30.0 42.30 15 16.4 65.6 216.0
33 15:36:36.981 23.0 30.0 42.40 15 14.6 58.2 216.0
34 15:36:42.481 22.0 30.0 45.87 18 15.4 61.5 216.1
35 15:36:47.482 24.0 30.0 41.77 19 15.6 62.4 216.1
36 15:36:52.484 23.0 30.0 44.59 22 14.9 59.6 216.1
37 15:36:57.996 21.0 30.0 44.59 23 11.7 46.7 216.1
38 15:37:03.507 23.0 30.0 44.64 24 15.0 60.0 216.1
39 15:37:08.981 21.0 30.0 45.61 27 15.7 62.9 216.1
40 15:37:14.000 23.0 30.0 47.88 28 14.1 56.2 216.1
41 15:37:19.483 19.0 30.0 49.01 31 12.2 48.8 216.1
42 15:37:24.983 16.0 30.0 52.48 36 12.5 49.9 216.1
43 15:37:30.497 16.0 30.0 56.05 39 10.4 41.5 216.1
44 15:37:35.982 18.0 30.0 57.24 43 11.7 46.6 216.1
45 15:37:40.982 20.0 30.0 53.84 43 12.8 51.3 216.1
46 15:37:45.983 17.0 30.0 62.73 46 11.3 45.1 216.1
47 15:37:51.486 18.0 30.0 61.56 49 10.4 41.7 216.1
48 15:37:56.983 13.0 30.0 65.21 54 9.0 36.0 216.1
49 15:38:01.984 18.0 30.0 66.29 56 13.2 52.7 216.1
50 15:38:06.986 13.0 30.0 72.05 60 9.4 37.5 216.1
51 15:38:12.483 15.0 30.0 71.79 62 9.5 38.0 216.1
52 15:38:17.495 17.0 30.0 72.88 66 10.2 40.9 216.1
53 15:38:23.038 17.0 30.0 68.84 69 13.8 55.4 216.1
54 15:38:28.541 10.0 30.0 79.95 75 8.9 35.6 216.1
55 15:38:33.992 16.0 30.0 81.68 76 12.8 51.4 216.1
56 15:38:39.492 17.0 30.0 68.20 77 10.9 43.7 216.1
57 15:38:44.984 17.0 30.0 68.20 80 9.7 38.9 216.1
58 15:38:49.999 15.0 30.0 70.99 85 9.7 38.7 216.1
59 15:38:55.496 13.0 30.0 76.36 86 8.9 35.8 216.1
60 15:39:00.986 8.0 30.0 78.92 88 6.0 24.1 216.1
61 15:39:05.996 10.0 30.0 83.36 91 9.8 39.0 216.1
62 15:39:11.492 10.0 30.0 91.48 96 5.0 20.0 216.1
63 15:39:16.985 14.0 30.0 96.91 101 10.4 41.6 216.1
64 15:39:21.986 9.0 30.0 84.60 101 6.2 24.7 216.1
65 15:39:26.991 15.0 30.0 78.74 107 10.6 42.5 216.1
66 15:39:32.486 8.0 30.0 89.75 111 9.1 36.4 216.1
67 15:39:37.523 11.0 30.0 86.73 113 8.3 33.2 216.1
68 15:39:42.987 12.0 30.0 96.83 117 8.6 34.4 216.1
69 15:39:47.998 11.0 30.0 99.02 119 6.0 23.9 216.1
70 15:39:53.486 10.0 30.0 86.08 121 6.8 27.1 216.1
71 15:39:58.986 17.0 30.0 83.31 123 13.0 52.0 216.1
72 15:40:03.987 11.0 30.0 86.83 127 9.5 37.9 216.1
73 15:40:08.987 9.0 30.0 92.66 130 8.8 35.3 216.1
74 15:40:14.487 12.0 30.0 92.42 131 9.2 36.6 216.1
75 15:40:19.488 11.0 30.0 94.93 134 9.0 36.0 216.1
76 15:40:24.987 11.0 30.0 92.65 136 9.4 37.5 216.1
77 15:40:30.487 13.0 30.0 91.07 139 12.1 48.2 216.1
78 15:40:35.488 11.0 30.0 90.47 142 6.5 25.8 216.1
79 15:40:40.512 13.0 30.0 96.50 145 7.5 30.2 216.1
80 15:40:45.988 7.0 30.0 102.85 147 2.9 11.7 216.1
81 15:40:51.008 9.0 30.0 101.70 150 6.6 26.4 216.1
82 15:40:56.019 13.0 30.0 98.46 154 6.4 25.7 216.1
83 15:41:01.488 11.0 30.0 90.95 158 5.4 21.5 216.9
84 15:41:06.989 18.0 30.0 70.04 158 9.3 37.2 216.8
85 15:41:12.489 20.0 30.0 48.11 158 7.8 31.2 216.7
86 15:41:17.502 21.0 30.0 47.28 161 9.4 37.7 216.7
87 15:41:22.989 23.0 30.0 43.47 164 10.9 43.6 217.0
88 15:41:28.490 23.0 30.0 47.93 168 5.9 23.6 211.3
89 15:41:33.492 25.0 30.0 41.81 169 6.5 26.1 211.3
90 15:41:39.004 11.0 30.0 45.64 172 8.9 35.6 214.7
91 15:41:44.490 25.0 30.0 41.52 172 10.1 40.3 214.7
92 15:41:49.497 26.0 30.0 41.00 172 11.2 44.7 213.8

View File

@@ -0,0 +1,373 @@
========================================================================
Duck Preview 0.1.0
Session: 2026-05-14_15-33-52
========================================================================
Platform : macOS-13.7.8-x86_64-i386-64bit
Python : 3.12.9
PySide6 : 6.11.1
Hardware : 4 logical CPUs, 16.0 GB RAM
Log file : /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_15-33-52.log
========================================================================
2026-05-14 15:33:52.971 [DEBUG ] app.logging_setup: Pruned old log: duck-preview_2026-05-14_08-10-53.log
2026-05-14 15:33:53.041 [INFO ] __main__: Application starting (session: 2026-05-14_15-33-52)
2026-05-14 15:33:53.203 [INFO ] app.ui.main_window: Telemetry CSV: /Users/rafalkaczka/Repos/duck-preview/logs/duck-preview_2026-05-14_15-33-52.csv
2026-05-14 15:33:53.204 [DEBUG ] app.ui.camera_view: Overlay layer added: TelemetryOverlay
2026-05-14 15:33:53.204 [DEBUG ] app.ui.camera_view: Overlay layer added: BboxOverlay
2026-05-14 15:33:53.387 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method CameraView.on_frame of <app.ui.camera_view.CameraView(0x7f7f245fd520) at 0x203728f80>> (drop_if_busy=True)
2026-05-14 15:33:53.387 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method TelemetryCollector.on_frame of <app.telemetry.telemetry_collector.TelemetryCollector(0x7f7f245ed5b0) at 0x203728440>> (drop_if_busy=False)
2026-05-14 15:33:53.394 [INFO ] __main__: Application shutting down
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 15:33:53.606 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 15:33:53.607 [INFO ] app.camera.camera_enumerator: Cameras found: 1
2026-05-14 15:33:53.607 [INFO ] app.camera.camera_enumerator: [0] HD USB CAMERA (id: 0x1420000032e40317)
2026-05-14 15:33:53.607 [INFO ] app.camera.camera_enumerator: NV12 3840x2160 @ 30.0 fps
2026-05-14 15:33:53.607 [INFO ] app.camera.camera_enumerator: NV12 2592x1944 @ 30.0 fps
2026-05-14 15:33:53.607 [INFO ] app.camera.camera_enumerator: NV12 2048x1536 @ 30.0 fps
2026-05-14 15:33:53.607 [INFO ] app.camera.camera_enumerator: NV12 1920x1080 @ 30.0 fps
2026-05-14 15:33:53.607 [INFO ] app.camera.camera_enumerator: NV12 1600x1200 @ 30.0 fps
2026-05-14 15:33:53.608 [INFO ] app.camera.camera_enumerator: NV12 1280x960 @ 30.0 fps
2026-05-14 15:33:53.608 [INFO ] app.camera.camera_enumerator: NV12 1280x720 @ 30.0 fps
2026-05-14 15:33:53.608 [INFO ] app.camera.camera_enumerator: NV12 1024x768 @ 30.0 fps
2026-05-14 15:33:53.608 [INFO ] app.camera.camera_enumerator: NV12 800x600 @ 30.0 fps
2026-05-14 15:33:53.608 [INFO ] app.camera.camera_enumerator: NV12 640x480 @ 30.0 fps
2026-05-14 15:33:53.608 [INFO ] app.camera.camera_enumerator: NV12 320x240 @ 30.0 fps
2026-05-14 15:33:53.633 [INFO ] app.camera.camera_service: Camera format requested: NV12 1280x720 @ 30 fps
2026-05-14 15:33:53.789 [INFO ] app.camera.camera_service: Qt multimedia backend: AVFoundation
2026-05-14 15:33:53.789 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 15:33:53.789 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1280x720 @ 30 fps
2026-05-14 15:33:53.789 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 15:33:53.789 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 15:33:53.790 [DEBUG ] app.camera.uvc.macos: pyuvc not available — macOS UVC controls disabled
2026-05-14 15:33:53.790 [WARNING] app.camera.uvc: UVC: pyuvc not installed — UVC controls unavailable on macOS
2026-05-14 15:34:04.214 [DEBUG ] app.ui.menu_bar: Model file selected: /Users/rafalkaczka/Repos/duck-preview/models/best_v1.pt
2026-05-14 15:34:04.215 [INFO ] app.ui.main_window: Loading model: /Users/rafalkaczka/Repos/duck-preview/models/best_v1.pt
2026-05-14 15:34:04.215 [INFO ] app.inference.worker_manager: Inference worker stopped
2026-05-14 15:34:04.239 [INFO ] app.inference.worker_manager: Inference worker started (pid=6551, model=/Users/rafalkaczka/Repos/duck-preview/models/best_v1.pt)
2026-05-14 15:34:04.241 [DEBUG ] app.inference.worker_manager: InferenceManager: resumed
2026-05-14 15:34:04.242 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7f7f24149f40) at 0x203728880>> (drop_if_busy=True)
2026-05-14 15:34:04.243 [INFO ] app.ui.main_window: Inference enabled
2026-05-14 15:34:04.244 [DEBUG ] app.inference.worker_manager: InferenceManager: paused
2026-05-14 15:34:04.244 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber not found for removal: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7f7f24149f40) at 0x203728880>>
2026-05-14 15:34:04.245 [INFO ] app.ui.main_window: Inference disabled
2026-05-14 15:34:10.027 [INFO ] app.inference.worker_manager: Inference device: cpu
2026-05-14 15:34:17.008 [DEBUG ] app.ui.menu_bar: Format selected: 1920x1080 @ 30 fps (NV12)
2026-05-14 15:34:17.008 [INFO ] app.ui.main_window: Format selected via menu: 1920x1080 @ 30 fps (NV12)
2026-05-14 15:34:17.009 [INFO ] app.camera.camera_service: Format change: 1920x1080 @ 30 fps (NV12) — restarting camera
2026-05-14 15:34:17.056 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 15:34:17.057 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 15:34:17.082 [INFO ] app.camera.camera_service: Camera format requested: NV12 1920x1080 @ 30 fps
2026-05-14 15:34:17.152 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 15:34:17.152 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1920x1080 @ 30 fps
2026-05-14 15:34:17.152 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 15:34:17.152 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 15:34:27.626 [DEBUG ] app.ui.menu_bar: Format selected: 1280x720 @ 30 fps (NV12)
2026-05-14 15:34:27.626 [INFO ] app.ui.main_window: Format selected via menu: 1280x720 @ 30 fps (NV12)
2026-05-14 15:34:27.627 [INFO ] app.camera.camera_service: Format change: 1280x720 @ 30 fps (NV12) — restarting camera
2026-05-14 15:34:27.657 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 15:34:27.658 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 15:34:27.689 [INFO ] app.camera.camera_service: Camera format requested: NV12 1280x720 @ 30 fps
2026-05-14 15:34:27.771 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 15:34:27.771 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1280x720 @ 30 fps
2026-05-14 15:34:27.772 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 15:34:27.772 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 15:34:32.403 [DEBUG ] app.ui.menu_bar: Format selected: 1920x1080 @ 30 fps (NV12)
2026-05-14 15:34:32.403 [INFO ] app.ui.main_window: Format selected via menu: 1920x1080 @ 30 fps (NV12)
2026-05-14 15:34:32.403 [INFO ] app.camera.camera_service: Format change: 1920x1080 @ 30 fps (NV12) — restarting camera
2026-05-14 15:34:32.452 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 15:34:32.453 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 15:34:32.479 [INFO ] app.camera.camera_service: Camera format requested: NV12 1920x1080 @ 30 fps
2026-05-14 15:34:32.545 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 15:34:32.545 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1920x1080 @ 30 fps
2026-05-14 15:34:32.545 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 15:34:32.545 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 15:34:43.662 [DEBUG ] app.ui.menu_bar: Format selected: 1600x1200 @ 30 fps (NV12)
2026-05-14 15:34:43.662 [INFO ] app.ui.main_window: Format selected via menu: 1600x1200 @ 30 fps (NV12)
2026-05-14 15:34:43.662 [INFO ] app.camera.camera_service: Format change: 1600x1200 @ 30 fps (NV12) — restarting camera
2026-05-14 15:34:43.691 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 15:34:43.692 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 15:34:43.719 [INFO ] app.camera.camera_service: Camera format requested: NV12 1600x1200 @ 30 fps
2026-05-14 15:34:43.803 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 15:34:43.803 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1600x1200 @ 30 fps
2026-05-14 15:34:43.803 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 15:34:43.803 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 15:34:48.524 [DEBUG ] app.ui.menu_bar: Format selected: 1920x1080 @ 30 fps (NV12)
2026-05-14 15:34:48.525 [INFO ] app.ui.main_window: Format selected via menu: 1920x1080 @ 30 fps (NV12)
2026-05-14 15:34:48.525 [INFO ] app.camera.camera_service: Format change: 1920x1080 @ 30 fps (NV12) — restarting camera
2026-05-14 15:34:48.575 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 15:34:48.576 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 15:34:48.602 [INFO ] app.camera.camera_service: Camera format requested: NV12 1920x1080 @ 30 fps
2026-05-14 15:34:48.682 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 15:34:48.682 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1920x1080 @ 30 fps
2026-05-14 15:34:48.682 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 15:34:48.682 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 15:35:17.684 [DEBUG ] app.inference.worker_manager: InferenceManager: resumed
2026-05-14 15:35:17.684 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber added: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7f7f24149f40) at 0x203728880>> (drop_if_busy=True)
2026-05-14 15:35:17.684 [INFO ] app.ui.main_window: Inference enabled
2026-05-14 15:35:59.079 [INFO ] app.inference.worker_manager: frame 146: 1 detection(s) in 164.0 ms — etykieta 0.27
2026-05-14 15:35:59.679 [INFO ] app.inference.worker_manager: frame 148: 1 detection(s) in 178.9 ms — etykieta 0.40
2026-05-14 15:35:59.980 [INFO ] app.inference.worker_manager: frame 149: 1 detection(s) in 176.6 ms — etykieta 0.45
2026-05-14 15:36:00.329 [INFO ] app.inference.worker_manager: frame 150: 1 detection(s) in 217.8 ms — etykieta 0.49
2026-05-14 15:36:00.635 [INFO ] app.inference.worker_manager: frame 151: 1 detection(s) in 173.4 ms — etykieta 0.47
2026-05-14 15:36:00.930 [INFO ] app.inference.worker_manager: frame 152: 1 detection(s) in 170.3 ms — etykieta 0.49
2026-05-14 15:36:01.229 [INFO ] app.inference.worker_manager: frame 153: 1 detection(s) in 178.0 ms — etykieta 0.46
2026-05-14 15:36:01.530 [INFO ] app.inference.worker_manager: frame 154: 1 detection(s) in 174.2 ms — etykieta 0.51
2026-05-14 15:36:01.790 [INFO ] app.inference.worker_manager: frame 155: 1 detection(s) in 173.2 ms — etykieta 0.54
2026-05-14 15:36:02.088 [INFO ] app.inference.worker_manager: frame 156: 1 detection(s) in 181.4 ms — etykieta 0.51
2026-05-14 15:36:02.387 [INFO ] app.inference.worker_manager: frame 157: 1 detection(s) in 170.7 ms — etykieta 0.50
2026-05-14 15:36:02.679 [INFO ] app.inference.worker_manager: frame 158: 1 detection(s) in 174.2 ms — etykieta 0.48
2026-05-14 15:36:02.980 [INFO ] app.inference.worker_manager: frame 159: 1 detection(s) in 179.4 ms — etykieta 0.47
2026-05-14 15:36:03.280 [INFO ] app.inference.worker_manager: frame 160: 1 detection(s) in 178.4 ms — etykieta 0.33
2026-05-14 15:36:03.579 [INFO ] app.inference.worker_manager: frame 161: 1 detection(s) in 172.8 ms — etykieta 0.38
2026-05-14 15:36:03.879 [INFO ] app.inference.worker_manager: frame 162: 1 detection(s) in 180.2 ms — etykieta 0.38
2026-05-14 15:36:04.179 [INFO ] app.inference.worker_manager: frame 163: 1 detection(s) in 174.0 ms — etykieta 0.40
2026-05-14 15:36:04.429 [INFO ] app.inference.worker_manager: frame 164: 1 detection(s) in 172.3 ms — etykieta 0.39
2026-05-14 15:36:04.730 [INFO ] app.inference.worker_manager: frame 165: 1 detection(s) in 172.8 ms — etykieta 0.37
2026-05-14 15:36:04.991 [INFO ] app.inference.worker_manager: frame 166: 1 detection(s) in 169.6 ms — etykieta 0.36
2026-05-14 15:36:05.346 [INFO ] app.inference.worker_manager: frame 167: 1 detection(s) in 213.3 ms — etykieta 0.38
2026-05-14 15:36:05.652 [INFO ] app.inference.worker_manager: frame 168: 1 detection(s) in 183.4 ms — etykieta 0.37
2026-05-14 15:36:05.981 [INFO ] app.inference.worker_manager: frame 169: 1 detection(s) in 169.8 ms — etykieta 0.38
2026-05-14 15:36:06.281 [INFO ] app.inference.worker_manager: frame 170: 1 detection(s) in 171.2 ms — etykieta 0.34
2026-05-14 15:36:06.580 [INFO ] app.inference.worker_manager: frame 171: 1 detection(s) in 181.4 ms — etykieta 0.37
2026-05-14 15:36:06.843 [INFO ] app.inference.worker_manager: frame 172: 1 detection(s) in 172.3 ms — etykieta 0.41
2026-05-14 15:36:07.132 [INFO ] app.inference.worker_manager: frame 173: 1 detection(s) in 179.4 ms — etykieta 0.48
2026-05-14 15:36:07.438 [INFO ] app.inference.worker_manager: frame 174: 1 detection(s) in 196.4 ms — etykieta 0.48
2026-05-14 15:36:07.782 [INFO ] app.inference.worker_manager: frame 175: 1 detection(s) in 192.5 ms — etykieta 0.46
2026-05-14 15:36:08.092 [INFO ] app.inference.worker_manager: frame 176: 1 detection(s) in 168.5 ms — etykieta 0.48
2026-05-14 15:36:08.388 [INFO ] app.inference.worker_manager: frame 177: 1 detection(s) in 170.8 ms — etykieta 0.44
2026-05-14 15:36:08.689 [INFO ] app.inference.worker_manager: frame 178: 1 detection(s) in 179.0 ms — etykieta 0.46
2026-05-14 15:36:08.985 [INFO ] app.inference.worker_manager: frame 179: 1 detection(s) in 165.9 ms — etykieta 0.46
2026-05-14 15:36:09.282 [INFO ] app.inference.worker_manager: frame 180: 1 detection(s) in 176.3 ms — etykieta 0.48
2026-05-14 15:36:09.582 [INFO ] app.inference.worker_manager: frame 181: 1 detection(s) in 181.8 ms — etykieta 0.45
2026-05-14 15:36:09.847 [INFO ] app.inference.worker_manager: frame 182: 1 detection(s) in 175.4 ms — etykieta 0.49
2026-05-14 15:36:10.137 [INFO ] app.inference.worker_manager: frame 183: 1 detection(s) in 186.4 ms — etykieta 0.45
2026-05-14 15:36:10.441 [INFO ] app.inference.worker_manager: frame 184: 1 detection(s) in 186.0 ms — etykieta 0.48
2026-05-14 15:36:10.739 [INFO ] app.inference.worker_manager: frame 185: 1 detection(s) in 179.4 ms — etykieta 0.46
2026-05-14 15:36:11.029 [INFO ] app.inference.worker_manager: frame 186: 1 detection(s) in 172.1 ms — etykieta 0.47
2026-05-14 15:36:11.330 [INFO ] app.inference.worker_manager: frame 187: 1 detection(s) in 174.4 ms — etykieta 0.47
2026-05-14 15:36:11.630 [INFO ] app.inference.worker_manager: frame 188: 1 detection(s) in 185.4 ms — etykieta 0.48
2026-05-14 15:36:11.930 [INFO ] app.inference.worker_manager: frame 189: 1 detection(s) in 176.0 ms — etykieta 0.45
2026-05-14 15:36:12.231 [INFO ] app.inference.worker_manager: frame 190: 1 detection(s) in 180.0 ms — etykieta 0.46
2026-05-14 15:36:12.530 [INFO ] app.inference.worker_manager: frame 191: 1 detection(s) in 176.8 ms — etykieta 0.48
2026-05-14 15:36:12.780 [INFO ] app.inference.worker_manager: frame 192: 1 detection(s) in 171.4 ms — etykieta 0.48
2026-05-14 15:36:13.049 [INFO ] app.inference.worker_manager: frame 193: 1 detection(s) in 176.5 ms — etykieta 0.46
2026-05-14 15:36:13.336 [INFO ] app.inference.worker_manager: frame 194: 1 detection(s) in 175.1 ms — etykieta 0.48
2026-05-14 15:36:13.639 [INFO ] app.inference.worker_manager: frame 195: 1 detection(s) in 181.3 ms — etykieta 0.47
2026-05-14 15:36:13.938 [INFO ] app.inference.worker_manager: frame 196: 1 detection(s) in 175.8 ms — etykieta 0.50
2026-05-14 15:36:17.137 [INFO ] app.inference.worker_manager: frame 207: 1 detection(s) in 164.8 ms — etykieta 0.30
2026-05-14 15:36:17.430 [INFO ] app.inference.worker_manager: frame 208: 1 detection(s) in 172.8 ms — etykieta 0.35
2026-05-14 15:36:17.733 [INFO ] app.inference.worker_manager: frame 209: 1 detection(s) in 168.9 ms — etykieta 0.36
2026-05-14 15:36:18.037 [INFO ] app.inference.worker_manager: frame 210: 1 detection(s) in 182.5 ms — etykieta 0.36
2026-05-14 15:36:18.330 [INFO ] app.inference.worker_manager: frame 211: 1 detection(s) in 183.8 ms — etykieta 0.36
2026-05-14 15:36:18.630 [INFO ] app.inference.worker_manager: frame 212: 1 detection(s) in 184.4 ms — etykieta 0.35
2026-05-14 15:36:18.930 [INFO ] app.inference.worker_manager: frame 213: 1 detection(s) in 176.1 ms — etykieta 0.36
2026-05-14 15:36:19.180 [INFO ] app.inference.worker_manager: frame 214: 1 detection(s) in 172.2 ms — etykieta 0.35
2026-05-14 15:36:19.480 [INFO ] app.inference.worker_manager: frame 215: 1 detection(s) in 179.9 ms — etykieta 0.35
2026-05-14 15:36:19.743 [INFO ] app.inference.worker_manager: frame 216: 1 detection(s) in 168.8 ms — etykieta 0.35
2026-05-14 15:36:20.045 [INFO ] app.inference.worker_manager: frame 217: 1 detection(s) in 200.3 ms — etykieta 0.35
2026-05-14 15:36:20.372 [INFO ] app.inference.worker_manager: frame 218: 1 detection(s) in 180.3 ms — etykieta 0.36
2026-05-14 15:36:20.635 [INFO ] app.inference.worker_manager: frame 219: 1 detection(s) in 176.6 ms — etykieta 0.35
2026-05-14 15:36:20.934 [INFO ] app.inference.worker_manager: frame 220: 1 detection(s) in 179.1 ms — etykieta 0.36
2026-05-14 15:36:21.231 [INFO ] app.inference.worker_manager: frame 221: 1 detection(s) in 175.7 ms — etykieta 0.36
2026-05-14 15:36:21.793 [INFO ] app.inference.worker_manager: frame 223: 1 detection(s) in 167.4 ms — etykieta 0.39
2026-05-14 15:36:22.086 [INFO ] app.inference.worker_manager: frame 224: 1 detection(s) in 170.2 ms — etykieta 0.37
2026-05-14 15:36:22.348 [INFO ] app.inference.worker_manager: frame 225: 1 detection(s) in 127.5 ms — etykieta 0.38
2026-05-14 15:36:22.630 [INFO ] app.inference.worker_manager: frame 226: 1 detection(s) in 147.5 ms — etykieta 0.40
2026-05-14 15:36:22.937 [INFO ] app.inference.worker_manager: frame 227: 1 detection(s) in 176.8 ms — etykieta 0.40
2026-05-14 15:36:23.241 [INFO ] app.inference.worker_manager: frame 228: 1 detection(s) in 178.4 ms — etykieta 0.38
2026-05-14 15:36:23.540 [INFO ] app.inference.worker_manager: frame 229: 1 detection(s) in 173.7 ms — etykieta 0.38
2026-05-14 15:36:23.831 [INFO ] app.inference.worker_manager: frame 230: 1 detection(s) in 174.4 ms — etykieta 0.41
2026-05-14 15:36:29.944 [INFO ] app.inference.worker_manager: frame 251: 1 detection(s) in 166.2 ms — etykieta 0.35
2026-05-14 15:36:30.232 [INFO ] app.inference.worker_manager: frame 252: 1 detection(s) in 172.0 ms — etykieta 0.35
2026-05-14 15:36:30.487 [INFO ] app.inference.worker_manager: frame 253: 1 detection(s) in 136.0 ms — etykieta 0.36
2026-05-14 15:36:30.781 [INFO ] app.inference.worker_manager: frame 254: 1 detection(s) in 173.9 ms — etykieta 0.33
2026-05-14 15:36:31.080 [INFO ] app.inference.worker_manager: frame 255: 1 detection(s) in 206.3 ms — etykieta 0.34
2026-05-14 15:36:31.391 [INFO ] app.inference.worker_manager: frame 256: 1 detection(s) in 168.2 ms — etykieta 0.34
2026-05-14 15:36:31.689 [INFO ] app.inference.worker_manager: frame 257: 1 detection(s) in 173.6 ms — etykieta 0.35
2026-05-14 15:36:31.981 [INFO ] app.inference.worker_manager: frame 258: 1 detection(s) in 179.1 ms — etykieta 0.33
2026-05-14 15:36:32.283 [INFO ] app.inference.worker_manager: frame 259: 1 detection(s) in 173.8 ms — etykieta 0.35
2026-05-14 15:36:32.547 [INFO ] app.inference.worker_manager: frame 260: 1 detection(s) in 170.4 ms — etykieta 0.34
2026-05-14 15:36:32.812 [INFO ] app.inference.worker_manager: frame 261: 1 detection(s) in 151.4 ms — etykieta 0.35
2026-05-14 15:36:33.071 [INFO ] app.inference.worker_manager: frame 262: 1 detection(s) in 149.8 ms — etykieta 0.35
2026-05-14 15:36:33.380 [INFO ] app.inference.worker_manager: frame 263: 1 detection(s) in 175.3 ms — etykieta 0.34
2026-05-14 15:36:33.680 [INFO ] app.inference.worker_manager: frame 264: 1 detection(s) in 170.8 ms — etykieta 0.34
2026-05-14 15:36:33.932 [INFO ] app.inference.worker_manager: frame 265: 1 detection(s) in 165.1 ms — etykieta 0.34
2026-05-14 15:36:34.230 [INFO ] app.inference.worker_manager: frame 266: 1 detection(s) in 166.6 ms — etykieta 0.35
2026-05-14 15:36:34.494 [INFO ] app.inference.worker_manager: frame 267: 1 detection(s) in 167.7 ms — etykieta 0.32
2026-05-14 15:36:34.791 [INFO ] app.inference.worker_manager: frame 268: 1 detection(s) in 177.3 ms — etykieta 0.35
2026-05-14 15:36:35.093 [INFO ] app.inference.worker_manager: frame 269: 1 detection(s) in 171.3 ms — etykieta 0.32
2026-05-14 15:36:35.380 [INFO ] app.inference.worker_manager: frame 270: 1 detection(s) in 166.1 ms — etykieta 0.33
2026-05-14 15:36:35.680 [INFO ] app.inference.worker_manager: frame 271: 1 detection(s) in 159.7 ms — etykieta 0.33
2026-05-14 15:36:35.987 [INFO ] app.inference.worker_manager: frame 272: 1 detection(s) in 179.8 ms — etykieta 0.34
2026-05-14 15:36:36.281 [INFO ] app.inference.worker_manager: frame 273: 1 detection(s) in 175.1 ms — etykieta 0.33
2026-05-14 15:36:36.581 [INFO ] app.inference.worker_manager: frame 274: 1 detection(s) in 178.9 ms — etykieta 0.34
2026-05-14 15:36:36.841 [INFO ] app.inference.worker_manager: frame 275: 1 detection(s) in 135.2 ms — etykieta 0.33
2026-05-14 15:36:37.088 [INFO ] app.inference.worker_manager: frame 276: 1 detection(s) in 148.1 ms — etykieta 0.34
2026-05-14 15:36:37.380 [INFO ] app.inference.worker_manager: frame 277: 1 detection(s) in 135.5 ms — etykieta 0.34
2026-05-14 15:36:37.633 [INFO ] app.inference.worker_manager: frame 278: 1 detection(s) in 168.7 ms — etykieta 0.33
2026-05-14 15:36:37.894 [INFO ] app.inference.worker_manager: frame 279: 1 detection(s) in 147.5 ms — etykieta 0.35
2026-05-14 15:36:38.189 [INFO ] app.inference.worker_manager: frame 280: 1 detection(s) in 173.9 ms — etykieta 0.32
2026-05-14 15:36:38.482 [INFO ] app.inference.worker_manager: frame 281: 1 detection(s) in 159.5 ms — etykieta 0.33
2026-05-14 15:36:40.730 [INFO ] app.inference.worker_manager: frame 289: 1 detection(s) in 182.5 ms — etykieta 0.26
2026-05-14 15:36:41.031 [INFO ] app.inference.worker_manager: frame 290: 1 detection(s) in 151.0 ms — etykieta 0.26
2026-05-14 15:38:28.434 [INFO ] app.inference.worker_manager: frame 676: 1 detection(s) in 122.8 ms — etykieta 0.25
2026-05-14 15:38:28.934 [INFO ] app.inference.worker_manager: frame 678: 1 detection(s) in 111.9 ms — etykieta 0.25
2026-05-14 15:38:29.433 [INFO ] app.inference.worker_manager: frame 680: 1 detection(s) in 158.7 ms — etykieta 0.26
2026-05-14 15:39:16.985 [INFO ] app.inference.worker_manager: frame 852: 1 detection(s) in 135.4 ms — etykieta 0.25
2026-05-14 15:39:17.349 [INFO ] app.inference.worker_manager: frame 853: 1 detection(s) in 141.6 ms — etykieta 0.25
2026-05-14 15:39:18.335 [INFO ] app.inference.worker_manager: frame 856: 1 detection(s) in 132.4 ms — etykieta 0.25
2026-05-14 15:39:22.885 [INFO ] app.inference.worker_manager: frame 872: 1 detection(s) in 108.7 ms — etykieta 0.26
2026-05-14 15:39:25.335 [INFO ] app.inference.worker_manager: frame 880: 1 detection(s) in 111.4 ms — etykieta 0.26
2026-05-14 15:39:26.398 [INFO ] app.inference.worker_manager: frame 884: 1 detection(s) in 112.6 ms — etykieta 0.25
2026-05-14 15:39:27.535 [INFO ] app.inference.worker_manager: frame 888: 1 detection(s) in 126.1 ms — etykieta 0.25
2026-05-14 15:39:31.286 [INFO ] app.inference.worker_manager: frame 902: 1 detection(s) in 141.3 ms — etykieta 0.25
2026-05-14 15:39:31.835 [INFO ] app.inference.worker_manager: frame 904: 1 detection(s) in 119.1 ms — etykieta 0.25
2026-05-14 15:39:33.886 [INFO ] app.inference.worker_manager: frame 911: 1 detection(s) in 119.2 ms — etykieta 0.27
2026-05-14 15:39:34.136 [INFO ] app.inference.worker_manager: frame 912: 1 detection(s) in 131.2 ms — etykieta 0.27
2026-05-14 15:39:37.445 [INFO ] app.inference.worker_manager: frame 925: 1 detection(s) in 110.6 ms — etykieta 0.27
2026-05-14 15:39:37.687 [INFO ] app.inference.worker_manager: frame 926: 1 detection(s) in 125.6 ms — etykieta 0.35
2026-05-14 15:39:38.144 [INFO ] app.inference.worker_manager: frame 927: 1 detection(s) in 165.3 ms — etykieta 0.30
2026-05-14 15:39:38.586 [INFO ] app.inference.worker_manager: frame 929: 1 detection(s) in 128.2 ms — etykieta 0.28
2026-05-14 15:39:38.836 [INFO ] app.inference.worker_manager: frame 930: 1 detection(s) in 112.5 ms — etykieta 0.35
2026-05-14 15:39:39.036 [INFO ] app.inference.worker_manager: frame 931: 1 detection(s) in 122.6 ms — etykieta 0.27
2026-05-14 15:39:41.986 [INFO ] app.inference.worker_manager: frame 941: 1 detection(s) in 114.9 ms — etykieta 0.30
2026-05-14 15:39:42.237 [INFO ] app.inference.worker_manager: frame 942: 1 detection(s) in 142.5 ms — etykieta 0.28
2026-05-14 15:39:48.186 [INFO ] app.inference.worker_manager: frame 962: 1 detection(s) in 143.5 ms — etykieta 0.31
2026-05-14 15:39:48.438 [INFO ] app.inference.worker_manager: frame 963: 1 detection(s) in 117.7 ms — etykieta 0.29
2026-05-14 15:39:48.736 [INFO ] app.inference.worker_manager: frame 964: 1 detection(s) in 138.2 ms — etykieta 0.31
2026-05-14 15:39:49.037 [INFO ] app.inference.worker_manager: frame 965: 1 detection(s) in 147.9 ms — etykieta 0.31
2026-05-14 15:39:49.436 [INFO ] app.inference.worker_manager: frame 966: 1 detection(s) in 145.9 ms — etykieta 0.32
2026-05-14 15:39:49.836 [INFO ] app.inference.worker_manager: frame 967: 1 detection(s) in 113.2 ms — etykieta 0.31
2026-05-14 15:39:50.086 [INFO ] app.inference.worker_manager: frame 968: 1 detection(s) in 140.9 ms — etykieta 0.32
2026-05-14 15:39:50.336 [INFO ] app.inference.worker_manager: frame 969: 1 detection(s) in 147.7 ms — etykieta 0.35
2026-05-14 15:39:50.586 [INFO ] app.inference.worker_manager: frame 970: 1 detection(s) in 123.4 ms — etykieta 0.29
2026-05-14 15:39:50.987 [INFO ] app.inference.worker_manager: frame 971: 1 detection(s) in 169.6 ms — etykieta 0.31
2026-05-14 15:39:51.187 [INFO ] app.inference.worker_manager: frame 972: 1 detection(s) in 113.4 ms — etykieta 0.32
2026-05-14 15:39:51.440 [INFO ] app.inference.worker_manager: frame 973: 1 detection(s) in 137.2 ms — etykieta 0.31
2026-05-14 15:39:51.787 [INFO ] app.inference.worker_manager: frame 974: 1 detection(s) in 121.9 ms — etykieta 0.35
2026-05-14 15:39:52.237 [INFO ] app.inference.worker_manager: frame 975: 1 detection(s) in 184.8 ms — etykieta 0.32
2026-05-14 15:39:52.493 [INFO ] app.inference.worker_manager: frame 976: 1 detection(s) in 125.9 ms — etykieta 0.34
2026-05-14 15:39:52.737 [INFO ] app.inference.worker_manager: frame 977: 1 detection(s) in 121.4 ms — etykieta 0.33
2026-05-14 15:39:53.036 [INFO ] app.inference.worker_manager: frame 978: 1 detection(s) in 124.5 ms — etykieta 0.35
2026-05-14 15:39:53.296 [INFO ] app.inference.worker_manager: frame 979: 1 detection(s) in 150.4 ms — etykieta 0.34
2026-05-14 15:39:53.736 [INFO ] app.inference.worker_manager: frame 980: 1 detection(s) in 141.1 ms — etykieta 0.34
2026-05-14 15:39:54.049 [INFO ] app.inference.worker_manager: frame 981: 1 detection(s) in 138.4 ms — etykieta 0.35
2026-05-14 15:39:54.387 [INFO ] app.inference.worker_manager: frame 982: 1 detection(s) in 127.2 ms — etykieta 0.34
2026-05-14 15:39:54.686 [INFO ] app.inference.worker_manager: frame 983: 1 detection(s) in 125.6 ms — etykieta 0.37
2026-05-14 15:39:54.936 [INFO ] app.inference.worker_manager: frame 984: 1 detection(s) in 154.2 ms — etykieta 0.35
2026-05-14 15:39:55.186 [INFO ] app.inference.worker_manager: frame 985: 1 detection(s) in 127.3 ms — etykieta 0.35
2026-05-14 15:39:55.486 [INFO ] app.inference.worker_manager: frame 986: 1 detection(s) in 170.5 ms — etykieta 0.35
2026-05-14 15:39:55.837 [INFO ] app.inference.worker_manager: frame 987: 1 detection(s) in 137.9 ms — etykieta 0.36
2026-05-14 15:39:56.098 [INFO ] app.inference.worker_manager: frame 988: 1 detection(s) in 146.9 ms — etykieta 0.35
2026-05-14 15:39:56.336 [INFO ] app.inference.worker_manager: frame 989: 1 detection(s) in 124.2 ms — etykieta 0.36
2026-05-14 15:39:56.538 [INFO ] app.inference.worker_manager: frame 990: 1 detection(s) in 125.4 ms — etykieta 0.37
2026-05-14 15:39:56.838 [INFO ] app.inference.worker_manager: frame 991: 1 detection(s) in 136.4 ms — etykieta 0.36
2026-05-14 15:39:57.086 [INFO ] app.inference.worker_manager: frame 992: 1 detection(s) in 151.6 ms — etykieta 0.37
2026-05-14 15:39:57.349 [INFO ] app.inference.worker_manager: frame 993: 1 detection(s) in 135.9 ms — etykieta 0.34
2026-05-14 15:39:57.642 [INFO ] app.inference.worker_manager: frame 994: 1 detection(s) in 138.6 ms — etykieta 0.36
2026-05-14 15:39:57.887 [INFO ] app.inference.worker_manager: frame 995: 1 detection(s) in 123.7 ms — etykieta 0.35
2026-05-14 15:39:58.239 [INFO ] app.inference.worker_manager: frame 996: 1 detection(s) in 158.9 ms — etykieta 0.35
2026-05-14 15:39:58.487 [INFO ] app.inference.worker_manager: frame 997: 1 detection(s) in 139.1 ms — etykieta 0.37
2026-05-14 15:39:58.736 [INFO ] app.inference.worker_manager: frame 998: 1 detection(s) in 130.6 ms — etykieta 0.36
2026-05-14 15:39:59.036 [INFO ] app.inference.worker_manager: frame 999: 1 detection(s) in 161.9 ms — etykieta 0.36
2026-05-14 15:39:59.386 [INFO ] app.inference.worker_manager: frame 1000: 1 detection(s) in 126.8 ms — etykieta 0.35
2026-05-14 15:39:59.687 [INFO ] app.inference.worker_manager: frame 1001: 1 detection(s) in 138.9 ms — etykieta 0.35
2026-05-14 15:40:00.036 [INFO ] app.inference.worker_manager: frame 1002: 1 detection(s) in 127.8 ms — etykieta 0.36
2026-05-14 15:40:00.287 [INFO ] app.inference.worker_manager: frame 1003: 1 detection(s) in 123.7 ms — etykieta 0.36
2026-05-14 15:40:00.637 [INFO ] app.inference.worker_manager: frame 1004: 1 detection(s) in 165.0 ms — etykieta 0.35
2026-05-14 15:40:00.887 [INFO ] app.inference.worker_manager: frame 1005: 1 detection(s) in 115.1 ms — etykieta 0.36
2026-05-14 15:40:01.236 [INFO ] app.inference.worker_manager: frame 1006: 1 detection(s) in 111.4 ms — etykieta 0.35
2026-05-14 15:40:01.586 [INFO ] app.inference.worker_manager: frame 1007: 1 detection(s) in 137.4 ms — etykieta 0.36
2026-05-14 15:40:01.998 [INFO ] app.inference.worker_manager: frame 1008: 1 detection(s) in 141.6 ms — etykieta 0.34
2026-05-14 15:40:02.236 [INFO ] app.inference.worker_manager: frame 1009: 1 detection(s) in 123.8 ms — etykieta 0.34
2026-05-14 15:40:02.537 [INFO ] app.inference.worker_manager: frame 1010: 1 detection(s) in 137.8 ms — etykieta 0.35
2026-05-14 15:40:02.886 [INFO ] app.inference.worker_manager: frame 1011: 1 detection(s) in 124.3 ms — etykieta 0.35
2026-05-14 15:40:03.287 [INFO ] app.inference.worker_manager: frame 1012: 1 detection(s) in 124.0 ms — etykieta 0.36
2026-05-14 15:40:03.537 [INFO ] app.inference.worker_manager: frame 1013: 1 detection(s) in 123.9 ms — etykieta 0.35
2026-05-14 15:40:03.887 [INFO ] app.inference.worker_manager: frame 1014: 1 detection(s) in 123.9 ms — etykieta 0.35
2026-05-14 15:40:04.148 [INFO ] app.inference.worker_manager: frame 1015: 1 detection(s) in 124.5 ms — etykieta 0.35
2026-05-14 15:40:04.437 [INFO ] app.inference.worker_manager: frame 1016: 1 detection(s) in 131.0 ms — etykieta 0.35
2026-05-14 15:40:04.737 [INFO ] app.inference.worker_manager: frame 1017: 1 detection(s) in 109.1 ms — etykieta 0.36
2026-05-14 15:40:04.988 [INFO ] app.inference.worker_manager: frame 1018: 1 detection(s) in 149.9 ms — etykieta 0.36
2026-05-14 15:40:05.238 [INFO ] app.inference.worker_manager: frame 1019: 1 detection(s) in 131.2 ms — etykieta 0.35
2026-05-14 15:40:05.487 [INFO ] app.inference.worker_manager: frame 1020: 1 detection(s) in 137.8 ms — etykieta 0.35
2026-05-14 15:40:05.836 [INFO ] app.inference.worker_manager: frame 1021: 1 detection(s) in 168.0 ms — etykieta 0.35
2026-05-14 15:40:06.087 [INFO ] app.inference.worker_manager: frame 1022: 1 detection(s) in 117.7 ms — etykieta 0.36
2026-05-14 15:40:06.388 [INFO ] app.inference.worker_manager: frame 1023: 1 detection(s) in 110.6 ms — etykieta 0.35
2026-05-14 15:40:06.751 [INFO ] app.inference.worker_manager: frame 1024: 1 detection(s) in 111.1 ms — etykieta 0.36
2026-05-14 15:40:06.987 [INFO ] app.inference.worker_manager: frame 1025: 1 detection(s) in 111.2 ms — etykieta 0.35
2026-05-14 15:40:07.288 [INFO ] app.inference.worker_manager: frame 1026: 1 detection(s) in 131.4 ms — etykieta 0.35
2026-05-14 15:40:07.588 [INFO ] app.inference.worker_manager: frame 1027: 1 detection(s) in 124.3 ms — etykieta 0.35
2026-05-14 15:40:07.987 [INFO ] app.inference.worker_manager: frame 1028: 1 detection(s) in 127.7 ms — etykieta 0.36
2026-05-14 15:40:08.237 [INFO ] app.inference.worker_manager: frame 1029: 1 detection(s) in 128.4 ms — etykieta 0.34
2026-05-14 15:40:08.488 [INFO ] app.inference.worker_manager: frame 1030: 1 detection(s) in 125.1 ms — etykieta 0.35
2026-05-14 15:40:08.787 [INFO ] app.inference.worker_manager: frame 1031: 1 detection(s) in 138.6 ms — etykieta 0.35
2026-05-14 15:40:09.087 [INFO ] app.inference.worker_manager: frame 1032: 1 detection(s) in 149.5 ms — etykieta 0.36
2026-05-14 15:40:09.337 [INFO ] app.inference.worker_manager: frame 1033: 1 detection(s) in 136.1 ms — etykieta 0.35
2026-05-14 15:40:09.637 [INFO ] app.inference.worker_manager: frame 1034: 1 detection(s) in 126.1 ms — etykieta 0.35
2026-05-14 15:40:09.937 [INFO ] app.inference.worker_manager: frame 1035: 1 detection(s) in 122.9 ms — etykieta 0.36
2026-05-14 15:40:10.192 [INFO ] app.inference.worker_manager: frame 1036: 1 detection(s) in 146.5 ms — etykieta 0.35
2026-05-14 15:40:10.443 [INFO ] app.inference.worker_manager: frame 1037: 1 detection(s) in 129.1 ms — etykieta 0.35
2026-05-14 15:40:10.687 [INFO ] app.inference.worker_manager: frame 1038: 1 detection(s) in 132.8 ms — etykieta 0.34
2026-05-14 15:40:10.988 [INFO ] app.inference.worker_manager: frame 1039: 1 detection(s) in 110.2 ms — etykieta 0.36
2026-05-14 15:40:11.238 [INFO ] app.inference.worker_manager: frame 1040: 1 detection(s) in 156.5 ms — etykieta 0.36
2026-05-14 15:40:11.587 [INFO ] app.inference.worker_manager: frame 1041: 1 detection(s) in 129.6 ms — etykieta 0.36
2026-05-14 15:40:11.906 [INFO ] app.inference.worker_manager: frame 1042: 1 detection(s) in 138.4 ms — etykieta 0.35
2026-05-14 15:40:12.237 [INFO ] app.inference.worker_manager: frame 1043: 1 detection(s) in 130.7 ms — etykieta 0.35
2026-05-14 15:40:12.537 [INFO ] app.inference.worker_manager: frame 1044: 1 detection(s) in 124.4 ms — etykieta 0.36
2026-05-14 15:40:12.787 [INFO ] app.inference.worker_manager: frame 1045: 1 detection(s) in 133.4 ms — etykieta 0.36
2026-05-14 15:40:13.038 [INFO ] app.inference.worker_manager: frame 1046: 1 detection(s) in 124.5 ms — etykieta 0.35
2026-05-14 15:40:13.488 [INFO ] app.inference.worker_manager: frame 1047: 1 detection(s) in 122.1 ms — etykieta 0.36
2026-05-14 15:40:13.838 [INFO ] app.inference.worker_manager: frame 1048: 1 detection(s) in 134.4 ms — etykieta 0.36
2026-05-14 15:40:14.088 [INFO ] app.inference.worker_manager: frame 1049: 1 detection(s) in 132.8 ms — etykieta 0.36
2026-05-14 15:40:14.687 [INFO ] app.inference.worker_manager: frame 1051: 1 detection(s) in 147.8 ms — etykieta 0.28
2026-05-14 15:40:14.937 [INFO ] app.inference.worker_manager: frame 1052: 1 detection(s) in 127.2 ms — etykieta 0.34
2026-05-14 15:40:15.187 [INFO ] app.inference.worker_manager: frame 1053: 1 detection(s) in 108.0 ms — etykieta 0.34
2026-05-14 15:40:15.541 [INFO ] app.inference.worker_manager: frame 1054: 1 detection(s) in 175.0 ms — etykieta 0.37
2026-05-14 15:40:15.792 [INFO ] app.inference.worker_manager: frame 1055: 1 detection(s) in 114.8 ms — etykieta 0.35
2026-05-14 15:40:16.238 [INFO ] app.inference.worker_manager: frame 1056: 1 detection(s) in 136.5 ms — etykieta 0.35
2026-05-14 15:40:16.587 [INFO ] app.inference.worker_manager: frame 1057: 1 detection(s) in 128.7 ms — etykieta 0.34
2026-05-14 15:40:16.838 [INFO ] app.inference.worker_manager: frame 1058: 1 detection(s) in 135.7 ms — etykieta 0.31
2026-05-14 15:40:17.149 [INFO ] app.inference.worker_manager: frame 1059: 1 detection(s) in 139.1 ms — etykieta 0.32
2026-05-14 15:40:17.438 [INFO ] app.inference.worker_manager: frame 1060: 1 detection(s) in 124.6 ms — etykieta 0.35
2026-05-14 15:40:17.687 [INFO ] app.inference.worker_manager: frame 1061: 1 detection(s) in 137.5 ms — etykieta 0.35
2026-05-14 15:40:17.937 [INFO ] app.inference.worker_manager: frame 1062: 1 detection(s) in 131.9 ms — etykieta 0.34
2026-05-14 15:40:18.337 [INFO ] app.inference.worker_manager: frame 1063: 1 detection(s) in 121.1 ms — etykieta 0.33
2026-05-14 15:41:00.160 [DEBUG ] app.inference.worker_manager: InferenceManager: paused
2026-05-14 15:41:00.160 [DEBUG ] app.pipeline.frame_dispatcher: Subscriber not found for removal: <bound method InferenceManager.submit_frame of <app.inference.worker_manager.InferenceManager(0x7f7f24149f40) at 0x203728880>>
2026-05-14 15:41:00.160 [INFO ] app.ui.main_window: Inference disabled
2026-05-14 15:41:25.791 [DEBUG ] app.ui.menu_bar: Format selected: 1280x720 @ 30 fps (NV12)
2026-05-14 15:41:25.792 [INFO ] app.ui.main_window: Format selected via menu: 1280x720 @ 30 fps (NV12)
2026-05-14 15:41:25.792 [INFO ] app.camera.camera_service: Format change: 1280x720 @ 30 fps (NV12) — restarting camera
2026-05-14 15:41:25.842 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 15:41:25.842 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 15:41:25.872 [INFO ] app.camera.camera_service: Camera format requested: NV12 1280x720 @ 30 fps
2026-05-14 15:41:25.949 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 15:41:25.950 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1280x720 @ 30 fps
2026-05-14 15:41:25.950 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 15:41:25.950 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 15:41:37.990 [DEBUG ] app.ui.menu_bar: Format selected: 1600x1200 @ 30 fps (NV12)
2026-05-14 15:41:37.990 [INFO ] app.ui.main_window: Format selected via menu: 1600x1200 @ 30 fps (NV12)
2026-05-14 15:41:37.990 [INFO ] app.camera.camera_service: Format change: 1600x1200 @ 30 fps (NV12) — restarting camera
2026-05-14 15:41:38.035 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 15:41:38.036 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 15:41:38.063 [INFO ] app.camera.camera_service: Camera format requested: NV12 1600x1200 @ 30 fps
2026-05-14 15:41:38.144 [INFO ] app.camera.camera_service: Camera active: HD USB CAMERA
2026-05-14 15:41:38.144 [INFO ] app.camera.camera_service: Camera format ACTUAL: NV12 1600x1200 @ 30 fps
2026-05-14 15:41:38.144 [INFO ] app.ui.main_window: Camera streaming: HD USB CAMERA
2026-05-14 15:41:38.144 [INFO ] app.camera.camera_service: Camera start requested: HD USB CAMERA
2026-05-14 15:41:52.964 [INFO ] app.inference.worker_manager: Inference worker stopped
2026-05-14 15:41:53.017 [INFO ] app.camera.camera_service: Camera inactive
2026-05-14 15:41:53.017 [DEBUG ] app.camera.camera_service: Camera stopped (internal)
2026-05-14 15:41:53.017 [INFO ] app.ui.main_window: CSV telemetry: 91 rows written

1
models/.gitkeep Normal file
View File

@@ -0,0 +1 @@

BIN
models/best_v1.pt Normal file

Binary file not shown.

324
notes/01-mvp-plan.md Normal file
View File

@@ -0,0 +1,324 @@
# Plan działania — MVP Camera Preview (PySide6)
## Środowisko
| Element | Wartość |
|---|---|
| Python | 3.12.10 (venv: `.venv-win`) |
| Framework GUI | PySide6 6.11.0 |
| Dev platform | Windows 11 |
| Target platform | Mac Mini (Intel i7, macOS Ventura) |
| Kamera docelowa | ELP USB Camera |
| Narzędzia | pytest, ruff, colorama |
---
## Fazy realizacji
### Faza 0 — Projekt i scaffolding
Cel: ustalenie struktury katalogów i modułów przed napisaniem pierwszej linii logiki.
#### 0.1 Struktura projektu
```
duck-preview2/
├── app/
│ ├── __init__.py
│ ├── main.py # entry point
│ ├── config.py # stałe, domyślne ustawienia
│ ├── camera/
│ │ ├── __init__.py
│ │ ├── camera_service.py # QCamera + QMediaCaptureSession
│ │ └── camera_enumerator.py # wykrywanie dostępnych kamer
│ ├── pipeline/
│ │ ├── __init__.py
│ │ └── frame_dispatcher.py # dystrybucja klatek do subskrybentów
│ ├── telemetry/
│ │ ├── __init__.py
│ │ └── telemetry_collector.py # zbieranie metryk FPS/frame time/CPU
│ ├── overlay/
│ │ ├── __init__.py
│ │ └── overlay_widget.py # przezroczysta warstwa QWidget
│ └── ui/
│ ├── __init__.py
│ ├── main_window.py # główne okno aplikacji
│ └── menu_bar.py # menu: kamera, rozdzielczość, FPS, debug
├── tests/
│ ├── __init__.py
│ ├── test_camera_enumerator.py
│ └── test_telemetry_collector.py
├── notes/
├── requirements.txt
├── requirements-dev.txt
└── pyproject.toml # konfiguracja ruff + pytest
```
#### 0.2 Pliki konfiguracyjne
- `pyproject.toml` — konfiguracja ruff (linter/formatter) i pytest
- `requirements.txt` — zależności produkcyjne (PySide6)
- `requirements-dev.txt` — zależności deweloperskie (pytest, ruff)
- `.gitignore` — aktualizacja o artefakty Pythona
---
### Faza 1 — Camera Service
Cel: stabilne pobranie obrazu z kamery przez QtMultimedia.
#### 1.1 Camera Enumerator
- `QMediaDevices.videoInputs()` — lista dostępnych kamer
- Zwraca listę `QCameraDevice` z nazwą, id i obsługiwanymi formatami
- Obsługa braku kamer (komunikat, nie crash)
- Test jednostkowy: mockowanie `QMediaDevices`
#### 1.2 Camera Service
- Opakowuje `QCamera` + `QMediaCaptureSession`
- API:
- `start(device: QCameraDevice)` — uruchamia kamerę
- `stop()` — zatrzymuje kamerę
- `set_resolution(width, height)` — ustawia format
- `set_fps(fps)` — ustawia docelowy FPS
- `reconnect()` — restart po błędzie
- `QVideoSink` jako punkt odbioru klatek
- Sygnał `frame_ready(QVideoFrame)` do Frame Dispatcher
- Obsługa błędów kamery (`QCamera.errorOccurred`)
#### 1.3 Uwagi platformowe
| Aspekt | Windows 11 (dev) | macOS Ventura (target) |
|---|---|---|
| Backend | DirectShow / Media Foundation | AVFoundation |
| Kamera ELP | USB, standardowy UVC driver | USB, UVC |
| Format klatek | YUYV / MJPEG | YUYV / MJPEG |
| GPU rendering | ANGLE (OpenGL ES) | Metal |
---
### Faza 2 — Frame Dispatcher
Cel: dystrybucja klatek do wielu odbiorców bez blokowania akwizycji.
#### 2.1 Frame Dispatcher
- Wzorzec: publish-subscribe (lista callbacków)
- `subscribe(callback: Callable[[QVideoFrame], None])`
- `unsubscribe(callback)`
- `dispatch(frame: QVideoFrame)` — wywołuje wszystkich subskrybentów
- Klatki NIE są kopiowane — subskrybenci działają na referencji
- Subskrybenci mogą **pominąć klatkę** (tryb drop-if-busy)
- Wywołanie `dispatch` następuje w wątku GUI (slot połączony z `frame_ready`)
#### 2.2 Subskrybenci w Fazie 1
| Subskrybent | Działanie |
|---|---|
| Video Renderer | przekazuje klatkę do `QVideoSink` / `QVideoWidget` |
| Telemetry Collector | mierzy czas, zlicza klatki |
---
### Faza 3 — Video Renderer
Cel: renderowanie klatki w GUI bez zbędnych kopii.
#### 3.1 Podejście
- `QVideoWidget` jako główny widget podglądu
- `QMediaCaptureSession.setVideoOutput(QVideoWidget)` — ścieżka bezpośrednia, zero kopii
- Alternatywnie: `QVideoSink``QGraphicsVideoItem` dla przyszłych overlayów
- Domyślnie: `QVideoWidget` (prosta, niska latencja)
#### 3.2 Wymagania
- Preview nie blokuje wątku GUI
- Obsługa aspect ratio (letter/pillarbox)
- Resize okna bez migotania
---
### Faza 4 — Telemetry Collector
Cel: dokładne metryki pipeline'u wideo.
#### 4.1 Zbierane metryki
| Metryka | Metoda pomiaru |
|---|---|
| Realtime FPS | licznik klatek / okno 1 s |
| Frame time | `time.perf_counter()` między klatkami |
| Frame acquisition time | timestamp wejście frame_ready → dispatch |
| Rendering time | czas `QVideoWidget.update()` (opcjonalnie) |
| Dropped frames | detekcja przez numerację lub timestamp gap |
| CPU usage | `psutil.cpu_percent()` (dodać do requirements) |
| Memory usage | `psutil.virtual_memory()` (opcjonalnie) |
#### 4.2 API
- `TelemetryCollector` — subskrybent Frame Dispatcher
- `on_frame(frame: QVideoFrame)` — rejestruje timestamp klatki
- `get_snapshot() -> TelemetrySnapshot` — aktualny stan metryk (dataclass)
- `update_interval_ms: int` — jak często odświeżać snapshot (domyślnie 500 ms)
- Sygnał `metrics_updated(TelemetrySnapshot)` — emitowany co `update_interval_ms`
#### 4.3 TelemetrySnapshot (dataclass)
```python
@dataclass
class TelemetrySnapshot:
fps: float
frame_time_ms: float
dropped_frames: int
cpu_percent: float
memory_mb: float | None
timestamp: float
```
---
### Faza 5 — Overlay System
Cel: wyświetlanie metryk na przezroczystej warstwie nad podglądem.
#### 5.1 Architektura
- `OverlayWidget(QWidget)` — przezroczysty widget (`WA_TransparentForMouseEvents`)
- Pozycjonowany absolutnie nad `QVideoWidget` (ten sam parent, wyższy z-index)
- `paintEvent` rysuje semi-przezroczysty prostokąt + tekst z metrykami
- Połączony z sygnałem `metrics_updated` — odświeża tylko gdy dane się zmienią
#### 5.2 Zawartość overlaya (MVP)
```
FPS: 60.0
Frame: 16.7 ms
Drop: 0
CPU: 12.3 %
```
#### 5.3 Sterowalność
- Widoczność overlaya: toggle przez menu Debug
- Pozycja: lewy górny róg (stała w MVP)
- Kolor tła: `rgba(0, 0, 0, 160)`
---
### Faza 6 — GUI / Main Window
Cel: minimalne, funkcjonalne okno aplikacji.
#### 6.1 MainWindow
- `QMainWindow` z `QVideoWidget` jako central widget
- `OverlayWidget` nałożony na video
- Obsługa resize → reposition overlay
- Tytuł okna: `Duck Preview`
#### 6.2 MenuBar
Menu **Camera**:
- Lista wykrytych kamer (radio-style)
- Separator
- Reconnect
Menu **Video**:
- Resolution submenu (pobierane dynamicznie z `QCameraDevice.videoFormats()`)
- FPS submenu
Menu **Debug**:
- Toggle overlay metryk
- Logowanie do konsoli (toggle)
#### 6.3 Startup flow
```
main.py
→ QApplication
→ CameraEnumerator.list_cameras()
→ MainWindow(cameras)
→ CameraService.start(cameras[0]) # pierwsza kamera lub ELP
→ FrameDispatcher.subscribe(telemetry, renderer)
→ app.exec()
```
---
### Faza 7 — Testy i walidacja
#### 7.1 Testy jednostkowe
| Moduł | Co testować |
|---|---|
| `CameraEnumerator` | lista kamer, brak kamer, format danych |
| `TelemetryCollector` | obliczenia FPS, wykrywanie dropów |
| `FrameDispatcher` | subskrypcja, odsubskrypcja, dispatch |
| `TelemetrySnapshot` | poprawność dataclass |
#### 7.2 Testy manualne (Windows dev)
- [ ] Uruchomienie z kamerą laptopa / USB webcam
- [ ] Przełączanie kamer
- [ ] Zmiana rozdzielczości
- [ ] Zmiana FPS
- [ ] Toggle overlay
- [ ] Reconnect po odłączeniu kamery
#### 7.3 Testy na Mac Mini (target)
- [ ] Wykrycie kamery ELP
- [ ] Poprawny format YUYV/MJPEG
- [ ] Wydajność AVFoundation vs DirectShow
- [ ] GPU rendering przez Metal
#### 7.4 Kryteria sukcesu (z PRD)
- Preview stabilny i płynny
- Latencja renderowania niska
- Dane telemetrii dokładne
- GUI responsywne
- Overlay działa poprawnie
- Architektura gotowa na subskrybentów AI
---
## Kolejność implementacji (sprint order)
```
Sprint 1: Faza 0 — scaffolding, pyproject.toml, requirements
Sprint 2: Faza 1 — CameraEnumerator + CameraService (bez GUI)
Sprint 3: Faza 3 — VideoRenderer + MainWindow (preview działa)
Sprint 4: Faza 2 — FrameDispatcher (refactor pipeline)
Sprint 5: Faza 4 — TelemetryCollector
Sprint 6: Faza 5 — OverlayWidget
Sprint 7: Faza 6 — MenuBar (camera/resolution/fps switch)
Sprint 8: Faza 7 — Testy, poprawki, walidacja na Mac Mini
```
---
## Zależności do dodania
```
# requirements.txt
PySide6>=6.7
psutil>=6.0
# requirements-dev.txt
pytest>=8.0
ruff>=0.4
```
---
## Uwagi cross-platform
1. **ELP camera** — kamera UVC, powinna działać bez dodatkowych sterowników na obu platformach. Sprawdzić obsługiwane rozdzielczości i FPS przez `QCameraDevice.videoFormats()`.
2. **Ścieżki absolutne** — unikać `os.path` na korzyść `pathlib.Path`.
3. **Threading** — wszystkie operacje Qt muszą odbywać się w wątku GUI. `TelemetryCollector` może używać `QTimer` zamiast osobnego wątku.
4. **Format klatek** — na macOS AVFoundation preferuje `BGRA` lub `NV12`. Konwersja powinna być leniwa i tylko gdy potrzebna (nie w hot path renderowania).
5. **High DPI** — włączyć `QApplication.setHighDpiScaleFactorRoundingPolicy` dla konsistencji Windows/Mac.
6. **Testowanie bez kamery**`CameraEnumerator` powinien umożliwiać dependency injection / mock dla środowisk CI.

View File

@@ -253,6 +253,7 @@ Architecture must support future additions:
* snapshots,
* streaming,
* remote sinks.
* play video files
Without major redesign.

194
notes/02-mvp-app.md Normal file
View File

@@ -0,0 +1,194 @@
# Stan aplikacji — MVP Camera Preview
Data: 2026-05-12
Środowisko dev: Windows 11, Python 3.12.10, PySide6 6.11.0
Środowisko docelowe: Mac Mini (Intel i7, macOS Ventura), kamera ELP USB
---
## Zaimplementowane moduły
### Struktura projektu
```
duck-preview2/
├── app/
│ ├── config.py # stałe i domyślne ustawienia
│ ├── main.py # entry point (python -m app.main)
│ ├── camera/
│ │ ├── camera_enumerator.py # wykrywanie kamer przez QMediaDevices
│ │ └── camera_service.py # zarządzanie QCamera + QMediaCaptureSession
│ ├── pipeline/
│ │ └── frame_dispatcher.py # pub/sub dystrybucja QVideoFrame
│ ├── telemetry/
│ │ └── telemetry_collector.py # pomiar FPS, frame time, CPU, pamięci
│ ├── overlay/
│ │ ├── overlay_layer.py # interfejs IOverlayLayer (ABC)
│ │ └── telemetry_overlay.py # implementacja — box z metrykami
│ └── ui/
│ ├── camera_view.py # CameraView — render + kompozycja overlayów
│ ├── main_window.py # główne okno, wiring komponentów
│ └── menu_bar.py # menu: Camera / Video / Debug
├── tests/
│ ├── test_frame_dispatcher.py # 8 testów jednostkowych
│ └── test_telemetry_collector.py # 7 testów jednostkowych
├── pyproject.toml # konfiguracja pytest + ruff
├── requirements.txt # PySide6, psutil
└── requirements-dev.txt # + pytest, ruff
```
### Opis komponentów
#### CameraEnumerator (`app/camera/camera_enumerator.py`)
- Wykrywa dostępne kamery przez `QMediaDevices.videoInputs()`
- Zwraca listę `CameraInfo` z nazwą, id i listą obsługiwanych formatów (rozdzielczość × FPS)
- Formaty deduplikowane i posortowane (największa rozdzielczość pierwsza)
- Obsługa braku kamer bez crasha
#### CameraService (`app/camera/camera_service.py`)
- Opakowuje `QCamera` + `QMediaCaptureSession` + `QVideoSink`
- API: `start(CameraInfo)`, `stop()`, `reconnect()`, `set_resolution()`, `set_fps()`
- Algorytm doboru formatu: score-based (priorytet: dopasowanie rozdzielczości, potem FPS)
- Sygnały: `frame_ready(QVideoFrame)`, `camera_started`, `camera_stopped`, `camera_error`
#### FrameDispatcher (`app/pipeline/frame_dispatcher.py`)
- Pub/sub: `subscribe(callback, drop_if_busy=True)` / `unsubscribe(callback)`
- `drop_if_busy=True` — klatka pomijana jeśli subskrybent jest zajęty (render)
- `drop_if_busy=False` — każda klatka dociera (telemetria)
- Odporny na wyjątki w subskrybentach (jeden nie blokuje pozostałych)
#### TelemetryCollector (`app/telemetry/telemetry_collector.py`)
- Subskrybuje każdą klatkę (`drop_if_busy=False`)
- Mierzy: FPS (okno 1 s), średni frame time (ring-buffer 120 próbek), dropped frames (heurystyka 2.5× avg)
- CPU: `psutil.Process.cpu_percent()` — tylko nasz proces, inicjalizowany w `__init__` (warmup)
- RAM: `memory_info().wset` (Windows private working set) lub `rss` (macOS/Linux)
- Emituje `metrics_updated(TelemetrySnapshot)` co 500 ms przez `QTimer`
#### IOverlayLayer (`app/overlay/overlay_layer.py`)
- Abstrakcja (ABC) dla pluggable overlayów
- Interface: `paint(painter: QPainter, video_rect: QRect)`, `visible: bool`, `name: str`
- Nowe overlaye nie wymagają modyfikacji żadnego istniejącego kodu
#### TelemetryOverlay (`app/overlay/telemetry_overlay.py`)
- Implementacja `IOverlayLayer`
- Rysuje semi-przezroczysty box z metrykami w lewym górnym rogu video
- Slot `on_metrics_updated(TelemetrySnapshot)` — odbiera dane z TelemetryCollector
- Format: FPS, Frame time, Drop count, CPU %, Mem MB
#### CameraView (`app/ui/camera_view.py`)
- Zwykły `QWidget` (nie `QVideoWidget`) — render przez `QPainter` w `paintEvent`
- Odbiera `QVideoFrame` przez slot `on_frame()`, konwertuje do `QImage.Format_RGB32`
- Letterboxing z zachowaniem aspect ratio
- Rejestr overlayów: `add_overlay_layer()`, `remove_overlay_layer()`, `set_all_overlays_visible()`
- W `paintEvent`: rysuje klatkę → iteruje po warstwach, każda dostaje `painter.save()/restore()`
#### MainWindow (`app/ui/main_window.py`)
- Wires together wszystkie komponenty
- Minimalny status bar (nazwa kamery, błędy)
- `closeEvent``CameraService.stop()`
#### AppMenuBar (`app/ui/menu_bar.py`)
- Menu **Camera**: lista wykrytych kamer (radio), Reconnect
- Menu **Video**: Resolution submenu, FPS submenu (pobierane z `QCameraDevice.videoFormats()`)
- Menu **Debug**: Show Overlay (toggle), Console Logging (toggle poziom logowania)
---
## Co działało na Windows 11 (dev)
- Wykrycie wbudowanej kamery laptopa
- Podgląd w czasie rzeczywistym
- Przełączanie rozdzielczości i FPS z menu
- Overlay z metrykami widoczny i aktualizowany
- Toggle overlay przez Debug menu
- Logowanie do konsoli przez Debug menu
- Reconnect po zmianie kamery
- Letterboxing przy resize okna
---
## Co próbowaliśmy i nie wyszło
### QVideoWidget jako renderer (porzucone)
**Podejście:** Użycie `QVideoWidget` jako centralnego widgetu + nałożenie `OverlayWidget` (child lub sibling).
**Problem:** Na Windows `QVideoWidget` tworzy natywne okno HWND z powierzchnią D3D (Media Foundation backend). Natywna powierzchnia jest rysowana poza hierarchią Qt i zawsze przykrywa wszystkie `QWidget` — niezależnie od:
- kolejności z-order (`raise_()`)
- rodzica (child/sibling)
- flag okna
- `WA_TranslucentBackground` / `WA_NoSystemBackground`
Żaden `QWidget` nie może się wyświetlić nad `QVideoWidget` na Windows.
**Próby ratowania:**
1. `OverlayWidget` jako child `QVideoWidget` — zasłonięty przez natywną powierzchnię
2. Kontener `QWidget` z `QVideoWidget` i `OverlayWidget` jako rodzeństwo — nadal zasłonięty
3. `setWindowFlags(FramelessWindowHint)` na child widget — odrywa widget od rodzica, tworzy osobne (niewidoczne) okno top-level
**Rozwiązanie:** Porzucenie `QVideoWidget`. Własny `CameraView(QWidget)` odbiera klatki przez `QVideoSink`, konwertuje do `QImage` i rysuje przez `QPainter`. Overlay w tym samym `paintEvent` — brak konfliktu z natywnym renderowaniem.
### OverlayWidget jako osobny QWidget (porzucone)
**Podejście:** `OverlayWidget` z `WA_TransparentForMouseEvents` i `WA_TranslucentBackground` nałożony na video.
**Problem:** Poza konfliktem z `QVideoWidget`, `WA_TranslucentBackground` na child widget działa tylko gdy rodzic też jest transparentny — Qt nie komponuje dziecka z tłem rodzica innym niż własne. W praktyce overlay był niewidoczny lub zasłaniał video czarnym prostokątem.
---
## Znane ograniczenia i uwagi
### Pomiar CPU
`psutil.Process.cpu_percent()` zwraca procent **względem jednego rdzenia** (np. 50% = pół rdzenia). Na wielordzeniowym procesorze 15% w Task Managerze (uśrednione po rdzeniach) może odpowiadać 50% na jednym rdzeniu. To nie jest błąd — to różna metodologia. Task Manager pokazuje `total_cpu / num_cores`, psutil pokazuje `cpu_time / wall_time`.
Jeśli potrzebny jest widok "jak Task Manager": `process.cpu_percent() / psutil.cpu_count()`.
### Pomiar RAM
`memory_info().wset` (Windows) = Private Working Set = to co Task Manager pokazuje w kolumnie "Pamięć". RSS zawiera też współdzielone biblioteki (Qt DLLs ~40 MB) i dlatego było zawyżone. Na macOS używane jest `rss` (tam `wset` nie istnieje).
### Wydajność konwersji klatek
`QVideoFrame.toImage()` + `convertToFormat(RGB32)` wykonuje się na CPU. Przy 1080p60 to koszt ~1-3 ms/klatkę. Dla MVP akceptowalny. Przy przyszłej integracji YOLO warto rozważyć bezpośredni dostęp do danych przez `QVideoFrame.map()` i przekazywanie raw bufora do GPU.
### Brak testów integracyjnych
Testy jednostkowe pokrywają `FrameDispatcher` i `TelemetryCollector` w izolacji (bez Qt event loop). Brak testów `CameraView`, `CameraService` i `MainWindow` — wymagałyby `QApplication` i mockowania urządzeń.
---
## Stan zgodności z PRD (01-mvp-preview.md)
| Wymaganie | Status |
|---|---|
| Realtime camera preview | Zaimplementowane |
| Camera switching | Zaimplementowane |
| Resolution selection | Zaimplementowane |
| FPS selection | Zaimplementowane |
| Reconnect/restart | Zaimplementowane |
| Realtime FPS metric | Zaimplementowane |
| Frame time metric | Zaimplementowane |
| Dropped frames detection | Zaimplementowane (heurystyka) |
| CPU usage metric | Zaimplementowane |
| Memory usage metric | Zaimplementowane |
| Overlay system | Zaimplementowane (IOverlayLayer) |
| Performance metrics display | Zaimplementowane (TelemetryOverlay) |
| Minimal GUI | Zaimplementowane |
| Camera menu | Zaimplementowane |
| Resolution/FPS menu | Zaimplementowane |
| Debug/telemetry options | Zaimplementowane |
| Architecture ready for AI subscribers | Zaimplementowane (IOverlayLayer + FrameDispatcher) |
| Low latency preview | Zaimplementowane (drop-if-busy w dispatcher) |
| Non-blocking GUI thread | Zaimplementowane |
---
## Następne kroki (poza MVP)
- Walidacja na Mac Mini z kamerą ELP (AVFoundation backend, macOS Ventura)
- Snapshot / recording
- Integracja YOLO: `YoloBboxOverlay(IOverlayLayer)` + worker w osobnym procesie
- Integracja OCR
- Optymalizacja konwersji klatek (QVideoFrame.map() → numpy → GPU)
- Testy integracyjne z QApplication + mock camera

480
notes/03-mvp-summary.md Normal file
View File

@@ -0,0 +1,480 @@
# Goal
Zbudować aplikację do podglądu kamery w czasie rzeczywistym (PySide6) z telemetrią, overlayami i systemem logowania do pliku — docelowo na Mac Mini z kamerą ELP.
---
# Constraints & Preferences
- Python 3.12.10, venv w `.venv-win`
- Dev: Windows 11
- Target: Mac Mini Intel i7, macOS Ventura, kamera ELP USB
- `PySide6 6.11.0`, `psutil`, `pytest`, `ruff`
- `QVideoWidget` porzucony — na Windows zasłania wszystkie child/sibling widgets przez natywny HWND D3D
- Logi:
- `logs/` w katalogu projektu
- nowy plik z timestamp per sesja
- max 20 plików
- CSV telemetrii:
- co 5 sekund
- `flush` po każdym wierszu
- CPU:
- pokazywać oba:
- sys %
- per-core % (jak Task Manager)
- Zmiana formatu kamery przez `stop+start`
- nie `setCameraFormat` na żywej kamerze
---
# Progress
## Done
### Faza 07 MVP
- scaffolding
- `CameraService`
- `FrameDispatcher`
- `TelemetryCollector`
- `CameraView`
- `TelemetryOverlay`
- `IOverlayLayer`
- `AppMenuBar`
- `MainWindow`
### Rendering
- Usunięto `QVideoWidget`
- `CameraView(QWidget)` z `paintEvent`
- render + overlay w jednym przejściu
### Telemetria
`TelemetrySnapshot` — pola:
- `fps`
- `target_fps`
- `frame_time_ms`
- `dropped_frames`
- `cpu_percent_sys`
- `cpu_percent_core`
- `memory_mb`
### Kamera
- `CameraService.format_changed(float)` sygnał
- `stop+start` przy:
- `set_resolution`
- `set_fps`
### Logowanie
- `_log_actual_format()`
- loguje rzeczywisty format po starcie kamery
- warning jeśli FPS się nie zgadza
### CameraFormat
`CameraFormat` dataclass z:
```python
pixel_format: str
```
Przykłady:
- `MJPG`
- `YUY2`
- `NV12`
w `camera_enumerator.py`
### Logging
`logging_setup.py`
- `FileHandler`
- `DEBUG`
- zawsze aktywny
- `StreamHandler`
- `WARNING`
- przełączalny
- nagłówek sesji:
- wersja
- platforma
- Python
- PySide6
- CPU
- RAM
- pruning starych logów
### CSV
`csv_logger.py`
- `CsvTelemetryLogger`
- throttling:
- 5 s
- `flush` po każdym wierszu
### Menu
`menu_bar.py`
- dostosowany do `CameraFormat`
- `set_log_file_path()`
- `set_console_level()`
### Config
`config.py`
- `LOG_DIR`
- `MAX_LOG_FILES`
- `TELEMETRY_CSV_INTERVAL_S`
### Main
`main.py`
- wywołuje `setup_logging()`
- przekazuje `log_path` do `MainWindow`
### Main Window
`main_window.py`
- przyjmuje:
```python
log_path: Path | None
```
- tworzy:
```python
CsvTelemetryLogger(log_path.with_suffix(".csv"))
```
- podpina do:
- `telemetry.metrics_updated`
- wywołuje:
- `menu.set_log_file_path()`
- `csv_logger.close()` w `closeEvent`
### Repo
`.gitignore`
- dodano `logs/`
### Testy i jakość
- 20 testów jednostkowych
- wszystkie zielone
- `ruff`
- czysty
- wszystkie błędy naprawione
### Artefakty sesji
Para plików per sesja:
```text
logs/duck-preview_<timestamp>.log
logs/duck-preview_<timestamp>.csv
```
---
## In Progress
- (none)
---
## Blocked
- (none)
---
# Key Decisions
## Rendering
### `QVideoWidget` → własny `CameraView(QWidget)`
Jedyny skuteczny sposób obejścia problemu z natywnym HWND na Windows.
## Overlay Architecture
### `IOverlayLayer` ABC
Pluggable overlaye bez modyfikacji `CameraView`.
Gotowe pod:
- YOLO
- OCR
- inne overlaye
## Pipeline
### `FrameDispatcher`
Pub/sub z `drop_if_busy`:
- render może gubić klatki
- telemetria nigdy
## CPU Metrics
- CPU per-core (`psutil`)
- dzielone przez `cpu_count`
- wynik zgodny z Task Manager
## Memory Metrics
- `memory_info().wset` (Windows)
- zamiast `rss`
- odpowiada `Private Working Set`
## Logging Strategy
Dwa osobne pliki per sesja:
- `.log` — diagnostyka
- `.csv` — metryki szeregów czasowych
## CameraFormat
`CameraFormat` dataclass zamiast krotki:
- niesie `pixel_format`
- potrzebny w logach
---
# Next Steps
1. Przetestować na Windows:
- czy logi powstają w `logs/`
- czy CSV zapisuje dane co ~5 s
2. Przenieść na Mac Mini i przetestować z kamerą ELP:
- sprawdzić `pixel_format` w logu
- sprawdzić wykrycie backendu AVFoundation
3. Zweryfikować overlay telemetrii na żywym obrazie
4. (Opcjonalnie) dodać kolejne `IOverlayLayer`
- YOLO
- OCR
---
# Critical Context
## Camera Enumerator
`CameraEnumerator.list_cameras()` zwraca:
```python
list[CameraInfo]
```
gdzie:
```python
formats: list[CameraFormat]
```
`menu_bar.py` i `camera_service.py` iterują po:
- `fmt.width`
- `fmt.height`
- `fmt.max_fps`
---
## CSV Logger
Ścieżka CSV:
```python
log_path.with_suffix(".csv")
```
Powstaje para:
- `.log`
- `.csv`
z tym samym timestamp.
---
## Console Logging
```python
logging_setup.set_console_level(debug: bool)
```
wywoływane z:
```python
menu_bar._on_log_toggled
```
---
## Pixel Format
```python
pixel_format_name()
```
eksportowane z:
```python
camera_enumerator.py
```
używane również w:
```python
camera_service.py
```
---
## Log Header
Nagłówek logu zawiera:
- wersję aplikacji
- platformę
- Python
- PySide6
- liczbę CPU
- RAM
---
## Uruchamianie
```bash
.venv-win\Scripts\python.exe -m app.main
```
---
## Ruff Fixes
Naprawiono:
- `E501`
- długa linia w `camera_enumerator.py`
- `F401`
- `I001`
- w `camera_service.py`
- w `main.py`
---
# Relevant Files
## Entry Point
- `app/main.py`
- wywołuje `setup_logging()`
- przekazuje `log_path`
## Config
- `app/config.py`
- `LOG_DIR`
- `MAX_LOG_FILES`
- `TELEMETRY_CSV_INTERVAL_S`
## Logging
- `app/logging_setup.py`
- konfiguracja logowania
- nagłówek sesji
- pruning
## Telemetry
- `app/telemetry/csv_logger.py`
- zapis CSV z throttlingiem
- `app/telemetry/telemetry_collector.py`
- `TelemetrySnapshot`
- `TelemetryCollector`
## Camera
- `app/camera/camera_enumerator.py`
- `CameraFormat`
- `CameraInfo`
- `pixel_format_name()`
- `app/camera/camera_service.py`
- `CameraService`
- `format_changed`
- `_log_actual_format()`
## UI
- `app/ui/main_window.py`
- kompletny
- podpięty `CsvTelemetryLogger`
- `app/ui/menu_bar.py`
- `set_log_file_path()`
- `set_console_level()`
- `app/ui/camera_view.py`
- `CameraView`
- rejestr `IOverlayLayer`
## Overlay
- `app/overlay/overlay_layer.py`
- `IOverlayLayer` ABC
- `app/overlay/telemetry_overlay.py`
- `TelemetryOverlay(IOverlayLayer)`
## Pipeline
- `app/pipeline/frame_dispatcher.py`
- pub/sub
- `drop_if_busy`
## Tests
- `tests/test_telemetry_collector.py`
- 12 testów
- mockuje `cpu_count`
- `tests/test_frame_dispatcher.py`
- 8 testów
## Repo / Docs
- `.gitignore`
- zawiera `logs/`
- `notes/01-mvp-preview.md`
- PRD
- `notes/01-mvp-plan.md`
- plan implementacji
- `notes/02-mvp-app.md`
- stan aplikacji
- historia prób i decyzji architektonicznych

368
notes/04-mvp-uvc.md Normal file
View File

@@ -0,0 +1,368 @@
# Stan projektu po sesji: UVC controls + przebudowa menu formatu
## Kontekst
Aplikacja uruchomiona na docelowym Mac Mini. Kamera ELP dostarcza klatki w formacie NV12 (przez backend AVFoundation). Stare menu z osobnymi podmenu `Resolution` i `FPS` nie nadawało się do użycia — kamera eksponuje formaty jako kombinacje (rozdzielczość + fps + pixel format), a ich niezależne ustawianie dawało nieskoordynowane wyniki.
Dodano też interfejs do kontroli parametrów obrazu kamery.
---
## Zmiany w tej sesji
### 1. Menu formatu — przebudowane (`app/ui/menu_bar.py`)
**Przed:** dwa osobne podmenu — `Video → Resolution` i `Video → FPS`.
**Po:** jedno podmenu `Video → Resolution` z pozycjami reprezentującymi pełny format:
```
1920×1080 @ 30fps (NV12)
1920×1080 @ 30fps (MJPG)
1280×720 @ 30fps (NV12)
1280×720 @ 5fps (YUY2)
640×480 @ 30fps (MJPG)
...
```
Każda pozycja odpowiada dokładnie jednemu `CameraFormat` z `CameraInfo.formats`. Kolejność odzwierciedla sortowanie z `CameraEnumerator` (największa powierzchnia → najwyższe fps).
**Sygnały:**
- Usunięto: `resolution_selected(int, int)`, `fps_selected(float)`
- Dodano: `format_selected(CameraFormat)` — emituje pełny obiekt formatu
- Dodano: `camera_settings_requested()` — otwiera dialog ustawień
**Metody publiczne:**
- `populate_formats(camera_info)` — zamiast `populate_resolutions` + `populate_fps`
- `set_active_format(fmt)` — zaznacza aktywny format po zmianie (matching po w/h/fps/pixel_format)
- Usunięto: `set_console_level()` (przeniesione do wewnętrznego `_on_log_toggled`)
**Nowe menu `Image`:**
```
Image
└── Camera Settings… → otwiera CameraSettingsDialog
```
---
### 2. CameraService — `set_format()` (`app/camera/camera_service.py`)
**Przed:** dwie metody `set_resolution(w, h)` i `set_fps(fps)` przechowujące `_desired_width`, `_desired_height`, `_desired_fps` osobno.
**Po:** jedna metoda `set_format(fmt: CameraFormat)` przechowująca `_desired_fmt: CameraFormat | None`.
```python
def set_format(self, fmt: CameraFormat) -> None:
self._desired_fmt = fmt
if self._current_info is not None:
self.start(self._current_info) # stop+start jak poprzednio
```
**Scoring przy wyborze formatu Qt (`_apply_format`):**
Poprzednio scoringował tylko rozdzielczość i fps. Teraz uwzględnia też pixel format:
```python
exact_res = int(w == desired.width and h == desired.height) * 1000
exact_pf = int(pf == desired.pixel_format) * 500
exact_fps = int(abs(f - desired.max_fps) < 0.5) * 100
area_diff = abs(w * h - desired.width * desired.height)
score = exact_res + exact_pf + exact_fps - area_diff
```
Pixel format jest drugim co do ważności kryterium (po rozdzielczości, przed fps).
**Nowa właściwość:**
```python
@property
def qt_camera(self) -> QCamera | None:
...
```
Udostępnia wewnętrzny `QCamera` dla `CameraSettingsDialog` (potrzebny do ustawiania WB i exposure przez Qt API).
**Poprawione logowanie:**
`_log_actual_format()` loguje teraz ostrzeżenia zarówno dla FPS jak i rozdzielczości jeśli kamera nie dostarczyła tego co było żądane.
---
### 3. Warstwa UVC (`app/camera/uvc/`)
Nowy pakiet z abstrakcją dla sterowania parametrami obrazu przez UVC (USB Video Class).
#### Dlaczego osobna warstwa, nie Qt?
`QCamera.Feature` w Qt6 obejmuje: `ColorTemperature`, `ExposureCompensation`, `IsoSensitivity`, `ManualExposureTime`, `CustomFocusPoint`, `FocusDistance`. **Nie obejmuje** brightness, contrast, saturation, hue, sharpness, gamma, backlight compensation — są to niskopoziomowe UVC/V4L2 controls poza zakresem Qt Multimedia.
Sterowanie nimi wymaga osobnych bibliotek natywnych, innych na każdej platformie.
#### Dostępne biblioteki
| Biblioteka | Platforma | Instalacja | Uwagi |
|---|---|---|---|
| `duvc-ctl` | Windows only | `pip install duvc-ctl` | DirectShow, stabilna, PyPI, MIT |
| `pyuvc` | macOS + Linux | `brew install libusb jpeg-turbo && pip install pyuvc` | libuvc bindings, wymaga kompilacji |
| `uvc-py` | macOS + Linux (Win w planie) | `pip install uvc-py` | dev/alpha, brak UVC controls |
#### Struktura
```
app/camera/uvc/
├── __init__.py # make_uvc_controller(name) — factory z detekcją platformy
├── base.py # UvcControllerBase ABC, UvcParam enum, UvcParamInfo dataclass
├── stub.py # NullUvcController — no-op fallback
├── windows.py # WindowsUvcController — duvc-ctl (DirectShow)
└── macos.py # MacUvcController — pyuvc (libuvc)
```
#### `UvcParam` enum
```python
class UvcParam(Enum):
BRIGHTNESS
CONTRAST
SATURATION
HUE
SHARPNESS
GAMMA
WHITE_BALANCE
BACKLIGHT_COMPENSATION
EXPOSURE
```
#### `UvcParamInfo` dataclass
```python
@dataclass
class UvcParamInfo:
param: UvcParam
supported: bool
minimum: int = 0
maximum: int = 100
default: int = 50
current: int = 50
step: int = 1
auto_supported: bool = False
auto_enabled: bool = False
```
#### `UvcControllerBase` ABC — interfejs
```python
def open(self, device_name: str) -> bool # otwiera uchwyt do urządzenia
def close(self) -> None
def is_open(self) -> bool
def get_param_info(self, param: UvcParam) -> UvcParamInfo
def get_all_params(self) -> list[UvcParamInfo]
def set_value(self, param: UvcParam, value: int) -> bool
def set_auto(self, param: UvcParam, enabled: bool) -> bool
```
#### `make_uvc_controller(device_name)` — factory
Detekcja platformy w runtime:
```python
if system == "Windows":
WindowsUvcController (duvc-ctl)
elif system == "Darwin":
MacUvcController (pyuvc)
else:
NullUvcController
```
Jeśli biblioteka nie jest zainstalowana — `ImportError` przechwycony, fallback na `NullUvcController`. Loguje `WARNING` z instrukcją instalacji.
#### `WindowsUvcController` — szczegóły
- Łączy się z pierwszym urządzeniem którego nazwa zawiera `device_name` (case-insensitive)
- Mapowanie `UvcParam → duvc-ctl VidProp/CamProp` string:
- `BRIGHTNESS → "brightness"`, `CONTRAST → "contrast"`, ..., `EXPOSURE → "exposure"`
- `set_auto()` używa `duvc.CamMode.AUTO/MANUAL`
- `_refresh_supported()` odpytuje `cam.get_supported_properties()` po otwarciu
#### `MacUvcController` — szczegóły
- Używa `uvc.device_list()` do wyszukania urządzenia po nazwie
- Otwiera przez `uvc.Capture(uid)`
- Mapowanie `UvcParam → pyuvc display_name` string:
- `WHITE_BALANCE → "White Balance temperature"`, `EXPOSURE → "Absolute Exposure Time"`, itd.
- `auto_mode` via `ctrl.auto_mode` (atrybut pyuvc control)
---
### 4. CameraSettingsDialog (`app/ui/camera_settings_dialog.py`)
`QDialog` ze scrollowaną zawartością, dwoma sekcjami. Zmiany aplikowane **na żywo** (brak OK/Apply — tylko Close).
#### Sekcja Qt Camera Controls
| Kontrolka | Widget | Akcja |
|---|---|---|
| White Balance | `QComboBox` (8 trybów + Manual) | `camera.setWhiteBalanceMode()` |
| Colour Temp (K) | `QSpinBox` 200010000, krok 100 | `camera.setColorTemperature()` — widoczny tylko w trybie Manual |
| Exposure Mode | `QComboBox` (Auto / Manual) | `camera.setExposureMode()` |
| Exposure Time (s) | `QDoubleSpinBox`, 6 miejsc po przecinku | `camera.setManualExposureTime()` — widoczny tylko w trybie Manual |
Dynamiczna widoczność: pola `Colour Temp` i `Exposure Time` pojawiają się tylko gdy wybrano tryb Manual.
Tryby WB sprawdzane przez `camera.isWhiteBalanceModeSupported()` przed aplikacją — jeśli kamera nie obsługuje trybu, zmiana jest ignorowana (logowane DEBUG).
#### Sekcja UVC Camera Controls
Dla każdego `UvcParam`:
- Jeśli `supported=False`: wyświetla "Not supported" (disabled)
- Jeśli `supported=True`: `QSlider` + `QSpinBox` zsynchronizowane dwukierunkowo
- Jeśli `auto_supported=True`: dodatkowy `QCheckBox "Auto"` — po zaznaczeniu blokuje slider i spinbox
Jeśli `NullUvcController` (żadne kontrolki nieobsługiwane): sekcja wyświetla notatkę:
```
UVC controls not available.
Install duvc-ctl (Windows) or pyuvc (macOS) to enable.
```
---
### 5. MainWindow — aktualizacje (`app/ui/main_window.py`)
**Nowe pola:**
```python
self._uvc: UvcControllerBase = NullUvcController()
```
**Nowe metody:**
- `_open_uvc(cam)` — zamyka poprzedni UVC controller i otwiera nowy dla wybranej kamery
- `_on_format_selected(fmt)``camera_service.set_format(fmt)`
- `_on_settings_requested()` → tworzy `CameraSettingsDialog(qt_camera, uvc)` i wywołuje `.exec()`
**Zmiany w `_wire_signals()`:**
- Usunięto: `menu.resolution_selected`, `menu.fps_selected`
- Dodano: `menu.format_selected → _on_format_selected`
- Dodano: `menu.camera_settings_requested → _on_settings_requested`
**`closeEvent`:**
- Dodano: `self._uvc.close()` jeśli `is_open()`
---
### 6. Testy (`tests/test_uvc.py`) — nowy plik, 15 testów
| Klasa testowa | Co testuje |
|---|---|
| `TestNullUvcController` | `open` → False, `is_open` → False, `close` bez wyjątku, `get_param_info` → unsupported, `get_all_params` zwraca wszystkie `UvcParam`, `set_value`/`set_auto` → False bez wyjątku |
| `TestUvcParamInfo` | wartości domyślne dataclass, flaga `supported=False`, custom range |
| `TestMakeUvcControllerFallback` | zwraca instancję `UvcControllerBase`, fallback na stub gdy `ImportError` |
Łącznie testów: **35** (20 poprzednich + 15 nowych), wszystkie zielone.
---
## Decyzje architektoniczne
### Dlaczego `set_format()` zamiast osobnych `set_resolution`/`set_fps`?
Kamera ELP przez AVFoundation eksponuje formaty jako niepodzielne kombinacje `(W × H, fps, pixel_format)`. Próba niezależnej zmiany rozdzielczości bez zmiany fps (i odwrotnie) prowadziła do niespójnych stanów — kamera ignorowała jedną z wartości albo wybierała domyślny format. Jeden `CameraFormat` jako atomowa jednostka wyboru eliminuje problem.
### Dlaczego warstwowa architektura UVC a nie jedna biblioteka?
Brak cross-platform biblioteki Python dla UVC controls. `duvc-ctl` — Windows only (DirectShow), `pyuvc` — macOS/Linux (libuvc). Warstwa abstrakcji z platformową detekcją w runtime pozwala:
- Działać bez żadnej biblioteki (NullUvcController — grayed-out w UI)
- Dodać obsługę nowej platformy bez zmiany warstwy wyżej
- Testować logikę UI niezależnie od dostępności sprzętu
### Dlaczego Qt controls i UVC controls w jednym dialogu?
WB i exposure są dostępne przez oba API (Qt i UVC). Qt API jest wyżej poziomowe (tryby, EV), UVC daje surowe wartości rejestrów. Użytkownik widzi jedno spójne miejsce — nie musi wiedzieć przez które API co działa.
---
## Pliki zmienione / dodane
### Zmienione
| Plik | Co zmieniono |
|---|---|
| `app/ui/menu_bar.py` | Przebudowa menu: `format_selected` zamiast `resolution_selected`+`fps_selected`, nowe menu Image |
| `app/camera/camera_service.py` | `set_format()` zamiast `set_resolution`/`set_fps`, `qt_camera` property, lepsza walidacja w `_log_actual_format` |
| `app/ui/main_window.py` | UVC lifecycle, nowe sygnały menu, `_on_settings_requested` |
### Dodane
| Plik | Co zawiera |
|---|---|
| `app/camera/uvc/__init__.py` | `make_uvc_controller()` factory |
| `app/camera/uvc/base.py` | `UvcControllerBase`, `UvcParam`, `UvcParamInfo` |
| `app/camera/uvc/stub.py` | `NullUvcController` |
| `app/camera/uvc/windows.py` | `WindowsUvcController` (duvc-ctl) |
| `app/camera/uvc/macos.py` | `MacUvcController` (pyuvc) |
| `app/ui/camera_settings_dialog.py` | `CameraSettingsDialog` |
| `tests/test_uvc.py` | 15 testów UVC layer |
---
## Instalacja bibliotek UVC
### Mac Mini (cel docelowy)
```bash
brew install libusb jpeg-turbo
pip install pyuvc
```
### Windows (dev)
```bash
pip install duvc-ctl
```
### Bez bibliotek
Aplikacja działa normalnie — sekcja UVC w `CameraSettingsDialog` wyświetla komunikat o braku biblioteki. Qt controls (WB, Exposure) działają niezależnie od UVC.
---
## Next Steps
1. Przetestować na Mac Mini z kamerą ELP:
- czy `MacUvcController` otwiera urządzenie po `pip install pyuvc`
- czy slidery zmieniają rzeczywiste parametry kamery
- czy `Camera Settings…` poprawnie pokazuje zakresy dla ELP (np. brightness 0255)
2. Sprawdzić czy AVFoundation na macOS honoruje `setCameraFormat` bez stop+start (Qt docs mówią że na macOS inny proces może nadpisać format — `_log_actual_format` oznaczy to jako warning)
3. Dodać `set_active_format()` call po `_log_actual_format()` żeby menu zaznaczało faktycznie działający format (nie żądany)
4. Rozważyć `QSlider` z etykietą wartości min/max/default w dialogu — aktualnie są tylko slider + spinbox
5. (Opcjonalnie) Preset systemu: zapisz/wczytaj ustawienia do JSON per kamera
---
## Critical Context
- `CameraService.qt_camera``QCamera | None` — potrzebny do `CameraSettingsDialog`, jest `None` gdy kamera zatrzymana
- `make_uvc_controller(name)` zwraca **już otwarty lub próbujący otworzyć** kontroler; `MainWindow._open_uvc()` nie wywołuje `.open()` ponownie jeśli `is_open()` zwróci True
- `MacUvcController` używa `uvc.device_list()` (pyuvc API) — wymaga że kamera **nie jest** aktywnie używana przez inny proces (ograniczenie libusb na macOS)
- `WindowsUvcController` działa **równolegle** z Qt — DirectShow pozwala na jednoczesny dostęp do controls i streamu
- Testy UVC nie wymagają sprzętu ani bibliotek natywnych — `NullUvcController` jest czystym Pythonem
## Uruchamianie
```bash
# Windows dev
.venv-win\Scripts\python.exe -m app.main
# Mac Mini (po instalacji pyuvc)
.venv/bin/python -m app.main
```

505
notes/05-mvp-yolo.md Normal file
View File

@@ -0,0 +1,505 @@
# Stan projektu po sesji: YOLO inference + odtwarzanie wideo
Poprzedni stan: `04-mvp-uvc.md`
---
## Kontekst
Po uruchomieniu aplikacji na Mac Mini z kamerą ELP, kolejny krok to weryfikacja
wytrenowanego modelu YOLO. Wymagania:
- Model działa w **osobnym procesie** — crash workera nie wywala GUI
- Inference **nie blokuje** i **nie spowalnia** podglądu kamery
- Worker **ignoruje klatki** dopóki analizuje poprzednią (drop-if-busy)
- Możliwość wczytania **pliku wideo** zamiast kamery do oceny modelu
- Bbox narysowany na nowej **warstwie overlay**
- W przyszłości OCR będzie działał w tym samym procesie co YOLO
---
## Nowe pakiety
### `app/video/`
```
app/video/
├── __init__.py
└── video_player.py
```
### `app/inference/`
```
app/inference/
├── __init__.py
├── worker.py # funkcja uruchamiana w subprocess
├── worker_manager.py # InferenceManager (QObject) — IPC, polling, auto-restart
└── bbox_overlay.py # BboxOverlay(IOverlayLayer) — rysuje bbox+label+conf
```
---
## Szczegółowy opis zmian
### 1. `app/video/video_player.py` — `VideoPlayer`
Nowa klasa `VideoPlayer(QObject)` — identyczny interfejs sygnałowy jak `CameraService`:
```python
frame_ready = Signal(QVideoFrame)
playback_started = Signal()
playback_stopped = Signal()
playback_error = Signal(str)
```
Wewnętrznie: `QMediaPlayer` + `QVideoSink`. Obsługuje formaty: `.mp4`, `.avi`,
`.mov`, `.mkv`, `.m4v`, `.webm` (cokolwiek obsługuje FFmpeg backend Qt).
Odtwarzanie w czasie rzeczywistym (1×). Brak seek/pauzy — tylko Open + Stop.
`MainWindow` podłącza do `FrameDispatcher` albo `CameraService.frame_ready`,
albo `VideoPlayer.frame_ready` — nigdy obu naraz. Przełączanie przez
`_switch_to_camera()` / `_switch_to_video()`.
---
### 2. `app/inference/worker.py` — worker process
#### Struktury IPC (NamedTuple — pickle-safe)
```python
class FramePacket(NamedTuple):
frame_id: int
raw_bytes: bytes # RGB, (H×W×3)
width: int
height: int
channels: int # zawsze 3
class WorkerReadyPacket(NamedTuple):
device: str # "cpu" | "mps" — wysyłany raz po załadowaniu modelu
class ResultPacket(NamedTuple):
frame_id: int
detections: list # list of (x1, y1, x2, y2, conf, label)
width: int
height: int
elapsed_ms: float # czas wywołania model() w ms
```
#### Protokół IPC
```
input_queue ← FramePacket
output_queue → WorkerReadyPacket (raz, zaraz po załadowaniu modelu)
→ ResultPacket (po każdej analizowanej klatce)
→ None (tylko przy błędzie ładowania modelu)
```
#### `_select_device()` — wybór urządzenia
Wywoływany **raz przy starcie workera** (nie per-frame jak wcześniej):
```python
def _select_device() -> str:
if platform.system() == "Darwin":
if torch.backends.mps.is_available():
return "mps" # Metal GPU na macOS
return "cpu"
```
`device` jest przekazywany do `_load_model()` i do każdego wywołania `_infer()`.
Eliminuje redundantne wykrywanie urządzenia przy każdej klatce.
#### `_infer()` — pomiar czasu
```python
t0 = time.perf_counter()
results = model(frame_np, device=device, verbose=False)
elapsed_ms = (time.perf_counter() - t0) * 1000.0
```
`elapsed_ms` trafia do `ResultPacket` i jest logowany w managerze przy detekcjach.
---
### 3. `app/inference/worker_manager.py` — `InferenceManager`
#### Sygnały
```python
detections_ready = Signal(object, object) # list[Detection], tuple[int,int]
detection_count_updated = Signal(int) # łączna liczba klatek z detekcją
inference_stats_updated = Signal(str, float) # device, avg_elapsed_ms
inference_device_changed = Signal(str) # emitowany raz po WorkerReadyPacket
inference_started = Signal()
inference_stopped = Signal()
inference_error = Signal(str)
```
#### Mechanizm drop-if-busy
```python
def submit_frame(self, frame: QVideoFrame) -> None:
if not self.is_running or self._paused or self._busy:
return # klatka odrzucona cicho
# konwersja + put_nowait → self._busy = True
```
`self._busy` wraca do `False` dopiero gdy `_poll_output()` odbierze `ResultPacket`.
Gwarantuje że nigdy nie ma więcej niż jedna klatka w locie.
#### Konwersja klatki w GUI thread
Zamiast `frame.bits(0)` (dawało tylko płaszczyznę Y dla NV12):
```python
image = frame.toImage() # Qt dekoduje NV12/YUV/MJPG → RGB
image = image.convertToFormat(Format_RGB32) # packed BGRX
arr = np.frombuffer(image.bits(), dtype=np.uint8).reshape((H, W, 4))
rgb = arr[:, :, [2, 1, 0]].copy() # BGRX → RGB, drop X
```
Obsługuje każdy pixel format jaki kamera może dostarczyć.
#### Rolling average elapsed_ms
```python
_elapsed_window: deque[float] # maxlen=10
avg_ms = sum(_elapsed_window) / len(_elapsed_window)
```
Emitowany przez `inference_stats_updated(device, avg_ms)` po każdym `ResultPacket`.
#### `_poll_output()` — obsługa `WorkerReadyPacket`
```python
if isinstance(item, WorkerReadyPacket):
self._current_device = item.device
self.inference_device_changed.emit(item.device)
continue
```
Odróżnienie od `ResultPacket` przez `isinstance` — nie wymaga sentinel wartości.
#### Auto-restart
- Watchdog co 2s sprawdza `process.is_alive()`
- Timeout 10s bez odpowiedzi → terminate + restart
- Max 3 restartów (konfigurowalny przez `INFERENCE_MAX_RESTARTS`)
- Po przekroczeniu: `QMessageBox.critical` + overlay wyłączony
#### Logowanie — tylko detekcje
```python
if detections:
logger.info(
"frame %d: %d detection(s) in %.1f ms — %s",
packet.frame_id, len(detections), packet.elapsed_ms, conf_summary,
)
```
Klatki bez detekcji: brak logu. `conf_summary = "label 0.94, label 0.81"`.
---
### 4. `app/inference/bbox_overlay.py` — `BboxOverlay`
```python
class Detection(NamedTuple):
x1: float; y1: float; x2: float; y2: float
conf: float
label: str
```
Współrzędne w pikselach **oryginalnej klatki**. `paint()` skaluje do `video_rect`:
```python
scale_x = video_rect.width() / src_w
scale_y = video_rect.height() / src_h
wx1 = video_rect.x() + int(det.x1 * scale_x)
# ...
```
Każdy bbox: prostokąt w kolorze `BBOX_COLOR` + label `"label 0.87"` na tle
`BBOX_LABEL_BG_COLOR` nad lewym górnym rogiem boxa (lub wewnątrz gdy brakuje miejsca).
`BboxOverlay.visible = False` domyślnie — pojawia się dopiero po włączeniu inference toggle.
---
### 5. Menu — zmiany w `app/ui/menu_bar.py`
Dodano dwa nowe menu przed istniejącymi:
```
File
├── Open Video… QFileDialog (.mp4 .avi .mov .mkv .m4v .webm)
└── Close Video disabled gdy źródło = kamera
Model
├── Load Model… QFileDialog (.pt .pth)
├── Enable Inference QAction checkable, disabled do momentu załadowania modelu
└── Model: (none) disabled — info o załadowanym pliku
```
Nowe sygnały:
- `video_file_selected(str)` — ścieżka pliku wideo
- `video_closed()` — powrót do kamery
- `model_file_selected(str)` — ścieżka modelu
- `inference_toggled(bool)` — włącz/wyłącz inference
---
### 6. `app/ui/main_window.py` — integracja
#### Przełączanie źródła klatek
```python
def _switch_to_camera(self):
video_player.frame_ready.disconnect(dispatcher.dispatch)
camera_service.frame_ready.connect(dispatcher.dispatch)
def _switch_to_video(self):
camera_service.frame_ready.disconnect(dispatcher.dispatch)
video_player.frame_ready.connect(dispatcher.dispatch)
```
Dispatcher i wszyscy subskrybenci (CameraView, TelemetryCollector,
InferenceManager) są podłączeni do dispatchera — źródło klatek jest dla nich
transparentne.
#### Inference toggle
```python
def _on_inference_toggled(self, enabled: bool):
if enabled:
inference.resume()
dispatcher.subscribe(inference.submit_frame, drop_if_busy=True)
bbox_overlay.visible = True
detection_label.setVisible(True)
else:
inference.pause()
dispatcher.unsubscribe(inference.submit_frame)
bbox_overlay.clear()
bbox_overlay.visible = False
detection_label.setVisible(False)
telemetry.clear_inference_stats()
```
`pause()` nie zatrzymuje procesu — tylko blokuje `submit_frame`. Proces
pozostaje załadowany w pamięci.
#### Status bar — counter detekcji
```python
self._detection_label = QLabel("") # addPermanentWidget (prawa strona)
```
Pokazywany tylko gdy inference włączone. Aktualizowany przez
`inference.detection_count_updated(int)``"Detections: 17 frames"`.
---
### 7. Telemetria + overlay — nowe pola inference
#### `TelemetrySnapshot` — nowe pola
```python
@dataclass
class TelemetrySnapshot:
# ... istniejące pola ...
inference_device: str | None = None # "cpu" | "mps" | None
inference_time_ms: float | None = None # rolling avg, None gdy wyłączone
```
#### `TelemetryCollector` — nowe metody
```python
def set_inference_stats(self, device: str, avg_ms: float) -> None: ...
def clear_inference_stats(self) -> None: ...
```
Wywoływane z `MainWindow` przy każdym `inference_stats_updated` i przy
wyłączeniu inference.
#### `TelemetryOverlay` — nowe wiersze
```
FPS req 30.0
FPS got 29.8
Frame 33.5 ms
Drop 0
CPU sys 8.2 %
CPU core 65.7 %
Mem 71 MB
Inf.dev mps ← widoczny tylko gdy model załadowany
Inf.time 87 ms ← rolling avg ostatnich 10 klatek
```
Wiersze `Inf.dev` i `Inf.time` znikają gdy inference jest wyłączone
(`clear_inference_stats()` → pola `None``_format_lines` ich nie emituje).
---
### 8. Bugfixes (zidentyfikowane po uruchomieniu)
#### `Unexpected frame size: 921600 bytes for 1280×720`
**Przyczyna:** `frame.bits(0)` zwraca tylko płaszczyznę 0 (luma Y) dla formatów
planarnych NV12/YUV420P — `1280 × 720 × 1 = 921600` zamiast `1280 × 720 × 3`.
**Naprawa:** zamiana na `frame.toImage() → Format_RGB32 → bits()`. Qt dekoduje
każdy format wewnętrznie. Identyczna ścieżka jak `CameraView.on_frame()`.
#### `Subscriber not found for removal` (WARNING w logu)
**Przyczyna:** `_on_inference_toggled(False)` wywoływał `dispatcher.unsubscribe()`
zanim subscriber był dodany (pierwsze wyłączenie przed włączeniem).
**Naprawa:** zmiana poziomu logu z `WARNING` na `DEBUG` w
`FrameDispatcher.unsubscribe()` — brak subscribera nie jest błędem.
---
## Decyzje architektoniczne
### Osobny proces zamiast wątku
`multiprocessing.Process(context="spawn")` zamiast `QThread` lub `threading.Thread`:
- Osobny GIL — inference nie blokuje Python event loop GUI
- Crash workera (segfault, OOM) nie wywala aplikacji
- `spawn` zamiast `fork` — wymagane na macOS od Python 3.12 (Apple deprecuje `fork`)
### `toImage()` zamiast `bits(0)`
`QVideoFrame.bits(plane)` daje surowe bajty jednej płaszczyzny. W formatach
planarnych (NV12: Y w plane 0, UV w plane 1) to tylko część obrazu. `toImage()`
wywołuje wewnętrzny dekoder Qt i zawsze zwraca kompletny obraz niezależnie od
pixel formatu.
### `WorkerReadyPacket` zamiast osobnego IPC kanału
Worker wysyła `WorkerReadyPacket(device)` do tej samej `output_queue` zaraz
po załadowaniu modelu. Manager odróżnia go przez `isinstance`. Eliminuje
potrzebę dodatkowej kolejki lub pipe tylko dla metadanych startu.
### Inference stats przez `TelemetryCollector`, nie bezpośrednio do overlay
`InferenceManager.inference_stats_updated``MainWindow._on_inference_stats_updated`
`TelemetryCollector.set_inference_stats()``TelemetrySnapshot.inference_*`
`TelemetryOverlay.paint()`.
Alternatywa: bezpośrednie połączenie `InferenceManager → TelemetryOverlay`.
Wybrano pośrednie przez `TelemetryCollector` bo:
- `TelemetrySnapshot` jest jedyną strukturą danych opisującą stan systemu
- CSV logger automatycznie dostaje inference stats bez dodatkowego kodu
- Overlay ma jeden spójny model danych
---
## Pliki dodane
| Plik | Zawartość |
|---|---|
| `app/video/__init__.py` | pusty |
| `app/video/video_player.py` | `VideoPlayer(QObject)` |
| `app/inference/__init__.py` | pusty |
| `app/inference/worker.py` | `run_worker()`, `FramePacket`, `WorkerReadyPacket`, `ResultPacket`, `_select_device()`, `_infer()` |
| `app/inference/worker_manager.py` | `InferenceManager(QObject)` |
| `app/inference/bbox_overlay.py` | `Detection(NamedTuple)`, `BboxOverlay(IOverlayLayer)` |
| `tests/test_bbox_overlay.py` | 16 testów — `Detection`, stan overlay, mapowanie współrzędnych bbox |
| `tests/test_inference_manager.py` | 13 testów — drop-if-busy, pause/resume, restart counter, is_running |
## Pliki zmienione
| Plik | Co zmieniono |
|---|---|
| `app/config.py` | `INFERENCE_WORKER_TIMEOUT_S`, `INFERENCE_MAX_RESTARTS`, `INFERENCE_POLL_INTERVAL_MS`, `INFERENCE_WATCHDOG_INTERVAL_MS`, `BBOX_COLOR`, `BBOX_LABEL_BG_COLOR`, `BBOX_LABEL_TEXT_COLOR`, `BBOX_LINE_WIDTH`, `BBOX_FONT_SIZE`, `VIDEO_FILE_EXTENSIONS`, `MODEL_FILE_EXTENSIONS` |
| `app/ui/menu_bar.py` | Menu `File` (Open Video…, Close Video), menu `Model` (Load Model…, Enable Inference, Model info) |
| `app/ui/main_window.py` | `VideoPlayer` lifecycle, `InferenceManager` lifecycle, source switching, detection counter w statusbar, `_on_inference_stats_updated` |
| `app/telemetry/telemetry_collector.py` | `TelemetrySnapshot.inference_device`, `TelemetrySnapshot.inference_time_ms`, `set_inference_stats()`, `clear_inference_stats()` |
| `app/overlay/telemetry_overlay.py` | Wiersze `Inf.dev` i `Inf.time` w `_format_lines()` |
| `app/pipeline/frame_dispatcher.py` | `unsubscribe()` brak subscribera: WARNING → DEBUG |
| `pyproject.toml` | `[project.optional-dependencies] inference = ["ultralytics>=8.0", "numpy>=1.24"]` |
| `tests/test_telemetry_collector.py` | `_make_collector()` uzupełniony o `_inference_device=None`, `_inference_time_ms=None` |
---
## Łączna liczba testów
**69 testów, wszystkie zielone.**
| Plik | Liczba testów |
|---|---|
| `test_frame_dispatcher.py` | 8 |
| `test_telemetry_collector.py` | 12 |
| `test_uvc.py` | 15 |
| `test_bbox_overlay.py` | 16 |
| `test_inference_manager.py` | 18 |
---
## Instalacja
```bash
# Wymagane do inference:
pip install ultralytics numpy
# lub:
pip install -e ".[inference]"
```
Aplikacja startuje bez tych pakietów — `Load Model…` zostaje aktywne, ale
`InferenceManager.start()` zgłosi błąd jeśli `ultralytics` nie jest zainstalowany
(obsłużony przez `try/except ImportError` w workerze → `output_queue.put(None)`
→ manager emituje `inference_error`).
---
## Uruchamianie
```bash
# Windows dev
.venv-win\Scripts\python.exe -m app.main
# Mac Mini
.venv/bin/python -m app.main
# Mac Mini z plikiem wideo od razu (CLI nie zaimplementowany — użyj File → Open Video…)
.venv/bin/python -m app.main
```
---
## Next Steps
1. Przetestować na Mac Mini z kamerą ELP:
- czy `_select_device()` wykrywa MPS i loguje `"MPS (Metal) available"`
- czy `Inf.dev mps` pojawia się w overlayzie
- czy `Inf.time` jest znacząco niższy niż na CPU
2. OCR w tym samym procesie co YOLO:
- Worker process może obsługiwać wiele zadań — dodać `OcrTask` do `FramePacket`
- lub uruchomić OCR jako osobny subscriber `FrameDispatcher` w osobnym procesie
3. Dodać możliwość regulacji progu confidence (`conf_threshold`) przez menu/dialog
— przekazać jako parametr do `run_worker()` w `FramePacket` lub przy starcie
4. `set_active_format()` call po `_log_actual_format()` żeby menu zaznaczało
faktycznie działający format (nie żądany) — z poprzedniej sesji
---
## Critical Context
- `WorkerReadyPacket` jest rozróżniany od `ResultPacket` przez `isinstance`
nie używaj `None` jako sentinela dla obu typów
- `_select_device()` wywołany raz przy starcie — jeśli zmienisz device w trakcie
działania, trzeba zrestartować workera
- `BboxOverlay.on_detections(detections, source_size)``source_size` to
`tuple[int, int]` (width, height) klatki która była inferowana, nie aktualnego
widgetu; potrzebne do poprawnego skalowania przy zmianie rozdzielczości
- `InferenceManager.pause()` nie zatrzymuje procesu — `submit_frame` tylko
sprawdza flagę; model pozostaje załadowany, można szybko wznowić
- `multiprocessing.get_context("spawn")` — wymagane na macOS/Windows;
`fork` jest domyślny na Linux ale niebezpieczny z Qt

182
notes/camera_elp.md Normal file
View File

@@ -0,0 +1,182 @@
# Camera Specification
## Model
**ELP-USB4KHDR01-MFV(5-50)**
(10X zoom manual lens)
### Optional
**ELP-USB4KHDR01-MFV(2.8-12)**
(2.8-12mm manual zoom lens)
---
## Sensor
- SONY IMX317 (1/2.5”)
## Max Resolution
- 3840(H) × 2160(V)
## Sensitivity
- 1000mV/Lux-sec
## Image Area
- 6100μm × 4524μm
## Picture Format
- MJPEG
- YUY2 (YUYV)
---
# Resolution & FPS
## MJPEG Modes
| Resolution | FPS |
|---|---|
| 3840×2160 | 30 fps |
| 2592×1944 | 30 fps |
| 2048×1536 | 30 fps |
| 1600×1200 | 30 fps |
| 1920×1080 | 30 fps |
| 1280×1024 | 30 fps |
| 1280×960 | 30 fps |
| 1280×720 | 30 fps |
| 1024×768 | 30 fps |
| 800×600 | 30 fps |
| 640×480 | 30 fps |
| 320×240 | 30 fps |
## YUY2 Modes
| Resolution | FPS |
|---|---|
| 3840×2160 | 1 fps |
| 2592×1944 | 1 fps |
| 2048×1536 | 3 fps |
| 1600×1200 | 3 fps |
| 1920×1080 | 3 fps |
| 1280×1024 | 3 fps |
| 1280×960 | 5 fps |
| 1280×720 | 5 fps |
| 1024×768 | 5 fps |
| 800×600 | 20 fps |
| 640×480 | 30 fps |
| 320×240 | 30 fps |
---
## Optical / Image Parameters
| Parameter | Value |
|---|---|
| Center Definition | 1000LW/PH (Center) |
| S/N Ratio | 26dB |
| Sensitivity | 0.65V/lux-sec@550nm |
| Low Illumination | 0.2lux |
| Shutter | Electronic rolling shutter / Frame exposure |
---
## Interface
- USB2.0 High Speed
---
## Supported Features
| Feature | Support |
|---|---|
| AEC | Yes |
| AEB | Yes |
| AGC | Yes |
---
## Adjustable Parameters
- Brightness
- Contrast
- Saturation
- Hue
- Sharpness
- Gamma
- White Balance
- Backlight Contrast
- Exposure
---
## Lens
- 2.8-12mm / 5-50mm varifocal manual lens optional
---
## Audio
- Built-in microphone
- Supports audio recording
---
## Power
| Parameter | Value |
|---|---|
| Power Supply | USB BUS POWER 4P-2.0mm socket |
| Voltage | DC5V |
| Current | 200mA |
---
## Physical Dimensions
- 45mm × 45mm × 50mm
---
## Temperature
| Parameter | Value |
|---|---|
| Storage Temperature | -20°C to 70°C |
| Working Temperature | 0°C to 60°C |
---
## USB Cable
- 3M standard
- Optional: 1M / 2M / 5M
---
# Supported Operating Systems
## Windows
- Windows XP
- Windows Vista
- Windows 7
- Windows 8
- Windows 10
- Windows 11
## Linux
- Linux with UVC support
- Kernel above linux-2.6.26
## macOS / Android
- macOS X 10.4.8 or later
- Android 4.0 or above with UVC

239
notes/pyuvc_ex2.txt Normal file
View File

@@ -0,0 +1,239 @@
(.venv-mac-uvc) rafalkaczka@Mac-mini-Rafal duck-preview % sudo $(which python) test_pyuvc_ex2.py
[11:11:55] DEBUG Searching CameraSpec(name='HD USB CAMERA', width=1280, height=720, fps=30, test_pyuvc_ex2.py:65
bandwidth_factor=2.0)...
DEBUG Found match by name test_pyuvc_ex2.py:68
DEBUG Found device that mached uid: 20:10 test_pyuvc_ex2.py:69
DEBUG Device info: {'name': 'HD USB CAMERA', 'manufacturer': '4K USB CAMERA', test_pyuvc_ex2.py:69
'serialNumber': '01.00.00', 'idProduct': 791, 'idVendor': 13028,
'device_address': 10, 'bus_number': 20, 'uid': '20:10'}
libusb: info [darwin_detach_kernel_driver] no capture entitlements. may not be able to detach the kernel driver for this device
DEBUG Device '20:10' opended. test_pyuvc_ex2.py:69
DEBUG Capture(name='HD USB CAMERA' manufacturer='4K USB CAMERA' test_pyuvc_ex2.py:69
serialNumber='01.00.00' idProduct=791 idVendor=13028 device_address=10
bus_number=20 uid='20:10') - all camera modes: [CameraMode(width=3840,
height=2160, fps=30, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=3840, height=2160, fps=25, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=3840, height=2160,
fps=20, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=3840, height=2160, fps=15, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=3840, height=2160,
fps=10, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=3840, height=2160, fps=5, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1920, height=1080,
fps=30, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1920, height=1080, fps=25, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1920, height=1080,
fps=20, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1920, height=1080, fps=15, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1920, height=1080,
fps=10, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1920, height=1080, fps=5, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=2592, height=1944,
fps=30, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=2592, height=1944, fps=25, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=2592, height=1944,
fps=20, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=2592, height=1944, fps=15, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=2592, height=1944,
fps=10, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=2592, height=1944, fps=5, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=2048, height=1536,
fps=30, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=2048, height=1536, fps=25, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=2048, height=1536,
fps=20, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=2048, height=1536, fps=15, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=2048, height=1536,
fps=10, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=2048, height=1536, fps=5, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1600, height=1200,
fps=30, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1600, height=1200, fps=25, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1600, height=1200,
fps=20, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1600, height=1200, fps=15, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1600, height=1200,
fps=10, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1600, height=1200, fps=5, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1280, height=960,
fps=30, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1280, height=960, fps=25, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1280, height=960,
fps=20, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1280, height=960, fps=15, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1280, height=960,
fps=10, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1280, height=960, fps=5, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1280, height=720,
fps=30, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1280, height=720, fps=25, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1280, height=720,
fps=20, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1280, height=720, fps=15, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1280, height=720,
fps=10, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1280, height=720, fps=5, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1024, height=768,
fps=30, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1024, height=768, fps=25, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1024, height=768,
fps=20, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1024, height=768, fps=15, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=1024, height=768,
fps=10, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=1024, height=768, fps=5, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=800, height=600,
fps=30, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=800, height=600, fps=25, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=800, height=600,
fps=20, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=800, height=600, fps=15, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=800, height=600,
fps=10, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=800, height=600, fps=5, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=640, height=480,
fps=30, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=640, height=480, fps=25, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=640, height=480,
fps=20, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=640, height=480, fps=15, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=640, height=480,
fps=10, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=640, height=480, fps=5, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=320, height=240,
fps=30, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=320, height=240, fps=25, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=320, height=240,
fps=20, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=320, height=240, fps=15, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=320, height=240,
fps=10, format_native=7, format_name='MJPG', supported=True),
CameraMode(width=320, height=240, fps=5, format_native=7,
format_name='MJPG', supported=True), CameraMode(width=3840, height=2160,
fps=1, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=1920, height=1080, fps=3, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=2592, height=1944,
fps=1, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=2048, height=1536, fps=3, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=1600, height=1200,
fps=3, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=1280, height=960, fps=5, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=1280, height=720,
fps=10, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=1280, height=720, fps=5, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=1024, height=768,
fps=10, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=1024, height=768, fps=5, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=800, height=600,
fps=20, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=800, height=600, fps=15, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=800, height=600,
fps=10, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=800, height=600, fps=5, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=640, height=480,
fps=30, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=640, height=480, fps=25, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=640, height=480,
fps=20, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=640, height=480, fps=15, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=640, height=480,
fps=10, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=640, height=480, fps=5, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=320, height=240,
fps=30, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=320, height=240, fps=25, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=320, height=240,
fps=20, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=320, height=240, fps=15, format_native=3,
format_name='YUY2', supported=False), CameraMode(width=320, height=240,
fps=10, format_native=3, format_name='YUY2', supported=False),
CameraMode(width=320, height=240, fps=5, format_native=3,
format_name='YUY2', supported=False)]
[11:11:56] DEBUG Adding "Auto Exposure Mode" control. test_pyuvc_ex2.py:69
DEBUG Adding "Absolute Exposure Time" control. test_pyuvc_ex2.py:69
DEBUG Adding "Auto Focus" control. test_pyuvc_ex2.py:69
DEBUG Adding "Absolute Focus" control. test_pyuvc_ex2.py:69
DEBUG Adding "Zoom absolute control" control. test_pyuvc_ex2.py:69
DEBUG Adding "Pan control" control. test_pyuvc_ex2.py:69
DEBUG Adding "Tilt control" control. test_pyuvc_ex2.py:69
libusb: warning [darwin_transfer_status] transfer error: device not responding (value = 0xe00002ed)
DEBUG Could not init Tilt control control! test_pyuvc_ex2.py:69
DEBUG Control info: {'display_name': 'Tilt control', 'unit': 'input_terminal', test_pyuvc_ex2.py:69
'control_id': 13, 'bit_mask': 2048, 'offset': 4, 'data_len': 4,
'buffer_len': 8, 'min_val': None, 'max_val': None, 'step': None, 'def_val':
None, 'd_type': <class 'int'>, 'doc': 'Tilt (Absolute) Control.',
'unit_id': 1}
DEBUG Traceback (most recent call last): test_pyuvc_ex2.py:69
File "uvc_bindings.pyx", line 779, in
uvc_bindings.Capture._enumerate_controls
File "../../../pyuvc-source/controls.pxi", line 625, in
uvc_bindings.Control.__init__
File "../../../pyuvc-source/controls.pxi", line 660, in
uvc_bindings.Control._uvc_get
Exception: Error: Input/output error
DEBUG Adding "Backlight Compensation" control. test_pyuvc_ex2.py:69
DEBUG Adding "Brightness" control. test_pyuvc_ex2.py:69
DEBUG Adding "Contrast" control. test_pyuvc_ex2.py:69
DEBUG Adding "Gain" control. test_pyuvc_ex2.py:69
DEBUG Adding "Power Line frequency" control. test_pyuvc_ex2.py:69
DEBUG Adding "Hue" control. test_pyuvc_ex2.py:69
DEBUG Adding "Saturation" control. test_pyuvc_ex2.py:69
DEBUG Adding "Sharpness" control. test_pyuvc_ex2.py:69
DEBUG Adding "Gamma" control. test_pyuvc_ex2.py:69
DEBUG Adding "White Balance temperature" control. test_pyuvc_ex2.py:69
DEBUG Adding "White Balance temperature,Auto" control. test_pyuvc_ex2.py:69
DEBUG Setting mode: CameraMode(width=1280, height=720, fps=30, format_native=7, test_pyuvc_ex2.py:73
format_name='MJPG', supported=True)
DEBUG Negotiated frame format: {'bmHint': 1, 'bFormatIndex': 1, 'bFrameIndex': 7, test_pyuvc_ex2.py:73
'dwFrameInterval': 333333, 'wKeyFrameRate': 0, 'wPFrameRate': 0,
'wCompQuality': 47, 'wCompWindowSize': 0, 'wDelay': 0,
'dwMaxVideoFrameSize': 1843200, 'dwMaxPayloadTransferSize': 3060,
'dwClockFrequency': 48000000, 'bmFramingInfo': 0, 'bPreferredVersion': 0,
'bMinVersion': 0, 'bMaxVersion': 0, 'bInterfaceNumber': 1}
DEBUG Stream start. test_pyuvc_ex2.py:36
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
libusb: warning [darwin_abort_transfers] aborting all transactions on interface 1 pipe 1
[11:13:41] DEBUG Stream stopped test_pyuvc_ex2.py:61
DEBUG Stream closed test_pyuvc_ex2.py:61
DEBUG Stream stop. test_pyuvc_ex2.py:61
libusb: error [do_close] Device handle closed while transfer was still being processed, but the device is still connected as far as we know
libusb: error [do_close] A cancellation hasn't even been scheduled on the transfer for which the device is closing
[11:13:42] DEBUG UVC device closed.

3
notes/terminal_pyuvc.txt Normal file

File diff suppressed because one or more lines are too long

51
pyproject.toml Normal file
View File

@@ -0,0 +1,51 @@
[project]
name = "duck-preview"
version = "0.1.0"
description = "Realtime camera preview application with telemetry"
requires-python = ">=3.12"
dependencies = [
"PySide6>=6.7",
"psutil>=6.0",
]
[project.optional-dependencies]
# Install inference support: pip install -e ".[inference]"
# or: pip install ultralytics numpy
inference = [
"ultralytics>=8.0",
"numpy>=1.24",
]
[project.scripts]
duck-preview = "app.main:main"
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-v"
[tool.ruff]
target-version = "py312"
line-length = 100
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"UP", # pyupgrade
"B", # flake8-bugbear
"N", # pep8-naming
]
ignore = [
"B008", # do not perform function calls in default arguments
]
[tool.ruff.lint.isort]
known-first-party = ["app"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"

3
requirements-dev.txt Normal file
View File

@@ -0,0 +1,3 @@
-r requirements.txt
pytest>=8.0
ruff>=0.4

7
requirements-uvc.txt Normal file
View File

@@ -0,0 +1,7 @@
numpy==2.4.4
psutil==7.2.2
pupil_labs_uvc==1.0.0b7
PySide6==6.11.1
PySide6_Addons==6.11.1
PySide6_Essentials==6.11.1
shiboken6==6.11.1

38
requirements-yolo.txt Normal file
View File

@@ -0,0 +1,38 @@
certifi==2026.4.22
charset-normalizer==3.4.7
contourpy==1.3.3
cycler==0.12.1
filelock==3.29.0
fonttools==4.62.1
fsspec==2026.4.0
idna==3.15
Jinja2==3.1.6
kiwisolver==1.5.0
MarkupSafe==3.0.3
matplotlib==3.10.9
mpmath==1.3.0
networkx==3.6.1
numpy==1.26.4
opencv-python==4.10.0.84
packaging==26.2
pillow==12.2.0
polars==1.40.1
polars-runtime-32==1.40.1
psutil==7.2.2
pyparsing==3.3.2
PySide6==6.11.1
PySide6_Addons==6.11.1
PySide6_Essentials==6.11.1
python-dateutil==2.9.0.post0
PyYAML==6.0.3
requests==2.34.1
scipy==1.17.1
shiboken6==6.11.1
six==1.17.0
sympy==1.14.0
torch==2.2.2
torchvision==0.17.2
typing_extensions==4.15.0
ultralytics==8.4.50
ultralytics-thop==2.0.19
urllib3==2.7.0

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
PySide6>=6.7
psutil>=6.0

140
test_opencv.py Normal file
View File

@@ -0,0 +1,140 @@
import cv2
import time
import sys
# =========================
# Otwieranie kamery
# =========================
cap = cv2.VideoCapture(0, cv2.CAP_AVFOUNDATION)
# Na macOS warto wymusić AVFoundation
# cap = cv2.VideoCapture(0, cv2.CAP_AVFOUNDATION)
if not cap.isOpened():
print("Nie można otworzyć kamery")
exit()
ret, frame = cap.read()
if not ret:
print("nie ma obrazu")
cap.release()
cv2.destroyAllWindows()
sys.exit(0)
print(f"{cap.get(cv2.CAP_PROP_BRIGHTNESS)}")
print(f"{cap.get(cv2.CAP_PROP_CONTRAST)}")
print(f"{cap.get(cv2.CAP_PROP_SATURATION)}")
print(f"{cap.get(cv2.CAP_PROP_HUE)}")
print(f"{cap.get(cv2.CAP_PROP_GAIN)}")
print(f"{cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)}")
print(f"{cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)}")
print(f"{cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25)}")
time.sleep(0.5)
print(f"{cap.set(cv2.CAP_PROP_EXPOSURE, -1)}")
# =========================
# Lista parametrów OpenCV
# =========================
camera_props = {
"BRIGHTNESS": cv2.CAP_PROP_BRIGHTNESS,
"CONTRAST": cv2.CAP_PROP_CONTRAST,
"SATURATION": cv2.CAP_PROP_SATURATION,
"HUE": cv2.CAP_PROP_HUE,
"GAIN": cv2.CAP_PROP_GAIN,
"EXPOSURE": cv2.CAP_PROP_EXPOSURE,
"SHARPNESS": getattr(cv2, "CAP_PROP_SHARPNESS", -1),
"GAMMA": getattr(cv2, "CAP_PROP_GAMMA", -1),
"AUTO_EXPOSURE": getattr(cv2, "CAP_PROP_AUTO_EXPOSURE", -1),
"WB_TEMPERATURE": getattr(cv2, "CAP_PROP_WB_TEMPERATURE", -1),
}
# =========================
# Okno i suwaki
# =========================
cv2.namedWindow("camera")
def nothing(x):
pass
# Zakresy są orientacyjne
cv2.createTrackbar("brightness", "camera", 128, 255, nothing)
cv2.createTrackbar("contrast", "camera", 128, 255, nothing)
cv2.createTrackbar("saturation", "camera", 128, 255, nothing)
cv2.createTrackbar("hue", "camera", 128, 255, nothing)
cv2.createTrackbar("gain", "camera", 0, 255, nothing)
cv2.createTrackbar("exposure", "camera", 0, 255, nothing)
# =========================
# Pętla główna
# =========================
while True:
try:
# Odczyt suwaków
brightness = cv2.getTrackbarPos("brightness", "camera")
contrast = cv2.getTrackbarPos("contrast", "camera")
saturation = cv2.getTrackbarPos("saturation", "camera")
hue = cv2.getTrackbarPos("hue", "camera")
gain = cv2.getTrackbarPos("gain", "camera")
exposure = cv2.getTrackbarPos("exposure", "camera")
# Ustawianie parametrów
print(f"{cap.set(cv2.CAP_PROP_BRIGHTNESS, brightness / 255.0)}")
print(f"{cap.set(cv2.CAP_PROP_CONTRAST, contrast / 255.0)}")
print(f"{cap.set(cv2.CAP_PROP_SATURATION, saturation / 255.0)}")
print(f"{cap.set(cv2.CAP_PROP_HUE, hue / 255.0)}")
print(f"{cap.set(cv2.CAP_PROP_GAIN, gain / 255.0)}")
# Exposure często wymaga innych wartości
# cap.set(cv2.CAP_PROP_EXPOSURE, float(exposure))
# Odczyt klatki
ret, frame = cap.read()
if not ret:
print("Błąd odczytu obrazu")
break
# Wyświetlanie aktualnych wartości REALNIE ustawionych
y = 20
for name, prop in camera_props.items():
if prop == -1:
continue
value = cap.get(prop)
text = f"{name}: {value:.3f}"
cv2.putText(
frame,
text,
(10, y),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 255, 0),
1
)
y += 20
cv2.imshow("camera", frame)
key = cv2.waitKey(1)
# ESC kończy
if key == 27:
break
except Exception:
print(f"exp:")
finally:
cap.release()
cv2.destroyAllWindows()
# =========================
# Sprzątanie
# =========================
cap.release()
cv2.destroyAllWindows()

21
test_pyuvc.py Normal file
View File

@@ -0,0 +1,21 @@
import uvc
# W Twojej wersji używamy bezpośrednio device_list()
devices = uvc.device_list()
print(f"Znalezione urządzenia: {devices}")
if devices:
try:
# Otwarcie pierwszego urządzenia z listy
# Często wymaga przekazania indeksu lub obiektu z listy
cap = uvc.Capture(devices[0]['uid'])
print(f"Połączono z: {cap.name}")
# Opcjonalnie: sprawdzenie czy działa
print(f"Dostępne tryby: {cap.available_modes}")
cap.close()
except Exception as e:
print(f"Błąd podczas otwierania kamery: {e}")
else:
print("Lista urządzeń jest pusta. Sprawdź podłączenie kamery.")

48
test_pyuvc_ex1.py Normal file
View File

@@ -0,0 +1,48 @@
import logging
from rich import print
from rich.logging import RichHandler
def main():
import uvc
devices = uvc.device_list()
print("Available devices", devices)
for device in devices:
try:
cap = uvc.Capture(device["uid"])
except uvc.DeviceNotFoundError:
continue
print(f"{cap.name}")
print("Available modes:")
for mode in cap.available_modes:
print(
f"MODE: {mode.width} x {mode.height} @ {mode.fps} ({mode.format_name})"
)
print("Iterating over frame sizes and rates")
for res in cap.frame_sizes:
cap.frame_size = res
for rate in cap.frame_rates:
cap.frame_rate = rate
print(f"RES/RATE: {res[0]} x {res[1]} @ {rate} Hz")
cap.close()
if __name__ == "__main__":
# import os
# os.environ["LIBUSB_DEBUG"] = "0"
logging.basicConfig(
level=logging.NOTSET,
handlers=[RichHandler(level="WARNING")],
format="%(message)s",
datefmt="[%X]",
)
main()

103
test_pyuvc_ex2.py Normal file
View File

@@ -0,0 +1,103 @@
import logging
import os
import time
from typing import Iterable, NamedTuple, Optional
import cv2
import uvc.uvc_bindings as uvc
from rich.logging import RichHandler
from rich.traceback import install as install_rich_traceback
class CameraSpec(NamedTuple):
name: str
width: int
height: int
fps: int
bandwidth_factor: float = 2.0
def main(camera_specs: Iterable[CameraSpec]):
devices = uvc.device_list()
cameras = {spec: init_camera_from_list(devices, spec) for spec in camera_specs}
if not all(cameras.values()):
raise RuntimeError(
"Could not initialize all specified cameras. Available: "
f"{[dev['name'] for dev in devices]}"
)
try:
keep_running = True
last_update = time.perf_counter()
while keep_running:
for spec, cam in cameras.items():
try:
frame = cam.get_frame(timeout=0.001)
except TimeoutError:
pass
# keep_running = False
# break
except uvc.InitError as err:
logging.debug(f"Failed to init {spec}: {err}")
keep_running = False
break
except uvc.StreamError as err:
logging.debug(f"Failed to get a frame for {spec}: {err}")
else:
data = frame.bgr if hasattr(frame, "bgr") else frame.gray
if frame.data_fully_received:
cv2.imshow(spec.name, data)
if (time.perf_counter() - last_update) > 1 / 60:
if cv2.waitKey(1) & 0xFF == 27:
break
last_update = time.perf_counter()
except KeyboardInterrupt:
pass
for cam in cameras.values():
cam.close()
def init_camera_from_list(devices, camera: CameraSpec) -> Optional[uvc.Capture]:
logging.debug(f"Searching {camera}...")
for device in devices:
if device["name"] == camera.name:
logging.debug(f"Found match by name")
capture = uvc.Capture(device["uid"])
capture.bandwidth_factor = camera.bandwidth_factor
for mode in capture.available_modes:
if mode[:3] == camera[1:4]: # compare width, height, fps
capture.frame_mode = mode
return capture
else:
logging.warning(
f"None of the available modes matched: {capture.available_modes}"
)
capture.close()
else:
logging.warning(f"No matching camera with name {camera.name!r} found")
if __name__ == "__main__":
os.environ["LIBUSB_DEBUG"] = "3"
install_rich_traceback()
logging.basicConfig(
level=logging.NOTSET,
handlers=[RichHandler(level="DEBUG")],
format="%(message)s",
datefmt="[%X]",
)
# logging.getLogger("uvc").setLevel("INFO")
main(
[
CameraSpec(
name="HD USB CAMERA",
width=1280,
height=720,
fps=30
)
]
)

363
test_qcamera.py Normal file
View File

@@ -0,0 +1,363 @@
import sys
import logging
from pathlib import Path
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QApplication, QMainWindow, QLabel
from PySide6.QtMultimedia import (
QMediaDevices,
QCamera,
QCameraDevice,
QMediaFormat,
QVideoFrameFormat,
QMediaCaptureSession
)
from PySide6.QtMultimediaWidgets import QVideoWidget
# ============================================================
# LOGGER
# ============================================================
LOG_DIR = Path("logs")
LOG_DIR.mkdir(exist_ok=True)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(LOG_DIR / "camera_debug.log", encoding="utf-8"),
logging.StreamHandler(sys.stdout),
],
)
logger = logging.getLogger("camera_app")
# ============================================================
# CAMERA WINDOW
# ============================================================
class CameraWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Camera Preview")
# Widget wyświetlający wyłącznie obraz
self.video_widget = QVideoWidget()
self.setCentralWidget(self.video_widget)
self.camera = None
self.camera_device = None
self.capture_session = QMediaCaptureSession()
self.init_camera()
# ========================================================
# CAMERA INIT
# ========================================================
def init_camera(self):
devices = QMediaDevices.videoInputs()
logger.info("==========================================")
logger.info("VIDEO DEVICES ENUMERATION")
logger.info("==========================================")
if not devices:
logger.error("Nie znaleziono kamer.")
return
for idx, device in enumerate(devices):
self.log_camera_device(device, idx)
# wybór pierwszej kamery
self.camera_device = devices[0]
logger.info(f"Wybrano kamerę: {self.camera_device.description()}")
self.camera = QCamera(self.camera_device)
# sygnały
self.camera.errorOccurred.connect(self.on_camera_error)
self.camera.activeChanged.connect(self.on_active_changed)
# video sink
# self.camera.setVideoOutput(self.video_widget)
self.capture_session.setCamera(self.camera)
self.capture_session.setVideoOutput(self.video_widget)
# przykładowe ustawienia
self.set_camera_format(
width=1280,
height=720,
fps=30,
preferred_pixel_format=QVideoFrameFormat.PixelFormat.Format_Jpeg
)
self.log_runtime_camera_capabilities()
self.camera.start()
logger.info("Kamera uruchomiona.")
# ========================================================
# FORMAT SELECTION
# ========================================================
def set_camera_format(
self,
width: int,
height: int,
fps: int,
preferred_pixel_format=QVideoFrameFormat.PixelFormat.Format_Jpeg,
):
# def set_camera_format(self, width: int, height: int, fps: int):
"""
Prototyp funkcji ustawiającej:
- rozdzielczość
- FPS
Wybiera najbliższy pasujący format.
"""
logger.info(
f"Próba ustawienia formatu: {width}x{height} @ {fps} FPS"
)
best_match = None
for fmt in self.camera_device.videoFormats():
resolution = fmt.resolution()
min_fps = fmt.minFrameRate()
max_fps = fmt.maxFrameRate()
pixel_format = fmt.pixelFormat()
logger.debug(
f"Sprawdzam format -> "
f"{resolution.width()}x{resolution.height()} "
f"FPS:{min_fps}-{max_fps} "
f"PIX:{pixel_format}"
)
if (
resolution.width() == width
and resolution.height() == height
and min_fps <= fps <= max_fps
and pixel_format == preferred_pixel_format
):
best_match = fmt
break
if best_match:
self.camera.setCameraFormat(best_match)
logger.info("Ustawiono format kamery:")
logger.info(
f"Resolution: "
f"{best_match.resolution().width()}x"
f"{best_match.resolution().height()}"
)
logger.info(
f"FPS range: "
f"{best_match.minFrameRate()} - "
f"{best_match.maxFrameRate()}"
)
logger.info(
f"Pixel format: {best_match.pixelFormat()}"
)
else:
logger.warning("Nie znaleziono pasującego formatu.")
# ========================================================
# DEVICE LOGGER
# ========================================================
def log_camera_device(self, device: QCameraDevice, idx: int):
logger.info("------------------------------------------")
logger.info(f"KAMERA #{idx}")
logger.info("------------------------------------------")
logger.info(f"Description: {device.description()}")
logger.info(f"ID: {device.id().data().decode(errors='ignore')}")
try:
logger.info(f"Is default: {device.isDefault()}")
except Exception as e:
logger.warning(f"isDefault() unsupported: {e}")
formats = device.videoFormats()
logger.info(f"Liczba formatów: {len(formats)}")
for i, fmt in enumerate(formats):
resolution = fmt.resolution()
logger.info(f"")
logger.info(f"FORMAT #{i}")
logger.info(
f"Resolution: "
f"{resolution.width()}x{resolution.height()}"
)
logger.info(
f"FPS min/max: "
f"{fmt.minFrameRate()} / {fmt.maxFrameRate()}"
)
logger.info(
f"Pixel format enum: {fmt.pixelFormat()}"
)
logger.info(
f"Pixel format name: "
f"{self.pixel_format_to_string(fmt.pixelFormat())}"
)
# ========================================================
# RUNTIME CAMERA CAPABILITIES
# ========================================================
def log_runtime_camera_capabilities(self):
logger.info("")
logger.info("==========================================")
logger.info("QCAMERA RUNTIME CAPABILITIES")
logger.info("==========================================")
try:
logger.info(f"Camera active: {self.camera.isActive()}")
except Exception as e:
logger.warning(e)
# Dostępne w zależności od backendu/platformy
properties = [
"focusMode",
"exposureMode",
"whiteBalanceMode",
"flashMode",
"torchMode",
]
for prop in properties:
try:
value = getattr(self.camera, prop)()
logger.info(f"{prop}: {value}")
except Exception as e:
logger.warning(f"{prop} unsupported: {e}")
# backend/platform info
logger.info("")
logger.info("QT MULTIMEDIA INFO")
try:
logger.info(f"Qt version: {QApplication.qtVersion()}")
except Exception:
pass
logger.info(
"Backend zależy od platformy:"
)
logger.info(
"- Windows -> MediaFoundation"
)
logger.info(
"- Linux -> GStreamer / PipeWire / V4L2"
)
logger.info(
"- macOS -> AVFoundation"
)
# ========================================================
# PIXEL FORMAT NAME
# ========================================================
def pixel_format_to_string(self, pixel_format):
mapping = {
QVideoFrameFormat.PixelFormat.Format_YUV420P: "YUV420P",
QVideoFrameFormat.PixelFormat.Format_NV12: "NV12",
QVideoFrameFormat.PixelFormat.Format_NV21: "NV21",
QVideoFrameFormat.PixelFormat.Format_UYVY: "UYVY",
QVideoFrameFormat.PixelFormat.Format_YUYV: "YUYV",
QVideoFrameFormat.PixelFormat.Format_Jpeg: "MJPG/JPEG",
}
return mapping.get(pixel_format, str(pixel_format))
# ========================================================
# SIGNALS
# ========================================================
def on_camera_error(self, error, error_string):
logger.error(f"CAMERA ERROR: {error}")
logger.error(f"ERROR STRING: {error_string}")
def on_active_changed(self, active):
logger.info(f"Camera active changed: {active}")
# ========================================================
# CLEANUP
# ========================================================
# def closeEvent(self, event):
# logger.info("Zamykanie aplikacji.")
# if self.camera:
# self.camera.stop()
# super().closeEvent(event)
def closeEvent(self, event):
logger.info("Zamykanie aplikacji.")
try:
if self.camera:
logger.info("Stopping camera...")
self.camera.stop()
if self.capture_session:
logger.info("Detaching camera from capture session...")
self.capture_session.setCamera(None)
self.capture_session.setVideoOutput(None)
except Exception as e:
logger.exception(e)
super().closeEvent(event)
# ============================================================
# MAIN
# ============================================================
def main():
app = QApplication(sys.argv)
window = CameraWindow()
# tylko obraz
window.resize(1280, 720)
# fullscreen:
# window.showFullScreen()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

0
tests/__init__.py Normal file
View File

180
tests/test_bbox_overlay.py Normal file
View File

@@ -0,0 +1,180 @@
"""Tests for BboxOverlay — coordinate mapping and state management."""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from PySide6.QtCore import QRect, QSize
from app.inference.bbox_overlay import BboxOverlay, Detection
class TestDetection:
def test_namedtuple_fields(self) -> None:
d = Detection(x1=10.0, y1=20.0, x2=100.0, y2=200.0, conf=0.87, label="label")
assert d.x1 == 10.0
assert d.label == "label"
assert d.conf == pytest.approx(0.87)
def test_immutable(self) -> None:
d = Detection(0, 0, 1, 1, 0.5, "x")
with pytest.raises(AttributeError):
d.conf = 0.9 # type: ignore[misc]
class TestBboxOverlayState:
def setup_method(self) -> None:
self.overlay = BboxOverlay()
def test_initially_no_detections(self) -> None:
assert self.overlay._detections == []
def test_initially_source_size_empty(self) -> None:
assert self.overlay._source_size.isEmpty()
def test_on_detections_stores_data(self) -> None:
dets = [Detection(0, 0, 100, 100, 0.9, "label")]
self.overlay.on_detections(dets, (640, 480))
assert self.overlay._detections == dets
assert self.overlay._source_size == QSize(640, 480)
def test_clear_removes_detections(self) -> None:
self.overlay.on_detections([Detection(0, 0, 10, 10, 0.5, "x")], (100, 100))
self.overlay.clear()
assert self.overlay._detections == []
def test_visible_by_default(self) -> None:
assert self.overlay.visible is True
def test_multiple_detections_stored(self) -> None:
dets = [
Detection(0, 0, 50, 50, 0.9, "label"),
Detection(100, 100, 200, 200, 0.75, "label"),
]
self.overlay.on_detections(dets, (640, 480))
assert len(self.overlay._detections) == 2
def test_replace_detections_on_new_call(self) -> None:
self.overlay.on_detections([Detection(0, 0, 10, 10, 0.5, "x")], (100, 100))
self.overlay.on_detections([], (100, 100))
assert self.overlay._detections == []
class TestBboxOverlayCoordinateMapping:
"""
Verify that BboxOverlay correctly maps source-frame pixel coordinates
onto the letterboxed video_rect when painting.
We don't test actual QPainter output — instead we verify that the
QRect values passed to painter.drawRect() correspond to the expected
scaled coordinates.
"""
def setup_method(self) -> None:
self.overlay = BboxOverlay()
def _make_painter_mock(self):
painter = MagicMock()
fm = MagicMock()
fm.height.return_value = 14
fm.ascent.return_value = 11
fm.horizontalAdvance.return_value = 60
painter.fontMetrics.return_value = fm
return painter
def test_paint_skips_when_no_detections(self) -> None:
painter = self._make_painter_mock()
self.overlay.paint(painter, QRect(0, 0, 640, 480))
painter.drawRect.assert_not_called()
def test_paint_skips_when_source_size_empty(self) -> None:
# detections present but source_size not set
self.overlay._detections = [Detection(0, 0, 100, 100, 0.9, "label")]
painter = self._make_painter_mock()
self.overlay.paint(painter, QRect(0, 0, 640, 480))
painter.drawRect.assert_not_called()
def test_bbox_scaled_to_full_video_rect(self) -> None:
"""
Source: 640×480, covers full frame.
video_rect: 640×480 at origin.
Detection: full-frame box → should map 1:1.
"""
self.overlay.on_detections(
[Detection(0.0, 0.0, 640.0, 480.0, 0.99, "label")],
(640, 480),
)
painter = self._make_painter_mock()
video_rect = QRect(0, 0, 640, 480)
self.overlay.paint(painter, video_rect)
# First drawRect call = the bounding box
first_call_rect: QRect = painter.drawRect.call_args_list[0][0][0]
assert first_call_rect.x() == 0
assert first_call_rect.y() == 0
assert first_call_rect.width() == 640
assert first_call_rect.height() == 480
def test_bbox_scaled_with_half_size_video_rect(self) -> None:
"""
Source: 640×480, video_rect: 320×240 at origin (0.5× scale).
Detection at (64, 48)→(128, 96) should map to (32, 24)→(64, 48).
"""
self.overlay.on_detections(
[Detection(64.0, 48.0, 128.0, 96.0, 0.8, "label")],
(640, 480),
)
painter = self._make_painter_mock()
video_rect = QRect(0, 0, 320, 240)
self.overlay.paint(painter, video_rect)
first_call_rect: QRect = painter.drawRect.call_args_list[0][0][0]
assert first_call_rect.x() == 32
assert first_call_rect.y() == 24
assert first_call_rect.width() == 32 # (128-64) * 0.5
assert first_call_rect.height() == 24 # (96-48) * 0.5
def test_bbox_offset_by_video_rect_origin(self) -> None:
"""
video_rect at (100, 50) — letterboxed with margins.
Detection at origin of source should map to (100, 50).
"""
self.overlay.on_detections(
[Detection(0.0, 0.0, 100.0, 100.0, 0.9, "label")],
(640, 480),
)
painter = self._make_painter_mock()
# video_rect 320×240 starting at (100, 50)
video_rect = QRect(100, 50, 320, 240)
self.overlay.paint(painter, video_rect)
first_call_rect: QRect = painter.drawRect.call_args_list[0][0][0]
# x: 100 + int(0 * 320/640) = 100
# y: 50 + int(0 * 240/480) = 50
assert first_call_rect.x() == 100
assert first_call_rect.y() == 50
class TestBboxOverlayWorkerPacket:
"""Test FramePacket and ResultPacket data structures."""
def test_frame_packet_fields(self) -> None:
from app.inference.worker import FramePacket
pkt = FramePacket(
frame_id=1,
raw_bytes=b"\x00" * 12,
width=2,
height=2,
channels=3,
)
assert pkt.frame_id == 1
assert pkt.width == 2
assert pkt.channels == 3
def test_result_packet_fields(self) -> None:
from app.inference.worker import ResultPacket
pkt = ResultPacket(frame_id=5, detections=[], width=640, height=480)
assert pkt.frame_id == 5
assert pkt.detections == []
assert pkt.width == 640

View File

@@ -0,0 +1,76 @@
"""Tests for FrameDispatcher."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from app.pipeline.frame_dispatcher import FrameDispatcher
def _make_frame():
frame = MagicMock()
frame.isValid.return_value = True
return frame
class TestFrameDispatcher:
def setup_method(self):
# FrameDispatcher is a QObject — needs QApplication
# Use minimal mock to avoid Qt dependency in unit tests
with patch("app.pipeline.frame_dispatcher.QObject.__init__", return_value=None):
self.dispatcher = FrameDispatcher.__new__(FrameDispatcher)
self.dispatcher._subscribers = []
self.dispatcher._frame_count = 0
self.dispatcher._last_dispatch_time = 0.0
def test_subscribe_adds_subscriber(self):
cb = MagicMock()
self.dispatcher.subscribe(cb)
assert self.dispatcher.subscriber_count() == 1
def test_subscribe_same_callback_twice_is_noop(self):
cb = MagicMock()
self.dispatcher.subscribe(cb)
self.dispatcher.subscribe(cb)
assert self.dispatcher.subscriber_count() == 1
def test_unsubscribe_removes_subscriber(self):
cb = MagicMock()
self.dispatcher.subscribe(cb)
self.dispatcher.unsubscribe(cb)
assert self.dispatcher.subscriber_count() == 0
def test_unsubscribe_nonexistent_does_not_raise(self):
cb = MagicMock()
self.dispatcher.unsubscribe(cb) # should not raise
def test_dispatch_calls_all_subscribers(self):
cb1 = MagicMock()
cb2 = MagicMock()
self.dispatcher.subscribe(cb1)
self.dispatcher.subscribe(cb2)
frame = _make_frame()
self.dispatcher.dispatch(frame)
cb1.assert_called_once_with(frame)
cb2.assert_called_once_with(frame)
def test_dispatch_increments_frame_count(self):
frame = _make_frame()
self.dispatcher.dispatch(frame)
self.dispatcher.dispatch(frame)
assert self.dispatcher.frame_count == 2
def test_dispatch_with_no_subscribers_does_not_raise(self):
frame = _make_frame()
self.dispatcher.dispatch(frame) # should not raise
def test_subscriber_exception_does_not_stop_others(self):
def bad_cb(f):
raise RuntimeError("boom")
good_cb = MagicMock()
self.dispatcher.subscribe(bad_cb)
self.dispatcher.subscribe(good_cb)
frame = _make_frame()
self.dispatcher.dispatch(frame) # should not raise
good_cb.assert_called_once_with(frame)

View File

@@ -0,0 +1,238 @@
"""Tests for InferenceManager — drop-if-busy, restart counter, model validation."""
from __future__ import annotations
import sys
from unittest.mock import MagicMock, patch
import pytest
from PySide6.QtWidgets import QApplication
from app.inference.worker_manager import InferenceManager
# Ensure a QApplication exists for tests that create Qt objects
_app = QApplication.instance() or QApplication(sys.argv)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_manager() -> InferenceManager:
"""Return an InferenceManager without starting any process."""
mgr = InferenceManager.__new__(InferenceManager)
mgr._model_path = None
mgr._process = None
mgr._input_queue = None
mgr._output_queue = None
mgr._stop_event = None
mgr._busy = False
mgr._frame_id = 0
mgr._restart_count = 0
mgr._last_result_time = 0.0
mgr._paused = False
return mgr
# ---------------------------------------------------------------------------
# Model path validation
# ---------------------------------------------------------------------------
class TestModelPathValidation:
def test_start_emits_error_for_missing_file(self, tmp_path) -> None:
"""start() with non-existent path must NOT spawn a process."""
mgr = InferenceManager()
errors: list[str] = []
mgr.inference_error.connect(errors.append)
mgr.start(str(tmp_path / "nonexistent.pt"))
assert errors, "Expected inference_error signal"
assert mgr._process is None
def test_start_does_not_raise_for_existing_file(self, tmp_path) -> None:
"""start() with existing file should attempt to start (we mock _start_worker)."""
model_file = tmp_path / "model.pt"
model_file.write_bytes(b"fake")
mgr = InferenceManager()
with patch.object(mgr, "_start_worker") as mock_start:
mgr.start(str(model_file))
mock_start.assert_called_once()
# ---------------------------------------------------------------------------
# Drop-if-busy logic
# ---------------------------------------------------------------------------
class TestDropIfBusy:
def test_submit_frame_drops_when_busy(self) -> None:
"""submit_frame must not enqueue when _busy is True."""
mgr = _make_manager()
mgr._busy = True
mgr._process = MagicMock()
mgr._process.is_alive.return_value = True
mgr._input_queue = MagicMock()
frame = MagicMock()
frame.isValid.return_value = True
mgr.submit_frame(frame)
mgr._input_queue.put_nowait.assert_not_called()
def test_submit_frame_drops_when_paused(self) -> None:
mgr = _make_manager()
mgr._paused = True
mgr._process = MagicMock()
mgr._process.is_alive.return_value = True
mgr._input_queue = MagicMock()
frame = MagicMock()
frame.isValid.return_value = True
mgr.submit_frame(frame)
mgr._input_queue.put_nowait.assert_not_called()
def test_submit_frame_drops_when_not_running(self) -> None:
mgr = _make_manager()
mgr._process = None
mgr._input_queue = MagicMock()
frame = MagicMock()
frame.isValid.return_value = True
mgr.submit_frame(frame)
mgr._input_queue.put_nowait.assert_not_called()
def test_submit_frame_drops_invalid_frame(self) -> None:
mgr = _make_manager()
mgr._process = MagicMock()
mgr._process.is_alive.return_value = True
mgr._input_queue = MagicMock()
frame = MagicMock()
frame.isValid.return_value = False
mgr.submit_frame(frame)
mgr._input_queue.put_nowait.assert_not_called()
# ---------------------------------------------------------------------------
# Pause / resume
# ---------------------------------------------------------------------------
class TestPauseResume:
def test_pause_sets_flag(self) -> None:
mgr = _make_manager()
assert mgr._paused is False
mgr.pause()
assert mgr._paused is True
def test_resume_clears_flag(self) -> None:
mgr = _make_manager()
mgr.pause()
mgr.resume()
assert mgr._paused is False
def test_is_paused_property(self) -> None:
mgr = _make_manager()
assert mgr.is_paused is False
mgr.pause()
assert mgr.is_paused is True
# ---------------------------------------------------------------------------
# Restart counter
# ---------------------------------------------------------------------------
class TestRestartCounter:
def test_handle_crash_increments_counter(self) -> None:
mgr = InferenceManager()
mgr._model_path = "fake.pt"
mgr._restart_count = 0
with (
patch.object(mgr, "_start_worker"),
patch.object(mgr._poll_timer, "stop"),
patch.object(mgr._watchdog_timer, "stop"),
):
mgr._handle_crash("test crash")
assert mgr._restart_count == 1
def test_handle_crash_emits_error_after_max_restarts(self) -> None:
from app.config import INFERENCE_MAX_RESTARTS
mgr = InferenceManager()
mgr._model_path = "fake.pt"
mgr._restart_count = INFERENCE_MAX_RESTARTS
errors: list[str] = []
mgr.inference_error.connect(errors.append)
with (
patch.object(mgr, "_start_worker") as mock_start,
patch.object(mgr._poll_timer, "stop"),
patch.object(mgr._watchdog_timer, "stop"),
):
mgr._handle_crash("final crash")
assert errors, "Expected inference_error signal after max restarts"
mock_start.assert_not_called()
def test_stop_resets_restart_count(self) -> None:
mgr = InferenceManager()
mgr._restart_count = 2
with patch.object(mgr, "_stop_worker"):
mgr.stop()
assert mgr._restart_count == 0
# ---------------------------------------------------------------------------
# is_running property
# ---------------------------------------------------------------------------
class TestIsRunning:
def test_not_running_when_process_is_none(self) -> None:
mgr = _make_manager()
assert mgr.is_running is False
def test_not_running_when_process_dead(self) -> None:
mgr = _make_manager()
proc = MagicMock()
proc.is_alive.return_value = False
mgr._process = proc
assert mgr.is_running is False
def test_running_when_process_alive(self) -> None:
mgr = _make_manager()
proc = MagicMock()
proc.is_alive.return_value = True
mgr._process = proc
assert mgr.is_running is True
# ---------------------------------------------------------------------------
# Worker data structures
# ---------------------------------------------------------------------------
class TestWorkerDataStructures:
def test_frame_packet_is_immutable(self) -> None:
from app.inference.worker import FramePacket
pkt = FramePacket(1, b"", 640, 480, 3)
with pytest.raises(AttributeError):
pkt.frame_id = 2 # type: ignore[misc]
def test_result_packet_is_immutable(self) -> None:
from app.inference.worker import ResultPacket
pkt = ResultPacket(1, [], 640, 480)
with pytest.raises(AttributeError):
pkt.frame_id = 2 # type: ignore[misc]
def test_select_device_returns_string(self) -> None:
from app.inference.worker import _select_device
device = _select_device()
assert isinstance(device, str)
assert device in ("cpu", "mps", "cuda")

View File

@@ -0,0 +1,142 @@
"""Tests for TelemetryCollector."""
from __future__ import annotations
import time
from collections import deque
from unittest.mock import MagicMock, patch
class TestTelemetryCollector:
"""Test telemetry calculations in isolation (no Qt event loop required)."""
def _make_collector(self, cpu_count: int = 8):
"""Construct a TelemetryCollector bypassing Qt machinery."""
from app.telemetry.telemetry_collector import TelemetryCollector
with patch.object(TelemetryCollector, "__init__", return_value=None):
col = TelemetryCollector.__new__(TelemetryCollector)
col._frame_times = deque(maxlen=120)
col._last_frame_time = 0.0
col._total_frames = 0
col._dropped_frames = 0
col._fps_window = deque()
col._fps_window_size_s = 1.0
col._target_fps = None
col._cpu_count = cpu_count
col._process = MagicMock()
# Simulate Windows: wset takes priority over rss
mem_info = MagicMock()
mem_info.wset = 50 * 1024 * 1024 # 50 MB private working set
mem_info.rss = 70 * 1024 * 1024 # RSS (larger, includes shared)
col._process.memory_info.return_value = mem_info
col._process.cpu_percent.return_value = 0.0
# Inference stats — None by default (inference disabled)
col._inference_device = None
col._inference_time_ms = None
return col
def test_initial_snapshot_has_zero_fps(self):
col = self._make_collector()
snap = col._compute_snapshot()
assert snap.fps == 0.0
def test_fps_counts_frames_in_window(self):
col = self._make_collector()
now = time.perf_counter()
for i in range(30):
col._fps_window.append(now - 0.9 + i * 0.03)
snap = col._compute_snapshot()
assert snap.fps == 30.0
def test_fps_excludes_old_frames(self):
col = self._make_collector()
now = time.perf_counter()
# 10 frames older than 1 second — should not count
for i in range(10):
col._fps_window.append(now - 2.0 + i * 0.05)
# 5 frames within the last second
for i in range(5):
col._fps_window.append(now - 0.4 + i * 0.05)
snap = col._compute_snapshot()
assert snap.fps == 5.0
def test_frame_time_average(self):
col = self._make_collector()
interval = 0.0333
for _ in range(10):
col._frame_times.append(interval)
snap = col._compute_snapshot()
assert abs(snap.frame_time_ms - interval * 1000) < 0.1
def test_drop_detection(self):
col = self._make_collector()
normal_interval = 0.016
now = time.perf_counter()
col._last_frame_time = now - normal_interval * 10
for _ in range(9):
col._last_frame_time += normal_interval
col._frame_times.append(normal_interval)
col._last_frame_time = now
with patch("app.telemetry.telemetry_collector.time") as mock_time:
col._last_frame_time = now
big_delta = normal_interval * 5 # 5× average → drop
mock_time.perf_counter.return_value = now + big_delta
delta = big_delta
col._frame_times.append(delta)
avg = sum(col._frame_times) / len(col._frame_times)
if delta > avg * 2.5:
col._dropped_frames += 1
assert col._dropped_frames == 1
def test_reset_counters(self):
col = self._make_collector()
col._total_frames = 100
col._dropped_frames = 5
col._frame_times.append(0.016)
col._fps_window.append(time.perf_counter())
col.reset_counters()
assert col._total_frames == 0
assert col._dropped_frames == 0
assert len(col._frame_times) == 0
assert len(col._fps_window) == 0
def test_snapshot_memory_mb(self):
col = self._make_collector()
snap = col._compute_snapshot()
assert snap.memory_mb == 50.0
def test_cpu_sys_is_core_divided_by_cpu_count(self):
col = self._make_collector(cpu_count=8)
col._process.cpu_percent.return_value = 80.0 # 80% of one core
snap = col._compute_snapshot()
assert snap.cpu_percent_core == 80.0
assert snap.cpu_percent_sys == round(80.0 / 8, 1)
def test_cpu_sys_never_exceeds_100_on_single_core_machine(self):
col = self._make_collector(cpu_count=1)
col._process.cpu_percent.return_value = 95.0
snap = col._compute_snapshot()
assert snap.cpu_percent_sys == snap.cpu_percent_core == 95.0
def test_cpu_sys_le_cpu_core(self):
"""cpu_percent_sys must always be <= cpu_percent_core."""
col = self._make_collector(cpu_count=4)
col._process.cpu_percent.return_value = 150.0
snap = col._compute_snapshot()
assert snap.cpu_percent_sys <= snap.cpu_percent_core
def test_target_fps_none_by_default(self):
col = self._make_collector()
snap = col._compute_snapshot()
assert snap.target_fps is None
def test_set_target_fps_reflected_in_snapshot(self):
col = self._make_collector()
col.set_target_fps(60.0)
snap = col._compute_snapshot()
assert snap.target_fps == 60.0

117
tests/test_uvc.py Normal file
View File

@@ -0,0 +1,117 @@
"""Tests for the UVC controller abstraction layer."""
from __future__ import annotations
from app.camera.uvc.base import UvcParam, UvcParamInfo
from app.camera.uvc.stub import NullUvcController
class TestNullUvcController:
"""NullUvcController must implement the full interface, all as no-ops."""
def setup_method(self) -> None:
self.ctrl = NullUvcController()
def test_is_not_open(self) -> None:
assert not self.ctrl.is_open()
def test_open_returns_false(self) -> None:
assert self.ctrl.open("any device") is False
def test_close_does_not_raise(self) -> None:
self.ctrl.close() # must not raise
def test_get_param_info_returns_unsupported(self) -> None:
info = self.ctrl.get_param_info(UvcParam.BRIGHTNESS)
assert isinstance(info, UvcParamInfo)
assert info.supported is False
assert info.param is UvcParam.BRIGHTNESS
def test_get_all_params_covers_all_uvc_params(self) -> None:
infos = self.ctrl.get_all_params()
returned_params = {i.param for i in infos}
all_params = set(UvcParam)
assert returned_params == all_params
def test_get_all_params_all_unsupported(self) -> None:
for info in self.ctrl.get_all_params():
assert info.supported is False
def test_set_value_returns_false(self) -> None:
assert self.ctrl.set_value(UvcParam.CONTRAST, 50) is False
def test_set_auto_returns_false(self) -> None:
assert self.ctrl.set_auto(UvcParam.WHITE_BALANCE, True) is False
def test_set_value_does_not_raise(self) -> None:
for param in UvcParam:
self.ctrl.set_value(param, 0) # must not raise
def test_set_auto_does_not_raise(self) -> None:
for param in UvcParam:
self.ctrl.set_auto(param, True) # must not raise
self.ctrl.set_auto(param, False)
class TestUvcParamInfo:
"""UvcParamInfo dataclass sanity checks."""
def test_defaults(self) -> None:
info = UvcParamInfo(param=UvcParam.BRIGHTNESS, supported=True)
assert info.minimum == 0
assert info.maximum == 100
assert info.default == 50
assert info.current == 50
assert info.step == 1
assert info.auto_supported is False
assert info.auto_enabled is False
def test_unsupported_flag(self) -> None:
info = UvcParamInfo(param=UvcParam.GAMMA, supported=False)
assert info.supported is False
def test_custom_range(self) -> None:
info = UvcParamInfo(
param=UvcParam.HUE,
supported=True,
minimum=-180,
maximum=180,
default=0,
current=-45,
)
assert info.minimum == -180
assert info.maximum == 180
assert info.current == -45
class TestMakeUvcControllerFallback:
"""make_uvc_controller falls back to NullUvcController when no lib available."""
def test_returns_controller_instance(self) -> None:
from app.camera.uvc import make_uvc_controller
from app.camera.uvc.base import UvcControllerBase
ctrl = make_uvc_controller("Test Camera")
assert isinstance(ctrl, UvcControllerBase)
def test_stub_used_when_native_lib_absent(self, monkeypatch) -> None:
"""If the native import fails, should return NullUvcController."""
import builtins
real_import = builtins.__import__
def patched_import(name, *args, **kwargs):
if name in ("duvc_ctl", "uvc"):
raise ImportError(f"Mocked missing: {name}")
return real_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", patched_import)
# Re-import to exercise factory with patched import
import importlib
import app.camera.uvc as uvc_pkg
importlib.reload(uvc_pkg)
ctrl = uvc_pkg.make_uvc_controller("Test Camera")
# Should be functional (not raise), may be Null or platform controller
from app.camera.uvc.base import UvcControllerBase
assert isinstance(ctrl, UvcControllerBase)
# Reload back to normal
importlib.reload(uvc_pkg)