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,577 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
from numba import njit, prange
|
|
6
|
+
|
|
7
|
+
from .frame_processor import FrameProcessor, FrameType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Optimized functions using Numba for better performance
|
|
11
|
+
@njit(cache=True, fastmath=True)
|
|
12
|
+
def generate_mapping_jit(
|
|
13
|
+
output_width,
|
|
14
|
+
output_height,
|
|
15
|
+
focal_length,
|
|
16
|
+
cx,
|
|
17
|
+
cy,
|
|
18
|
+
R00,
|
|
19
|
+
R01,
|
|
20
|
+
R02,
|
|
21
|
+
R10,
|
|
22
|
+
R11,
|
|
23
|
+
R12,
|
|
24
|
+
R20,
|
|
25
|
+
R21,
|
|
26
|
+
R22,
|
|
27
|
+
frame_height,
|
|
28
|
+
frame_width,
|
|
29
|
+
):
|
|
30
|
+
"""JIT-compiled coordinate mapping generation for maximum speed"""
|
|
31
|
+
|
|
32
|
+
# Pre-allocate output arrays
|
|
33
|
+
pixel_x = np.empty((output_height, output_width), dtype=np.float32)
|
|
34
|
+
pixel_y = np.empty((output_height, output_width), dtype=np.float32)
|
|
35
|
+
|
|
36
|
+
# Pre-calculate constants
|
|
37
|
+
inv_focal = 1.0 / focal_length
|
|
38
|
+
inv_2pi = 1.0 / (2.0 * np.pi)
|
|
39
|
+
inv_pi = 1.0 / np.pi
|
|
40
|
+
half_pi = np.pi * 0.5
|
|
41
|
+
frame_width_minus_1 = frame_width - 1
|
|
42
|
+
frame_height_minus_1 = frame_height - 1
|
|
43
|
+
|
|
44
|
+
# Process each pixel
|
|
45
|
+
for j in range(output_height):
|
|
46
|
+
y_norm = (j - cy) * inv_focal
|
|
47
|
+
for i in range(output_width):
|
|
48
|
+
x_norm = (i - cx) * inv_focal
|
|
49
|
+
|
|
50
|
+
# Normalize direction vector
|
|
51
|
+
norm_factor = 1.0 / math.sqrt(x_norm * x_norm + y_norm * y_norm + 1.0)
|
|
52
|
+
x_unit = x_norm * norm_factor
|
|
53
|
+
y_unit = y_norm * norm_factor
|
|
54
|
+
z_unit = norm_factor
|
|
55
|
+
|
|
56
|
+
# Apply rotation matrix
|
|
57
|
+
x_rot = R00 * x_unit + R01 * y_unit + R02 * z_unit
|
|
58
|
+
y_rot = R10 * x_unit + R11 * y_unit + R12 * z_unit
|
|
59
|
+
z_rot = R20 * x_unit + R21 * y_unit + R22 * z_unit
|
|
60
|
+
|
|
61
|
+
# Convert to spherical coordinates
|
|
62
|
+
theta = math.atan2(x_rot, z_rot) # Azimuth
|
|
63
|
+
phi = math.asin(max(-1.0, min(1.0, y_rot))) # Elevation (clamped)
|
|
64
|
+
|
|
65
|
+
# Convert to pixel coordinates
|
|
66
|
+
u = (theta + np.pi) * inv_2pi
|
|
67
|
+
v = (phi + half_pi) * inv_pi
|
|
68
|
+
|
|
69
|
+
pixel_x[j, i] = max(0.0, min(frame_width_minus_1, u * frame_width_minus_1))
|
|
70
|
+
pixel_y[j, i] = max(0.0, min(frame_height_minus_1, v * frame_height_minus_1))
|
|
71
|
+
|
|
72
|
+
return pixel_x, pixel_y
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@njit(cache=True, fastmath=True, parallel=True)
|
|
76
|
+
def generate_mapping_jit_parallel(
|
|
77
|
+
output_width,
|
|
78
|
+
output_height,
|
|
79
|
+
focal_length,
|
|
80
|
+
cx,
|
|
81
|
+
cy,
|
|
82
|
+
R00,
|
|
83
|
+
R01,
|
|
84
|
+
R02,
|
|
85
|
+
R10,
|
|
86
|
+
R11,
|
|
87
|
+
R12,
|
|
88
|
+
R20,
|
|
89
|
+
R21,
|
|
90
|
+
R22,
|
|
91
|
+
frame_height,
|
|
92
|
+
frame_width,
|
|
93
|
+
):
|
|
94
|
+
"""Parallel JIT-compiled coordinate mapping for multi-core systems"""
|
|
95
|
+
|
|
96
|
+
# Pre-allocate output arrays
|
|
97
|
+
pixel_x = np.empty((output_height, output_width), dtype=np.float32)
|
|
98
|
+
pixel_y = np.empty((output_height, output_width), dtype=np.float32)
|
|
99
|
+
|
|
100
|
+
# Pre-calculate constants
|
|
101
|
+
inv_focal = 1.0 / focal_length
|
|
102
|
+
inv_2pi = 1.0 / (2.0 * np.pi)
|
|
103
|
+
inv_pi = 1.0 / np.pi
|
|
104
|
+
half_pi = np.pi * 0.5
|
|
105
|
+
frame_width_minus_1 = frame_width - 1
|
|
106
|
+
frame_height_minus_1 = frame_height - 1
|
|
107
|
+
|
|
108
|
+
# Process rows in parallel using prange
|
|
109
|
+
for j in prange(output_height):
|
|
110
|
+
y_norm = (j - cy) * inv_focal
|
|
111
|
+
for i in range(output_width):
|
|
112
|
+
x_norm = (i - cx) * inv_focal
|
|
113
|
+
|
|
114
|
+
# Normalize direction vector
|
|
115
|
+
norm_factor = 1.0 / math.sqrt(x_norm * x_norm + y_norm * y_norm + 1.0)
|
|
116
|
+
x_unit = x_norm * norm_factor
|
|
117
|
+
y_unit = y_norm * norm_factor
|
|
118
|
+
z_unit = norm_factor
|
|
119
|
+
|
|
120
|
+
# Apply rotation matrix
|
|
121
|
+
x_rot = R00 * x_unit + R01 * y_unit + R02 * z_unit
|
|
122
|
+
y_rot = R10 * x_unit + R11 * y_unit + R12 * z_unit
|
|
123
|
+
z_rot = R20 * x_unit + R21 * y_unit + R22 * z_unit
|
|
124
|
+
|
|
125
|
+
# Convert to spherical coordinates
|
|
126
|
+
theta = math.atan2(x_rot, z_rot)
|
|
127
|
+
phi = math.asin(max(-1.0, min(1.0, y_rot)))
|
|
128
|
+
|
|
129
|
+
# Convert to pixel coordinates
|
|
130
|
+
u = (theta + np.pi) * inv_2pi
|
|
131
|
+
v = (phi + half_pi) * inv_pi
|
|
132
|
+
|
|
133
|
+
pixel_x[j, i] = max(0.0, min(frame_width_minus_1, u * frame_width_minus_1))
|
|
134
|
+
pixel_y[j, i] = max(0.0, min(frame_height_minus_1, v * frame_height_minus_1))
|
|
135
|
+
|
|
136
|
+
return pixel_x, pixel_y
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Advanced optimized functions for maximum performance
|
|
140
|
+
@njit(cache=True, fastmath=True, inline="always")
|
|
141
|
+
def generate_mapping_jit_ultra(
|
|
142
|
+
output_width,
|
|
143
|
+
output_height,
|
|
144
|
+
focal_length,
|
|
145
|
+
cx,
|
|
146
|
+
cy,
|
|
147
|
+
R00,
|
|
148
|
+
R01,
|
|
149
|
+
R02,
|
|
150
|
+
R10,
|
|
151
|
+
R11,
|
|
152
|
+
R12,
|
|
153
|
+
R20,
|
|
154
|
+
R21,
|
|
155
|
+
R22,
|
|
156
|
+
frame_height,
|
|
157
|
+
frame_width,
|
|
158
|
+
):
|
|
159
|
+
"""Ultra-optimized coordinate mapping with memory layout optimization"""
|
|
160
|
+
|
|
161
|
+
# Pre-allocate output arrays with optimal memory layout
|
|
162
|
+
pixel_x = np.empty((output_height, output_width), dtype=np.float32)
|
|
163
|
+
pixel_y = np.empty((output_height, output_width), dtype=np.float32)
|
|
164
|
+
|
|
165
|
+
# Pre-calculate all constants (more than before)
|
|
166
|
+
inv_focal = 1.0 / focal_length
|
|
167
|
+
inv_2pi = 0.15915494309189535 # 1/(2*pi) precomputed
|
|
168
|
+
inv_pi = 0.3183098861837907 # 1/pi precomputed
|
|
169
|
+
half_pi = 1.5707963267948966 # pi/2 precomputed
|
|
170
|
+
frame_width_f = float(frame_width - 1)
|
|
171
|
+
frame_height_f = float(frame_height - 1)
|
|
172
|
+
|
|
173
|
+
# Process each pixel with optimized inner loop
|
|
174
|
+
for j in range(output_height):
|
|
175
|
+
y_norm = (j - cy) * inv_focal
|
|
176
|
+
y_norm_sq = y_norm * y_norm
|
|
177
|
+
for i in range(output_width):
|
|
178
|
+
x_norm = (i - cx) * inv_focal
|
|
179
|
+
|
|
180
|
+
# Optimized normalization using precomputed y_norm_sq
|
|
181
|
+
norm_factor = 1.0 / math.sqrt(x_norm * x_norm + y_norm_sq + 1.0)
|
|
182
|
+
x_unit = x_norm * norm_factor
|
|
183
|
+
y_unit = y_norm * norm_factor
|
|
184
|
+
z_unit = norm_factor
|
|
185
|
+
|
|
186
|
+
# Apply rotation matrix (unrolled for speed)
|
|
187
|
+
x_rot = R00 * x_unit + R01 * y_unit + R02 * z_unit
|
|
188
|
+
y_rot = R10 * x_unit + R11 * y_unit + R12 * z_unit
|
|
189
|
+
z_rot = R20 * x_unit + R21 * y_unit + R22 * z_unit
|
|
190
|
+
|
|
191
|
+
# Optimized spherical coordinate calculation
|
|
192
|
+
theta = math.atan2(x_rot, z_rot)
|
|
193
|
+
phi = math.asin(max(-1.0, min(1.0, y_rot)))
|
|
194
|
+
|
|
195
|
+
# Direct pixel coordinate calculation with precomputed constants
|
|
196
|
+
u = (theta + math.pi) * inv_2pi
|
|
197
|
+
v = (phi + half_pi) * inv_pi
|
|
198
|
+
|
|
199
|
+
# Final pixel coordinates with bounds checking
|
|
200
|
+
pixel_x[j, i] = max(0.0, min(frame_width_f, u * frame_width_f))
|
|
201
|
+
pixel_y[j, i] = max(0.0, min(frame_height_f, v * frame_height_f))
|
|
202
|
+
|
|
203
|
+
return pixel_x, pixel_y
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@njit(cache=True, fastmath=True, parallel=True)
|
|
207
|
+
def generate_mapping_jit_ultra_parallel(
|
|
208
|
+
output_width,
|
|
209
|
+
output_height,
|
|
210
|
+
focal_length,
|
|
211
|
+
cx,
|
|
212
|
+
cy,
|
|
213
|
+
R00,
|
|
214
|
+
R01,
|
|
215
|
+
R02,
|
|
216
|
+
R10,
|
|
217
|
+
R11,
|
|
218
|
+
R12,
|
|
219
|
+
R20,
|
|
220
|
+
R21,
|
|
221
|
+
R22,
|
|
222
|
+
frame_height,
|
|
223
|
+
frame_width,
|
|
224
|
+
):
|
|
225
|
+
"""Ultra-optimized parallel coordinate mapping using a flattened approach for
|
|
226
|
+
better Numba parallelization."""
|
|
227
|
+
|
|
228
|
+
# Pre-allocate output arrays
|
|
229
|
+
pixel_x = np.empty((output_height, output_width), dtype=np.float32)
|
|
230
|
+
pixel_y = np.empty((output_height, output_width), dtype=np.float32)
|
|
231
|
+
|
|
232
|
+
# Pre-calculate constants
|
|
233
|
+
inv_focal = 1.0 / focal_length
|
|
234
|
+
inv_2pi = 0.15915494309189535
|
|
235
|
+
inv_pi = 0.3183098861837907
|
|
236
|
+
half_pi = 1.5707963267948966
|
|
237
|
+
frame_width_f = float(frame_width - 1)
|
|
238
|
+
frame_height_f = float(frame_height - 1)
|
|
239
|
+
pi = 3.141592653589793
|
|
240
|
+
|
|
241
|
+
# Flatten to 1D for better parallelization
|
|
242
|
+
total_pixels = output_height * output_width
|
|
243
|
+
|
|
244
|
+
# Use prange for parallel processing of individual pixels
|
|
245
|
+
for idx in prange(total_pixels):
|
|
246
|
+
# Convert 1D index back to 2D coordinates
|
|
247
|
+
j = idx // output_width
|
|
248
|
+
i = idx % output_width
|
|
249
|
+
|
|
250
|
+
# Same calculation as before
|
|
251
|
+
x_norm = (i - cx) * inv_focal
|
|
252
|
+
y_norm = (j - cy) * inv_focal
|
|
253
|
+
|
|
254
|
+
# Fast normalization
|
|
255
|
+
norm_factor = 1.0 / math.sqrt(x_norm * x_norm + y_norm * y_norm + 1.0)
|
|
256
|
+
x_unit = x_norm * norm_factor
|
|
257
|
+
y_unit = y_norm * norm_factor
|
|
258
|
+
z_unit = norm_factor
|
|
259
|
+
|
|
260
|
+
# Matrix multiply
|
|
261
|
+
x_rot = R00 * x_unit + R01 * y_unit + R02 * z_unit
|
|
262
|
+
y_rot = R10 * x_unit + R11 * y_unit + R12 * z_unit
|
|
263
|
+
z_rot = R20 * x_unit + R21 * y_unit + R22 * z_unit
|
|
264
|
+
|
|
265
|
+
# Spherical conversion
|
|
266
|
+
theta = math.atan2(x_rot, z_rot)
|
|
267
|
+
phi = math.asin(max(-1.0, min(1.0, y_rot)))
|
|
268
|
+
|
|
269
|
+
# Pixel mapping
|
|
270
|
+
u = (theta + pi) * inv_2pi
|
|
271
|
+
v = (phi + half_pi) * inv_pi
|
|
272
|
+
|
|
273
|
+
pixel_x[j, i] = max(0.0, min(frame_width_f, u * frame_width_f))
|
|
274
|
+
pixel_y[j, i] = max(0.0, min(frame_height_f, v * frame_height_f))
|
|
275
|
+
|
|
276
|
+
return pixel_x, pixel_y
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class Equirectangular2PinholeProcessor(FrameProcessor):
|
|
280
|
+
"""Convert equirectangular 360 frames to pinhole projections."""
|
|
281
|
+
|
|
282
|
+
def __init__(self, fov: float = 90.0, output_width: int = 1920, output_height: int = 1080):
|
|
283
|
+
super().__init__()
|
|
284
|
+
self._parameters = {
|
|
285
|
+
"roll": 0.0,
|
|
286
|
+
"pitch": 0.0,
|
|
287
|
+
"yaw": 0.0,
|
|
288
|
+
"fov": fov,
|
|
289
|
+
"output_width": output_width,
|
|
290
|
+
"output_height": output_height,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# Coordinate mapping cache
|
|
294
|
+
self._map_cache = {}
|
|
295
|
+
self._cache_size_limit = 50
|
|
296
|
+
|
|
297
|
+
def process(self, frame: FrameType) -> FrameType:
|
|
298
|
+
"""Convert equirectangular frame to pinhole projection."""
|
|
299
|
+
roll = self._parameters["roll"]
|
|
300
|
+
pitch = self._parameters["pitch"]
|
|
301
|
+
yaw = self._parameters["yaw"]
|
|
302
|
+
fov = self._parameters["fov"]
|
|
303
|
+
output_width = self._parameters["output_width"]
|
|
304
|
+
output_height = self._parameters["output_height"]
|
|
305
|
+
|
|
306
|
+
# Normalize angles for consistent caching
|
|
307
|
+
norm_yaw, norm_pitch, norm_roll = self._normalize_angles(yaw, pitch, roll)
|
|
308
|
+
|
|
309
|
+
# Create cache key for coordinate mapping
|
|
310
|
+
cache_key = (
|
|
311
|
+
norm_yaw,
|
|
312
|
+
norm_pitch,
|
|
313
|
+
norm_roll,
|
|
314
|
+
fov,
|
|
315
|
+
output_width,
|
|
316
|
+
output_height,
|
|
317
|
+
frame.shape[0],
|
|
318
|
+
frame.shape[1],
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Check cache for coordinate mapping
|
|
322
|
+
if cache_key in self._map_cache:
|
|
323
|
+
pixel_x, pixel_y = self._map_cache[cache_key]
|
|
324
|
+
else:
|
|
325
|
+
# Generate new mapping
|
|
326
|
+
pixel_x, pixel_y = self._generate_coordinate_mapping(
|
|
327
|
+
norm_yaw, norm_pitch, norm_roll, fov, output_width, output_height, frame.shape
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Cache management
|
|
331
|
+
if len(self._map_cache) >= self._cache_size_limit:
|
|
332
|
+
oldest_key = next(iter(self._map_cache))
|
|
333
|
+
del self._map_cache[oldest_key]
|
|
334
|
+
|
|
335
|
+
self._map_cache[cache_key] = (pixel_x, pixel_y)
|
|
336
|
+
|
|
337
|
+
# Apply remapping
|
|
338
|
+
return cv2.remap(frame, pixel_x, pixel_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_WRAP)
|
|
339
|
+
|
|
340
|
+
def _generate_coordinate_mapping(
|
|
341
|
+
self,
|
|
342
|
+
yaw: float,
|
|
343
|
+
pitch: float,
|
|
344
|
+
roll: float,
|
|
345
|
+
fov: float,
|
|
346
|
+
output_width: int,
|
|
347
|
+
output_height: int,
|
|
348
|
+
frame_shape: tuple[int, ...],
|
|
349
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
350
|
+
"""Generate coordinate mapping for equirectangular to pinhole projection."""
|
|
351
|
+
# Convert to radians
|
|
352
|
+
yaw_rad = math.radians(yaw)
|
|
353
|
+
pitch_rad = math.radians(pitch)
|
|
354
|
+
roll_rad = math.radians(roll)
|
|
355
|
+
fov_rad = math.radians(fov)
|
|
356
|
+
|
|
357
|
+
# Pre-calculate constants
|
|
358
|
+
focal_length = output_width / (2 * math.tan(fov_rad / 2))
|
|
359
|
+
cx = output_width * 0.5
|
|
360
|
+
cy = output_height * 0.5
|
|
361
|
+
|
|
362
|
+
# Create rotation matrix elements
|
|
363
|
+
cos_r, sin_r = math.cos(roll_rad), math.sin(roll_rad)
|
|
364
|
+
cos_p, sin_p = math.cos(pitch_rad), math.sin(pitch_rad)
|
|
365
|
+
cos_y, sin_y = math.cos(yaw_rad), math.sin(yaw_rad)
|
|
366
|
+
|
|
367
|
+
# Combined rotation matrix elements
|
|
368
|
+
R00 = cos_y * cos_r + sin_y * sin_r * sin_p
|
|
369
|
+
R01 = cos_y * (-sin_r) + sin_y * cos_r * sin_p
|
|
370
|
+
R02 = sin_y * cos_p
|
|
371
|
+
R10 = sin_r * cos_p
|
|
372
|
+
R11 = cos_r * cos_p
|
|
373
|
+
R12 = -sin_p
|
|
374
|
+
R20 = -sin_y * cos_r + cos_y * sin_r * sin_p
|
|
375
|
+
R21 = -sin_y * (-sin_r) + cos_y * cos_r * sin_p
|
|
376
|
+
R22 = cos_y * cos_p
|
|
377
|
+
|
|
378
|
+
# Choose appropriate JIT implementation based on output size
|
|
379
|
+
total_pixels = output_width * output_height
|
|
380
|
+
|
|
381
|
+
if total_pixels > 1000000:
|
|
382
|
+
return generate_mapping_jit_ultra_parallel(
|
|
383
|
+
output_width,
|
|
384
|
+
output_height,
|
|
385
|
+
focal_length,
|
|
386
|
+
cx,
|
|
387
|
+
cy,
|
|
388
|
+
R00,
|
|
389
|
+
R01,
|
|
390
|
+
R02,
|
|
391
|
+
R10,
|
|
392
|
+
R11,
|
|
393
|
+
R12,
|
|
394
|
+
R20,
|
|
395
|
+
R21,
|
|
396
|
+
R22,
|
|
397
|
+
frame_shape[0],
|
|
398
|
+
frame_shape[1],
|
|
399
|
+
)
|
|
400
|
+
elif total_pixels > 500000:
|
|
401
|
+
return generate_mapping_jit_parallel(
|
|
402
|
+
output_width,
|
|
403
|
+
output_height,
|
|
404
|
+
focal_length,
|
|
405
|
+
cx,
|
|
406
|
+
cy,
|
|
407
|
+
R00,
|
|
408
|
+
R01,
|
|
409
|
+
R02,
|
|
410
|
+
R10,
|
|
411
|
+
R11,
|
|
412
|
+
R12,
|
|
413
|
+
R20,
|
|
414
|
+
R21,
|
|
415
|
+
R22,
|
|
416
|
+
frame_shape[0],
|
|
417
|
+
frame_shape[1],
|
|
418
|
+
)
|
|
419
|
+
elif total_pixels > 200000:
|
|
420
|
+
return generate_mapping_jit_ultra(
|
|
421
|
+
output_width,
|
|
422
|
+
output_height,
|
|
423
|
+
focal_length,
|
|
424
|
+
cx,
|
|
425
|
+
cy,
|
|
426
|
+
R00,
|
|
427
|
+
R01,
|
|
428
|
+
R02,
|
|
429
|
+
R10,
|
|
430
|
+
R11,
|
|
431
|
+
R12,
|
|
432
|
+
R20,
|
|
433
|
+
R21,
|
|
434
|
+
R22,
|
|
435
|
+
frame_shape[0],
|
|
436
|
+
frame_shape[1],
|
|
437
|
+
)
|
|
438
|
+
else:
|
|
439
|
+
return generate_mapping_jit(
|
|
440
|
+
output_width,
|
|
441
|
+
output_height,
|
|
442
|
+
focal_length,
|
|
443
|
+
cx,
|
|
444
|
+
cy,
|
|
445
|
+
R00,
|
|
446
|
+
R01,
|
|
447
|
+
R02,
|
|
448
|
+
R10,
|
|
449
|
+
R11,
|
|
450
|
+
R12,
|
|
451
|
+
R20,
|
|
452
|
+
R21,
|
|
453
|
+
R22,
|
|
454
|
+
frame_shape[0],
|
|
455
|
+
frame_shape[1],
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def _normalize_angles(
|
|
459
|
+
self, yaw: float, pitch: float, roll: float
|
|
460
|
+
) -> tuple[float, float, float]:
|
|
461
|
+
"""Normalize angles to canonical ranges for cache consistency."""
|
|
462
|
+
# Normalize yaw to [0, 360)
|
|
463
|
+
yaw = yaw % 360
|
|
464
|
+
|
|
465
|
+
# Normalize pitch properly
|
|
466
|
+
pitch = ((pitch + 180) % 360) - 180
|
|
467
|
+
|
|
468
|
+
# Handle pitch overflow beyond valid range [-90, 90]
|
|
469
|
+
if pitch > 90:
|
|
470
|
+
pitch = 180 - pitch
|
|
471
|
+
yaw = (yaw + 180) % 360
|
|
472
|
+
roll = (roll + 180) % 360
|
|
473
|
+
elif pitch < -90:
|
|
474
|
+
pitch = -180 - pitch
|
|
475
|
+
yaw = (yaw + 180) % 360
|
|
476
|
+
roll = (roll + 180) % 360
|
|
477
|
+
|
|
478
|
+
# Normalize roll to [0, 360)
|
|
479
|
+
roll = roll % 360
|
|
480
|
+
|
|
481
|
+
return yaw, pitch, roll
|
|
482
|
+
|
|
483
|
+
def clear_cache(self) -> None:
|
|
484
|
+
"""Clear the coordinate mapping cache."""
|
|
485
|
+
self._map_cache.clear()
|
|
486
|
+
|
|
487
|
+
def pixel_to_spherical(
|
|
488
|
+
self, x: int, y: int, frame_width: int, frame_height: int
|
|
489
|
+
) -> tuple[float, float]:
|
|
490
|
+
"""
|
|
491
|
+
Convert pixel coordinates in equirectangular image to spherical coordinates.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
x: Pixel x coordinate (0 to frame_width-1)
|
|
495
|
+
y: Pixel y coordinate (0 to frame_height-1)
|
|
496
|
+
frame_width: Width of the equirectangular frame
|
|
497
|
+
frame_height: Height of the equirectangular frame
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Tuple of (longitude_deg, latitude_deg) where:
|
|
501
|
+
- longitude_deg: -180° to +180° (yaw)
|
|
502
|
+
- latitude_deg: -90° to +90° (pitch, inverted for correct orientation)
|
|
503
|
+
"""
|
|
504
|
+
# Convert pixel coordinates to spherical coordinates
|
|
505
|
+
# Equirectangular mapping: x -> longitude (-180° to +180°), y -> latitude (-90° to +90°)
|
|
506
|
+
longitude_deg = (x / frame_width) * 360.0 - 180.0 # -180 to +180
|
|
507
|
+
latitude_deg = (y / frame_height) * 180.0 - 90.0 # -90 to +90
|
|
508
|
+
|
|
509
|
+
return longitude_deg, latitude_deg
|
|
510
|
+
|
|
511
|
+
def spherical_to_processor_angles(
|
|
512
|
+
self, longitude_deg: float, latitude_deg: float, current_roll: float = 0.0
|
|
513
|
+
) -> tuple[float, float, float]:
|
|
514
|
+
"""
|
|
515
|
+
Convert spherical coordinates to processor angles.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
longitude_deg: Longitude in degrees (-180° to +180°)
|
|
519
|
+
latitude_deg: Latitude in degrees (-90° to +90°)
|
|
520
|
+
current_roll: Current roll value to preserve (default: 0.0)
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Tuple of (yaw, pitch, roll) where:
|
|
524
|
+
- yaw: longitude (left/right rotation)
|
|
525
|
+
- pitch: -latitude (up/down rotation, inverted)
|
|
526
|
+
- roll: preserved current_roll value
|
|
527
|
+
"""
|
|
528
|
+
yaw = longitude_deg
|
|
529
|
+
pitch = -latitude_deg # Invert for correct orientation
|
|
530
|
+
roll = current_roll # Preserve current roll
|
|
531
|
+
|
|
532
|
+
return yaw, pitch, roll
|
|
533
|
+
|
|
534
|
+
def processor_angles_to_equirectangular_coords(
|
|
535
|
+
self, yaw: float, pitch: float, roll: float, frame_width: int, frame_height: int
|
|
536
|
+
) -> tuple[int, int]:
|
|
537
|
+
"""
|
|
538
|
+
Convert processor angles to equirectangular pixel coordinates.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
yaw: Yaw angle in degrees
|
|
542
|
+
pitch: Pitch angle in degrees
|
|
543
|
+
roll: Roll angle in degrees (used for 3D calculations)
|
|
544
|
+
frame_width: Width of the equirectangular frame
|
|
545
|
+
frame_height: Height of the equirectangular frame
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Tuple of (x, y) pixel coordinates in the equirectangular image
|
|
549
|
+
"""
|
|
550
|
+
# Convert angles to radians
|
|
551
|
+
pitch_rad = math.radians(-pitch) # Invert pitch for calculations
|
|
552
|
+
yaw_rad = math.radians(yaw)
|
|
553
|
+
|
|
554
|
+
# For center of projected image, calculate 3D ray direction
|
|
555
|
+
cos_pitch = math.cos(pitch_rad)
|
|
556
|
+
sin_pitch = math.sin(pitch_rad)
|
|
557
|
+
cos_yaw = math.cos(yaw_rad)
|
|
558
|
+
sin_yaw = math.sin(yaw_rad)
|
|
559
|
+
|
|
560
|
+
# Apply pitch and yaw rotation to forward vector
|
|
561
|
+
ray_x = sin_yaw * cos_pitch
|
|
562
|
+
ray_y = -sin_pitch
|
|
563
|
+
ray_z = cos_yaw * cos_pitch
|
|
564
|
+
|
|
565
|
+
# Convert 3D ray to equirectangular coordinates
|
|
566
|
+
longitude = math.atan2(ray_x, ray_z)
|
|
567
|
+
latitude = math.asin(ray_y)
|
|
568
|
+
|
|
569
|
+
# Convert to pixel coordinates in equirectangular image
|
|
570
|
+
eq_x = int((longitude + math.pi) / (2 * math.pi) * frame_width)
|
|
571
|
+
eq_y = int((math.pi / 2 - latitude) / math.pi * frame_height)
|
|
572
|
+
|
|
573
|
+
# Clamp to image bounds
|
|
574
|
+
eq_x = max(0, min(eq_x, frame_width - 1))
|
|
575
|
+
eq_y = max(0, min(eq_y, frame_height - 1))
|
|
576
|
+
|
|
577
|
+
return eq_x, eq_y
|