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