diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-05-07 13:42:30 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-06-03 09:50:17 -0600 |
| commit | 8e5d0aec942fb9c49040fc525f0d697fd24d7c2f (patch) | |
| tree | a9636db29be900698541441cb93b486b9a3c0acb /plugins/motion_capture/device.py | |
| parent | deb3ad65d25d4c3167a3cbeca2ac19b5c45b0627 (diff) | |
Added Motion Capture plugin
Tracks a user-selected point via webcam (OpenCV CSRT tracker) and
streams x_pos / y_pos pixel coordinates as live channels into the
acquisition pipeline. Falls back to a Lissajous figure simulation
when opencv-python is not installed, so the plugin works immediately
without hardware.
- tracker.py: background thread, real camera + sim modes, thread-safe API
- device.py: BaseDevice reading x/y from tracker, wires into engine
- filter.py: custom pixel_to_mm filter (registered in FILTER_CLASSES)
- window.py: floating tool window, live feed, click-to-track ROI selection
- plugin.py: LabPlugin wiring all pieces together, toolbar button,
settings widget, profile save/restore
Also: PluginAction.button_ref_callback lets plugins store a ref to their
toolbar button so they can uncheck it when their window closes.
opencv-python install note added to requirements.txt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'plugins/motion_capture/device.py')
| -rw-r--r-- | plugins/motion_capture/device.py | 85 |
1 files changed, 85 insertions, 0 deletions
diff --git a/plugins/motion_capture/device.py b/plugins/motion_capture/device.py new file mode 100644 index 0000000..f0d082c --- /dev/null +++ b/plugins/motion_capture/device.py @@ -0,0 +1,85 @@ +""" +motion_capture/device.py + +BaseDevice that reads x/y position from CameraTracker. + +Channels: + x_pos — horizontal position [px] (left=0, right=frame_width) + y_pos — vertical position [px] (top=0, bottom=frame_height) + +Apply the pixel_to_mm filter in Signals → Channels to convert to mm. +""" + +from __future__ import annotations + +import threading +from typing import Any, Dict + +from devices.base_device import ( + BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus, +) + + +class MotionCaptureDevice(BaseDevice): + + def __init__(self, tracker, device_id: str = "motion_capture"): + super().__init__(DeviceInfo( + device_id = device_id, + name = "Motion Capture", + device_type = "virtual", + description = "Camera point-tracking — x/y position in pixels.", + icon = "🎥", + channels = [ + ChannelConfig( + channel_id = "x_pos", + name = "X Position", + unit = "px", + min_value = 0.0, + max_value = 1920.0, + color = "#00d4ff", + ), + ChannelConfig( + channel_id = "y_pos", + name = "Y Position", + unit = "px", + min_value = 0.0, + max_value = 1080.0, + color = "#f72585", + ), + ], + )) + self._tracker = tracker + self._lock = threading.Lock() + self._last = (0.0, 0.0) + + # ── BaseDevice interface ────────────────────────────────────────────── + + def connect(self) -> bool: + self.status = DeviceStatus.SIMULATED + return True + + def disconnect(self) -> None: + self.status = DeviceStatus.DISCONNECTED + + def read_channels(self) -> Dict[str, float]: + pos = self._tracker.get_position() + if pos is not None: + with self._lock: + self._last = pos + with self._lock: + x, y = self._last + return {"x_pos": x, "y_pos": y} + + def write_channel(self, channel_id: str, value: Any) -> bool: + return False + + def get_config_widget(self): + from PyQt6.QtWidgets import QLabel + lbl = QLabel( + "Configure via the Motion Capture toolbar window.\n\n" + "Apply 'pixel_to_mm' filter in Signals → Channels\n" + "to convert pixel coordinates to millimetres." + ) + lbl.setObjectName("traceSource") + lbl.setWordWrap(True) + return lbl |
