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.
Files changed (37) hide show
  1. frame_processors/__init__.py +37 -0
  2. frame_source/__init__.py +50 -0
  3. framesource/__init__.py +93 -0
  4. framesource/_msmf_config.py +26 -0
  5. framesource/_version.py +24 -0
  6. framesource/discovery.py +109 -0
  7. framesource/errors.py +82 -0
  8. framesource/factory.py +290 -0
  9. framesource/processors/__init__.py +34 -0
  10. framesource/processors/equirectangular360_processor.py +577 -0
  11. framesource/processors/fisheye2equirectangular_processor.py +328 -0
  12. framesource/processors/frame_processor.py +30 -0
  13. framesource/processors/hyperspectral_processor.py +23 -0
  14. framesource/processors/realsense_depth_processor.py +90 -0
  15. framesource/py.typed +0 -0
  16. framesource/sources/__init__.py +40 -0
  17. framesource/sources/audiospectrogram_capture.py +1068 -0
  18. framesource/sources/basler_capture.py +477 -0
  19. framesource/sources/folder_capture.py +920 -0
  20. framesource/sources/genicam_capture.py +681 -0
  21. framesource/sources/huateng_capture.py +245 -0
  22. framesource/sources/ipcamera_capture.py +254 -0
  23. framesource/sources/mvsdk.py +2454 -0
  24. framesource/sources/realsense_capture.py +565 -0
  25. framesource/sources/screen_capture.py +800 -0
  26. framesource/sources/video_capture_base.py +560 -0
  27. framesource/sources/video_file_capture.py +259 -0
  28. framesource/sources/webcam_capture.py +511 -0
  29. framesource/sources/ximea_capture.py +299 -0
  30. framesource/threading_utils.py +790 -0
  31. framesource-0.3.0.dist-info/METADATA +787 -0
  32. framesource-0.3.0.dist-info/RECORD +37 -0
  33. framesource-0.3.0.dist-info/WHEEL +5 -0
  34. framesource-0.3.0.dist-info/licenses/LICENSE +21 -0
  35. framesource-0.3.0.dist-info/scm_file_list.json +276 -0
  36. framesource-0.3.0.dist-info/scm_version.json +8 -0
  37. framesource-0.3.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,560 @@
1
+ import time
2
+ import uuid as _uuid
3
+ from abc import ABC, abstractmethod
4
+ from collections import deque
5
+ from typing import Any, Optional, Protocol, runtime_checkable
6
+
7
+ import cv2
8
+ import numpy as np
9
+
10
+
11
+ class Frame(np.ndarray):
12
+ """A numpy array subclass that carries per-frame metadata.
13
+
14
+ Because ``Frame`` *is* a numpy array it works transparently with any
15
+ OpenCV or numpy function::
16
+
17
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # works as-is
18
+ print(frame.timestamp, frame.uuid) # metadata intact
19
+
20
+ Attributes:
21
+ timestamp (float): UNIX wall-clock time of capture (``time.time()``).
22
+ Use this for human-readable timestamps, logging, or correlating
23
+ frames with external wall-clock events. It is subject to system
24
+ clock adjustments (NTP sync, manual changes, DST), so it is not
25
+ reliable for measuring elapsed time or latency.
26
+ monotonic (float): Monotonic clock reading at capture
27
+ (``time.monotonic()``). Use this for latency and FPS math
28
+ (elapsed-time computations) since it always moves forward and is
29
+ immune to wall-clock jumps.
30
+ count (int | None): Monotonically-increasing counter per source.
31
+ uuid (str): UUID4 string uniquely identifying this frame.
32
+ source (str | None): Identifies the capture source.
33
+ metadata (dict): Free-form key/value store for extra fields.
34
+ """
35
+
36
+ def __new__(
37
+ cls,
38
+ input_array: np.ndarray,
39
+ timestamp: Optional[float] = None,
40
+ count: Optional[int] = None,
41
+ uuid: Optional[str] = None,
42
+ source: Optional[str] = None,
43
+ metadata: Optional[dict] = None,
44
+ monotonic: Optional[float] = None,
45
+ ) -> "Frame":
46
+ obj = np.asarray(input_array).view(cls)
47
+ obj.timestamp = timestamp if timestamp is not None else time.time()
48
+ obj.count = count
49
+ obj.uuid = uuid if uuid is not None else str(_uuid.uuid4())
50
+ obj.source = source
51
+ obj.metadata = metadata if metadata is not None else {}
52
+ obj.monotonic = monotonic if monotonic is not None else time.monotonic()
53
+ return obj
54
+
55
+ def __array_finalize__(self, obj: Optional[np.ndarray]) -> None:
56
+ """Called whenever a new view / copy of a Frame is produced."""
57
+ if obj is None:
58
+ return
59
+ self.timestamp = getattr(obj, "timestamp", None)
60
+ self.count = getattr(obj, "count", None)
61
+ self.uuid = getattr(obj, "uuid", None)
62
+ self.source = getattr(obj, "source", None)
63
+ self.metadata = getattr(obj, "metadata", {})
64
+ self.monotonic = getattr(obj, "monotonic", None)
65
+
66
+ def __reduce__(self):
67
+ """Extend numpy pickling so metadata survives pickle / deepcopy."""
68
+ pickled = super().__reduce__()
69
+ extra = (self.timestamp, self.count, self.uuid, self.source, self.metadata, self.monotonic)
70
+ np_state = pickled[2] if isinstance(pickled[2], tuple) else (pickled[2],)
71
+ return (pickled[0], pickled[1], np_state + extra)
72
+
73
+ def __setstate__(self, state):
74
+ *np_state, ts, cnt, uid, src, meta, mono = state
75
+ super().__setstate__(tuple(np_state))
76
+ self.timestamp = ts
77
+ self.count = cnt
78
+ self.uuid = uid
79
+ self.source = src
80
+ self.metadata = meta
81
+ self.monotonic = mono
82
+
83
+
84
+ class VideoCaptureBase(ABC):
85
+ # Class attribute indicating if this capture type supports device discovery
86
+ has_discovery = False
87
+
88
+ # Capability flags. These are base-class defaults; subclasses that lack a
89
+ # given capability (e.g. a source with no controllable exposure/gain, or
90
+ # a depth-producing device) should override them. `supports_discovery`
91
+ # is derived below from `has_discovery` rather than duplicated.
92
+ supports_exposure: bool = True
93
+ supports_gain: bool = True
94
+ supports_depth: bool = False
95
+
96
+ """Abstract base class for video capture implementations."""
97
+
98
+ def __init__(self, source: Any = None, **kwargs):
99
+ """
100
+ Initialize the capture device.
101
+
102
+ Args:
103
+ source: Source identifier (device index, URL, etc.)
104
+ **kwargs: Additional parameters specific to the capture type
105
+ """
106
+ self.source = source
107
+ self.is_connected = False
108
+ self._exposure = None
109
+ self._gain = None
110
+ self._frame_count = 0
111
+ self.config = kwargs
112
+ self.type = self.__class__.__name__
113
+ # Rolling window of recent frame timestamps for fps_actual()
114
+ self._frame_timestamps: deque = deque(maxlen=30)
115
+
116
+ def __str__(self) -> str:
117
+ return f"{self.type}(source={self.source}, connected={self.is_connected})"
118
+
119
+ @abstractmethod
120
+ def connect(self) -> bool:
121
+ """
122
+ Connect to the capture device.
123
+
124
+ Returns:
125
+ bool: True if connection successful, False otherwise
126
+ """
127
+ pass
128
+
129
+ @abstractmethod
130
+ def disconnect(self) -> bool:
131
+ """
132
+ Disconnect from the capture device.
133
+
134
+ Returns:
135
+ bool: True if disconnection successful, False otherwise
136
+ """
137
+ pass
138
+
139
+ @abstractmethod
140
+ def _read_implementation(self) -> tuple[bool, Optional[np.ndarray]]:
141
+ """
142
+ Read a single frame from the capture device.
143
+
144
+ This is the synchronous, blocking primitive each subclass must
145
+ implement. Callers should use :meth:`read` (which wraps the result in
146
+ a :class:`Frame` and applies any attached processors) rather than
147
+ calling this directly.
148
+
149
+ Returns:
150
+ Tuple[bool, Optional[np.ndarray]]: (success, frame)
151
+ """
152
+ pass
153
+
154
+ @abstractmethod
155
+ def enable_auto_exposure(self, enable: bool = True) -> bool:
156
+ """
157
+ Enable or disable auto exposure.
158
+
159
+ Args:
160
+ enable: True to enable, False to disable
161
+
162
+ Returns:
163
+ bool: True if set successfully, False otherwise
164
+ """
165
+ pass
166
+
167
+ def get_exposure_range(self) -> Optional[tuple[float, float]]:
168
+ """
169
+ Get the minimum and maximum exposure values supported by the device.
170
+ Returns:
171
+ Optional[Tuple[float, float]]: (min_exposure, max_exposure) or None if not available
172
+ """
173
+ return None
174
+
175
+ def get_gain_range(self) -> Optional[tuple[float, float]]:
176
+ """
177
+ Get the minimum and maximum gain values supported by the device.
178
+ Returns:
179
+ Optional[Tuple[float, float]]: (min_gain, max_gain) or None if not available
180
+ """
181
+ return None
182
+
183
+ @abstractmethod
184
+ def set_exposure(self, value: float) -> bool:
185
+ """
186
+ Set exposure value.
187
+
188
+ Args:
189
+ value: Exposure value (range depends on implementation)
190
+
191
+ Returns:
192
+ bool: True if set successfully, False otherwise
193
+ """
194
+ pass
195
+
196
+ @abstractmethod
197
+ def get_exposure(self) -> Optional[float]:
198
+ """
199
+ Get current exposure value.
200
+
201
+ Returns:
202
+ Optional[float]: Current exposure value or None if not available
203
+ """
204
+ pass
205
+
206
+ @abstractmethod
207
+ def set_gain(self, value: float) -> bool:
208
+ """
209
+ Set gain value.
210
+
211
+ Args:
212
+ value: Gain value (range depends on implementation)
213
+
214
+ Returns:
215
+ bool: True if set successfully, False otherwise
216
+ """
217
+ pass
218
+
219
+ @abstractmethod
220
+ def get_gain(self) -> Optional[float]:
221
+ """
222
+ Get current gain value.
223
+
224
+ Returns:
225
+ Optional[float]: Current gain value or None if not available
226
+ """
227
+ pass
228
+
229
+ @classmethod
230
+ def discover(cls) -> list:
231
+ """
232
+ Discover available devices for this capture type.
233
+
234
+ Returns:
235
+ list: List of available devices. Format depends on implementation:
236
+ - For cameras: List of device indices, serial numbers, or device info dicts
237
+ - For audio: List of microphone devices with info
238
+ - For network cameras: List of discovered IP cameras
239
+ - For files: Empty list (no discovery needed)
240
+
241
+ Note:
242
+ This is a class method that can be called without instantiating the capture class.
243
+ Subclasses should override this method to implement device-specific discovery.
244
+ Default implementation returns empty list.
245
+ """
246
+ return []
247
+
248
+ def get_frame_size(self) -> Optional[tuple[int, int]]:
249
+ """
250
+ Get frame dimensions (width, height).
251
+
252
+ Returns:
253
+ Optional[Tuple[int, int]]: Frame size or None if not available
254
+ """
255
+ return None
256
+
257
+ def set_frame_size(self, width: int, height: int) -> bool:
258
+ """
259
+ Set frame dimensions.
260
+
261
+ Args:
262
+ width: Frame width
263
+ height: Frame height
264
+
265
+ Returns:
266
+ bool: True if set successfully, False otherwise
267
+ """
268
+ return False
269
+
270
+ def get_fps(self) -> Optional[float]:
271
+ """Get current FPS."""
272
+ return None
273
+
274
+ def set_fps(self, fps: float) -> bool:
275
+ """Set FPS."""
276
+ return False
277
+
278
+ def __enter__(self):
279
+ """Context manager entry."""
280
+ if not self.is_connected:
281
+ self.connect()
282
+ return self
283
+
284
+ def __exit__(self, exc_type, exc_val, exc_tb):
285
+ """Context manager exit."""
286
+ if self.is_connected:
287
+ self.disconnect()
288
+
289
+ def isOpened(self) -> bool:
290
+ """
291
+ Check if the capture device is opened/connected.
292
+ OpenCV-compatible API method.
293
+
294
+ Returns:
295
+ bool: True if device is connected, False otherwise
296
+ """
297
+ return self.is_connected
298
+
299
+ def is_open(self) -> bool:
300
+ """Check if the capture device is connected (Pythonic alias for isOpened)."""
301
+ return self.is_connected
302
+
303
+ @property
304
+ def supports_discovery(self) -> bool:
305
+ """Whether this capture type supports :meth:`discover`.
306
+
307
+ Read-only alias for the ``has_discovery`` class attribute, exposed as
308
+ a property so it lines up with the other ``supports_*`` capability
309
+ flags (``supports_exposure``, ``supports_gain``, ``supports_depth``).
310
+ """
311
+ return bool(type(self).has_discovery)
312
+
313
+ def release(self) -> None:
314
+ """
315
+ Release the capture device.
316
+ OpenCV-compatible API method that disconnects from the device.
317
+ """
318
+ if self.is_connected:
319
+ self.disconnect()
320
+
321
+ def set(self, prop_id: int, value: Any) -> bool:
322
+ """
323
+ Set a property of the capture device.
324
+ OpenCV-compatible API method.
325
+
326
+ Args:
327
+ prop_id: Property identifier (OpenCV constant)
328
+ value: Value to set
329
+ Returns:
330
+ bool: True if set successfully, False otherwise
331
+ """
332
+
333
+ if prop_id == cv2.CAP_PROP_EXPOSURE:
334
+ self.set_exposure(value)
335
+ return True
336
+ elif prop_id == cv2.CAP_PROP_BRIGHTNESS:
337
+ return False
338
+ elif prop_id == cv2.CAP_PROP_CONTRAST:
339
+ return False
340
+ elif prop_id == cv2.CAP_PROP_SATURATION:
341
+ return False
342
+ elif prop_id == cv2.CAP_PROP_GAIN:
343
+ self.set_gain(value)
344
+ return True
345
+ elif prop_id == cv2.CAP_PROP_FPS:
346
+ self.set_fps(value)
347
+ return True
348
+ elif prop_id == cv2.CAP_PROP_FRAME_WIDTH:
349
+ size = self.get_frame_size()
350
+ if size:
351
+ self.set_frame_size(int(value), size[1])
352
+ return True
353
+ else:
354
+ return False
355
+ elif prop_id == cv2.CAP_PROP_FRAME_HEIGHT:
356
+ size = self.get_frame_size()
357
+ if size:
358
+ self.set_frame_size(size[0], int(value))
359
+ return True
360
+ else:
361
+ return False
362
+ else:
363
+ return False
364
+
365
+ def get(self, prop_id: int) -> Any:
366
+ """
367
+ Get a property of the capture device.
368
+ OpenCV-compatible API method.
369
+
370
+ Args:
371
+ prop_id: Property identifier (OpenCV constant)
372
+ Returns:
373
+ Any: Property value or None if not available
374
+ """
375
+ if prop_id == cv2.CAP_PROP_EXPOSURE:
376
+ return self.get_exposure()
377
+ elif prop_id == cv2.CAP_PROP_BRIGHTNESS:
378
+ # return self.get_brightness()
379
+ return None
380
+ elif prop_id == cv2.CAP_PROP_CONTRAST:
381
+ # return self.get_contrast()
382
+ return None
383
+ elif prop_id == cv2.CAP_PROP_SATURATION:
384
+ # return self.get_saturation()
385
+ return None
386
+ elif prop_id == cv2.CAP_PROP_GAIN:
387
+ return self.get_gain()
388
+ elif prop_id == cv2.CAP_PROP_FPS:
389
+ return self.get_fps()
390
+ elif prop_id == cv2.CAP_PROP_FRAME_WIDTH:
391
+ size = self.get_frame_size()
392
+ if size:
393
+ return size[0]
394
+ else:
395
+ return None
396
+ elif prop_id == cv2.CAP_PROP_FRAME_HEIGHT:
397
+ size = self.get_frame_size()
398
+ if size:
399
+ return size[1]
400
+ else:
401
+ return None
402
+ else:
403
+ return None
404
+
405
+ def attach_processor(self, processor):
406
+ """Attach a frame processor to this camera."""
407
+ if not hasattr(self, "_processors"):
408
+ self._processors = []
409
+ self._processors.append(processor)
410
+ return processor
411
+
412
+ def detach_processor(self, processor):
413
+ """Remove a processor from this camera."""
414
+ if hasattr(self, "_processors"):
415
+ if processor in self._processors:
416
+ self._processors.remove(processor)
417
+ return True
418
+ return False
419
+
420
+ def clear_processors(self):
421
+ """Remove all processors."""
422
+ if hasattr(self, "_processors"):
423
+ self._processors.clear()
424
+
425
+ def get_processors(self):
426
+ """Get all attached processors."""
427
+ if not hasattr(self, "_processors"):
428
+ self._processors = []
429
+ return self._processors
430
+
431
+ def read(self):
432
+ """Read a frame, wrap it in a Frame with metadata, and apply processors."""
433
+ ret, raw = self._read_implementation()
434
+
435
+ if ret and raw is not None:
436
+ frame: np.ndarray = Frame(
437
+ raw,
438
+ count=self._frame_count,
439
+ source=str(self.source),
440
+ )
441
+ self._frame_count += 1
442
+ # Use the monotonic clock (not wall-clock timestamp) so fps_actual()
443
+ # stays correct across system clock adjustments.
444
+ self._frame_timestamps.append(frame.monotonic)
445
+ if hasattr(self, "_processors") and self._processors:
446
+ for processor in self._processors:
447
+ frame = processor.process(frame)
448
+ else:
449
+ frame = raw # type: ignore[assignment] # raw is None when ret is False
450
+
451
+ return ret, frame
452
+
453
+ def fps_actual(self) -> Optional[float]:
454
+ """Measured frame rate over the most recent frames.
455
+
456
+ Computes the average FPS from the monotonic-clock readings
457
+ (:attr:`Frame.monotonic`) of recently read frames (rolling window of
458
+ up to 30). The monotonic clock is used rather than the wall-clock
459
+ ``timestamp`` so the measurement stays accurate even if the system
460
+ clock jumps (NTP sync, DST, manual changes). Returns ``None`` until
461
+ at least two frames have been read.
462
+
463
+ Returns:
464
+ Optional[float]: Measured frames-per-second, or None if not enough
465
+ frames have been read yet.
466
+ """
467
+ timestamps = self._frame_timestamps
468
+ if len(timestamps) < 2:
469
+ return None
470
+ span = timestamps[-1] - timestamps[0]
471
+ if span <= 0:
472
+ return None
473
+ return (len(timestamps) - 1) / span
474
+
475
+ def wait_until_ready(self, timeout: float = 5.0) -> bool:
476
+ """Block until the source is actually producing frames.
477
+
478
+ Some sources (notably RTSP/network cameras) report a successful
479
+ ``connect()`` before the underlying stream has started delivering
480
+ frames -- the socket/session is open, but the first ``read()`` calls
481
+ may fail for a bit while the stream negotiates. This method polls
482
+ :meth:`read` until a frame is successfully returned or ``timeout``
483
+ seconds have elapsed, so callers can avoid treating that startup
484
+ window as a hard failure.
485
+
486
+ Note:
487
+ This consumes a frame: on success, the frame that proved
488
+ readiness has already been read (and passed through any
489
+ attached processors) and is not returned or buffered. Callers
490
+ that need that particular frame should call :meth:`read` again
491
+ immediately after, or capture it by overriding/wrapping this
492
+ method.
493
+
494
+ Args:
495
+ timeout: Maximum number of seconds to wait for a frame.
496
+
497
+ Returns:
498
+ bool: True as soon as a frame is successfully read, False if no
499
+ frame arrived before ``timeout`` elapsed.
500
+ """
501
+ deadline = time.monotonic() + timeout
502
+ while True:
503
+ ret, frame = self.read()
504
+ if ret and frame is not None:
505
+ return True
506
+ if time.monotonic() >= deadline:
507
+ return False
508
+ time.sleep(0.01)
509
+
510
+ def __iter__(self) -> "VideoCaptureBase":
511
+ """Return self, allowing the capture to be used as an iterator.
512
+
513
+ Enables the idiom::
514
+
515
+ with cap:
516
+ for frame in cap:
517
+ ... # process each frame until the source is exhausted
518
+
519
+ Returns:
520
+ VideoCaptureBase: self.
521
+ """
522
+ return self
523
+
524
+ def __next__(self):
525
+ """Read and return the next frame, or stop iteration.
526
+
527
+ Returns:
528
+ Frame: The next frame from :meth:`read`.
529
+
530
+ Raises:
531
+ StopIteration: If ``read()`` reports failure (``ret`` is False)
532
+ or returns ``None`` for the frame (end of stream / no data).
533
+ """
534
+ ret, frame = self.read()
535
+ if not ret or frame is None:
536
+ raise StopIteration
537
+ return frame
538
+
539
+
540
+ @runtime_checkable
541
+ class FrameSourceProtocol(Protocol):
542
+ """Structural interface implemented by every FrameSource capture class.
543
+
544
+ Use this for static type hints or ``isinstance`` checks when you want to
545
+ accept any frame source without requiring inheritance from
546
+ :class:`VideoCaptureBase`::
547
+
548
+ def run(source: FrameSourceProtocol) -> None:
549
+ source.connect()
550
+ ok, frame = source.read()
551
+ ...
552
+ """
553
+
554
+ def connect(self) -> bool: ...
555
+
556
+ def disconnect(self) -> bool: ...
557
+
558
+ def read(self) -> tuple[bool, Optional[np.ndarray]]: ...
559
+
560
+ def is_open(self) -> bool: ...