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
framesource/factory.py ADDED
@@ -0,0 +1,290 @@
1
+ """
2
+ Video Capture System with Factory Pattern
3
+
4
+ A comprehensive video capture system that supports multiple backends:
5
+ - Webcam (OpenCV)
6
+ - IP Camera (RTSP/HTTP)
7
+ - Industrial cameras (Basler, GenICam)
8
+ - Custom capture APIs
9
+
10
+ Usage:
11
+ capture = FrameSourceFactory.create('webcam', source=0)
12
+ capture.connect()
13
+ capture.set_exposure(50)
14
+ frame = capture.read()
15
+ """
16
+
17
+ import importlib
18
+ import json
19
+ import logging
20
+ import os
21
+ import warnings
22
+ from pathlib import Path
23
+ from typing import Any, Literal, Optional, Union
24
+
25
+ from .errors import MissingDependencyError, UnknownSourceTypeError
26
+ from .sources.video_capture_base import VideoCaptureBase
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ def _warn_deprecated_param(old: str, new: str) -> None:
32
+ """Log and warn that a factory parameter name is deprecated."""
33
+ message = f"Parameter '{old}' is deprecated, use '{new}' instead"
34
+ logger.warning(message)
35
+ warnings.warn(message, DeprecationWarning, stacklevel=3)
36
+
37
+
38
+ # Import capture classes with graceful handling for missing optional
39
+ # dependencies (e.g. vendor SDKs). Each entry maps a factory key to its module
40
+ # and class name; sources that fail to import are simply omitted.
41
+ _OPTIONAL_SOURCES = [
42
+ ("webcam", "webcam_capture", "WebcamCapture"),
43
+ ("ipcam", "ipcamera_capture", "IPCameraCapture"),
44
+ ("basler", "basler_capture", "BaslerCapture"),
45
+ ("genicam", "genicam_capture", "GenicamCapture"),
46
+ ("realsense", "realsense_capture", "RealsenseCapture"),
47
+ ("ximea", "ximea_capture", "XimeaCapture"),
48
+ ("huateng", "huateng_capture", "HuatengCapture"),
49
+ ("video_file", "video_file_capture", "VideoFileCapture"),
50
+ ("folder", "folder_capture", "FolderCapture"),
51
+ ("screen", "screen_capture", "ScreenCapture"),
52
+ ("audio_spectrogram", "audiospectrogram_capture", "AudioSpectrogramCapture"),
53
+ ]
54
+
55
+ _capture_imports: dict[str, type] = {}
56
+
57
+ for _key, _module_name, _class_name in _OPTIONAL_SOURCES:
58
+ try:
59
+ _module = importlib.import_module(f".sources.{_module_name}", __package__)
60
+ _capture_imports[_key] = getattr(_module, _class_name)
61
+ except Exception as e: # noqa: BLE001 - vendor SDKs may raise non-ImportError
62
+ logger.debug("%s unavailable: %s", _class_name, e)
63
+
64
+
65
+ class FrameSourceFactory:
66
+ """Factory class for creating video capture instances."""
67
+
68
+ MediaSource = Literal[
69
+ "folder",
70
+ "video_file",
71
+ "webcam",
72
+ "ipcam",
73
+ "basler",
74
+ "realsense",
75
+ "screen",
76
+ "genicam",
77
+ "ximea",
78
+ "huateng",
79
+ "audio_spectrogram",
80
+ ]
81
+
82
+ CameraSource = Literal["webcam", "realsense", "genicam", "basler", "ximea", "huateng"]
83
+
84
+ _capture_types: dict[str, type] = _capture_imports
85
+
86
+ @classmethod
87
+ def create(
88
+ cls,
89
+ source_type: Any = None,
90
+ source_id: Any = None,
91
+ capture_type: Any = None,
92
+ source: Any = None,
93
+ **kwargs,
94
+ ) -> VideoCaptureBase:
95
+ """
96
+ Create a video capture instance.
97
+
98
+ Args:
99
+ source_type: Type of capture ('webcam', 'ipcam', 'basler', 'genicam', 'custom')
100
+ source_id: Source identifier
101
+ capture_type: (DEPRECATED) Use source_type instead
102
+ source: (DEPRECATED) Use source_id instead
103
+ connect: If True (default), connect() is called before returning.
104
+ **kwargs: Additional parameters for the specific capture type
105
+
106
+ Returns:
107
+ VideoCaptureBase: Configured capture instance
108
+
109
+ Raises:
110
+ UnknownSourceTypeError: If source_type is not supported (also
111
+ catchable as ValueError for backwards compatibility).
112
+ """
113
+ # Handle backward compatibility for capture_type -> source_type
114
+ if source_type is None and capture_type is not None:
115
+ _warn_deprecated_param("capture_type", "source_type")
116
+ source_type = capture_type
117
+
118
+ # Try to get source_type from kwargs if still not set
119
+ if source_type is None:
120
+ source_type = kwargs.pop("source_type", None)
121
+
122
+ # Fall back to capture_type in kwargs for backward compatibility
123
+ if source_type is None:
124
+ source_type = kwargs.pop("capture_type", None)
125
+ if source_type is not None:
126
+ _warn_deprecated_param("capture_type", "source_type")
127
+
128
+ if not source_type or source_type not in cls._capture_types:
129
+ available_types = ", ".join(cls._capture_types.keys())
130
+ raise UnknownSourceTypeError(
131
+ f"Unsupported capture type: {source_type}. Available types: {available_types}"
132
+ )
133
+
134
+ # Handle backward compatibility for source -> source_id
135
+ if source_id is None and source is not None:
136
+ _warn_deprecated_param("source", "source_id")
137
+ source_id = source
138
+
139
+ # Try to get source_id from kwargs if still not set
140
+ if source_id is None:
141
+ source_id = kwargs.pop("source_id", None)
142
+
143
+ # Fall back to source in kwargs for backward compatibility
144
+ if source_id is None:
145
+ source_id = kwargs.pop("source", None)
146
+ if source_id is not None:
147
+ _warn_deprecated_param("source", "source_id")
148
+
149
+ if source_id is None:
150
+ logger.warning("Source not provided, defaulting to 0")
151
+ source_id = 0
152
+
153
+ # Pop control kwargs before they reach the capture constructor.
154
+ connect = kwargs.pop("connect", True)
155
+
156
+ capture_class = cls._capture_types[source_type]
157
+ cc = capture_class(source=source_id, **kwargs)
158
+
159
+ if connect:
160
+ cc.connect()
161
+
162
+ return cc
163
+
164
+ @classmethod
165
+ def from_config(cls, config: Union[dict[str, Any], str, os.PathLike]) -> VideoCaptureBase:
166
+ """Create a capture instance from a configuration dict or file.
167
+
168
+ The configuration supplies ``source_type`` and (optionally)
169
+ ``source_id`` plus any additional keyword arguments understood by
170
+ :meth:`create` (including ``connect``). Every key other than
171
+ ``source_type`` and ``source_id`` is forwarded to :meth:`create`
172
+ unchanged.
173
+
174
+ Args:
175
+ config: Either a mapping of configuration values, or a path (``str``
176
+ or :class:`os.PathLike`) to a ``.json``, ``.yaml`` or ``.yml``
177
+ file containing that mapping. A dict argument is never mutated.
178
+
179
+ Returns:
180
+ VideoCaptureBase: The configured capture instance.
181
+
182
+ Raises:
183
+ MissingDependencyError: If a ``.yaml``/``.yml`` file is given but
184
+ PyYAML is not installed.
185
+ ValueError: If a file with an unsupported extension is given.
186
+ FileNotFoundError: If the given path does not exist.
187
+ UnknownSourceTypeError: If ``source_type`` is missing or unknown.
188
+
189
+ Example:
190
+ ```python
191
+ from framesource import FrameSourceFactory
192
+
193
+ # From an in-memory dict:
194
+ cap = FrameSourceFactory.from_config({
195
+ "source_type": "webcam",
196
+ "source_id": 0,
197
+ "connect": False,
198
+ })
199
+
200
+ # From a JSON or YAML file:
201
+ cap = FrameSourceFactory.from_config("camera.json")
202
+ ```
203
+ """
204
+ if isinstance(config, (str, os.PathLike)):
205
+ config = cls._read_config_file(config)
206
+
207
+ # Shallow-copy so the caller's dict is never mutated.
208
+ params = dict(config)
209
+ source_type = params.pop("source_type", None)
210
+ source_id = params.pop("source_id", None)
211
+ return cls.create(source_type, source_id=source_id, **params)
212
+
213
+ @staticmethod
214
+ def _read_config_file(path: Union[str, os.PathLike]) -> dict[str, Any]:
215
+ """Load a config mapping from a ``.json``, ``.yaml`` or ``.yml`` file."""
216
+ path = Path(path)
217
+ suffix = path.suffix.lower()
218
+ if suffix == ".json":
219
+ with open(path, encoding="utf-8") as fh:
220
+ return json.load(fh)
221
+ if suffix in (".yaml", ".yml"):
222
+ try:
223
+ import yaml
224
+ except ImportError as e:
225
+ raise MissingDependencyError(
226
+ "pyyaml",
227
+ extra=None,
228
+ details="required to load .yaml config files",
229
+ ) from e
230
+ with open(path, encoding="utf-8") as fh:
231
+ return yaml.safe_load(fh)
232
+ raise ValueError(
233
+ f"Unsupported config file extension: {path.suffix!r} (expected .json, .yaml or .yml)"
234
+ )
235
+
236
+ @classmethod
237
+ def register_capture_type(cls, name: str, capture_class: type):
238
+ """
239
+ Register a new capture type.
240
+
241
+ Args:
242
+ name: Name of the capture type
243
+ capture_class: Class implementing VideoCaptureBase
244
+ """
245
+ if not issubclass(capture_class, VideoCaptureBase):
246
+ raise ValueError("Capture class must inherit from VideoCaptureBase")
247
+
248
+ if name in cls._capture_types:
249
+ logger.warning(f"Capture type '{name}' already registered, replacing with new class.")
250
+
251
+ cls._capture_types[name] = capture_class
252
+ logger.info(f"Registered new capture type: {name}")
253
+
254
+ @classmethod
255
+ def discover_devices(cls, sources: Optional[list[str]] = None) -> dict:
256
+ """
257
+ Discover available capture devices from the registered capture types.
258
+
259
+ This method queries each capture type for connected devices and
260
+ returns a dictionary mapping source names to their discovery results.
261
+
262
+ Args:
263
+ sources (list[str], optional): Specific source keys to limit discovery to.
264
+ If None, all registered capture types are queried.
265
+
266
+ Returns:
267
+ dict: A mapping of source keys to the discovered devices for each.
268
+ Sources that return no devices are excluded.
269
+ """
270
+
271
+ _sources = (
272
+ cls._capture_types.items()
273
+ if sources is None
274
+ else ((k, v) for k, v in cls._capture_types.items() if k in sources)
275
+ )
276
+
277
+ return {k: ret for k, v in _sources if (ret := v.discover())}
278
+
279
+ @classmethod
280
+ def get_available_types(cls) -> list:
281
+ """Get list of available capture types."""
282
+ return list(cls._capture_types.keys())
283
+
284
+ @classmethod
285
+ def unregister_capture_type(cls, capture_type: str):
286
+ """Unregister a capture type (convenience function)"""
287
+ if capture_type not in cls._capture_types:
288
+ raise UnknownSourceTypeError(f"Capture type '{capture_type}' is not registered")
289
+ del cls._capture_types[capture_type]
290
+ logger.info(f"Unregistered capture type: {capture_type}")
@@ -0,0 +1,34 @@
1
+ """Frame processors package for FrameSource.
2
+
3
+ This package contains various frame processing modules for handling different
4
+ types of image transformations and manipulations.
5
+ """
6
+
7
+ from .equirectangular360_processor import Equirectangular2PinholeProcessor
8
+ from .fisheye2equirectangular_processor import Fisheye2EquirectangularProcessor
9
+ from .frame_processor import FrameProcessor
10
+
11
+ __all__ = [
12
+ "FrameProcessor",
13
+ "Equirectangular2PinholeProcessor",
14
+ "Fisheye2EquirectangularProcessor",
15
+ ]
16
+
17
+ # Conditionally import RealsenseDepthProcessor if pyrealsense2 is available
18
+ try:
19
+ from .realsense_depth_processor import RealsenseDepthProcessor as RealsenseDepthProcessor
20
+
21
+ __all__.append("RealsenseDepthProcessor")
22
+ except ImportError:
23
+ # pyrealsense2 not available
24
+ pass
25
+
26
+ # Conditionally import HyperspectralChannelSelector if its dependencies are available
27
+ try:
28
+ from .hyperspectral_processor import (
29
+ HyperspectralChannelSelector as HyperspectralChannelSelector,
30
+ )
31
+
32
+ __all__.append("HyperspectralChannelSelector")
33
+ except ImportError:
34
+ pass