diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-06-04 12:29:24 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-06-04 12:41:14 -0600 |
| commit | 864642a84d8e93cc4971c7915e39a60740cdd6fa (patch) | |
| tree | 447cd7eedf597f4da51feff4b15ae8d0a5e4bdf0 /plugins/motion_capture/plugin.py | |
| parent | 254df94e71586216991486242ab4064706177465 (diff) | |
Redesign motion capture plugin as Camera device type
Plugin now registers a "Camera" option in the Add Device dialog on load
(removes it on unload) instead of auto-creating a single device.
- camera_panel.py (new): CameraPanel with CameraScanThread that probes
OpenCV indices 0–5, lists found cameras with resolution, shows
"No cameras detected" when none available, simulation checkbox.
build_device() returns CameraDevice for the selected index.
- device.py: rewrite MotionCaptureDevice → CameraDevice. Each instance
owns its own CameraTracker created in connect(). Status is CONNECTED
or SIMULATED based on tracker.simulated. get_config_widget() shows
source/status and an "Open Camera View" button that launches the
existing MotionCaptureWindow for that device's tracker. Backward-compat
alias MotionCaptureDevice = CameraDevice kept for saved profiles.
- plugin.py: simplified to on_load/_PANELS registration + pixel_to_mm
filter. No auto-devices, no toolbar button, no settings widget.
Camera data flows through the standard Device → Signal → Channel → Plot
pipeline like any other device.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'plugins/motion_capture/plugin.py')
| -rw-r--r-- | plugins/motion_capture/plugin.py | 189 |
1 files changed, 21 insertions, 168 deletions
diff --git a/plugins/motion_capture/plugin.py b/plugins/motion_capture/plugin.py index 1c655c5..6eb5508 100644 --- a/plugins/motion_capture/plugin.py +++ b/plugins/motion_capture/plugin.py @@ -3,30 +3,22 @@ motion_capture/plugin.py LabDAQ Motion Capture plugin. -Tracks a user-selected point via webcam (OpenCV CSRT tracker) and -streams x/y pixel coordinates as live channels into the acquisition -pipeline. Falls back to a Lissajous simulation when OpenCV is absent. - -Custom filter: - pixel_to_mm — convert pixel coords to millimetres using a - px_per_mm calibration value. Add it in Signals → Channels on - the x_pos or y_pos channel. - -Install OpenCV to use a real camera: - Arch Linux: sudo pacman -S python-opencv - Other: pip install opencv-python -""" +When enabled, adds a "Camera" device type to the Add Device dialog. +Each camera added by the user becomes a first-class device in the +acquisition pipeline — data flows through the standard +Device → Signal → Channel → Plot stream. -from __future__ import annotations +Custom filter registered globally: + pixel_to_mm — convert pixel coords to mm using a px_per_mm value. + Add it in Signals → Channels on x_pos or y_pos. -from typing import Any, Dict, List, Optional +Install OpenCV for real camera support: + pip install opencv-python +""" -from PyQt6.QtWidgets import ( - QComboBox, QDoubleSpinBox, QFormLayout, QHBoxLayout, - QLabel, QPushButton, QWidget, -) +from __future__ import annotations -from plugins.base_plugin import LabPlugin, PluginAction, PluginContext +from plugins.base_plugin import LabPlugin, PluginContext class MotionCapturePlugin(LabPlugin): @@ -44,8 +36,10 @@ class MotionCapturePlugin(LabPlugin): @property def description(self) -> str: - return ("Track a point via webcam and stream x/y position as live signals. " - "Apply pixel_to_mm filter to convert to real-world units.") + return ( + "Adds a Camera device type to the Add Device dialog. " + "Stream x/y tracking coordinates directly into the signal pipeline." + ) @property def author(self) -> str: return "LabDAQ" @@ -54,157 +48,16 @@ class MotionCapturePlugin(LabPlugin): def on_load(self, context: PluginContext) -> None: self._ctx = context - self._win: Optional[object] = None - self._toolbar_btn = None - - from tracker import CameraTracker - from device import MotionCaptureDevice - - self._tracker = CameraTracker() - self._device = MotionCaptureDevice(self._tracker) + from ui.add_device_dialog import _PANELS + from camera_panel import CameraPanel + _PANELS["Camera"] = (CameraPanel, "cam") def on_unload(self) -> None: - self._tracker.stop() - if self._win is not None: - self._win.close() - self._win = None + from ui.add_device_dialog import _PANELS + _PANELS.pop("Camera", None) # ── Integration hooks ───────────────────────────────────────────────── - def get_devices(self) -> list: - return [self._device] - def get_filter_classes(self) -> dict: from filter import PixelToMMFilter return {"pixel_to_mm": PixelToMMFilter} - - def get_toolbar_actions(self) -> List[PluginAction]: - return [ - PluginAction( - label = "Motion Capture", - icon = "🎥", - tooltip = "Open motion capture window", - checkable = True, - callback = self._toggle_window, - button_ref_callback = self._store_btn, - ) - ] - - def get_settings_widget(self) -> Optional[QWidget]: - from tracker import list_cameras - w = QWidget() - lay = QFormLayout(w) - lay.setContentsMargins(0, 4, 0, 4) - - cam_row = QHBoxLayout() - self._s_cam = QComboBox() - self._s_cam.setObjectName("channelPickerCb") - self._s_cameras: list = [] - self._s_refresh_btn = QPushButton("⟳") - self._s_refresh_btn.setObjectName("configButton") - self._s_refresh_btn.setFixedWidth(28) - self._s_refresh_btn.setToolTip("Rescan cameras") - self._s_refresh_btn.clicked.connect(lambda: self._populate_settings_cam(list_cameras)) - cam_row.addWidget(self._s_cam, 1) - cam_row.addWidget(self._s_refresh_btn) - lay.addRow("Camera:", cam_row) - - self._populate_settings_cam(list_cameras) - self._s_cam.currentIndexChanged.connect(self._sync_cam_to_window) - - self._s_pxmm = QDoubleSpinBox() - self._s_pxmm.setRange(0.01, 100_000) - self._s_pxmm.setDecimals(2) - self._s_pxmm.setSuffix(" px/mm") - self._s_pxmm.setValue( - self._win.get_px_per_mm() if self._win else 10.0) - self._s_pxmm.setObjectName("cfgGlobalSpin") - self._s_pxmm.valueChanged.connect(self._sync_pxmm_to_window) - lay.addRow("Calibration:", self._s_pxmm) - - hint = QLabel( - "Calibration: count how many pixels span a known real distance\n" - "in the camera frame, then divide pixels ÷ mm." - ) - hint.setObjectName("traceSource") - hint.setWordWrap(True) - lay.addRow(hint) - - return w - - # ── Profile state ───────────────────────────────────────────────────── - - def get_save_state(self) -> Dict[str, Any]: - return { - "camera_index": self._win.get_camera_index() if self._win else 0, - "px_per_mm": self._win.get_px_per_mm() if self._win else 10.0, - } - - def apply_save_state(self, state: Dict[str, Any]) -> None: - if self._win is not None: - self._win.set_camera_index(state.get("camera_index", 0)) - self._win.set_px_per_mm(state.get("px_per_mm", 10.0)) - # Store for when window is opened later - self._pending_state = state - - # ── Internal ────────────────────────────────────────────────────────── - - def _store_btn(self, btn) -> None: - self._toolbar_btn = btn - - def _toggle_window(self, checked: bool) -> None: - if self._win is None: - from window import MotionCaptureWindow - self._win = MotionCaptureWindow( - self._tracker, self._ctx.main_window) - self._win.closed.connect(self._on_win_closed) - - # Apply any state that arrived before window was created - state = getattr(self, "_pending_state", None) - if state: - self._win.set_camera_index(state.get("camera_index", 0)) - self._win.set_px_per_mm(state.get("px_per_mm", 10.0)) - - if checked: - if not self._win.isVisible(): - from PyQt6.QtWidgets import QApplication - mw = self._ctx.main_window - screen = QApplication.screenAt(mw.geometry().center()) or QApplication.primaryScreen() - avail = screen.availableGeometry() - geo = mw.normalGeometry() - self._win.adjustSize() - x = max(avail.left(), min(geo.right() + 8, avail.right() - self._win.width())) - y = max(avail.top(), min(geo.top() + 40, avail.bottom() - self._win.height())) - self._win.move(x, y) - self._win.show() - self._win.raise_() - else: - self._win.hide() - - def _on_win_closed(self) -> None: - if self._toolbar_btn is not None: - self._toolbar_btn.setChecked(False) - - def _populate_settings_cam(self, list_cameras_fn) -> None: - current = self._win.get_camera_index() if self._win else 0 - self._s_cam.blockSignals(True) - self._s_cam.clear() - self._s_cameras = list_cameras_fn() - for _, label in self._s_cameras: - self._s_cam.addItem(label) - for i, (idx, _) in enumerate(self._s_cameras): - if idx == current: - self._s_cam.setCurrentIndex(i) - break - self._s_cam.blockSignals(False) - - def _sync_cam_to_window(self, combo_i: int) -> None: - if not self._s_cameras: - return - idx = self._s_cameras[combo_i][0] - if self._win: - self._win.set_camera_index(idx) - - def _sync_pxmm_to_window(self, v: float) -> None: - if self._win: - self._win.set_px_per_mm(v) |
