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,787 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: framesource
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A flexible, extensible Python framework for acquiring frames from cameras, video files, image folders, screen capture, and audio spectrograms with built-in frame processors.
|
|
5
|
+
Author: Oliver Hamilton
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Oliver Hamilton
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/olkham/FrameSource
|
|
29
|
+
Project-URL: Repository, https://github.com/olkham/FrameSource
|
|
30
|
+
Project-URL: Issues, https://github.com/olkham/FrameSource/issues
|
|
31
|
+
Keywords: camera,computer-vision,video-capture,webcam,rtsp,realsense,basler,screen-capture,frame-grabber,opencv
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: OS Independent
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: Multimedia :: Video :: Capture
|
|
44
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
45
|
+
Classifier: Typing :: Typed
|
|
46
|
+
Requires-Python: >=3.9
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
License-File: LICENSE
|
|
49
|
+
Requires-Dist: opencv-python
|
|
50
|
+
Requires-Dist: numpy
|
|
51
|
+
Requires-Dist: mss
|
|
52
|
+
Requires-Dist: numba
|
|
53
|
+
Requires-Dist: watchdog
|
|
54
|
+
Requires-Dist: cv2-enumerate-cameras
|
|
55
|
+
Requires-Dist: pywin32; platform_system == "Windows"
|
|
56
|
+
Requires-Dist: pyobjc-framework-Quartz; platform_system == "Darwin"
|
|
57
|
+
Provides-Extra: audio
|
|
58
|
+
Requires-Dist: librosa; extra == "audio"
|
|
59
|
+
Requires-Dist: soundfile; extra == "audio"
|
|
60
|
+
Requires-Dist: pyaudio; extra == "audio"
|
|
61
|
+
Provides-Extra: basler
|
|
62
|
+
Requires-Dist: pypylon; extra == "basler"
|
|
63
|
+
Provides-Extra: realsense
|
|
64
|
+
Requires-Dist: pyrealsense2; extra == "realsense"
|
|
65
|
+
Provides-Extra: genicam
|
|
66
|
+
Requires-Dist: harvesters; extra == "genicam"
|
|
67
|
+
Provides-Extra: full
|
|
68
|
+
Requires-Dist: librosa; extra == "full"
|
|
69
|
+
Requires-Dist: soundfile; extra == "full"
|
|
70
|
+
Requires-Dist: pyaudio; extra == "full"
|
|
71
|
+
Requires-Dist: pypylon; extra == "full"
|
|
72
|
+
Requires-Dist: pyrealsense2; extra == "full"
|
|
73
|
+
Requires-Dist: harvesters; extra == "full"
|
|
74
|
+
Dynamic: license-file
|
|
75
|
+
|
|
76
|
+
# FrameSource 📷🖼️
|
|
77
|
+
|
|
78
|
+
[](https://github.com/olkham/FrameSource/actions/workflows/ci.yml)
|
|
79
|
+
[](https://pypi.org/project/framesource/)
|
|
80
|
+
[](https://pypi.org/project/framesource/)
|
|
81
|
+
|
|
82
|
+
FrameSource is a flexible, extensible Python framework for acquiring frames from a wide variety of sources such as webcams, industrial cameras, IP cameras, video files, and even folders of images—using a unified interface. It was created to support my many projects that require switching between different frame providers without changing the downstream frame processing code.
|
|
83
|
+
|
|
84
|
+
> **Note:** This project was mostly written with the help of GitHub Copilot 🤖, making development fast, fun, and consistent! Even this README was largely generated by Copilot.
|
|
85
|
+
|
|
86
|
+
## Supported Sources
|
|
87
|
+
|
|
88
|
+
### Camera Sources
|
|
89
|
+
- 🖥️ **Webcam** (OpenCV) - Standard USB webcams, built-in laptop cameras
|
|
90
|
+
- 🌐 **IP Camera** (RTSP/HTTP) - Network cameras, security cameras
|
|
91
|
+
- 🏭 **Industrial Cameras**:
|
|
92
|
+
- **Basler** cameras (via pypylon SDK) - High-performance industrial imaging
|
|
93
|
+
- **Ximea** cameras - Scientific and machine vision cameras
|
|
94
|
+
- **Huateng** cameras - Cost-effective industrial cameras
|
|
95
|
+
- 🔍 **Intel RealSense** - RGB-D cameras with depth sensing (tested with D456)
|
|
96
|
+
|
|
97
|
+
### Media Sources
|
|
98
|
+
- 🎥 **Video File** (MP4, AVI, etc.) - Playback with looping and controls
|
|
99
|
+
- 🗂️ **Folder of Images** - Sorted by name or creation time with configurable FPS
|
|
100
|
+
- 🖼️ **Screen Capture** - Live region capture from desktop
|
|
101
|
+
- 🎵 **Audio Spectrogram** - Real-time audio visualization from microphone or files
|
|
102
|
+
|
|
103
|
+
## Demo
|
|
104
|
+
|
|
105
|
+

|
|
106
|
+
|
|
107
|
+
### Interactive 360° Camera Demo
|
|
108
|
+
|
|
109
|
+

|
|
110
|
+
|
|
111
|
+
The 360° camera example (`examples/camera_360_example.py`) provides an intuitive interface for exploring equirectangular footage:
|
|
112
|
+
- **Click & Drag**: Click anywhere on the 360° image and drag to smoothly pan the view
|
|
113
|
+
- **Mouse Wheel**: Scroll to zoom in/out by adjusting the field of view
|
|
114
|
+
- **Keyboard Controls**: Fine-tune pitch, yaw, roll, and FOV with precise keyboard shortcuts
|
|
115
|
+
- **Real-time Processing**: Live conversion from equirectangular to pinhole projection
|
|
116
|
+
|
|
117
|
+
## Why FrameSource?
|
|
118
|
+
|
|
119
|
+
When I work on computer vision, robotics, or video analytics projects, I often need to swap between different sources of frames: a webcam for quick tests, a folder of images for batch processing, a video file for reproducibility, or a specialized camera for deployment. FrameSource lets me do this with minimal code changes—just swap the provider!
|
|
120
|
+
|
|
121
|
+
## Why not plain OpenCV?
|
|
122
|
+
|
|
123
|
+
`cv2.VideoCapture` is great, but it only covers webcams, video files, and a handful of streams. The moment you need a Basler/Ximea industrial camera, a RealSense depth stream, a folder of images replayed at a fixed FPS, screen capture, or an audio spectrogram, you end up writing a different integration for each — with different connect/read/release semantics.
|
|
124
|
+
|
|
125
|
+
FrameSource is a thin **adapter layer** that gives every one of those sources the same `connect()` / `read()` / `disconnect()` contract (and an OpenCV-compatible `isOpened()` / `read()` surface), so your downstream processing code never has to care where frames come from. You opt into the heavier backends only when you install the matching extra.
|
|
126
|
+
|
|
127
|
+
## Design Goals
|
|
128
|
+
|
|
129
|
+
- **One interface, many sources** — identical `connect()`/`read()`/`disconnect()` contract across every backend, validated by a runtime-checkable `FrameSourceProtocol`.
|
|
130
|
+
- **Synchronous and predictable** — `read()` is a plain blocking call. No hidden background threads, no shared mutable frame buffers inside the capture objects.
|
|
131
|
+
- **Bring-your-own concurrency** — when you want threading or multiprocessing, opt in explicitly with the helpers in `framesource.threading_utils` so the threading model stays visible and under your control.
|
|
132
|
+
|
|
133
|
+
## Non-Goals
|
|
134
|
+
|
|
135
|
+
- **Frame-accurate multi-source synchronization** — FrameSource does not hardware-sync or timestamp-align multiple cameras for you.
|
|
136
|
+
- **High-throughput zero-copy pipelines** — it favours a simple, readable API over squeezing out maximum FPS or avoiding every copy.
|
|
137
|
+
- **GPU-first decoding** — decoding uses the backend's defaults (mostly CPU/OpenCV); it is not a CUDA/NVDEC acceleration layer.
|
|
138
|
+
|
|
139
|
+
## Architecture
|
|
140
|
+
|
|
141
|
+
```mermaid
|
|
142
|
+
graph LR
|
|
143
|
+
Factory["FrameSourceFactory<br/>.create() / .from_config()"] --> Sources["VideoCaptureBase subclasses<br/>(webcam, ipcam, video_file, folder, screen, industrial, audio)"]
|
|
144
|
+
Sources --> Frame["Frame<br/>(ndarray + timestamp / count / uuid / metadata)"]
|
|
145
|
+
Frame -.->|optional attach_processor| Processor["FrameProcessor"]
|
|
146
|
+
Frame --> Threading["threading_utils<br/>(FrameProducer / SharedProducer / AsyncFrameSource)"]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Features ✨
|
|
150
|
+
|
|
151
|
+
- Unified interface for all frame sources (cameras, video files, image folders, screen capture, audio spectrograms)
|
|
152
|
+
- Built-in frame processors for specialized transformations (360° equirectangular to pinhole projection)
|
|
153
|
+
- Easily extensible with new capture types and processing modules
|
|
154
|
+
- Optional external threading/multiprocessing helpers (`framesource.threading_utils`) for smooth, decoupled frame acquisition
|
|
155
|
+
- Control over exposure, gain, resolution, FPS (where supported by the source)
|
|
156
|
+
- Real-time playback and looping for video and image folders
|
|
157
|
+
- Simple factory pattern for instantiating sources
|
|
158
|
+
|
|
159
|
+
## Installation
|
|
160
|
+
|
|
161
|
+
### Install from PyPI
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
pip install framesource
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
With optional extras:
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
# Audio spectrogram support
|
|
171
|
+
pip install "framesource[audio]"
|
|
172
|
+
|
|
173
|
+
# Basler camera support
|
|
174
|
+
pip install "framesource[basler]"
|
|
175
|
+
|
|
176
|
+
# RealSense camera support
|
|
177
|
+
pip install "framesource[realsense]"
|
|
178
|
+
|
|
179
|
+
# GenICam (Harvester) support
|
|
180
|
+
pip install "framesource[genicam]"
|
|
181
|
+
|
|
182
|
+
# Everything with a PyPI-installable dependency
|
|
183
|
+
pip install "framesource[full]"
|
|
184
|
+
|
|
185
|
+
# Multiple extras at once
|
|
186
|
+
pip install "framesource[audio,basler,realsense]"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
> **Note on vendor SDK cameras:** Ximea and Huateng/MindVision backends ship with
|
|
190
|
+
> the package but rely on proprietary drivers that are **not** available on PyPI.
|
|
191
|
+
> Install the vendor SDK separately (Ximea `xiapi`, or the Huateng/MindVision
|
|
192
|
+
> camera drivers) and the corresponding backend will activate automatically.
|
|
193
|
+
>
|
|
194
|
+
> The 360°/fisheye processors only require the core dependencies (`numpy`,
|
|
195
|
+
> `opencv-python`) and work out of the box — no extra needed.
|
|
196
|
+
|
|
197
|
+
### Install Directly from GitHub
|
|
198
|
+
|
|
199
|
+
You can install FrameSource directly from GitHub without cloning:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
# Latest version from main branch
|
|
203
|
+
pip install git+https://github.com/olkham/FrameSource.git
|
|
204
|
+
|
|
205
|
+
# Specific branch
|
|
206
|
+
pip install git+https://github.com/olkham/FrameSource.git@branch-name
|
|
207
|
+
|
|
208
|
+
# Specific tag or commit
|
|
209
|
+
pip install git+https://github.com/olkham/FrameSource.git@v1.0.0
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Install from Local Clone
|
|
213
|
+
|
|
214
|
+
Clone the repository and install with pip:
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
git clone https://github.com/olkham/FrameSource.git
|
|
218
|
+
cd FrameSource
|
|
219
|
+
pip install .
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Or for development (editable) install:
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
pip install -e .
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Installation Options
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
FrameSource supports optional dependencies for additional features:
|
|
232
|
+
|
|
233
|
+
**From GitHub:**
|
|
234
|
+
```sh
|
|
235
|
+
# Basic installation (core frame sources only)
|
|
236
|
+
pip install git+https://github.com/olkham/FrameSource.git
|
|
237
|
+
|
|
238
|
+
# With audio spectrogram support
|
|
239
|
+
pip install "framesource[audio] @ git+https://github.com/olkham/FrameSource.git"
|
|
240
|
+
|
|
241
|
+
# With Basler camera support
|
|
242
|
+
pip install "framesource[basler] @ git+https://github.com/olkham/FrameSource.git"
|
|
243
|
+
|
|
244
|
+
# With RealSense camera support
|
|
245
|
+
pip install "framesource[realsense] @ git+https://github.com/olkham/FrameSource.git"
|
|
246
|
+
|
|
247
|
+
# With GenICam (Harvester) support
|
|
248
|
+
pip install "framesource[genicam] @ git+https://github.com/olkham/FrameSource.git"
|
|
249
|
+
|
|
250
|
+
# With all optional features
|
|
251
|
+
pip install "framesource[full] @ git+https://github.com/olkham/FrameSource.git"
|
|
252
|
+
|
|
253
|
+
# Multiple extras at once
|
|
254
|
+
pip install "framesource[audio,basler,realsense] @ git+https://github.com/olkham/FrameSource.git"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**From local installation:**
|
|
258
|
+
```sh
|
|
259
|
+
# Basic installation (core frame sources only)
|
|
260
|
+
pip install .
|
|
261
|
+
|
|
262
|
+
# With audio spectrogram support
|
|
263
|
+
pip install .[audio]
|
|
264
|
+
|
|
265
|
+
# With Basler camera support
|
|
266
|
+
pip install .[basler]
|
|
267
|
+
|
|
268
|
+
# With RealSense camera support
|
|
269
|
+
pip install .[realsense]
|
|
270
|
+
|
|
271
|
+
# With all optional features
|
|
272
|
+
pip install .[full]
|
|
273
|
+
|
|
274
|
+
# Multiple extras at once
|
|
275
|
+
pip install .[audio,basler,realsense]
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Manual Dependency Installation
|
|
279
|
+
|
|
280
|
+
Alternatively, you can install dependencies manually:
|
|
281
|
+
|
|
282
|
+
```sh
|
|
283
|
+
# Audio processing
|
|
284
|
+
pip install librosa soundfile pyaudio
|
|
285
|
+
|
|
286
|
+
# Basler cameras
|
|
287
|
+
pip install pypylon
|
|
288
|
+
|
|
289
|
+
# RealSense cameras
|
|
290
|
+
pip install pyrealsense2
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Example Usage
|
|
294
|
+
|
|
295
|
+
> 💡 **Tip**: For comprehensive examples of each capture type, see the `examples/` directory. Run `python examples/run_examples.py` for an interactive demo menu.
|
|
296
|
+
|
|
297
|
+
### 1. Using the Factory
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
from framesource import FrameSourceFactory
|
|
302
|
+
|
|
303
|
+
# Webcam — create() connects automatically by default; pass connect=False
|
|
304
|
+
# if you need to configure the source before connecting yourself.
|
|
305
|
+
cap = FrameSourceFactory.create('webcam', source_id=0, connect=False)
|
|
306
|
+
cap.connect()
|
|
307
|
+
ret, frame = cap.read()
|
|
308
|
+
cap.disconnect()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# Video file (see demo in media/demo.mp4) — auto-connects, no explicit connect() needed
|
|
312
|
+
cap = FrameSourceFactory.create('video_file', source_id='media/demo.mp4', loop=True)
|
|
313
|
+
while cap.is_connected:
|
|
314
|
+
ret, frame = cap.read()
|
|
315
|
+
if not ret:
|
|
316
|
+
break
|
|
317
|
+
cap.disconnect()
|
|
318
|
+
|
|
319
|
+
# Folder of images — auto-connects, no explicit connect() needed
|
|
320
|
+
cap = FrameSourceFactory.create('folder', source_id='media/image_seq', sort_by='date', fps=10, loop=True)
|
|
321
|
+
while cap.is_connected:
|
|
322
|
+
ret, frame = cap.read()
|
|
323
|
+
if not ret:
|
|
324
|
+
break
|
|
325
|
+
cap.disconnect()
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Sources are also iterable — the loop above can be written as:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
with FrameSourceFactory.create('video_file', source_id='media/demo.mp4', connect=False) as cap:
|
|
332
|
+
for frame in cap: # yields Frame objects until the source is exhausted
|
|
333
|
+
process(frame)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Each `frame` is a `Frame` — a `numpy.ndarray` subclass that works in any
|
|
337
|
+
OpenCV/numpy call and carries `timestamp` (wall clock), `monotonic` (for
|
|
338
|
+
latency/FPS math), `count`, `uuid`, `source`, and a free-form `metadata` dict.
|
|
339
|
+
|
|
340
|
+
### 2. Config-Driven Creation
|
|
341
|
+
|
|
342
|
+
Sources can also be built from a plain `dict`, or from a `.json`/`.yaml` file, via
|
|
343
|
+
`FrameSourceFactory.from_config()`. This is handy for storing camera configs alongside
|
|
344
|
+
your application config instead of hand-writing `create()` calls.
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from framesource import FrameSourceFactory
|
|
348
|
+
|
|
349
|
+
# From a dict
|
|
350
|
+
cap = FrameSourceFactory.from_config({
|
|
351
|
+
"source_type": "webcam",
|
|
352
|
+
"source_id": 0,
|
|
353
|
+
"connect": False, # extra keys pass straight through as create() kwargs
|
|
354
|
+
})
|
|
355
|
+
cap.connect()
|
|
356
|
+
|
|
357
|
+
# From a file (JSON or YAML — YAML requires PyYAML installed)
|
|
358
|
+
cap = FrameSourceFactory.from_config("configs/webcam.yaml")
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The config shape mirrors `create()`'s parameters: `source_type` and `source_id` are read
|
|
362
|
+
directly, and any other keys (e.g. `fps`, `loop`, `connect`) are forwarded as keyword
|
|
363
|
+
arguments.
|
|
364
|
+
|
|
365
|
+
#### Webcam frame rate at high resolution (MJPG / backend)
|
|
366
|
+
|
|
367
|
+
If a USB webcam requests 1080p (or higher) at 30 fps but only delivers a handful of frames
|
|
368
|
+
per second, the cause is almost always the **pixel format**: the camera is streaming an
|
|
369
|
+
**uncompressed** format (e.g. YUY2) that saturates the USB link, where the Windows Camera app
|
|
370
|
+
would request compressed **MJPG**. Two knobs fix this:
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
cap = FrameSourceFactory.create(
|
|
374
|
+
'webcam', source_id=0,
|
|
375
|
+
width=1920, height=1080, fps=30,
|
|
376
|
+
fourcc='MJPG', # request a compressed format (applied before the resolution)
|
|
377
|
+
backend='msmf', # on Windows, MSMF negotiates MJPG where DirectShow often won't
|
|
378
|
+
)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
- **`fourcc`** — four-character pixel format, e.g. `'MJPG'`. Applied before the resolution so
|
|
382
|
+
the driver doesn't reset it.
|
|
383
|
+
- **`backend`** — `'msmf'`, `'dshow'`, `'v4l2'`, `'avfoundation'`, `'gstreamer'`, `'ffmpeg'`,
|
|
384
|
+
`'any'`, or a raw `cv2.CAP_*` int. Defaults to the OS default (DirectShow on Windows). Note
|
|
385
|
+
that DirectShow and MSMF interpret `set_exposure()`/`set_gain()` values differently.
|
|
386
|
+
|
|
387
|
+
`connect()` logs a warning if it detects an uncompressed format negotiated at 720p or above, so
|
|
388
|
+
this failure mode is visible rather than silent.
|
|
389
|
+
|
|
390
|
+
> **Windows / MSMF slow open:** OpenCV's MSMF backend can take 20+ seconds to *open* some
|
|
391
|
+
> webcams because it initializes hardware Media Foundation transforms. FrameSource disables
|
|
392
|
+
> those transforms (`OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS=0`) when you `import framesource`,
|
|
393
|
+
> which drops the open time to a fraction of a second with no throughput cost. This only takes
|
|
394
|
+
> effect if `framesource` is imported before `cv2` opens an MSMF device; set the environment
|
|
395
|
+
> variable yourself (to `0`) if your app imports and uses `cv2` first, or to `1` to opt back
|
|
396
|
+
> into the default OpenCV behaviour.
|
|
397
|
+
|
|
398
|
+
### 3. Direct Use
|
|
399
|
+
|
|
400
|
+
#### Intel RealSense Camera
|
|
401
|
+
```python
|
|
402
|
+
from framesource.sources.realsense_capture import RealsenseCapture
|
|
403
|
+
from framesource.processors import RealsenseDepthProcessor
|
|
404
|
+
from framesource.processors.realsense_depth_processor import RealsenseProcessingOutput
|
|
405
|
+
|
|
406
|
+
# Tested with Intel RealSense D456 camera
|
|
407
|
+
cap = RealsenseCapture(width=640, height=480)
|
|
408
|
+
processor = RealsenseDepthProcessor(output_format=RealsenseProcessingOutput.ALIGNED_SIDE_BY_SIDE)
|
|
409
|
+
cap.attach_processor(processor)
|
|
410
|
+
cap.connect()
|
|
411
|
+
while cap.is_connected:
|
|
412
|
+
ret, frame = cap.read()
|
|
413
|
+
if not ret:
|
|
414
|
+
break
|
|
415
|
+
# Frame contains RGB and depth side-by-side or other configured format
|
|
416
|
+
cap.disconnect()
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
#### Folder of Images
|
|
420
|
+
```python
|
|
421
|
+
from framesource.sources.folder_capture import FolderCapture
|
|
422
|
+
cap = FolderCapture('media/image_seq', sort_by='name', width=640, height=480, fps=15, real_time=True, loop=True)
|
|
423
|
+
cap.connect()
|
|
424
|
+
while cap.is_connected:
|
|
425
|
+
ret, frame = cap.read()
|
|
426
|
+
if not ret:
|
|
427
|
+
break
|
|
428
|
+
cap.disconnect()
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
#### Screen Capture
|
|
432
|
+
```python
|
|
433
|
+
from framesource.sources.screen_capture import ScreenCapture
|
|
434
|
+
cap = ScreenCapture(x=100, y=100, w=800, h=600, fps=30)
|
|
435
|
+
cap.connect()
|
|
436
|
+
while cap.is_connected:
|
|
437
|
+
ret, frame = cap.read()
|
|
438
|
+
if not ret:
|
|
439
|
+
break
|
|
440
|
+
# process or display frame
|
|
441
|
+
cap.disconnect()
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### Audio Spectrogram Capture
|
|
445
|
+
```python
|
|
446
|
+
# Audio spectrogram from microphone (real-time) — auto-connects, no explicit connect() needed
|
|
447
|
+
cap = FrameSourceFactory.create('audio_spectrogram',
|
|
448
|
+
source_id=None, # None = default microphone
|
|
449
|
+
n_mels=128,
|
|
450
|
+
window_duration=2.0,
|
|
451
|
+
freq_range=(20, 8000),
|
|
452
|
+
colormap=cv2.COLORMAP_VIRIDIS)
|
|
453
|
+
while cap.is_connected:
|
|
454
|
+
ret, frame = cap.read()
|
|
455
|
+
if not ret:
|
|
456
|
+
break
|
|
457
|
+
# frame is now a visual spectrogram that can be processed like any other image
|
|
458
|
+
cap.disconnect()
|
|
459
|
+
|
|
460
|
+
# Audio spectrogram from file
|
|
461
|
+
cap = FrameSourceFactory.create('audio_spectrogram',
|
|
462
|
+
source_id='path/to/audio.wav',
|
|
463
|
+
n_mels=64,
|
|
464
|
+
frame_rate=30)
|
|
465
|
+
while cap.is_connected:
|
|
466
|
+
ret, frame = cap.read()
|
|
467
|
+
if not ret:
|
|
468
|
+
break
|
|
469
|
+
cap.disconnect()
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## Concurrency (External Threading) 🧵
|
|
473
|
+
|
|
474
|
+
Capture objects are **synchronous**: `read()` blocks until the next frame is ready and the source never spins up hidden background threads. When you want to decouple frame acquisition from processing, you opt in explicitly with the helpers in `framesource.threading_utils`. This keeps the threading model visible and under your control.
|
|
475
|
+
|
|
476
|
+
### Quickest: a producer thread feeding a queue
|
|
477
|
+
|
|
478
|
+
```python
|
|
479
|
+
import queue, threading
|
|
480
|
+
from framesource import FrameSourceFactory
|
|
481
|
+
from framesource.threading_utils import simple_frame_producer
|
|
482
|
+
|
|
483
|
+
camera = FrameSourceFactory.create('webcam', source_id=0) # auto-connects
|
|
484
|
+
|
|
485
|
+
frame_queue = queue.Queue(maxsize=10)
|
|
486
|
+
stop_event = threading.Event()
|
|
487
|
+
|
|
488
|
+
producer = threading.Thread(
|
|
489
|
+
target=simple_frame_producer,
|
|
490
|
+
args=(camera, frame_queue, stop_event, 30), # target 30 FPS
|
|
491
|
+
daemon=True,
|
|
492
|
+
)
|
|
493
|
+
producer.start()
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
while True:
|
|
497
|
+
ret, frame = frame_queue.get(timeout=1.0)
|
|
498
|
+
if not ret:
|
|
499
|
+
continue
|
|
500
|
+
# ... process / display frame ...
|
|
501
|
+
finally:
|
|
502
|
+
stop_event.set()
|
|
503
|
+
producer.join(timeout=2)
|
|
504
|
+
camera.disconnect()
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Managed: `FrameProducer` with built-in stats
|
|
508
|
+
|
|
509
|
+
```python
|
|
510
|
+
from framesource import FrameSourceFactory
|
|
511
|
+
from framesource.threading_utils import FrameProducer
|
|
512
|
+
|
|
513
|
+
camera = FrameSourceFactory.create('webcam', source_id=0) # auto-connects
|
|
514
|
+
|
|
515
|
+
producer = FrameProducer(camera, max_queue_size=10, target_fps=30)
|
|
516
|
+
producer.start()
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
while True:
|
|
520
|
+
ret, frame = producer.get_frame(timeout=1.0)
|
|
521
|
+
if not ret:
|
|
522
|
+
continue
|
|
523
|
+
# ... process / display frame ...
|
|
524
|
+
finally:
|
|
525
|
+
producer.stop()
|
|
526
|
+
print(producer.get_stats()) # frames_captured, frames_dropped, fps, avg_latency, ...
|
|
527
|
+
camera.disconnect()
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Heavier workloads: multiprocessing
|
|
531
|
+
|
|
532
|
+
For CPU-bound consumers you can move acquisition into a separate process. `multiprocess_frame_producer` takes a plain source-config dict and builds the capture inside the child process:
|
|
533
|
+
|
|
534
|
+
```python
|
|
535
|
+
import multiprocessing as mp
|
|
536
|
+
from framesource.threading_utils import multiprocess_frame_producer
|
|
537
|
+
|
|
538
|
+
source_config = {'source_type': 'webcam', 'source_id': 0}
|
|
539
|
+
frame_queue = mp.Queue(maxsize=10)
|
|
540
|
+
stop_event = mp.Event()
|
|
541
|
+
|
|
542
|
+
worker = mp.Process(
|
|
543
|
+
target=multiprocess_frame_producer,
|
|
544
|
+
args=(source_config, frame_queue, stop_event),
|
|
545
|
+
daemon=True,
|
|
546
|
+
)
|
|
547
|
+
worker.start()
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
while True:
|
|
551
|
+
ret, frame = frame_queue.get(timeout=1.0)
|
|
552
|
+
if not ret:
|
|
553
|
+
continue
|
|
554
|
+
# ... process frame ...
|
|
555
|
+
finally:
|
|
556
|
+
stop_event.set()
|
|
557
|
+
worker.join(timeout=2)
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Sharing one camera between consumers: `SharedProducer`
|
|
561
|
+
|
|
562
|
+
A physical camera can only be opened once, but a UI preview, a recorder, and a
|
|
563
|
+
network stream may all want its frames. `SharedProducer` is the explicit way to
|
|
564
|
+
fan one source out to many consumers — one visible producer thread, one queue
|
|
565
|
+
per subscriber, no hidden global state:
|
|
566
|
+
|
|
567
|
+
```python
|
|
568
|
+
from framesource import FrameSourceFactory, SharedProducer
|
|
569
|
+
|
|
570
|
+
camera = FrameSourceFactory.create('webcam', source_id=0, connect=False)
|
|
571
|
+
|
|
572
|
+
producer = SharedProducer(camera, target_fps=30)
|
|
573
|
+
ui_queue = producer.subscribe(maxsize=5) # subscribe before or after start()
|
|
574
|
+
recorder_queue = producer.subscribe(maxsize=30)
|
|
575
|
+
producer.start() # connects the source if needed
|
|
576
|
+
|
|
577
|
+
# ... each consumer drains its own queue at its own pace ...
|
|
578
|
+
ret, frame = ui_queue.get(timeout=1.0)
|
|
579
|
+
|
|
580
|
+
producer.stop() # stops the producer thread
|
|
581
|
+
camera.disconnect() # you own the source lifecycle
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### asyncio: `AsyncFrameSource`
|
|
585
|
+
|
|
586
|
+
For async applications, wrap any source in `AsyncFrameSource` — reads are
|
|
587
|
+
offloaded to a dedicated single worker thread so the event loop never blocks,
|
|
588
|
+
and the capture core stays fully synchronous:
|
|
589
|
+
|
|
590
|
+
```python
|
|
591
|
+
import asyncio
|
|
592
|
+
from framesource import FrameSourceFactory, AsyncFrameSource
|
|
593
|
+
|
|
594
|
+
async def main():
|
|
595
|
+
camera = FrameSourceFactory.create('webcam', source_id=0, connect=False)
|
|
596
|
+
async with AsyncFrameSource(camera) as source:
|
|
597
|
+
for _ in range(100):
|
|
598
|
+
ret, frame = await source.read()
|
|
599
|
+
|
|
600
|
+
asyncio.run(main())
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Waiting for a stream to become ready
|
|
604
|
+
|
|
605
|
+
Network sources (RTSP/HTTP) can report "connected" before they actually
|
|
606
|
+
deliver frames. `wait_until_ready()` polls until the first frame arrives:
|
|
607
|
+
|
|
608
|
+
```python
|
|
609
|
+
cap = FrameSourceFactory.create('ipcam', source_id='rtsp://...', connect=True)
|
|
610
|
+
if not cap.wait_until_ready(timeout=10.0):
|
|
611
|
+
raise RuntimeError("stream connected but produced no frames")
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
> See `examples/threading_utils_examples.py` and `examples/multiple_cameras_external_threading.py` for complete runnable demos.
|
|
615
|
+
|
|
616
|
+
## Frame Processors 🔄
|
|
617
|
+
|
|
618
|
+
FrameSource includes powerful frame processors for specialized transformations:
|
|
619
|
+
|
|
620
|
+
### Equirectangular 360° to Pinhole Projection
|
|
621
|
+
|
|
622
|
+
Convert 360° equirectangular footage to normal pinhole camera views with interactive controls:
|
|
623
|
+
|
|
624
|
+
```python
|
|
625
|
+
from framesource import FrameSourceFactory
|
|
626
|
+
from framesource.processors.equirectangular360_processor import Equirectangular2PinholeProcessor
|
|
627
|
+
|
|
628
|
+
# Load 360° video or connect to 360° webcam — auto-connects, no explicit connect() needed
|
|
629
|
+
cap = FrameSourceFactory.create('video_file', source_id='360_video.mp4')
|
|
630
|
+
# Or for live 360° camera: cap = FrameSourceFactory.create('webcam', source_id=0)
|
|
631
|
+
|
|
632
|
+
# Create processor for 90° FOV pinhole view
|
|
633
|
+
processor = Equirectangular2PinholeProcessor(fov=90.0, output_width=1920, output_height=1080)
|
|
634
|
+
|
|
635
|
+
# Set viewing angles (in degrees)
|
|
636
|
+
processor.set_parameter('yaw', 45.0) # Look right
|
|
637
|
+
processor.set_parameter('pitch', 0.0) # Look straight ahead
|
|
638
|
+
processor.set_parameter('roll', 0.0) # No rotation
|
|
639
|
+
|
|
640
|
+
# Attach processor to the frame source for automatic processing
|
|
641
|
+
cap.attach_processor(processor)
|
|
642
|
+
|
|
643
|
+
while cap.is_connected:
|
|
644
|
+
ret, frame = cap.read() # Frame is automatically processed by attached processor
|
|
645
|
+
if not ret:
|
|
646
|
+
break
|
|
647
|
+
|
|
648
|
+
# The frame is now the processed pinhole projection
|
|
649
|
+
cv2.imshow('360° to Pinhole', frame)
|
|
650
|
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
|
651
|
+
break
|
|
652
|
+
|
|
653
|
+
cap.disconnect()
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
> 💡 **Interactive Demo**: Try `python examples/camera_360_example.py` for a fully interactive 360° viewer with mouse controls! Click and drag on the equirectangular image to look around, use the mouse wheel to zoom, and keyboard shortcuts for fine adjustments.
|
|
657
|
+
|
|
658
|
+
You can also manually process frames without attaching:
|
|
659
|
+
|
|
660
|
+
```python
|
|
661
|
+
# Manual processing (without attach)
|
|
662
|
+
while cap.is_connected:
|
|
663
|
+
ret, frame = cap.read()
|
|
664
|
+
if not ret:
|
|
665
|
+
break
|
|
666
|
+
|
|
667
|
+
# Manually process the frame
|
|
668
|
+
pinhole_frame = processor.process(frame)
|
|
669
|
+
cv2.imshow('360° to Pinhole', pinhole_frame)
|
|
670
|
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
|
671
|
+
break
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### Creating Custom Frame Processors
|
|
675
|
+
|
|
676
|
+
Extend the `FrameProcessor` base class for your own transformations:
|
|
677
|
+
|
|
678
|
+
```python
|
|
679
|
+
from framesource.processors.frame_processor import FrameProcessor
|
|
680
|
+
import cv2
|
|
681
|
+
|
|
682
|
+
class GrayscaleProcessor(FrameProcessor):
|
|
683
|
+
def process(self, frame):
|
|
684
|
+
return cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
685
|
+
|
|
686
|
+
# Use your custom processor
|
|
687
|
+
processor = GrayscaleProcessor()
|
|
688
|
+
|
|
689
|
+
# Option 1: Attach to frame source for automatic processing
|
|
690
|
+
cap = FrameSourceFactory.create('webcam', source_id=0) # auto-connects
|
|
691
|
+
cap.attach_processor(processor)
|
|
692
|
+
|
|
693
|
+
while cap.is_connected:
|
|
694
|
+
ret, frame = cap.read() # Frame is automatically converted to grayscale
|
|
695
|
+
if not ret:
|
|
696
|
+
break
|
|
697
|
+
cv2.imshow('Grayscale', frame)
|
|
698
|
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
|
699
|
+
break
|
|
700
|
+
|
|
701
|
+
# Option 2: Manual processing
|
|
702
|
+
processed_frame = processor.process(original_frame)
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
## Extending FrameSource
|
|
706
|
+
|
|
707
|
+
### Adding New Frame Sources
|
|
708
|
+
|
|
709
|
+
Want to add a new camera or source? Just subclass `VideoCaptureBase` and register it:
|
|
710
|
+
|
|
711
|
+
```python
|
|
712
|
+
from framesource import FrameSourceFactory
|
|
713
|
+
FrameSourceFactory.register_capture_type('my_camera', MyCameraCapture)
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
You don't have to inherit from `VideoCaptureBase` to work with the concurrency
|
|
717
|
+
helpers, though. Anything that structurally satisfies `FrameSourceProtocol`
|
|
718
|
+
(`connect()`, `disconnect()`, `read()`, `is_open()`) is accepted by
|
|
719
|
+
`simple_frame_producer`, `FrameProducer`, `SharedProducer`, and
|
|
720
|
+
`AsyncFrameSource` — the protocol is `runtime_checkable`, so
|
|
721
|
+
`isinstance(obj, FrameSourceProtocol)` also works:
|
|
722
|
+
|
|
723
|
+
```python
|
|
724
|
+
from framesource import FrameSourceProtocol
|
|
725
|
+
|
|
726
|
+
def run(source: FrameSourceProtocol) -> None:
|
|
727
|
+
source.connect()
|
|
728
|
+
ret, frame = source.read()
|
|
729
|
+
...
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
### Error handling
|
|
733
|
+
|
|
734
|
+
`read()` keeps the OpenCV-style `(ret, frame)` contract — it never raises for
|
|
735
|
+
an ordinary failed read. Structured exceptions (all subclasses of
|
|
736
|
+
`framesource.FrameSourceError`) are raised only for setup problems, and they
|
|
737
|
+
multiple-inherit from the builtin you would already be catching:
|
|
738
|
+
|
|
739
|
+
- `MissingDependencyError` (also an `ImportError`) — an optional SDK/extra is
|
|
740
|
+
not installed; the message names the exact `pip install framesource[extra]`.
|
|
741
|
+
- `UnknownSourceTypeError` (also a `ValueError`) — unknown factory source type.
|
|
742
|
+
|
|
743
|
+
### Adding New Frame Processors
|
|
744
|
+
|
|
745
|
+
Create custom frame processors by extending `FrameProcessor`:
|
|
746
|
+
|
|
747
|
+
```python
|
|
748
|
+
from framesource.processors.frame_processor import FrameProcessor
|
|
749
|
+
|
|
750
|
+
class MyCustomProcessor(FrameProcessor):
|
|
751
|
+
def __init__(self, custom_param=1.0):
|
|
752
|
+
super().__init__()
|
|
753
|
+
self.set_parameter('custom_param', custom_param)
|
|
754
|
+
|
|
755
|
+
def process(self, frame):
|
|
756
|
+
# Your custom processing logic here
|
|
757
|
+
custom_param = self.get_parameter('custom_param')
|
|
758
|
+
# ... apply transformation ...
|
|
759
|
+
return processed_frame
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
## Roadmap
|
|
763
|
+
|
|
764
|
+
Planned for after this release (see `.github/prompts/future.md` for full briefs):
|
|
765
|
+
|
|
766
|
+
- **Auto-reconnect wrapper** (`ReconnectingSource`) for flaky RTSP/IP camera sources — opt-in,
|
|
767
|
+
handles backoff and reconnection without hiding it inside the capture classes.
|
|
768
|
+
- **Plugin packages** for vendor SDKs (Ximea, Huateng) and new sources (NDI, picamera2),
|
|
769
|
+
discovered via Python entry points instead of editing the factory directly.
|
|
770
|
+
- **`FrameSink` write/record API** — `VideoFileSink`/`FolderSink`/`DisplaySink` mirroring the
|
|
771
|
+
source contract, with metadata round-trip (a `FolderCapture` can read back the sidecar
|
|
772
|
+
metadata a `FolderSink` wrote).
|
|
773
|
+
- **Pipeline abstraction** — a small `source → processors → sink` runner built on top of the
|
|
774
|
+
sinks and reconnect work above.
|
|
775
|
+
- **PyAV backend selection** for decode, with optional hardware decode — an opt-in `backend=`
|
|
776
|
+
kwarg alongside the OpenCV default, for accurate PTS timestamps and RTSP transport control.
|
|
777
|
+
|
|
778
|
+
## Credits
|
|
779
|
+
|
|
780
|
+
- Written by me, with lots of help from GitHub Copilot 🤖
|
|
781
|
+
- OpenCV and other camera SDKs for backend support
|
|
782
|
+
- public RTSP URL from https://github.com/grigory-lobkov/rtsp-camera-view/issues/3
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
Happy frame grabbing! 🚀
|