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,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
+ [![CI](https://github.com/olkham/FrameSource/actions/workflows/ci.yml/badge.svg)](https://github.com/olkham/FrameSource/actions/workflows/ci.yml)
79
+ [![PyPI version](https://img.shields.io/pypi/v/framesource)](https://pypi.org/project/framesource/)
80
+ [![Python versions](https://img.shields.io/pypi/pyversions/framesource)](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
+ ![FrameSource Demo](media/demo.gif)
106
+
107
+ ### Interactive 360° Camera Demo
108
+
109
+ ![Interactive 360° Demo](media/interactive_360_demo.gif)
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! 🚀