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/device.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/device.py')
| -rw-r--r-- | plugins/motion_capture/device.py | 124 |
1 files changed, 96 insertions, 28 deletions
diff --git a/plugins/motion_capture/device.py b/plugins/motion_capture/device.py index f0d082c..9806030 100644 --- a/plugins/motion_capture/device.py +++ b/plugins/motion_capture/device.py @@ -1,34 +1,39 @@ """ motion_capture/device.py -BaseDevice that reads x/y position from CameraTracker. +BaseDevice implementation for a camera-based motion tracker. + +Each CameraDevice owns a CameraTracker that runs its own background +capture thread. connect() opens the camera; disconnect() releases it. 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. +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 typing import Any, Dict, Optional from devices.base_device import ( BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus, ) -class MotionCaptureDevice(BaseDevice): +class CameraDevice(BaseDevice): - def __init__(self, tracker, device_id: str = "motion_capture"): + def __init__(self, device_id: str = "cam_0", + camera_index: int = 0, + simulate: bool = False): + name = "Camera (Sim)" if simulate else f"Camera {camera_index}" super().__init__(DeviceInfo( device_id = device_id, - name = "Motion Capture", - device_type = "virtual", + name = name, + device_type = "camera", description = "Camera point-tracking — x/y position in pixels.", - icon = "🎥", + icon = "📷", channels = [ ChannelConfig( channel_id = "x_pos", @@ -48,38 +53,101 @@ class MotionCaptureDevice(BaseDevice): ), ], )) - self._tracker = tracker - self._lock = threading.Lock() - self._last = (0.0, 0.0) + self._camera_index = camera_index + self._simulate = simulate + self._tracker: Optional[Any] = None + self._tracking_win = None - # ── BaseDevice interface ────────────────────────────────────────────── + # ── BaseDevice ──────────────────────────────────────────────────────── def connect(self) -> bool: - self.status = DeviceStatus.SIMULATED + from tracker import CameraTracker + self._tracker = CameraTracker() + self._tracker.start(-1 if self._simulate else self._camera_index) + if self._tracker.simulated: + self.status = DeviceStatus.SIMULATED + else: + self.status = DeviceStatus.CONNECTED return True def disconnect(self) -> None: + if self._tracking_win is not None: + self._tracking_win.close() + self._tracking_win = None + if self._tracker is not None: + self._tracker.stop() + self._tracker = None self.status = DeviceStatus.DISCONNECTED def read_channels(self) -> Dict[str, float]: + if self._tracker is None: + return {"x_pos": 0.0, "y_pos": 0.0} 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} + if pos is None: + return {"x_pos": 0.0, "y_pos": 0.0} + x, y = pos + return {"x_pos": float(x), "y_pos": float(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." + from PyQt6.QtWidgets import ( + QFormLayout, QLabel, QPushButton, QVBoxLayout, QWidget, + ) + + w = QWidget() + lay = QVBoxLayout(w) + lay.setContentsMargins(8, 8, 8, 8) + lay.setSpacing(10) + + form = QFormLayout() + form.setContentsMargins(0, 0, 0, 0) + + src_lbl = QLabel( + "Simulation" if self._simulate else f"Camera {self._camera_index}" + ) + src_lbl.setObjectName("traceSource") + form.addRow("Source:", src_lbl) + + self._status_lbl = QLabel(self.status.name.title()) + self._status_lbl.setObjectName("traceSource") + form.addRow("Status:", self._status_lbl) + + lay.addLayout(form) + + open_btn = QPushButton("🎥 Open Camera View") + open_btn.setObjectName("addTraceBtn") + open_btn.clicked.connect(self._open_tracking_window) + lay.addWidget(open_btn) + + hint = QLabel( + "In the Camera View: click the frame to set the tracking\n" + "point. Apply the 'pixel_to_mm' filter in Signals → Channels\n" + "to convert pixel coordinates to real-world units." ) - lbl.setObjectName("traceSource") - lbl.setWordWrap(True) - return lbl + hint.setObjectName("traceSource") + hint.setWordWrap(True) + lay.addWidget(hint) + lay.addStretch() + + return w + + # ── Internal ────────────────────────────────────────────────────────── + + def _open_tracking_window(self): + if self._tracker is None: + return + from window import MotionCaptureWindow + if self._tracking_win is None: + self._tracking_win = MotionCaptureWindow(self._tracker) + self._tracking_win.closed.connect(self._on_tracking_win_closed) + self._tracking_win.show() + self._tracking_win.raise_() + + def _on_tracking_win_closed(self): + self._tracking_win = None + + +# Backward-compat alias for any profiles that reference the old class name +MotionCaptureDevice = CameraDevice |
