1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
"""
motion_capture/plugin.py
LabDAQ Motion Capture plugin.
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 List, Optional
from plugins.base_plugin import LabPlugin, PluginAction, PluginContext
class MotionCapturePlugin(LabPlugin):
# ── Metadata ──────────────────────────────────────────────────────────
@property
def plugin_id(self) -> str: return "motion_capture"
@property
def name(self) -> str: return "Motion Capture"
@property
def version(self) -> str: return "1.0.0"
@property
def description(self) -> str:
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"
# ── Lifecycle ─────────────────────────────────────────────────────────
def on_load(self, context: PluginContext) -> None:
self._ctx = context
self._btn = None # toolbar button reference
self._windows: dict = {} # device_id → MotionCaptureWindow
self._window_state: dict = {} # device_id → {px_per_mm, camera_idx}
from ui.add_device_dialog import _PANELS
from camera_panel import CameraPanel
_PANELS["Camera"] = (CameraPanel, "cam")
from core.profile import ProfileManager
from device import CameraDevice
ProfileManager.register_device_factory(CameraDevice.DEVICE_TYPE, CameraDevice)
def on_unload(self) -> None:
for win in list(self._windows.values()):
win.close()
self._windows.clear()
from ui.add_device_dialog import _PANELS
_PANELS.pop("Camera", None)
from core.profile import ProfileManager
from device import CameraDevice
ProfileManager.unregister_device_factory(CameraDevice.DEVICE_TYPE)
# ── Integration hooks ─────────────────────────────────────────────────
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}
# ── Internal ──────────────────────────────────────────────────────────
def _camera_devices(self) -> list:
from device import CameraDevice
return [d for d in self._ctx.registry.all_instances()
if isinstance(d, CameraDevice)]
def _on_btn_toggled(self, checked: bool) -> None:
from PyQt6.QtWidgets import QInputDialog, QMessageBox
if not checked:
return
cameras = self._camera_devices()
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
if len(cameras) == 1:
self._open_window(cameras[0])
else:
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
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)
# Restore mode/shape from live tracker state
win.restore_ui_state(
mode=device._tracker.track_mode,
shape=device._tracker.template_shape,
)
# Restore px/mm and camera index from last session
if dev_id in self._window_state:
state = self._window_state[dev_id]
win.set_px_per_mm(state["px_per_mm"])
win.set_camera_index(state["camera_idx"])
win.closed.connect(lambda d=dev_id, w=win: self._on_window_closed(d, w))
self._windows[dev_id] = win
win.show()
def _on_window_closed(self, dev_id: str, win) -> None:
self._window_state[dev_id] = {
"px_per_mm": win.get_px_per_mm(),
"camera_idx": win.get_camera_index(),
}
self._windows.pop(dev_id, None)
if not self._windows and self._btn is not None:
self._btn.setChecked(False)
|