python-camera-manager-directshow 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. python_camera_manager_directshow-0.2.0/GUI/main_GUI.py +814 -0
  2. python_camera_manager_directshow-0.2.0/PKG-INFO +205 -0
  3. python_camera_manager_directshow-0.2.0/README.md +179 -0
  4. python_camera_manager_directshow-0.2.0/app/main.py +26 -0
  5. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/camera/camera_device_bridge.py +117 -14
  6. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/camera/camera_manager.py +30 -4
  7. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/pyproject.toml +5 -4
  8. python_camera_manager_directshow-0.2.0/python_camera_manager_directshow.egg-info/PKG-INFO +205 -0
  9. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/python_camera_manager_directshow.egg-info/requires.txt +1 -0
  10. python_camera_manager_directshow-0.1.0/GUI/main_GUI.py +0 -1006
  11. python_camera_manager_directshow-0.1.0/PKG-INFO +0 -239
  12. python_camera_manager_directshow-0.1.0/README.md +0 -214
  13. python_camera_manager_directshow-0.1.0/app/main.py +0 -106
  14. python_camera_manager_directshow-0.1.0/python_camera_manager_directshow.egg-info/PKG-INFO +0 -239
  15. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/GUI/__init__.py +0 -0
  16. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/LICENSE +0 -0
  17. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/app/__init__.py +0 -0
  18. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/camera/__init__.py +0 -0
  19. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/camera/camera_inspector_bridge.py +0 -0
  20. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/python_camera_manager_directshow.egg-info/SOURCES.txt +0 -0
  21. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/python_camera_manager_directshow.egg-info/dependency_links.txt +0 -0
  22. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/python_camera_manager_directshow.egg-info/entry_points.txt +0 -0
  23. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/python_camera_manager_directshow.egg-info/top_level.txt +0 -0
  24. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/runtime/__init__.py +0 -0
  25. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/runtime/dotnet/DirectShowLib.dll +0 -0
  26. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/runtime/dotnet/DirectShowLibWrapper.dll +0 -0
  27. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/runtime/dotnet/__init__.py +0 -0
  28. {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.0}/setup.cfg +0 -0
@@ -0,0 +1,814 @@
1
+ import sys
2
+ import threading
3
+ from PyQt5.QtWidgets import (
4
+ QApplication,
5
+ QMainWindow,
6
+ QAction,
7
+ QDialog,
8
+ QVBoxLayout,
9
+ QHBoxLayout,
10
+ QLabel,
11
+ QComboBox,
12
+ QPushButton,
13
+ QWidget,
14
+ QMessageBox,
15
+ QScrollArea,
16
+ QGroupBox,
17
+ QCheckBox,
18
+ QSlider,
19
+ QSizePolicy,
20
+ )
21
+ from PyQt5.QtCore import Qt, pyqtSignal
22
+
23
+
24
+ class CameraDialog(QDialog):
25
+ def __init__(self, camera_infos, parent=None):
26
+ """
27
+ ==========================================
28
+ Initialize the camera selection dialog.
29
+ ==========================================
30
+ """
31
+ super().__init__(parent)
32
+ self.setWindowTitle("Select Camera")
33
+ self.setModal(True)
34
+ self.setFixedSize(350, 230)
35
+ self.camera_infos = camera_infos
36
+ self.formats_cache = {}
37
+ layout = QVBoxLayout()
38
+ label = QLabel("Connected Cameras:")
39
+ layout.addWidget(label)
40
+ self.combo = QComboBox()
41
+ self.cameras = [c.name if hasattr(c, 'name') else str(c) for c in camera_infos]
42
+ self.combo.addItems(self.cameras)
43
+ layout.addWidget(self.combo)
44
+
45
+ # Cache formats and ranges for each camera (by index)
46
+ self.ranges_cache = {}
47
+ for idx, cam in enumerate(camera_infos):
48
+ # Try to get formats and ranges attributes, fallback to empty list/dict
49
+ formats = getattr(cam, 'formats', [])
50
+ ranges = getattr(cam, 'ranges', {})
51
+ self.formats_cache[idx] = formats
52
+ self.ranges_cache[idx] = ranges
53
+
54
+ # Add format dropdown
55
+ self.format_label = QLabel("Select Format:")
56
+ layout.addWidget(self.format_label)
57
+ self.format_combo = QComboBox()
58
+ layout.addWidget(self.format_combo)
59
+
60
+ # Populate formats for the initially selected camera
61
+ self.update_formats(0)
62
+ self.combo.currentIndexChanged.connect(self.update_formats)
63
+
64
+ # Add RGB24 conversion checkbox
65
+ from PyQt5.QtWidgets import QCheckBox
66
+ self.rgb24_checkbox = QCheckBox("Request RGB24 conversion (force RGB output)")
67
+ self.rgb24_checkbox.setChecked(False)
68
+ layout.addWidget(self.rgb24_checkbox)
69
+
70
+ self.ok_button = QPushButton("OK")
71
+ self.ok_button.clicked.connect(self.accept)
72
+ layout.addWidget(self.ok_button)
73
+ self.setLayout(layout)
74
+
75
+ def request_rgb24(self):
76
+ """
77
+ ==========================================
78
+ Return True if RGB24 conversion is requested.
79
+ ==========================================
80
+ """
81
+ return self.rgb24_checkbox.isChecked()
82
+
83
+ def update_formats(self, camera_index):
84
+ """
85
+ ==========================================
86
+ Update the format combo box for the selected camera.
87
+ ==========================================
88
+ """
89
+ self.format_combo.clear()
90
+ formats = self.formats_cache.get(camera_index, [])
91
+ if not formats:
92
+ self.format_combo.addItem("No formats available")
93
+ else:
94
+ # Show as WxH @ FPS (PixelFormat)
95
+ for fmt in formats:
96
+ if hasattr(fmt, 'width') and hasattr(fmt, 'height') and hasattr(fmt, 'fps') and hasattr(fmt, 'pixel_format'):
97
+ label = f"{fmt.width}x{fmt.height} @ {fmt.fps} ({fmt.pixel_format})"
98
+ else:
99
+ label = str(fmt)
100
+ self.format_combo.addItem(label)
101
+
102
+
103
+ class MainWindow(QMainWindow):
104
+ frame_update_signal = pyqtSignal(bool, object)
105
+ _format_changed_signal = pyqtSignal(bool)
106
+
107
+ def __init__(self, camera):
108
+ """
109
+ ==========================================
110
+ Initialize the main application window and layout.
111
+ ==========================================
112
+ """
113
+ super().__init__()
114
+ self.camera = camera
115
+ self.device_path = None
116
+ self.setWindowTitle("Rolling Shutter Correction App")
117
+ self.setGeometry(100, 100, 900, 700)
118
+ self._create_menu()
119
+
120
+ # Main split layout: video area on the left, camera controls on the right.
121
+ self.central_widget = QWidget()
122
+ self.setCentralWidget(self.central_widget)
123
+ self.main_layout = QHBoxLayout(self.central_widget)
124
+
125
+ self.video_container = QWidget()
126
+ self.video_layout = QVBoxLayout(self.video_container)
127
+ self.video_layout.setContentsMargins(0, 0, 0, 0)
128
+
129
+ self.video_stage = QWidget()
130
+ self.video_stage_layout = QVBoxLayout(self.video_stage)
131
+ self.video_stage_layout.setContentsMargins(0, 0, 0, 0)
132
+
133
+ self.video_label = QLabel("Camera stream will appear here.")
134
+ self.video_label.setAlignment(Qt.AlignCenter)
135
+ self.video_label.setMinimumSize(0, 0)
136
+ self.video_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
137
+ self.video_stage_layout.addWidget(self.video_label)
138
+
139
+ self.fps_label = QLabel(".NET FPS: -- | Received: -- | Displayed: --", self.video_stage)
140
+ self.fps_label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
141
+ self.fps_label.setStyleSheet("font-size: 10pt; color: #333; background: rgba(255,255,255,0.7); padding: 2px 6px;")
142
+ self.fps_label.setAttribute(Qt.WA_TransparentForMouseEvents, True)
143
+ self.fps_label.move(8, 8)
144
+ self.fps_label.adjustSize()
145
+ self.fps_label.raise_()
146
+
147
+ self.video_layout.addWidget(self.video_stage)
148
+ self.main_layout.addWidget(self.video_container, stretch=3)
149
+
150
+ self.controls_scroll_area = QScrollArea()
151
+ self.controls_scroll_area.setWidgetResizable(True)
152
+ self.controls_scroll_area.setFixedWidth(360)
153
+ self.controls_scroll_area.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
154
+ self.controls_widget = QWidget()
155
+ self.controls_layout = QVBoxLayout(self.controls_widget)
156
+ self.controls_scroll_area.setWidget(self.controls_widget)
157
+ self.main_layout.addWidget(self.controls_scroll_area)
158
+
159
+ self.controls_group = QGroupBox("Camera Controls")
160
+ self.controls_group_layout = QVBoxLayout(self.controls_group)
161
+ self.controls_layout.addWidget(self.controls_group)
162
+
163
+ self.current_format_label = QLabel("Current format: N/A")
164
+ self.current_format_label.setWordWrap(True)
165
+ self.controls_group_layout.addWidget(self.current_format_label)
166
+
167
+ self.format_button = QPushButton("Camera Format Options")
168
+ self.format_button.setEnabled(False)
169
+ self.format_button.clicked.connect(self.show_camera_format_options)
170
+ self.controls_group_layout.addWidget(self.format_button)
171
+
172
+ self.reset_settings_button = QPushButton("Reset Settings")
173
+ self.reset_settings_button.setEnabled(False)
174
+ self.reset_settings_button.clicked.connect(self.show_reset_settings_options)
175
+ self.controls_group_layout.addWidget(self.reset_settings_button)
176
+
177
+ self.auto_title = QLabel("Auto/Manual Controls")
178
+ self.controls_group_layout.addWidget(self.auto_title)
179
+ self.auto_controls_widget = QWidget()
180
+ self.auto_controls_layout = QVBoxLayout(self.auto_controls_widget)
181
+ self.auto_controls_layout.setContentsMargins(0, 0, 0, 0)
182
+ self.controls_group_layout.addWidget(self.auto_controls_widget)
183
+
184
+ self.property_title = QLabel("Property Controls")
185
+ self.controls_group_layout.addWidget(self.property_title)
186
+ self.property_controls_widget = QWidget()
187
+ self.property_controls_layout = QVBoxLayout(self.property_controls_widget)
188
+ self.property_controls_layout.setContentsMargins(0, 0, 0, 0)
189
+ self.controls_group_layout.addWidget(self.property_controls_widget)
190
+
191
+ self.controls_layout.addStretch(1)
192
+
193
+ self.auto_mode_checkboxes = {}
194
+ self.property_sliders = {}
195
+ self.property_slider_labels = {}
196
+ self._updating_property_sliders = set()
197
+
198
+ self.current_camera = None
199
+ self._last_frame_time = None
200
+ self._frame_count = 0
201
+ self._displayed_count = 0
202
+ self._last_fps_update = None
203
+ self._received_fps = 0.0
204
+ self._displayed_fps = 0.0
205
+ self._dotnet_fps_value = None
206
+
207
+ # Connect the signal to the GUI update slot
208
+ self.frame_update_signal.connect(self._update_video_frame_gui)
209
+ self._format_changed_signal.connect(self._on_format_changed)
210
+
211
+ def _create_menu(self):
212
+ """
213
+ ==========================================
214
+ Create the application menu bar and top-level menus.
215
+ ==========================================
216
+ """
217
+ menubar = self.menuBar()
218
+ camera_menu = menubar.addMenu("Camera")
219
+
220
+ select_action = QAction("Select Camera", self)
221
+ select_action.triggered.connect(self.show_camera_dialog)
222
+ camera_menu.addAction(select_action)
223
+
224
+ format_action = QAction("Camera Format Options", self)
225
+ format_action.triggered.connect(self.show_camera_format_options)
226
+ camera_menu.addAction(format_action)
227
+
228
+ reset_action = QAction("Reset Settings", self)
229
+ reset_action.triggered.connect(self.show_reset_settings_options)
230
+ camera_menu.addAction(reset_action)
231
+
232
+ def show_camera_dialog(self):
233
+ """
234
+ ==========================================
235
+ Show the camera selection dialog and open the selected camera.
236
+ ==========================================
237
+ """
238
+ # Use the passed-in camera manager or class to get connected cameras, formats, and ranges
239
+ try:
240
+ camera_infos = self.camera.get_connected_cameras(get_formats=True, get_ranges=True)
241
+ except Exception:
242
+ camera_infos = []
243
+ if not camera_infos:
244
+ QMessageBox.warning(self, "No Cameras", "No cameras are connected.")
245
+ return
246
+ dialog = CameraDialog(camera_infos, self)
247
+ dialog.setWindowModality(Qt.ApplicationModal)
248
+ # Center the dialog in the main window
249
+ parent_geom = self.geometry()
250
+ dialog.move(
251
+ parent_geom.center().x() - dialog.width() // 2,
252
+ parent_geom.center().y() - dialog.height() // 2
253
+ )
254
+ if dialog.exec_() == QDialog.Accepted:
255
+ cam_idx = dialog.combo.currentIndex()
256
+ fmt_idx = dialog.format_combo.currentIndex()
257
+ rgb24 = dialog.request_rgb24()
258
+ cam_info = camera_infos[cam_idx]
259
+ # Defensive: check formats
260
+ if not cam_info.formats or fmt_idx < 0 or fmt_idx >= len(cam_info.formats):
261
+ QMessageBox.warning(self, "Format Error", "No valid format selected.")
262
+ return
263
+ camera_format = cam_info.formats[fmt_idx]
264
+ device_path = cam_info.path
265
+ # Open the camera
266
+ try:
267
+ # Close any existing camera before opening a new one.
268
+ if self.current_camera is not None:
269
+ try:
270
+ self.camera.close()
271
+ except Exception:
272
+ pass
273
+ # Set the frame callback to our handler (update_video_frame)
274
+ self.camera.set_frame_callback(self.update_video_frame)
275
+ self.camera.open(device_path, camera_format, request_rgb24_conversion=rgb24)
276
+ self.current_camera = self.camera
277
+ self.device_path = device_path
278
+ except Exception as e:
279
+ QMessageBox.critical(self, "Camera Open Error", f"Failed to open camera: {e}")
280
+ return
281
+
282
+ self.format_button.setEnabled(True)
283
+ self.reset_settings_button.setEnabled(True)
284
+ self._refresh_current_format_label()
285
+ self._refresh_auto_mode_controls()
286
+ self._refresh_property_value_controls()
287
+
288
+ # Reset FPS counters
289
+ from time import time
290
+ self._last_frame_time = time()
291
+ self._last_fps_update = time()
292
+ self._frame_count = 0
293
+ self._displayed_count = 0
294
+ self._received_fps = 0.0
295
+ self._displayed_fps = 0.0
296
+
297
+ @staticmethod
298
+ def _format_to_display_text(camera_format):
299
+ """
300
+ ==========================================
301
+ Format camera mode details into a user-friendly string.
302
+ ==========================================
303
+ """
304
+ return (
305
+ f"{camera_format.width} x {camera_format.height} @ "
306
+ f"{float(camera_format.fps):.2f} FPS ({camera_format.pixel_format})"
307
+ )
308
+
309
+ def _set_format_status_color(self, color_name):
310
+ """
311
+ ==========================================
312
+ Apply status color to current format label.
313
+ ==========================================
314
+ """
315
+ self.current_format_label.setStyleSheet(f"color: {color_name};")
316
+
317
+ def _refresh_current_format_label(self, format_change_succeeded=None):
318
+ """
319
+ ==========================================
320
+ Refresh current format text and status color.
321
+ ==========================================
322
+ """
323
+ current_format = getattr(self.camera, "current_format", None) if self.camera is not None else None
324
+ if self.camera is None or current_format is None:
325
+ self.current_format_label.setText("Current format: N/A")
326
+ self._set_format_status_color("black")
327
+ return
328
+
329
+ label_text = f"Current format: {self._format_to_display_text(current_format)}"
330
+ pixel_format = str(getattr(current_format, "pixel_format", "") or "").strip().upper()
331
+ if pixel_format in ("MJPG", "MJPEG") and hasattr(self.camera, "get_active_mjpg_decoder_name"):
332
+ decoder_name = self.camera.get_active_mjpg_decoder_name()
333
+ if decoder_name is not None:
334
+ label_text = f"{label_text}\nDecoder: {decoder_name}"
335
+
336
+ self.current_format_label.setText(label_text)
337
+ if format_change_succeeded is True:
338
+ self._set_format_status_color("green")
339
+ elif format_change_succeeded is False:
340
+ self._set_format_status_color("red")
341
+ else:
342
+ self._set_format_status_color("black")
343
+
344
+ def _clear_layout(self, layout):
345
+ """
346
+ ==========================================
347
+ Remove all widgets from a Qt layout.
348
+ ==========================================
349
+ """
350
+ while layout.count():
351
+ item = layout.takeAt(0)
352
+ widget = item.widget()
353
+ if widget is not None:
354
+ widget.deleteLater()
355
+
356
+ @staticmethod
357
+ def _as_float(value, default_value=0.0):
358
+ """
359
+ ==========================================
360
+ Convert values from camera range objects to float safely.
361
+ ==========================================
362
+ """
363
+ try:
364
+ return float(value)
365
+ except Exception:
366
+ return float(default_value)
367
+
368
+ def _get_property_range_for_name(self, property_name):
369
+ """
370
+ ==========================================
371
+ Find property range using case-insensitive name matching.
372
+ ==========================================
373
+ """
374
+ if self.camera is None:
375
+ return None, None
376
+
377
+ ranges = getattr(self.camera, "property_ranges", {}) or {}
378
+ for name, camera_range in ranges.items():
379
+ if str(name).lower() == str(property_name).lower():
380
+ return name, camera_range
381
+ return None, None
382
+
383
+ def _refresh_auto_mode_controls(self):
384
+ """
385
+ ==========================================
386
+ Rebuild auto/manual controls from camera property ranges.
387
+ ==========================================
388
+ """
389
+ self._clear_layout(self.auto_controls_layout)
390
+ self.auto_mode_checkboxes = {}
391
+
392
+ if self.camera is None:
393
+ return
394
+
395
+ ranges = getattr(self.camera, "property_ranges", {}) or {}
396
+ supported = []
397
+ for name, camera_range in ranges.items():
398
+ if bool(getattr(camera_range, "property_supported", False)) and bool(getattr(camera_range, "auto_supported", False)):
399
+ supported.append((name, camera_range))
400
+
401
+ if not supported:
402
+ self.auto_controls_layout.addWidget(QLabel("No auto/manual controls available"))
403
+ return
404
+
405
+ for property_name, camera_range in sorted(supported, key=lambda x: str(x[0]).lower()):
406
+ checkbox = QCheckBox(f"{property_name} Auto")
407
+ checkbox.setChecked(bool(getattr(camera_range, "is_auto", False)))
408
+ checkbox.toggled.connect(
409
+ lambda checked, n=property_name, cb=checkbox: self._on_auto_mode_toggle(n, checked, cb)
410
+ )
411
+ self.auto_controls_layout.addWidget(checkbox)
412
+ self.auto_mode_checkboxes[str(property_name)] = checkbox
413
+
414
+ def _on_auto_mode_toggle(self, property_name, requested_auto_on, checkbox):
415
+ """
416
+ ==========================================
417
+ Handle user toggling of one auto/manual property checkbox.
418
+ ==========================================
419
+ """
420
+ if self.camera is None:
421
+ return
422
+
423
+ try:
424
+ success, is_auto_enabled = self.camera.set_property_auto_mode(str(property_name), bool(requested_auto_on))
425
+ except Exception:
426
+ success, is_auto_enabled = False, bool(not requested_auto_on)
427
+
428
+ checkbox.blockSignals(True)
429
+ checkbox.setChecked(bool(is_auto_enabled))
430
+ checkbox.blockSignals(False)
431
+
432
+ self._set_format_status_color("green" if success else "red")
433
+ self._refresh_property_value_controls()
434
+
435
+ def _refresh_property_value_controls(self):
436
+ """
437
+ ==========================================
438
+ Rebuild property sliders from camera property ranges.
439
+ ==========================================
440
+ """
441
+ self._clear_layout(self.property_controls_layout)
442
+ self.property_sliders = {}
443
+ self.property_slider_labels = {}
444
+ self._updating_property_sliders = set()
445
+
446
+ if self.camera is None:
447
+ self.property_controls_layout.addWidget(QLabel("Property controls not available"))
448
+ return
449
+
450
+ ranges = getattr(self.camera, "property_ranges", {}) or {}
451
+ supported = []
452
+ for name, camera_range in ranges.items():
453
+ if bool(getattr(camera_range, "property_supported", False)):
454
+ supported.append((name, camera_range))
455
+
456
+ if not supported:
457
+ self.property_controls_layout.addWidget(QLabel("Property controls not available"))
458
+ return
459
+
460
+ for property_name, camera_range in sorted(supported, key=lambda x: str(x[0]).lower()):
461
+ display_name = str(property_name)
462
+ label = QLabel(display_name)
463
+ self.property_controls_layout.addWidget(label)
464
+
465
+ min_value = self._as_float(getattr(camera_range, "min", 0), 0)
466
+ max_value = self._as_float(getattr(camera_range, "max", 0), 0)
467
+ step_value = self._as_float(getattr(camera_range, "step", 1), 1)
468
+ if step_value <= 0:
469
+ step_value = 1.0
470
+ current_value = self._as_float(getattr(camera_range, "current", min_value), min_value)
471
+
472
+ slider = QSlider(Qt.Horizontal)
473
+ slider.setMinimum(int(round(min_value)))
474
+ slider.setMaximum(int(round(max_value)))
475
+ slider.setSingleStep(int(max(1, round(step_value))))
476
+ slider.setPageStep(int(max(1, round(step_value))))
477
+ slider.setValue(int(round(current_value)))
478
+ slider.valueChanged.connect(lambda value, n=display_name: self._on_property_slider_change(n, value))
479
+
480
+ is_auto = bool(getattr(camera_range, "auto_supported", False) and getattr(camera_range, "is_auto", False))
481
+ slider.setEnabled(not is_auto)
482
+ self.property_controls_layout.addWidget(slider)
483
+
484
+ value_label = QLabel(
485
+ f"Min: {int(round(min_value))} Max: {int(round(max_value))} Value: {int(round(current_value))}"
486
+ )
487
+ self.property_controls_layout.addWidget(value_label)
488
+
489
+ self.property_sliders[display_name] = slider
490
+ self.property_slider_labels[display_name] = value_label
491
+
492
+ def _on_property_slider_change(self, property_name, raw_value):
493
+ """
494
+ ==========================================
495
+ Handle property slider movement and apply snapped values.
496
+ ==========================================
497
+ """
498
+ if self.camera is None:
499
+ return
500
+
501
+ property_key = str(property_name)
502
+ if property_key in self._updating_property_sliders:
503
+ return
504
+
505
+ _, selected_range = self._get_property_range_for_name(property_name)
506
+ if selected_range is None:
507
+ return
508
+
509
+ min_value = self._as_float(getattr(selected_range, "min", 0), 0)
510
+ max_value = self._as_float(getattr(selected_range, "max", 0), 0)
511
+ step_value = self._as_float(getattr(selected_range, "step", 1), 1)
512
+ if step_value <= 0:
513
+ step_value = 1.0
514
+
515
+ raw_numeric = float(raw_value)
516
+ snapped_value = min_value + round((raw_numeric - min_value) / step_value) * step_value
517
+ snapped_value = max(min_value, min(max_value, snapped_value))
518
+ target_value = int(round(snapped_value))
519
+
520
+ try:
521
+ success, actual_value = self.camera.set_property_value(str(property_name), target_value)
522
+ except Exception:
523
+ success, actual_value = False, target_value
524
+
525
+ actual_value = int(round(self._as_float(actual_value, target_value)))
526
+
527
+ slider = self.property_sliders.get(property_key)
528
+ if slider is not None:
529
+ self._updating_property_sliders.add(property_key)
530
+ slider.setValue(actual_value)
531
+ self._updating_property_sliders.discard(property_key)
532
+
533
+ value_label = self.property_slider_labels.get(property_key)
534
+ if value_label is not None:
535
+ value_label.setText(
536
+ f"Min: {int(round(min_value))} Max: {int(round(max_value))} Value: {int(round(actual_value))}"
537
+ )
538
+
539
+ self._set_format_status_color("green" if success else "red")
540
+
541
+ @staticmethod
542
+ def _show_reset_failure_message(parent, action_title, success_count, total_count):
543
+ """
544
+ ==========================================
545
+ Show one aggregated failure message for reset operations.
546
+ ==========================================
547
+ """
548
+ failed_count = max(0, int(total_count) - int(success_count))
549
+
550
+ if int(total_count) <= 0:
551
+ message = f"{action_title} failed: no supported properties were available to reset."
552
+ elif int(success_count) <= 0:
553
+ message = f"{action_title} failed: 0/{int(total_count)} properties were reset."
554
+ else:
555
+ message = (
556
+ f"{action_title} partially failed: "
557
+ f"{int(success_count)}/{int(total_count)} succeeded, {failed_count} failed."
558
+ )
559
+
560
+ QMessageBox.critical(parent, "Reset Failed", message)
561
+
562
+ def show_reset_settings_options(self):
563
+ """
564
+ ==========================================
565
+ Show reset actions dialog for properties and property flags.
566
+ ==========================================
567
+ """
568
+ if self.camera is None:
569
+ return
570
+
571
+ dialog = QDialog(self)
572
+ dialog.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
573
+ dialog.setWindowTitle("Reset Settings")
574
+ dialog.setModal(True)
575
+ dialog.setFixedSize(420, 170)
576
+
577
+ layout = QVBoxLayout(dialog)
578
+ title = QLabel("Choose reset action")
579
+ title.setAlignment(Qt.AlignCenter)
580
+ layout.addWidget(title)
581
+
582
+ reset_properties_btn = QPushButton("Reset Properties")
583
+ reset_flags_btn = QPushButton("Reset Property Flags")
584
+ close_btn = QPushButton("Close")
585
+
586
+ layout.addWidget(reset_properties_btn)
587
+ layout.addWidget(reset_flags_btn)
588
+ layout.addWidget(close_btn)
589
+
590
+ def on_reset_properties():
591
+ try:
592
+ all_success, reset_count, total_supported = self.camera.reset_all_properties_to_default_values()
593
+ except Exception:
594
+ all_success, reset_count, total_supported = False, 0, 0
595
+
596
+ if all_success:
597
+ reset_properties_btn.setText("Reset Properties - Success")
598
+ reset_properties_btn.setEnabled(False)
599
+ self._set_format_status_color("green")
600
+ self._refresh_property_value_controls()
601
+ return
602
+
603
+ self._set_format_status_color("red")
604
+ self._refresh_property_value_controls()
605
+ self._show_reset_failure_message(self, "Reset Properties", reset_count, total_supported)
606
+
607
+ def on_reset_flags():
608
+ try:
609
+ all_success, updated_count, total_auto_supported = self.camera.reset_all_property_flags()
610
+ except Exception:
611
+ all_success, updated_count, total_auto_supported = False, 0, 0
612
+
613
+ if all_success:
614
+ reset_flags_btn.setText("Reset Property Flags - Success")
615
+ reset_flags_btn.setEnabled(False)
616
+ self._set_format_status_color("green")
617
+ self._refresh_auto_mode_controls()
618
+ self._refresh_property_value_controls()
619
+ return
620
+
621
+ self._set_format_status_color("red")
622
+ self._refresh_auto_mode_controls()
623
+ self._refresh_property_value_controls()
624
+ self._show_reset_failure_message(self, "Reset Property Flags", updated_count, total_auto_supported)
625
+
626
+ reset_properties_btn.clicked.connect(on_reset_properties)
627
+ reset_flags_btn.clicked.connect(on_reset_flags)
628
+ close_btn.clicked.connect(dialog.accept)
629
+
630
+ dialog.exec_()
631
+
632
+ def show_camera_format_options(self):
633
+ """
634
+ ==========================================
635
+ Show available formats and allow switching to a different one.
636
+ ==========================================
637
+ """
638
+ if self.camera is None or self.device_path is None:
639
+ return
640
+
641
+ try:
642
+ available_formats = self.camera.get_camera_formats(self.device_path) or []
643
+ except Exception:
644
+ available_formats = []
645
+
646
+ if not available_formats:
647
+ available_formats = getattr(self.camera, "available_formats", []) or []
648
+ else:
649
+ self.camera.available_formats = available_formats
650
+
651
+ if not available_formats:
652
+ QMessageBox.warning(self, "Camera Format Options", "No formats are available for the current camera.")
653
+ return
654
+
655
+ current_format = getattr(self.camera, "current_format", None)
656
+
657
+ dialog = QDialog(self)
658
+ dialog.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
659
+ dialog.setWindowTitle("Camera Format Options")
660
+ dialog.setModal(True)
661
+ dialog.setFixedSize(460, 200)
662
+ layout = QVBoxLayout(dialog)
663
+
664
+ current_text = "Current format: "
665
+ if current_format is not None:
666
+ current_text += self._format_to_display_text(current_format)
667
+ else:
668
+ current_text += "Unknown"
669
+
670
+ current_label = QLabel(current_text)
671
+ current_label.setWordWrap(True)
672
+ layout.addWidget(current_label)
673
+
674
+ combo_formats = QComboBox()
675
+ display_formats = [self._format_to_display_text(fmt) for fmt in available_formats]
676
+ combo_formats.addItems(display_formats)
677
+ layout.addWidget(combo_formats)
678
+
679
+ request_rgb24_checkbox = QCheckBox("Request RGB24")
680
+ request_rgb24_checkbox.setChecked(bool(getattr(self.camera, "_request_rgb24_conversion", False)))
681
+ layout.addWidget(request_rgb24_checkbox)
682
+
683
+ if current_format is not None:
684
+ for idx, fmt in enumerate(available_formats):
685
+ if fmt == current_format:
686
+ combo_formats.setCurrentIndex(idx)
687
+ break
688
+
689
+ button_row = QHBoxLayout()
690
+ apply_btn = QPushButton("Apply")
691
+ close_btn = QPushButton("Close")
692
+ button_row.addWidget(apply_btn)
693
+ button_row.addWidget(close_btn)
694
+ layout.addLayout(button_row)
695
+
696
+ def on_apply():
697
+ selected_idx = combo_formats.currentIndex()
698
+ if selected_idx < 0:
699
+ dialog.accept()
700
+ return
701
+
702
+ target_format = available_formats[selected_idx]
703
+ request_rgb24 = request_rgb24_checkbox.isChecked()
704
+ dialog.accept()
705
+ self._set_format_status_color("black")
706
+
707
+ def apply_format_in_background():
708
+ try:
709
+ format_changed = bool(
710
+ self.camera.set_format(target_format, request_rgb24_conversion=bool(request_rgb24))
711
+ )
712
+ except Exception:
713
+ format_changed = False
714
+
715
+ self._format_changed_signal.emit(format_changed)
716
+
717
+ threading.Thread(target=apply_format_in_background, daemon=True).start()
718
+
719
+ apply_btn.clicked.connect(on_apply)
720
+ close_btn.clicked.connect(dialog.accept)
721
+
722
+ dialog.exec_()
723
+
724
+
725
+ def update_video_frame(self, success, frame):
726
+ """
727
+ ==========================================
728
+ Frame callback: schedule GUI update for new frame (thread-safe).
729
+ ==========================================
730
+ """
731
+ # Emit the signal to update the GUI in the main thread
732
+ self.frame_update_signal.emit(success, frame)
733
+
734
+ def _on_format_changed(self, format_changed: bool):
735
+ self._refresh_current_format_label(format_changed)
736
+ self._refresh_auto_mode_controls()
737
+ self._refresh_property_value_controls()
738
+
739
+ def _update_video_frame_gui(self, success, frame):
740
+ """
741
+ ==========================================
742
+ Update the GUI with the new video frame.
743
+ ==========================================
744
+ """
745
+ import time
746
+ if not success or frame is None:
747
+ return
748
+ now = time.time()
749
+ try:
750
+ self._frame_count += 1
751
+ # Assume frame is a numpy array (H, W, 3) in RGB
752
+ import numpy as np
753
+ from PyQt5.QtGui import QImage, QPixmap
754
+ if frame.dtype != np.uint8:
755
+ frame = frame.astype(np.uint8)
756
+
757
+ frame_rgb = np.ascontiguousarray(frame[:, :, ::-1]) # BGR→RGB, contiguous
758
+ h, w, ch = frame_rgb.shape
759
+ bytes_per_line = ch * w
760
+ qimg = QImage(frame_rgb.data, w, h, bytes_per_line, QImage.Format_RGB888)
761
+ pixmap = QPixmap.fromImage(qimg)
762
+ label_size = self.video_label.size()
763
+ if pixmap.width() > label_size.width() or pixmap.height() > label_size.height():
764
+ pixmap = pixmap.scaled(label_size, Qt.KeepAspectRatio, Qt.FastTransformation)
765
+ self.video_label.setPixmap(pixmap)
766
+ self._displayed_count += 1
767
+ except Exception as e:
768
+ # Optionally log or show error
769
+ pass
770
+ self._update_fps_label()
771
+
772
+
773
+ def _update_fps_label(self):
774
+ """
775
+ ==========================================
776
+ Update the FPS label with current stats.
777
+ ==========================================
778
+ """
779
+ import time
780
+ now = time.time()
781
+ # Update every 1s
782
+ if self._last_fps_update is None:
783
+ self._last_fps_update = now
784
+ elapsed = now - self._last_fps_update
785
+ if elapsed >= 1:
786
+ self._received_fps = self._frame_count / elapsed
787
+ self._displayed_fps = self._displayed_count / elapsed
788
+ # Periodically fetch .NET FPS and store in self._dotnet_fps_value
789
+ if self.camera is not None:
790
+ try:
791
+ dotnet_fps = float(self.camera.get_current_fps())
792
+ self._dotnet_fps_value = dotnet_fps if dotnet_fps > 0 else None
793
+ except Exception:
794
+ self._dotnet_fps_value = None
795
+ else:
796
+ self._dotnet_fps_value = None
797
+ dotnet_text = '--' if self._dotnet_fps_value is None else f"{self._dotnet_fps_value:.2f}"
798
+ self.fps_label.setText(f".NET FPS: {dotnet_text} | Received: {self._received_fps:.2f} | Displayed: {self._displayed_fps:.2f}")
799
+ self.fps_label.adjustSize()
800
+ self._last_fps_update = now
801
+ self._frame_count = 0
802
+ self._displayed_count = 0
803
+
804
+ def run_gui(camera_manager):
805
+ """
806
+ ==========================================
807
+ Launch the main GUI application.
808
+ ==========================================
809
+ """
810
+ app = QApplication(sys.argv)
811
+ window = MainWindow(camera_manager)
812
+ window.show()
813
+ sys.exit(app.exec_())
814
+