summaryrefslogtreecommitdiff
path: root/plugins/motion_capture/device.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-06-04 12:29:24 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-06-04 12:41:14 -0600
commit864642a84d8e93cc4971c7915e39a60740cdd6fa (patch)
tree447cd7eedf597f4da51feff4b15ae8d0a5e4bdf0 /plugins/motion_capture/device.py
parent254df94e71586216991486242ab4064706177465 (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.py124
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