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.py249
1 files changed, 91 insertions, 158 deletions
diff --git a/plugins/motion_capture/plugin.py b/plugins/motion_capture/plugin.py
index 1c655c5..0b44cb4 100644
--- a/plugins/motion_capture/plugin.py
+++ b/plugins/motion_capture/plugin.py
@@ -3,28 +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.
+
+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.
+
+Install OpenCV for real camera support:
+ pip install opencv-python
"""
from __future__ import annotations
-from typing import Any, Dict, List, Optional
-
-from PyQt6.QtWidgets import (
- QComboBox, QDoubleSpinBox, QFormLayout, QHBoxLayout,
- QLabel, QPushButton, QWidget,
-)
+from typing import List, Optional
from plugins.base_plugin import LabPlugin, PluginAction, PluginContext
@@ -44,8 +38,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"
@@ -53,158 +49,95 @@ class MotionCapturePlugin(LabPlugin):
# ── Lifecycle ─────────────────────────────────────────────────────────
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)
+ self._ctx = context
+ self._btn = None # toolbar button reference
+ self._windows: dict = {} # device_id → MotionCaptureWindow
+ 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
+ for win in list(self._windows.values()):
+ win.close()
+ self._windows.clear()
+ from ui.add_device_dialog import _PANELS
+ _PANELS.pop("Camera", None)
# ── Integration hooks ─────────────────────────────────────────────────
- def get_devices(self) -> list:
- return [self._device]
+ def get_toolbar_actions(self) -> list:
+ return [PluginAction(
+ label="Motion Capture",
+ icon="🎥",
+ tooltip="Open camera tracking view",
+ checkable=True,
+ callback=self._on_btn_toggled,
+ button_ref_callback=lambda btn: setattr(self, "_btn", btn),
+ )]
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
+ # ── Internal ──────────────────────────────────────────────────────────
- # ── Profile state ─────────────────────────────────────────────────────
+ def _camera_devices(self) -> list:
+ from device import CameraDevice
+ return [d for d in self._ctx.registry.all_instances()
+ if isinstance(d, CameraDevice)]
- 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 _on_btn_toggled(self, checked: bool) -> None:
+ from PyQt6.QtWidgets import QInputDialog, QMessageBox
+ if not checked:
+ return
- 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
+ cameras = self._camera_devices()
- # ── Internal ──────────────────────────────────────────────────────────
+ if not cameras:
+ QMessageBox.information(
+ None, "No Camera Devices",
+ "Add a Camera device first via Devices → Add Device.",
+ )
+ if self._btn is not None:
+ self._btn.setChecked(False)
+ return
- 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_()
+ if len(cameras) == 1:
+ self._open_window(cameras[0])
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:
+ names = [f"{d.info.name} ({d.info.device_id})" for d in cameras]
+ choice, ok = QInputDialog.getItem(
+ None, "Select Camera", "Open tracking view for:", names, 0, False,
+ )
+ if not ok:
+ if self._btn is not None:
+ self._btn.setChecked(False)
+ return
+ idx = names.index(choice)
+ self._open_window(cameras[idx])
+
+ def _open_window(self, device) -> None:
+ from window import MotionCaptureWindow
+ dev_id = device.info.device_id
+ if dev_id in self._windows:
+ win = self._windows[dev_id]
+ win.show(); win.raise_()
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)
+ if device._tracker is None:
+ from PyQt6.QtWidgets import QMessageBox
+ QMessageBox.warning(
+ None, "Camera Not Connected",
+ f"{device.info.name} is not connected. Connect it first.",
+ )
+ if self._btn is not None:
+ self._btn.setChecked(False)
+ return
+ win = MotionCaptureWindow(device._tracker)
+ win.closed.connect(lambda: self._close_window(dev_id))
+ self._windows[dev_id] = win
+ win.show()
+
+ def _close_window(self, dev_id: str) -> None:
+ self._windows.pop(dev_id, None)
+ if not self._windows and self._btn is not None:
+ self._btn.setChecked(False)