summaryrefslogtreecommitdiff
path: root/plugins/motion_capture/plugin.py
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/motion_capture/plugin.py')
-rw-r--r--plugins/motion_capture/plugin.py189
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)