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,328 @@
1
+ import math
2
+ from typing import Optional
3
+
4
+ import cv2
5
+ import numpy as np
6
+ from numba import njit, prange
7
+
8
+ from .frame_processor import FrameProcessor, FrameType
9
+
10
+
11
+ @njit(cache=True, fastmath=True)
12
+ def generate_fisheye_mapping_jit(
13
+ output_width, output_height, fisheye_cx, fisheye_cy, fisheye_radius, frame_height, frame_width
14
+ ):
15
+ """JIT-compiled coordinate mapping for fisheye to equirectangular conversion.
16
+
17
+ Maps a 180-degree equidistant fisheye (circular) image to equirectangular format.
18
+ The front hemisphere of the equirectangular output contains the fisheye content,
19
+ while the back hemisphere (beyond ±90° yaw) will sample outside the fisheye circle.
20
+ """
21
+
22
+ pixel_x = np.empty((output_height, output_width), dtype=np.float32)
23
+ pixel_y = np.empty((output_height, output_width), dtype=np.float32)
24
+
25
+ # Pre-calculate constants
26
+ pi = 3.141592653589793
27
+ half_pi = pi * 0.5
28
+ two_pi = pi * 2.0
29
+
30
+ # For each output pixel, calculate corresponding fisheye coordinates
31
+ for j in range(output_height):
32
+ # Latitude (elevation): top of image is +90°, bottom is -90°
33
+ # v ranges from 0 to 1
34
+ v = j / (output_height - 1) if output_height > 1 else 0.5
35
+ latitude = half_pi - v * pi # +90° to -90°
36
+
37
+ for i in range(output_width):
38
+ # Longitude (azimuth): left edge is -180°, right edge is +180°
39
+ # u ranges from 0 to 1
40
+ u = i / (output_width - 1) if output_width > 1 else 0.5
41
+ longitude = (u * two_pi) - pi # -180° to +180°
42
+
43
+ # Convert spherical coordinates to 3D unit vector
44
+ # Convention: Z is forward, X is right, Y is up
45
+ cos_lat = math.cos(latitude)
46
+ x = cos_lat * math.sin(longitude)
47
+ y = math.sin(latitude)
48
+ z = cos_lat * math.cos(longitude)
49
+
50
+ # For fisheye with optical axis along +Z:
51
+ # Calculate angle from optical axis (z-axis)
52
+ # z = cos(theta) where theta is angle from optical axis
53
+ theta = math.acos(max(-1.0, min(1.0, z)))
54
+
55
+ # Calculate azimuthal angle in the fisheye plane (XY plane)
56
+ phi = math.atan2(y, x)
57
+
58
+ # For equidistant fisheye projection:
59
+ # r = (theta / (pi/2)) * fisheye_radius
60
+ # This maps 0° (center) to r=0, and 90° (edge) to r=fisheye_radius
61
+ # Beyond 90° (back hemisphere), r > fisheye_radius (will sample outside circle)
62
+ r = (theta / half_pi) * fisheye_radius
63
+
64
+ # Convert polar (r, phi) to cartesian fisheye pixel coordinates
65
+ fisheye_x = fisheye_cx + r * math.cos(phi)
66
+ fisheye_y = fisheye_cy - r * math.sin(
67
+ phi
68
+ ) # Negative because image Y increases downward
69
+
70
+ pixel_x[j, i] = fisheye_x
71
+ pixel_y[j, i] = fisheye_y
72
+
73
+ return pixel_x, pixel_y
74
+
75
+
76
+ @njit(cache=True, fastmath=True, parallel=True)
77
+ def generate_fisheye_mapping_jit_parallel(
78
+ output_width, output_height, fisheye_cx, fisheye_cy, fisheye_radius, frame_height, frame_width
79
+ ):
80
+ """Parallel JIT-compiled coordinate mapping for fisheye to equirectangular conversion."""
81
+
82
+ pixel_x = np.empty((output_height, output_width), dtype=np.float32)
83
+ pixel_y = np.empty((output_height, output_width), dtype=np.float32)
84
+
85
+ pi = 3.141592653589793
86
+ half_pi = pi * 0.5
87
+ two_pi = pi * 2.0
88
+
89
+ total_pixels = output_height * output_width
90
+
91
+ for idx in prange(total_pixels):
92
+ j = idx // output_width
93
+ i = idx % output_width
94
+
95
+ v = j / (output_height - 1) if output_height > 1 else 0.5
96
+ latitude = half_pi - v * pi
97
+
98
+ u = i / (output_width - 1) if output_width > 1 else 0.5
99
+ longitude = (u * two_pi) - pi
100
+
101
+ cos_lat = math.cos(latitude)
102
+ x = cos_lat * math.sin(longitude)
103
+ y = math.sin(latitude)
104
+ z = cos_lat * math.cos(longitude)
105
+
106
+ theta = math.acos(max(-1.0, min(1.0, z)))
107
+ phi = math.atan2(y, x)
108
+
109
+ r = (theta / half_pi) * fisheye_radius
110
+
111
+ fisheye_x = fisheye_cx + r * math.cos(phi)
112
+ fisheye_y = fisheye_cy - r * math.sin(phi)
113
+
114
+ pixel_x[j, i] = fisheye_x
115
+ pixel_y[j, i] = fisheye_y
116
+
117
+ return pixel_x, pixel_y
118
+
119
+
120
+ class Fisheye2EquirectangularProcessor(FrameProcessor):
121
+ """Convert 180-degree circular fisheye images to equirectangular format.
122
+
123
+ This processor takes a circular fisheye image (typically from a 180° fisheye lens)
124
+ and projects it onto an equirectangular format. The output can then be chained
125
+ with Equirectangular2PinholeProcessor to enable pan/tilt/zoom control.
126
+
127
+ The fisheye is assumed to use equidistant projection, which is common for
128
+ 180-degree fisheye lenses. The circular fisheye image is centered and fills
129
+ the front hemisphere (±90° from center) of the equirectangular output.
130
+
131
+ Parameters:
132
+ fisheye_cx: X coordinate of fisheye center in input image
133
+ (default: auto-detect as image center)
134
+ fisheye_cy: Y coordinate of fisheye center in input image
135
+ (default: auto-detect as image center)
136
+ fisheye_radius: Radius of the fisheye circle in pixels
137
+ (default: auto-detect as min(width, height)/2)
138
+ output_width: Width of equirectangular output (default: 1920)
139
+ output_height: Height of equirectangular output (default: 960, standard 2:1 aspect ratio)
140
+
141
+ Example usage:
142
+ # Create processor chain for fisheye PTZ control
143
+ fisheye2equi = Fisheye2EquirectangularProcessor(output_width=1920, output_height=960)
144
+ equi2pinhole = Equirectangular2PinholeProcessor(
145
+ fov=90, output_width=1280, output_height=720
146
+ )
147
+
148
+ # Process frame
149
+ equi_frame = fisheye2equi.process(fisheye_frame)
150
+ output_frame = equi2pinhole.process(equi_frame)
151
+
152
+ # Control view direction
153
+ equi2pinhole.set_parameter('yaw', 30) # Pan right 30°
154
+ equi2pinhole.set_parameter('pitch', -15) # Tilt up 15°
155
+ """
156
+
157
+ def __init__(
158
+ self,
159
+ output_width: int = 1920,
160
+ output_height: int = 960,
161
+ fisheye_cx: Optional[float] = None,
162
+ fisheye_cy: Optional[float] = None,
163
+ fisheye_radius: Optional[float] = None,
164
+ ):
165
+ super().__init__()
166
+ self._parameters = {
167
+ "fisheye_cx": fisheye_cx,
168
+ "fisheye_cy": fisheye_cy,
169
+ "fisheye_radius": fisheye_radius,
170
+ "output_width": output_width,
171
+ "output_height": output_height,
172
+ }
173
+
174
+ # Coordinate mapping cache
175
+ self._map_cache = {}
176
+ self._cache_size_limit = 10
177
+
178
+ def process(self, frame: FrameType) -> FrameType:
179
+ """Convert fisheye frame to equirectangular projection."""
180
+ output_width = self._parameters["output_width"]
181
+ output_height = self._parameters["output_height"]
182
+
183
+ # Auto-detect fisheye parameters if not specified
184
+ frame_height, frame_width = frame.shape[:2]
185
+
186
+ fisheye_cx = self._parameters["fisheye_cx"]
187
+ if fisheye_cx is None:
188
+ fisheye_cx = frame_width / 2.0
189
+
190
+ fisheye_cy = self._parameters["fisheye_cy"]
191
+ if fisheye_cy is None:
192
+ fisheye_cy = frame_height / 2.0
193
+
194
+ fisheye_radius = self._parameters["fisheye_radius"]
195
+ if fisheye_radius is None:
196
+ fisheye_radius = min(frame_width, frame_height) / 2.0
197
+
198
+ # Create cache key
199
+ cache_key = (
200
+ fisheye_cx,
201
+ fisheye_cy,
202
+ fisheye_radius,
203
+ output_width,
204
+ output_height,
205
+ frame_height,
206
+ frame_width,
207
+ )
208
+
209
+ # Check cache for coordinate mapping
210
+ if cache_key in self._map_cache:
211
+ pixel_x, pixel_y = self._map_cache[cache_key]
212
+ else:
213
+ # Generate new mapping
214
+ pixel_x, pixel_y = self._generate_coordinate_mapping(
215
+ fisheye_cx,
216
+ fisheye_cy,
217
+ fisheye_radius,
218
+ output_width,
219
+ output_height,
220
+ frame_height,
221
+ frame_width,
222
+ )
223
+
224
+ # Cache management
225
+ if len(self._map_cache) >= self._cache_size_limit:
226
+ oldest_key = next(iter(self._map_cache))
227
+ del self._map_cache[oldest_key]
228
+
229
+ self._map_cache[cache_key] = (pixel_x, pixel_y)
230
+
231
+ # Apply remapping with black border for out-of-bounds areas
232
+ return cv2.remap(
233
+ frame,
234
+ pixel_x,
235
+ pixel_y,
236
+ cv2.INTER_LINEAR,
237
+ borderMode=cv2.BORDER_CONSTANT,
238
+ borderValue=(0, 0, 0),
239
+ )
240
+
241
+ def _generate_coordinate_mapping(
242
+ self,
243
+ fisheye_cx: float,
244
+ fisheye_cy: float,
245
+ fisheye_radius: float,
246
+ output_width: int,
247
+ output_height: int,
248
+ frame_height: int,
249
+ frame_width: int,
250
+ ) -> tuple[np.ndarray, np.ndarray]:
251
+ """Generate coordinate mapping for fisheye to equirectangular projection."""
252
+
253
+ total_pixels = output_width * output_height
254
+
255
+ if total_pixels > 500000:
256
+ return generate_fisheye_mapping_jit_parallel(
257
+ output_width,
258
+ output_height,
259
+ fisheye_cx,
260
+ fisheye_cy,
261
+ fisheye_radius,
262
+ frame_height,
263
+ frame_width,
264
+ )
265
+ else:
266
+ return generate_fisheye_mapping_jit(
267
+ output_width,
268
+ output_height,
269
+ fisheye_cx,
270
+ fisheye_cy,
271
+ fisheye_radius,
272
+ frame_height,
273
+ frame_width,
274
+ )
275
+
276
+ def clear_cache(self) -> None:
277
+ """Clear the coordinate mapping cache."""
278
+ self._map_cache.clear()
279
+
280
+ def set_fisheye_circle(self, cx: float, cy: float, radius: float) -> None:
281
+ """Set the fisheye circle parameters.
282
+
283
+ Args:
284
+ cx: X coordinate of fisheye center
285
+ cy: Y coordinate of fisheye center
286
+ radius: Radius of the fisheye circle in pixels
287
+ """
288
+ self._parameters["fisheye_cx"] = cx
289
+ self._parameters["fisheye_cy"] = cy
290
+ self._parameters["fisheye_radius"] = radius
291
+ self.clear_cache()
292
+
293
+ def auto_detect_fisheye_circle(self, frame: FrameType) -> tuple[float, float, float]:
294
+ """Attempt to auto-detect the fisheye circle from the frame.
295
+
296
+ This uses a simple heuristic based on finding the largest dark border.
297
+ For best results, manually specify the fisheye parameters.
298
+
299
+ Args:
300
+ frame: Input fisheye frame
301
+
302
+ Returns:
303
+ Tuple of (cx, cy, radius)
304
+ """
305
+ # Convert to grayscale if needed
306
+ if len(frame.shape) == 3:
307
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
308
+ else:
309
+ gray = frame
310
+
311
+ # Threshold to find the bright fisheye area
312
+ _, binary = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY)
313
+
314
+ # Find contours
315
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
316
+
317
+ if contours:
318
+ # Find the largest contour
319
+ largest = max(contours, key=cv2.contourArea)
320
+
321
+ # Fit a circle to the contour
322
+ (cx, cy), radius = cv2.minEnclosingCircle(largest)
323
+
324
+ return float(cx), float(cy), float(radius)
325
+
326
+ # Fallback to image center
327
+ h, w = frame.shape[:2]
328
+ return w / 2.0, h / 2.0, min(w, h) / 2.0
@@ -0,0 +1,30 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Union
3
+
4
+ import numpy as np
5
+
6
+ FrameType = Union[np.ndarray, dict[str, np.ndarray]]
7
+
8
+
9
+ class FrameProcessor(ABC):
10
+ """Base class for all frame processors."""
11
+
12
+ def __init__(self):
13
+ self._parameters = {}
14
+
15
+ @abstractmethod
16
+ def process(self, frame: FrameType) -> FrameType:
17
+ """Process a frame and return the processed frame."""
18
+ pass
19
+
20
+ def set_parameter(self, name: str, value: Any) -> None:
21
+ """Set a processing parameter."""
22
+ self._parameters[name] = value
23
+
24
+ def get_parameter(self, name: str) -> Any:
25
+ """Get a processing parameter."""
26
+ return self._parameters.get(name)
27
+
28
+ def get_parameters(self) -> dict[str, Any]:
29
+ """Get all processing parameters."""
30
+ return self._parameters.copy()
@@ -0,0 +1,23 @@
1
+ from .frame_processor import FrameProcessor, FrameType
2
+
3
+
4
+ class HyperspectralChannelSelector(FrameProcessor):
5
+ """Select specific channels from hyperspectral data."""
6
+
7
+ # This is a placeholder example showing how to extend FrameProcessor
8
+ # for domain-specific applications like hyperspectral imaging.
9
+ # Replace this implementation with actual hyperspectral processing logic.
10
+
11
+ def __init__(self, channel: int = 0):
12
+ super().__init__()
13
+ self._parameters = {"channel": channel, "false_color": False}
14
+
15
+ def process(self, frame: FrameType) -> FrameType:
16
+ """Extract the specified channel from hyperspectral data."""
17
+ # Implementation depends on your hyperspectral data format
18
+ _channel = self._parameters["channel"]
19
+ _false_color = self._parameters["false_color"]
20
+
21
+ # Your channel selection logic would go here
22
+ # For now, returning original frame as placeholder
23
+ return frame
@@ -0,0 +1,90 @@
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+ import cv2
5
+ import numpy as np
6
+
7
+ try:
8
+ import pyrealsense2 as rs # noqa: F401 - availability probe; raises a friendlier error below
9
+ except ImportError as e:
10
+ raise ImportError(
11
+ "pyrealsense2 is required for RealsenseDepthProcessor. "
12
+ "Install it with: pip install pyrealsense2"
13
+ ) from e
14
+
15
+ from .frame_processor import FrameProcessor, FrameType
16
+
17
+
18
+ class RealsenseProcessingOutput(Enum):
19
+ RGB = 1
20
+ ALIGNED_SIDE_BY_SIDE = 2
21
+ ALIGNED_DEPTH = 4
22
+ ALIGNED_DEPTH_COLORIZED = 5
23
+ RGBD = 6
24
+
25
+
26
+ class RealsenseDepthProcessor(FrameProcessor):
27
+ """Base class for all frame processors."""
28
+
29
+ def __init__(
30
+ self,
31
+ output_format: RealsenseProcessingOutput = RealsenseProcessingOutput.ALIGNED_SIDE_BY_SIDE,
32
+ ):
33
+ super().__init__()
34
+ self.output_format = output_format
35
+
36
+ def process(self, frame: FrameType) -> FrameType:
37
+ """Process a frame and return the processed frame."""
38
+ if isinstance(frame, np.ndarray):
39
+ return frame
40
+
41
+ color_frame = frame["aligned_color"]
42
+ aligned_depth_frame = frame["aligned_depth"]
43
+
44
+ color_image = np.asanyarray(color_frame.get_data())
45
+ aligned_depth_image = np.asanyarray(aligned_depth_frame.get_data())
46
+
47
+ # Apply colormap on depth image (image must be converted to 8-bit per pixel first)
48
+ depth_colormap = cv2.applyColorMap(
49
+ cv2.convertScaleAbs(aligned_depth_image, alpha=0.03), cv2.COLORMAP_JET
50
+ )
51
+
52
+ depth_colormap_dim = depth_colormap.shape
53
+ color_colormap_dim = color_image.shape
54
+
55
+ if self.output_format == RealsenseProcessingOutput.RGB:
56
+ return color_image
57
+ elif self.output_format == RealsenseProcessingOutput.RGBD:
58
+ rgbd = np.dstack((color_image, aligned_depth_image))
59
+ return rgbd
60
+ elif self.output_format == RealsenseProcessingOutput.ALIGNED_SIDE_BY_SIDE:
61
+ # If depth and color resolutions are different, resize color image to
62
+ # match depth image for display
63
+ if depth_colormap_dim != color_colormap_dim:
64
+ resized_color_image = cv2.resize(
65
+ color_image,
66
+ dsize=(depth_colormap_dim[1], depth_colormap_dim[0]),
67
+ interpolation=cv2.INTER_AREA,
68
+ )
69
+ images = np.hstack((resized_color_image, depth_colormap))
70
+ else:
71
+ images = np.hstack((color_image, depth_colormap))
72
+ return images
73
+ elif self.output_format == RealsenseProcessingOutput.ALIGNED_DEPTH:
74
+ return aligned_depth_image
75
+ elif self.output_format == RealsenseProcessingOutput.ALIGNED_DEPTH_COLORIZED:
76
+ return depth_colormap
77
+
78
+ return frame
79
+
80
+ def set_parameter(self, name: str, value: Any) -> None:
81
+ """Set a processing parameter."""
82
+ self._parameters[name] = value
83
+
84
+ def get_parameter(self, name: str) -> Any:
85
+ """Get a processing parameter."""
86
+ return self._parameters.get(name)
87
+
88
+ def get_parameters(self) -> dict[str, Any]:
89
+ """Get all processing parameters."""
90
+ return self._parameters.copy()
framesource/py.typed ADDED
File without changes
@@ -0,0 +1,40 @@
1
+ """Frame source capture backends for FrameSource.
2
+
3
+ This package contains the individual capture implementations. Most users should
4
+ use :class:`framesource.FrameSourceFactory` rather than importing these directly.
5
+ Optional backends (vendor SDKs) are imported lazily and may be unavailable.
6
+ """
7
+
8
+ import importlib
9
+ import logging
10
+
11
+ from .video_capture_base import VideoCaptureBase
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ __all__ = ["VideoCaptureBase"]
16
+
17
+ # Map of public class name -> module within this package. Optional/vendor
18
+ # backends are imported defensively so missing SDKs don't break the package.
19
+ _OPTIONAL_CLASSES = {
20
+ "WebcamCapture": "webcam_capture",
21
+ "IPCameraCapture": "ipcamera_capture",
22
+ "BaslerCapture": "basler_capture",
23
+ "GenicamCapture": "genicam_capture",
24
+ "RealsenseCapture": "realsense_capture",
25
+ "XimeaCapture": "ximea_capture",
26
+ "HuatengCapture": "huateng_capture",
27
+ "VideoFileCapture": "video_file_capture",
28
+ "FolderCapture": "folder_capture",
29
+ "ScreenCapture": "screen_capture",
30
+ "AudioSpectrogramCapture": "audiospectrogram_capture",
31
+ }
32
+
33
+ for _class_name, _module_name in _OPTIONAL_CLASSES.items():
34
+ try:
35
+ _module = importlib.import_module(f".{_module_name}", __name__)
36
+ globals()[_class_name] = getattr(_module, _class_name)
37
+ __all__.append(_class_name)
38
+ except Exception as e: # noqa: BLE001 - vendor SDKs may raise non-ImportError
39
+ globals()[_class_name] = None
40
+ logger.debug("%s unavailable: %s", _class_name, e)