python-camera-manager-directshow 0.1.0__tar.gz → 0.2.1__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.
- python_camera_manager_directshow-0.2.1/DirectShow_Wrapper/GUI/main_GUI.py +814 -0
- python_camera_manager_directshow-0.2.1/DirectShow_Wrapper/__init__.py +9 -0
- python_camera_manager_directshow-0.2.1/DirectShow_Wrapper/app/main.py +17 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1/DirectShow_Wrapper}/camera/camera_device_bridge.py +117 -14
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1/DirectShow_Wrapper}/camera/camera_manager.py +30 -4
- python_camera_manager_directshow-0.2.1/PKG-INFO +205 -0
- python_camera_manager_directshow-0.2.1/README.md +179 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1}/pyproject.toml +11 -7
- python_camera_manager_directshow-0.2.1/python_camera_manager_directshow.egg-info/PKG-INFO +205 -0
- python_camera_manager_directshow-0.2.1/python_camera_manager_directshow.egg-info/SOURCES.txt +22 -0
- python_camera_manager_directshow-0.2.1/python_camera_manager_directshow.egg-info/entry_points.txt +2 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1}/python_camera_manager_directshow.egg-info/requires.txt +1 -0
- python_camera_manager_directshow-0.2.1/python_camera_manager_directshow.egg-info/top_level.txt +1 -0
- python_camera_manager_directshow-0.1.0/GUI/main_GUI.py +0 -1006
- python_camera_manager_directshow-0.1.0/PKG-INFO +0 -239
- python_camera_manager_directshow-0.1.0/README.md +0 -214
- python_camera_manager_directshow-0.1.0/app/main.py +0 -106
- python_camera_manager_directshow-0.1.0/python_camera_manager_directshow.egg-info/PKG-INFO +0 -239
- python_camera_manager_directshow-0.1.0/python_camera_manager_directshow.egg-info/SOURCES.txt +0 -21
- python_camera_manager_directshow-0.1.0/python_camera_manager_directshow.egg-info/entry_points.txt +0 -2
- python_camera_manager_directshow-0.1.0/python_camera_manager_directshow.egg-info/top_level.txt +0 -4
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1/DirectShow_Wrapper}/GUI/__init__.py +0 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1/DirectShow_Wrapper}/app/__init__.py +0 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1/DirectShow_Wrapper}/camera/__init__.py +0 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1/DirectShow_Wrapper}/camera/camera_inspector_bridge.py +0 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1/DirectShow_Wrapper}/runtime/__init__.py +0 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1/DirectShow_Wrapper}/runtime/dotnet/DirectShowLib.dll +0 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1/DirectShow_Wrapper}/runtime/dotnet/DirectShowLibWrapper.dll +0 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1/DirectShow_Wrapper}/runtime/dotnet/__init__.py +0 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1}/LICENSE +0 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1}/python_camera_manager_directshow.egg-info/dependency_links.txt +0 -0
- {python_camera_manager_directshow-0.1.0 → python_camera_manager_directshow-0.2.1}/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
|
+
|