diff options
| author | Christian Kolset <ckolset@colostate.edu> | 2026-08-17 10:02:16 -0600 |
|---|---|---|
| committer | Christian Kolset <ckolset@colostate.edu> | 2026-08-17 10:02:16 -0600 |
| commit | abab8f7938dada9a2abff31e719186b99934601a (patch) | |
| tree | 6f013cb54256205b3eadf467001e86a882a7f57b /plugins/motion_capture/window.py | |
| parent | e749cca30da2d02839aca736873e7e5ad45e191c (diff) | |
Fix motion capture UI freezes: throttling, async camera open/scan, stale poll
Several compounding freeze sources in plugins/motion_capture/:
- tracker.py: _run_real had no frame-rate cap independent of the camera's
configured fps (up to 120fps/2560x1440 was possible), and no bound on
consecutive failed reads — a camera that "opens" (cv2 isOpened() can lie,
seen with Windows MSMF/obsensor) but never actually delivers frames would
get hammered forever. Added a 20Hz processing cap, a real test-read before
committing to a camera, and a give-up-after-30-failures fallback to
simulation.
- tracker.py/device.py: cv2.VideoCapture() opening a real camera is a slow,
blocking call — CameraTracker.start() used to do this synchronously on
the caller's thread, which froze the whole app when ProfileManager.apply()
called it during profile load. Now runs on the background thread; status
reporting (CameraDevice) moved to an on_result callback since the caller
can no longer learn the outcome synchronously.
- window.py: the Camera View preview ran a 30Hz GUI-thread timer doing a
full-frame copy + QImage/QPixmap rebuild every tick unconditionally (no
visibility check) — dropped to 10Hz and skips entirely when hidden.
Camera enumeration (list_cameras(), up to 8 sequential cv2.VideoCapture
probes) also ran inline in the window constructor, blocking it open the
same way camera_panel.py's CameraScanThread already avoided for the Add
Device dialog — moved to the same async pattern here.
config_window.py: separately, ConfigWindow._dev_poll (device status label
refresh) was stopped in closeEvent and never restarted, since MainWindow
reuses the same ConfigWindow instance across opens — invisible before
because connect() was synchronous, but with the above change a device can
sit in CONNECTING for a while, and a stale poll made that look stuck
forever after reopening the window. Restart it in showEvent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'plugins/motion_capture/window.py')
| -rw-r--r-- | plugins/motion_capture/window.py | 47 |
1 files changed, 40 insertions, 7 deletions
diff --git a/plugins/motion_capture/window.py b/plugins/motion_capture/window.py index 46b8850..328c61b 100644 --- a/plugins/motion_capture/window.py +++ b/plugins/motion_capture/window.py @@ -16,7 +16,7 @@ from typing import Optional, Tuple import numpy as np -from PyQt6.QtCore import Qt, QPointF, QTimer, pyqtSignal +from PyQt6.QtCore import Qt, QPointF, QTimer, QThread, pyqtSignal from PyQt6.QtGui import ( QCloseEvent, QColor, QFont, QImage, QMouseEvent, QPainter, QPen, QPixmap, @@ -27,6 +27,17 @@ from PyQt6.QtWidgets import ( ) +# ── Background camera enumeration ─────────────────────────────────────────── + +class _CameraListThread(QThread): + """Runs tracker.list_cameras() off the GUI thread — see _scan_cameras.""" + done = pyqtSignal(list) + + def run(self): + from tracker import list_cameras + self.done.emit(list_cameras()) + + # ── Frame display + rubber-band selection widget ────────────────────────────── class FrameSelector(QWidget): @@ -262,7 +273,8 @@ class MotionCaptureWindow(QWidget): self._refresh = QTimer(self) self._refresh.timeout.connect(self._update) - self._refresh.start(33) + self._refresh.start(100) # 10 Hz — a position readout doesn't need 30fps, + # and this is a full-frame copy + repaint on the GUI thread # ── UI ──────────────────────────────────────────────────────────────── @@ -377,15 +389,31 @@ class MotionCaptureWindow(QWidget): # ── Actions ─────────────────────────────────────────────────────────── def _scan_cameras(self): - from tracker import list_cameras - current_idx = self.get_camera_index() + # list_cameras() tries cv2.VideoCapture() on several indices + # sequentially — each one can be slow to fail (backend/driver + # probing), so this used to run inline here and block the whole + # app while the window opened. Run it on a background thread instead + # (same pattern camera_panel.py's CameraScanThread already uses for + # the Add Device dialog's scan). + self._pending_cam_idx = self.get_camera_index() + self._refresh_btn.setEnabled(False) self._cam_combo.blockSignals(True) self._cam_combo.clear() - self._cameras = list_cameras() + self._cam_combo.addItem("Scanning…") + self._cam_combo.blockSignals(False) + self._scan_thread = _CameraListThread(self) + self._scan_thread.done.connect(self._on_cameras_scanned) + self._scan_thread.start() + + def _on_cameras_scanned(self, cameras: list): + self._refresh_btn.setEnabled(True) + self._cam_combo.blockSignals(True) + self._cam_combo.clear() + self._cameras = cameras for _, label in self._cameras: self._cam_combo.addItem(label) for i, (idx, _) in enumerate(self._cameras): - if idx == current_idx: + if idx == self._pending_cam_idx: self._cam_combo.setCurrentIndex(i) break self._cam_combo.blockSignals(False) @@ -466,9 +494,14 @@ class MotionCaptureWindow(QWidget): # ── Display update ───────────────────────────────────────────────────── def _update(self): - if not self._connected: + if not self._connected or not self.isVisible(): return + # simulated flips async now (camera opens on the tracker's background + # thread, see CameraTracker._open_and_run) — keep the badge in sync + # instead of reading a value that was still stale at Connect-time. + self._sim_badge.setVisible(self._tracker.simulated) + if not self._selecting: frame = self._tracker.get_frame() if frame is not None: |
