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
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
|