framesource 0.3.0__py3-none-any.whl
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.
- frame_processors/__init__.py +37 -0
- frame_source/__init__.py +50 -0
- framesource/__init__.py +93 -0
- framesource/_msmf_config.py +26 -0
- framesource/_version.py +24 -0
- framesource/discovery.py +109 -0
- framesource/errors.py +82 -0
- framesource/factory.py +290 -0
- framesource/processors/__init__.py +34 -0
- framesource/processors/equirectangular360_processor.py +577 -0
- framesource/processors/fisheye2equirectangular_processor.py +328 -0
- framesource/processors/frame_processor.py +30 -0
- framesource/processors/hyperspectral_processor.py +23 -0
- framesource/processors/realsense_depth_processor.py +90 -0
- framesource/py.typed +0 -0
- framesource/sources/__init__.py +40 -0
- framesource/sources/audiospectrogram_capture.py +1068 -0
- framesource/sources/basler_capture.py +477 -0
- framesource/sources/folder_capture.py +920 -0
- framesource/sources/genicam_capture.py +681 -0
- framesource/sources/huateng_capture.py +245 -0
- framesource/sources/ipcamera_capture.py +254 -0
- framesource/sources/mvsdk.py +2454 -0
- framesource/sources/realsense_capture.py +565 -0
- framesource/sources/screen_capture.py +800 -0
- framesource/sources/video_capture_base.py +560 -0
- framesource/sources/video_file_capture.py +259 -0
- framesource/sources/webcam_capture.py +511 -0
- framesource/sources/ximea_capture.py +299 -0
- framesource/threading_utils.py +790 -0
- framesource-0.3.0.dist-info/METADATA +787 -0
- framesource-0.3.0.dist-info/RECORD +37 -0
- framesource-0.3.0.dist-info/WHEEL +5 -0
- framesource-0.3.0.dist-info/licenses/LICENSE +21 -0
- framesource-0.3.0.dist-info/scm_file_list.json +276 -0
- framesource-0.3.0.dist-info/scm_version.json +8 -0
- framesource-0.3.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import platform
|
|
3
|
+
import warnings
|
|
4
|
+
from typing import Any, Optional, Union
|
|
5
|
+
|
|
6
|
+
import cv2
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ..discovery import DeviceInfo
|
|
10
|
+
from .video_capture_base import VideoCaptureBase
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# String aliases for OpenCV capture backends, resolved to the cv2.CAP_* ints.
|
|
15
|
+
# Only backends present in this OpenCV build are included.
|
|
16
|
+
_BACKEND_ALIASES: dict[str, int] = {}
|
|
17
|
+
for _alias, _const_name in (
|
|
18
|
+
("any", "CAP_ANY"),
|
|
19
|
+
("auto", "CAP_ANY"),
|
|
20
|
+
("dshow", "CAP_DSHOW"),
|
|
21
|
+
("msmf", "CAP_MSMF"),
|
|
22
|
+
("v4l2", "CAP_V4L2"),
|
|
23
|
+
("avfoundation", "CAP_AVFOUNDATION"),
|
|
24
|
+
("gstreamer", "CAP_GSTREAMER"),
|
|
25
|
+
("ffmpeg", "CAP_FFMPEG"),
|
|
26
|
+
):
|
|
27
|
+
if hasattr(cv2, _const_name):
|
|
28
|
+
_BACKEND_ALIASES[_alias] = getattr(cv2, _const_name)
|
|
29
|
+
|
|
30
|
+
# Uncompressed pixel formats. When one of these is negotiated at high
|
|
31
|
+
# resolution the USB link is easily saturated, throttling the frame rate far
|
|
32
|
+
# below what MJPG (compressed) would sustain.
|
|
33
|
+
_UNCOMPRESSED_FOURCCS = frozenset(
|
|
34
|
+
{"YUY2", "YUYV", "UYVY", "I420", "IYUV", "YV12", "NV12", "RGB3", "BGR3"}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Resolutions at or above this pixel count are where uncompressed transport
|
|
38
|
+
# starts to hurt (720p and up).
|
|
39
|
+
_HIGH_RES_PIXELS = 1280 * 720
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class WebcamCapture(VideoCaptureBase):
|
|
43
|
+
has_discovery = True
|
|
44
|
+
supports_exposure = True
|
|
45
|
+
supports_gain = True
|
|
46
|
+
|
|
47
|
+
"""Webcam capture using OpenCV."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
source: int = 0,
|
|
52
|
+
*,
|
|
53
|
+
width: Optional[int] = None,
|
|
54
|
+
height: Optional[int] = None,
|
|
55
|
+
fps: Optional[float] = None,
|
|
56
|
+
backend: Optional[Union[str, int]] = None,
|
|
57
|
+
fourcc: Optional[str] = None,
|
|
58
|
+
**kwargs,
|
|
59
|
+
):
|
|
60
|
+
"""Initialize the webcam capture.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
source: Camera device index, or an ``api_pref:index`` /
|
|
64
|
+
``api_pref:path`` string (as produced in the ``id`` field of
|
|
65
|
+
:meth:`discover`).
|
|
66
|
+
width: Desired frame width in pixels. Applied on :meth:`connect`
|
|
67
|
+
(together with ``height``) when provided.
|
|
68
|
+
height: Desired frame height in pixels. Applied on
|
|
69
|
+
:meth:`connect` (together with ``width``) when provided.
|
|
70
|
+
fps: Desired frames per second. Applied on :meth:`connect` when
|
|
71
|
+
provided.
|
|
72
|
+
backend: OpenCV capture backend to open the device with. Accepts a
|
|
73
|
+
name (``'msmf'``, ``'dshow'``, ``'v4l2'``, ``'avfoundation'``,
|
|
74
|
+
``'gstreamer'``, ``'ffmpeg'``, ``'any'``) or a raw
|
|
75
|
+
``cv2.CAP_*`` integer. ``None`` (default) uses the OS default
|
|
76
|
+
(DirectShow on Windows, AVFoundation on macOS, V4L2 on Linux).
|
|
77
|
+
Note that on Windows ``'dshow'`` and ``'msmf'`` interpret the
|
|
78
|
+
exposure/gain values in :meth:`set_exposure`/:meth:`set_gain`
|
|
79
|
+
differently. If a high-resolution stream is stuck at a low
|
|
80
|
+
frame rate, ``'msmf'`` will often negotiate a compressed format
|
|
81
|
+
where DirectShow will not.
|
|
82
|
+
fourcc: Desired pixel format as a four-character code, e.g.
|
|
83
|
+
``'MJPG'``. Applied on :meth:`connect` *before* the resolution
|
|
84
|
+
so it is not reset by the resolution change. ``None`` (default)
|
|
85
|
+
leaves the backend's default format untouched. Requesting
|
|
86
|
+
``'MJPG'`` is the usual fix for uncompressed formats saturating
|
|
87
|
+
the USB link at 1080p and above (see the warning logged by
|
|
88
|
+
:meth:`connect`). Not all backends honour every code.
|
|
89
|
+
**kwargs: Additional passthrough options stored on ``self.config``.
|
|
90
|
+
"""
|
|
91
|
+
# Preserve the historical ``self.config`` contents: these options were
|
|
92
|
+
# previously mined from ``**kwargs`` and read back via ``self.config``
|
|
93
|
+
# in ``connect()``. Only forward explicitly-provided values so the
|
|
94
|
+
# ``'width' in self.config`` presence checks keep their old meaning.
|
|
95
|
+
for _key, _val in (("width", width), ("height", height), ("fps", fps)):
|
|
96
|
+
if _val is not None:
|
|
97
|
+
kwargs[_key] = _val
|
|
98
|
+
super().__init__(source, **kwargs)
|
|
99
|
+
self.cap = None
|
|
100
|
+
self.fourcc = fourcc
|
|
101
|
+
self.api_preference = self._resolve_backend(backend)
|
|
102
|
+
|
|
103
|
+
self.source = source
|
|
104
|
+
|
|
105
|
+
if "is_mono" in kwargs:
|
|
106
|
+
logger.warning(
|
|
107
|
+
"'is_mono' argument is only used for certain industrial cameras "
|
|
108
|
+
"and has no effect for webcams."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _resolve_backend(backend: Optional[Union[str, int]]) -> int:
|
|
113
|
+
"""Resolve a backend name/int to a cv2.CAP_* constant.
|
|
114
|
+
|
|
115
|
+
``None`` maps to the OS default; an int is used verbatim; a string is
|
|
116
|
+
looked up (case-insensitively) in :data:`_BACKEND_ALIASES`.
|
|
117
|
+
"""
|
|
118
|
+
if backend is None:
|
|
119
|
+
if platform.system() == "Windows":
|
|
120
|
+
return cv2.CAP_DSHOW
|
|
121
|
+
elif platform.system() == "Darwin":
|
|
122
|
+
return cv2.CAP_AVFOUNDATION
|
|
123
|
+
else:
|
|
124
|
+
return cv2.CAP_V4L2
|
|
125
|
+
if isinstance(backend, int):
|
|
126
|
+
return backend
|
|
127
|
+
key = backend.strip().lower()
|
|
128
|
+
if key not in _BACKEND_ALIASES:
|
|
129
|
+
available = ", ".join(sorted(_BACKEND_ALIASES))
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Unknown webcam backend {backend!r}. Use one of: {available}, "
|
|
132
|
+
f"or pass a cv2.CAP_* integer."
|
|
133
|
+
)
|
|
134
|
+
return _BACKEND_ALIASES[key]
|
|
135
|
+
|
|
136
|
+
def connect(self) -> bool:
|
|
137
|
+
"""Connect to webcam."""
|
|
138
|
+
try:
|
|
139
|
+
src = self.source
|
|
140
|
+
api_pref = self.api_preference
|
|
141
|
+
# Support `api_pref/index` format or `api_pref/path` format used in
|
|
142
|
+
# list_devices `id` field
|
|
143
|
+
if isinstance(src, str) and ":" in src:
|
|
144
|
+
parts = src.split(":")
|
|
145
|
+
api_pref, src = parts[0], parts[1]
|
|
146
|
+
|
|
147
|
+
if api_pref.isdigit():
|
|
148
|
+
api_pref = int(api_pref)
|
|
149
|
+
|
|
150
|
+
if src.isdigit():
|
|
151
|
+
src = int(src)
|
|
152
|
+
|
|
153
|
+
self.cap = cv2.VideoCapture(src, api_pref)
|
|
154
|
+
if not self.cap.isOpened():
|
|
155
|
+
logger.error(f"Failed to open webcam {src}")
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# Pixel format must be set before the resolution: several drivers
|
|
159
|
+
# reset the format back to their default when the frame size
|
|
160
|
+
# changes.
|
|
161
|
+
if self.fourcc:
|
|
162
|
+
self._apply_fourcc(self.fourcc)
|
|
163
|
+
|
|
164
|
+
# Set additional parameters if provided
|
|
165
|
+
if "width" in self.config and "height" in self.config:
|
|
166
|
+
self.set_frame_size(self.config["width"], self.config["height"])
|
|
167
|
+
if "fps" in self.config:
|
|
168
|
+
self.cap.set(cv2.CAP_PROP_FPS, self.config["fps"])
|
|
169
|
+
|
|
170
|
+
self.is_connected = True
|
|
171
|
+
self._warn_if_bandwidth_limited()
|
|
172
|
+
logger.info(f"Connected to webcam {src}")
|
|
173
|
+
return True
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(f"Error connecting to webcam: {e}")
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
def disconnect(self) -> bool:
|
|
179
|
+
"""Disconnect from webcam."""
|
|
180
|
+
try:
|
|
181
|
+
if self.cap is not None:
|
|
182
|
+
self.cap.release()
|
|
183
|
+
self.cap = None
|
|
184
|
+
self.is_connected = False
|
|
185
|
+
logger.info("Disconnected from webcam")
|
|
186
|
+
return True
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f"Error disconnecting from webcam: {e}")
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def _read_implementation(self) -> tuple[bool, Optional[np.ndarray]]:
|
|
192
|
+
"""
|
|
193
|
+
Read a single frame from the webcam.
|
|
194
|
+
Returns:
|
|
195
|
+
Tuple[bool, Optional[np.ndarray]]: (success, frame)
|
|
196
|
+
"""
|
|
197
|
+
if not self.is_connected or self.cap is None:
|
|
198
|
+
return False, None
|
|
199
|
+
ret, frame = self.cap.read()
|
|
200
|
+
return ret, frame if ret else None
|
|
201
|
+
|
|
202
|
+
def set_exposure(self, value: float) -> bool:
|
|
203
|
+
"""Set exposure (-13 to -1 for most webcams)."""
|
|
204
|
+
if not self.is_connected or self.cap is None:
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
self.cap.set(cv2.CAP_PROP_EXPOSURE, value)
|
|
209
|
+
self._exposure = value
|
|
210
|
+
return True
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.error(f"Error setting exposure: {e}")
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
def get_exposure(self) -> Optional[float]:
|
|
216
|
+
"""Get current exposure."""
|
|
217
|
+
if not self.is_connected or self.cap is None:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
return self.cap.get(cv2.CAP_PROP_EXPOSURE)
|
|
222
|
+
except Exception:
|
|
223
|
+
return self._exposure
|
|
224
|
+
|
|
225
|
+
def set_gain(self, value: float) -> bool:
|
|
226
|
+
"""Set gain (0-255 for most webcams)."""
|
|
227
|
+
if not self.is_connected or self.cap is None:
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
self.cap.set(cv2.CAP_PROP_GAIN, value)
|
|
232
|
+
self._gain = value
|
|
233
|
+
return True
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.error(f"Error setting gain: {e}")
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
def get_gain(self) -> Optional[float]:
|
|
239
|
+
"""Get current gain."""
|
|
240
|
+
if not self.is_connected or self.cap is None:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
return self.cap.get(cv2.CAP_PROP_GAIN)
|
|
245
|
+
except Exception:
|
|
246
|
+
return self._gain
|
|
247
|
+
|
|
248
|
+
def get_frame_size(self) -> Optional[tuple[int, int]]:
|
|
249
|
+
"""Get frame size."""
|
|
250
|
+
if not self.is_connected or self.cap is None:
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
254
|
+
height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
255
|
+
return (width, height)
|
|
256
|
+
|
|
257
|
+
def set_frame_size(self, width: int, height: int) -> bool:
|
|
258
|
+
"""Set frame size."""
|
|
259
|
+
if self.cap is None:
|
|
260
|
+
return False
|
|
261
|
+
result1 = self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
|
|
262
|
+
result2 = self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
|
|
263
|
+
logger.info(f"Set webcam resolution to {width}x{height} (success: {result1 and result2})")
|
|
264
|
+
return result1 and result2
|
|
265
|
+
|
|
266
|
+
def _apply_fourcc(self, fourcc: str) -> bool:
|
|
267
|
+
"""Request a pixel format from the device by its four-character code.
|
|
268
|
+
|
|
269
|
+
Returns True if OpenCV accepted the request. Note that acceptance does
|
|
270
|
+
not guarantee the driver actually switches format (see
|
|
271
|
+
:meth:`get_fourcc` / :meth:`_warn_if_bandwidth_limited`).
|
|
272
|
+
"""
|
|
273
|
+
if self.cap is None:
|
|
274
|
+
return False
|
|
275
|
+
if len(fourcc) != 4:
|
|
276
|
+
logger.warning("Ignoring invalid fourcc %r (must be 4 characters, e.g. 'MJPG')", fourcc)
|
|
277
|
+
return False
|
|
278
|
+
# VideoWriter_fourcc moved to VideoWriter.fourcc in newer OpenCV builds.
|
|
279
|
+
fourcc_fn = getattr(cv2, "VideoWriter_fourcc", None) or cv2.VideoWriter.fourcc
|
|
280
|
+
code = fourcc_fn(*fourcc)
|
|
281
|
+
ok = bool(self.cap.set(cv2.CAP_PROP_FOURCC, code))
|
|
282
|
+
logger.info("Requested pixel format %s (accepted: %s)", fourcc, ok)
|
|
283
|
+
return ok
|
|
284
|
+
|
|
285
|
+
def get_fourcc(self) -> Optional[str]:
|
|
286
|
+
"""Return the currently negotiated pixel format as a 4-char code.
|
|
287
|
+
|
|
288
|
+
Returns None if not connected. Some backends (notably MSMF) report an
|
|
289
|
+
empty string even while delivering a compressed stream.
|
|
290
|
+
"""
|
|
291
|
+
if not self.is_connected or self.cap is None:
|
|
292
|
+
return None
|
|
293
|
+
value = int(self.cap.get(cv2.CAP_PROP_FOURCC))
|
|
294
|
+
return "".join(chr((value >> (8 * i)) & 0xFF) for i in range(4)).strip("\x00 ")
|
|
295
|
+
|
|
296
|
+
def _warn_if_bandwidth_limited(self) -> None:
|
|
297
|
+
"""Log a warning when a high-res stream negotiated an uncompressed format.
|
|
298
|
+
|
|
299
|
+
This is the common cause of a webcam that requests 1080p30 but only
|
|
300
|
+
delivers a handful of frames per second: an uncompressed format (e.g.
|
|
301
|
+
YUY2) saturates the USB link. Requesting ``fourcc='MJPG'`` or a
|
|
302
|
+
different ``backend`` usually resolves it.
|
|
303
|
+
"""
|
|
304
|
+
size = self.get_frame_size()
|
|
305
|
+
if not size:
|
|
306
|
+
return
|
|
307
|
+
width, height = size
|
|
308
|
+
if width * height < _HIGH_RES_PIXELS:
|
|
309
|
+
return
|
|
310
|
+
negotiated = self.get_fourcc()
|
|
311
|
+
# MSMF reports an empty code even on a fast compressed stream, so only
|
|
312
|
+
# warn when we positively identify an uncompressed format.
|
|
313
|
+
if negotiated and negotiated.upper() in _UNCOMPRESSED_FOURCCS:
|
|
314
|
+
logger.warning(
|
|
315
|
+
"Webcam negotiated uncompressed %s at %dx%d; USB bandwidth may cap the "
|
|
316
|
+
"frame rate well below the requested value. Pass fourcc='MJPG', or try a "
|
|
317
|
+
"different backend (e.g. backend='msmf' on Windows).",
|
|
318
|
+
negotiated,
|
|
319
|
+
width,
|
|
320
|
+
height,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def get_fps(self) -> Optional[float]:
|
|
324
|
+
"""Get FPS."""
|
|
325
|
+
if not self.is_connected or self.cap is None:
|
|
326
|
+
return None
|
|
327
|
+
return self.cap.get(cv2.CAP_PROP_FPS)
|
|
328
|
+
|
|
329
|
+
def set_fps(self, fps: float) -> bool:
|
|
330
|
+
"""Set FPS."""
|
|
331
|
+
if not self.is_connected or self.cap is None:
|
|
332
|
+
return False
|
|
333
|
+
self.cap.set(cv2.CAP_PROP_FPS, fps)
|
|
334
|
+
return True
|
|
335
|
+
|
|
336
|
+
def enable_auto_exposure(self, enable: bool = True) -> bool:
|
|
337
|
+
"""
|
|
338
|
+
Enable or disable auto exposure for webcam.
|
|
339
|
+
"""
|
|
340
|
+
if not self.is_connected or self.cap is None:
|
|
341
|
+
return False
|
|
342
|
+
try:
|
|
343
|
+
# OpenCV expects 0.75 for auto, 0.25 for manual (on many webcams)
|
|
344
|
+
value = 0.75 if enable else 0.25
|
|
345
|
+
result = self.cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, value)
|
|
346
|
+
logger.info(f"Set auto exposure to {enable} (cv2 value: {value})")
|
|
347
|
+
return result
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.error(f"Error setting auto exposure: {e}")
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
@classmethod
|
|
353
|
+
def discover(cls) -> list[DeviceInfo]:
|
|
354
|
+
try:
|
|
355
|
+
devices: list[DeviceInfo] = []
|
|
356
|
+
from cv2.videoio_registry import getBackendName
|
|
357
|
+
from cv2_enumerate_cameras import enumerate_cameras
|
|
358
|
+
|
|
359
|
+
camera_list = []
|
|
360
|
+
if platform.system() == "Windows":
|
|
361
|
+
backends = [cv2.CAP_DSHOW, cv2.CAP_MSMF]
|
|
362
|
+
elif platform.system() == "Darwin": # macOS
|
|
363
|
+
backends = [cv2.CAP_AVFOUNDATION]
|
|
364
|
+
else: # Linux
|
|
365
|
+
backends = [cv2.CAP_V4L2]
|
|
366
|
+
|
|
367
|
+
for backend in backends:
|
|
368
|
+
camera_list.extend(enumerate_cameras(backend))
|
|
369
|
+
|
|
370
|
+
# Sort camera list by index
|
|
371
|
+
camera_list.sort(key=lambda cam: cam.index)
|
|
372
|
+
|
|
373
|
+
for camera_info in camera_list:
|
|
374
|
+
# OS enumeration index -> not a stable identifier across replug.
|
|
375
|
+
devices.append(
|
|
376
|
+
DeviceInfo(
|
|
377
|
+
device_id=f"{camera_info.backend}:{camera_info.index}:{camera_info.path}",
|
|
378
|
+
index=camera_info.index,
|
|
379
|
+
name=camera_info.name,
|
|
380
|
+
driver="opencv",
|
|
381
|
+
id_stable=False,
|
|
382
|
+
metadata={
|
|
383
|
+
"backend_index": camera_info.backend,
|
|
384
|
+
"backend_name": getBackendName(camera_info.backend),
|
|
385
|
+
},
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
logger.info(f"Found {cls.__name__} input device: {devices}")
|
|
389
|
+
return devices
|
|
390
|
+
except ImportError:
|
|
391
|
+
logger.warning(
|
|
392
|
+
"cv2-enumerate-cameras module not available. Install cv2-enumerate-cameras "
|
|
393
|
+
"to list available (web)cameras."
|
|
394
|
+
)
|
|
395
|
+
return []
|
|
396
|
+
|
|
397
|
+
@classmethod
|
|
398
|
+
def get_config_schema(cls) -> dict[str, Any]:
|
|
399
|
+
"""Get configuration schema for webcam capture"""
|
|
400
|
+
warnings.warn(
|
|
401
|
+
"get_config_schema() is deprecated and will be removed in a future release; "
|
|
402
|
+
"UI form schemas belong in the consuming application.",
|
|
403
|
+
DeprecationWarning,
|
|
404
|
+
stacklevel=2,
|
|
405
|
+
)
|
|
406
|
+
return {
|
|
407
|
+
"title": "Webcam Configuration",
|
|
408
|
+
"description": "Configure USB webcam or built-in camera settings",
|
|
409
|
+
"fields": [
|
|
410
|
+
{
|
|
411
|
+
"name": "source",
|
|
412
|
+
"label": "Camera Index",
|
|
413
|
+
"type": "number",
|
|
414
|
+
"min": 0,
|
|
415
|
+
"max": 10,
|
|
416
|
+
"placeholder": "0",
|
|
417
|
+
"description": "Camera device index (0 for default, 1 for second camera, etc.)",
|
|
418
|
+
"required": False,
|
|
419
|
+
"default": 0,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"name": "width",
|
|
423
|
+
"label": "Width",
|
|
424
|
+
"type": "number",
|
|
425
|
+
"min": 160,
|
|
426
|
+
"max": 4096,
|
|
427
|
+
"placeholder": "1920",
|
|
428
|
+
"description": "Frame width in pixels",
|
|
429
|
+
"required": False,
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
"name": "height",
|
|
433
|
+
"label": "Height",
|
|
434
|
+
"type": "number",
|
|
435
|
+
"min": 120,
|
|
436
|
+
"max": 2160,
|
|
437
|
+
"placeholder": "1080",
|
|
438
|
+
"description": "Frame height in pixels",
|
|
439
|
+
"required": False,
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
"name": "fps",
|
|
443
|
+
"label": "Frame Rate (FPS)",
|
|
444
|
+
"type": "number",
|
|
445
|
+
"min": 1,
|
|
446
|
+
"max": 120,
|
|
447
|
+
"placeholder": "30",
|
|
448
|
+
"description": "Frames per second",
|
|
449
|
+
"required": False,
|
|
450
|
+
"default": 30,
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
"name": "exposure",
|
|
454
|
+
"label": "Exposure",
|
|
455
|
+
"type": "number",
|
|
456
|
+
"min": -13,
|
|
457
|
+
"max": -1,
|
|
458
|
+
"step": 1,
|
|
459
|
+
"placeholder": "-6",
|
|
460
|
+
"description": "Manual exposure value (-13 to -1, lower = brighter)",
|
|
461
|
+
"required": False,
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
"name": "gain",
|
|
465
|
+
"label": "Gain",
|
|
466
|
+
"type": "number",
|
|
467
|
+
"min": 0,
|
|
468
|
+
"max": 255,
|
|
469
|
+
"placeholder": "0",
|
|
470
|
+
"description": "Camera gain (0-255)",
|
|
471
|
+
"required": False,
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
if __name__ == "__main__":
|
|
478
|
+
# Example usage
|
|
479
|
+
|
|
480
|
+
print("Discovering webcams...")
|
|
481
|
+
webcams = WebcamCapture.discover()
|
|
482
|
+
print(f"\nFound {len(webcams)} webcam(s):\n")
|
|
483
|
+
for cam in webcams:
|
|
484
|
+
print(f" [{cam['index']}] {cam['name']}")
|
|
485
|
+
|
|
486
|
+
if webcams:
|
|
487
|
+
print("\nTesting first camera...")
|
|
488
|
+
camera = WebcamCapture(source=0)
|
|
489
|
+
if camera.connect():
|
|
490
|
+
print("Webcam connected successfully.")
|
|
491
|
+
print(f"Exposure: {camera.get_exposure()}")
|
|
492
|
+
print(f"Gain: {camera.get_gain()}")
|
|
493
|
+
print(f"Frame size: {camera.get_frame_size()}")
|
|
494
|
+
|
|
495
|
+
# Read a few frames
|
|
496
|
+
print("Press 'q' to quit...")
|
|
497
|
+
while camera.is_connected:
|
|
498
|
+
ret, frame = camera.read()
|
|
499
|
+
if ret:
|
|
500
|
+
cv2.imshow("Webcam", frame) # type: ignore
|
|
501
|
+
if cv2.waitKey(1) & 0xFF == ord("q"):
|
|
502
|
+
break
|
|
503
|
+
else:
|
|
504
|
+
print("Failed to read frame from webcam.")
|
|
505
|
+
break
|
|
506
|
+
|
|
507
|
+
camera.disconnect()
|
|
508
|
+
else:
|
|
509
|
+
print("Failed to connect to webcam.")
|
|
510
|
+
else:
|
|
511
|
+
print("\nNo webcams found.")
|