From 864642a84d8e93cc4971c7915e39a60740cdd6fa Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Thu, 4 Jun 2026 12:29:24 -0600 Subject: Redesign motion capture plugin as Camera device type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- plugins/motion_capture/camera_panel.py | 132 +++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 plugins/motion_capture/camera_panel.py (limited to 'plugins/motion_capture/camera_panel.py') diff --git a/plugins/motion_capture/camera_panel.py b/plugins/motion_capture/camera_panel.py new file mode 100644 index 0000000..611c942 --- /dev/null +++ b/plugins/motion_capture/camera_panel.py @@ -0,0 +1,132 @@ +""" +motion_capture/camera_panel.py + +CameraPanel for AddDeviceDialog — scans for OpenCV cameras and builds +a CameraDevice when the user clicks "Add Device". +""" + +from __future__ import annotations + +from PyQt6.QtCore import Qt, QThread, pyqtSignal +from PyQt6.QtWidgets import ( + QCheckBox, QGroupBox, QHBoxLayout, QLabel, QListWidget, + QListWidgetItem, QPushButton, QVBoxLayout, QWidget, +) + + +class CameraScanThread(QThread): + cameras_found = pyqtSignal(list) # list of (index: int, label: str) + + def run(self): + results = [] + try: + import cv2 + for i in range(6): + cap = cv2.VideoCapture(i) + if cap.isOpened(): + ret, _ = cap.read() + if ret: + w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + results.append((i, f"Camera {i} ({w}×{h})")) + cap.release() + except ImportError: + pass + self.cameras_found.emit(results) + + +class CameraPanel(QWidget): + """Config panel for Camera device in AddDeviceDialog.""" + + def __init__(self): + super().__init__() + self._cameras: list = [] + self._selected_index: int = 0 + self._scanner = None + + lay = QVBoxLayout(self) + lay.setContentsMargins(0, 4, 0, 4) + lay.setSpacing(8) + + self.sim_chk = QCheckBox("Simulation mode (Lissajous pattern, no camera needed)") + self.sim_chk.setChecked(False) + lay.addWidget(self.sim_chk) + + scan_grp = QGroupBox("Available Cameras") + sg = QVBoxLayout(scan_grp) + sg.setSpacing(4) + + top = QHBoxLayout() + self._scan_btn = QPushButton("🔍 Scan Cameras") + self._scan_btn.setObjectName("addTraceBtn") + self._scan_btn.clicked.connect(self._scan) + self._status = QLabel("Click Scan to detect cameras") + self._status.setObjectName("traceSource") + top.addWidget(self._scan_btn) + top.addWidget(self._status, 1) + sg.addLayout(top) + + self._list = QListWidget() + self._list.setMaximumHeight(120) + self._list.setObjectName("portList") + self._list.itemClicked.connect(self._on_select) + self._list.setToolTip("Click a camera to select it") + sg.addWidget(self._list) + + hint = QLabel("↑ Click a camera above to select it") + hint.setObjectName("traceSource") + sg.addWidget(hint) + + lay.addWidget(scan_grp) + + note = QLabel( + "Requires OpenCV: pip install opencv-python\n" + "Use Simulation mode if no camera is available." + ) + note.setObjectName("traceSource") + note.setWordWrap(True) + lay.addWidget(note) + lay.addStretch() + + # ── Scan ───────────────────────────────────────────────────────────── + + def _scan(self): + self._scan_btn.setEnabled(False) + self._status.setText("Scanning…") + self._list.clear() + self._scanner = CameraScanThread() + self._scanner.cameras_found.connect(self._on_found) + self._scanner.start() + + def _on_found(self, cameras): + self._scan_btn.setEnabled(True) + self._cameras = cameras + self._list.clear() + if not cameras: + self._status.setText("No cameras found") + item = QListWidgetItem(" No cameras detected") + item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable) + self._list.addItem(item) + else: + self._status.setText(f"{len(cameras)} camera(s) found") + for idx, label in cameras: + item = QListWidgetItem(f" {label}") + item.setData(Qt.ItemDataRole.UserRole, idx) + self._list.addItem(item) + self._list.setCurrentRow(0) + self._selected_index = cameras[0][0] + + def _on_select(self, item: QListWidgetItem): + idx = item.data(Qt.ItemDataRole.UserRole) + if idx is not None: + self._selected_index = idx + + # ── Build ───────────────────────────────────────────────────────────── + + def build_device(self, device_id: str): + from device import CameraDevice + return CameraDevice( + device_id=device_id, + camera_index=self._selected_index, + simulate=self.sim_chk.isChecked(), + ) -- cgit v1.2.3