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
|
"""
motion_capture/device.py
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.
"""
from __future__ import annotations
from typing import Any, Dict, Optional
from devices.base_device import (
BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus,
)
class CameraDevice(BaseDevice):
def __init__(self, device_id: str = "cam_0",
camera_index: int = 0,
simulate: bool = False,
resolution=None):
name = "Camera (Sim)" if simulate else f"Camera {camera_index}"
super().__init__(DeviceInfo(
device_id = device_id,
name = name,
device_type = "camera",
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._camera_index = camera_index
self._simulate = simulate
self._resolution = resolution
self._tracker: Optional[Any] = None
self._tracking_win = None
# ── BaseDevice ────────────────────────────────────────────────────────
def connect(self) -> bool:
from tracker import CameraTracker
self._tracker = CameraTracker()
self._tracker.start(
-1 if self._simulate else self._camera_index,
resolution=self._resolution,
)
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 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 (
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."
)
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
|