1
1
from collections .abc import Sequence
2
2
from enum import IntEnum
3
+ from functools import lru_cache
3
4
from typing import Any , Literal , Optional , TypeVar , Union
4
5
5
6
import numpy as np
43
44
"quintic" ,
44
45
]
45
46
DType = TypeVar ("DType" , bound = np .generic , covariant = True )
47
+ _CACHE_SIZE = 8
46
48
47
49
48
50
class Face (IntEnum ):
@@ -85,6 +87,7 @@ def slice_chunk(index: int, width: int, offset=0):
85
87
return slice (start , start + width )
86
88
87
89
90
+ @lru_cache (_CACHE_SIZE )
88
91
def xyzcube (face_w : int ) -> NDArray [np .float32 ]:
89
92
"""
90
93
Return the xyz coordinates of the unit cube in [F R B L U D] format.
@@ -145,15 +148,23 @@ def face_slice(index):
145
148
out [:, face_slice (Face .DOWN ), Dim .Y ] = - 0.5
146
149
out [:, face_slice (Face .DOWN ), Dim .Z ] = y
147
150
151
+ # Since we are using lru_cache, we want the return value to be immutable.
152
+ out .setflags (write = False )
148
153
return out
149
154
150
155
156
+ @lru_cache (_CACHE_SIZE )
151
157
def equirect_uvgrid (h : int , w : int ) -> tuple [NDArray [np .float32 ], NDArray [np .float32 ]]:
152
158
u = np .linspace (- np .pi , np .pi , num = w , dtype = np .float32 )
153
159
v = np .linspace (np .pi / 2 , - np .pi / 2 , num = h , dtype = np .float32 )
154
- return np .meshgrid (u , v ) # pyright: ignore[reportReturnType]
160
+ uu , vv = np .meshgrid (u , v )
161
+ # Since we are using lru_cache, we want the return value to be immutable.
162
+ uu .setflags (write = False )
163
+ vv .setflags (write = False )
164
+ return uu , vv # pyright: ignore[reportReturnType]
155
165
156
166
167
+ @lru_cache (_CACHE_SIZE )
157
168
def equirect_facetype (h : int , w : int ) -> NDArray [np .int32 ]:
158
169
"""Generate a 2D equirectangular segmentation image for each facetype.
159
170
@@ -225,10 +236,15 @@ def equirect_facetype(h: int, w: int) -> NDArray[np.int32]:
225
236
tp [:h3 , s .stop :][mask [:, :remainder ]] = Face .UP # pyright: ignore[reportPossiblyUnboundVariable]
226
237
tp [- h3 :, s .stop :][flip_mask [:, :remainder ]] = Face .DOWN # pyright: ignore[reportPossiblyUnboundVariable]
227
238
239
+ # Since we are using lru_cache, we want the return value to be immutable.
240
+ tp .setflags (write = False )
241
+
228
242
return tp
229
243
230
244
231
- def xyzpers (h_fov : float , v_fov : float , u : float , v : float , out_hw : tuple [int , int ], in_rot : float ) -> NDArray :
245
+ def xyzpers (
246
+ h_fov : float , v_fov : float , u : float , v : float , out_hw : tuple [int , int ], in_rot : float
247
+ ) -> NDArray [np .float32 ]:
232
248
out = np .ones ((* out_hw , 3 ), np .float32 )
233
249
234
250
x_max = np .tan (h_fov / 2 )
@@ -240,7 +256,7 @@ def xyzpers(h_fov: float, v_fov: float, u: float, v: float, out_hw: tuple[int, i
240
256
Ry = rotation_matrix (u , Dim .Y )
241
257
Ri = rotation_matrix (in_rot , np .array ([0 , 0 , 1.0 ]).dot (Rx ).dot (Ry ))
242
258
243
- return out .dot (Rx ).dot (Ry ).dot (Ri )
259
+ return out .dot (Rx ).dot (Ry ).dot (Ri ). astype ( np . float32 )
244
260
245
261
246
262
def xyz2uv (xyz : NDArray [DType ]) -> tuple [NDArray [DType ], NDArray [DType ]]:
@@ -380,6 +396,56 @@ def _pad(self, img: NDArray[DType]) -> NDArray[DType]:
380
396
padded [- 1 , :] = np .roll (img [[- 1 ]], w // 2 , 1 )
381
397
return padded
382
398
399
+ @classmethod
400
+ @lru_cache (_CACHE_SIZE )
401
+ def from_cubemap (cls , face_w : int , h : int , w : int , order : int ):
402
+ """Construct a EquirecSampler from cubemap specs.
403
+
404
+ Parameters
405
+ ----------
406
+ face_w: int
407
+ Length of each face of the output cubemap.
408
+ h: int
409
+ Height of input equirec image.
410
+ w: int
411
+ Width of input equirec image.
412
+ order: int
413
+ The order of the spline interpolation. See ``scipy.ndimage.map_coordinates``.
414
+ """
415
+ xyz = xyzcube (face_w )
416
+ u , v = xyz2uv (xyz )
417
+ coor_x , coor_y = uv2coor (u , v , h , w )
418
+ return cls (coor_x , coor_y , order = order )
419
+
420
+ @classmethod
421
+ @lru_cache (_CACHE_SIZE )
422
+ def from_perspective (cls , h_fov : float , v_fov : float , u , v , in_rot : float , h : int , w : int , order : int ):
423
+ """Construct a EquirecSampler from perspective specs.
424
+
425
+ Parameters
426
+ ----------
427
+ h_fov: float
428
+ Horizontal field of view in radians.
429
+ v_fov: float
430
+ Horizontal field of view in radians.
431
+ u: float
432
+ Horizontal viewing angle in radians
433
+ v: float
434
+ Vertical viewing angle in radians
435
+ in_rot: float
436
+ Inplane rotation in radians.
437
+ h: int
438
+ Height of input equirec image.
439
+ w: int
440
+ Width of input equirec image.
441
+ order: int
442
+ The order of the spline interpolation. See ``scipy.ndimage.map_coordinates``.
443
+ """
444
+ xyz = xyzpers (h_fov , v_fov , u , v , (h , w ), in_rot )
445
+ u , v = xyz2uv (xyz )
446
+ coor_x , coor_y = uv2coor (u , v , h , w )
447
+ return cls (coor_x , coor_y , order = order )
448
+
383
449
384
450
class CubeFaceSampler :
385
451
"""Arranged as a class so coordinate computations can be re-used across multiple image interpolations."""
@@ -511,6 +577,59 @@ def _pad(self, cube_faces: NDArray[DType]) -> NDArray[DType]:
511
577
512
578
return padded
513
579
580
+ @classmethod
581
+ @lru_cache (_CACHE_SIZE )
582
+ def from_equirec (cls , face_w : int , h : int , w : int , order : int ):
583
+ """Construct a CubemapSampler from equirectangular specs.
584
+
585
+ Parameters
586
+ ----------
587
+ face_w: int
588
+ Length of each face of the input cubemap.
589
+ h: int
590
+ Output equirectangular image height.
591
+ w: int
592
+ Output equirectangular image width.
593
+ order: int
594
+ The order of the spline interpolation. See ``scipy.ndimage.map_coordinates``.
595
+ """
596
+ u , v = equirect_uvgrid (h , w )
597
+
598
+ # Get face id to each pixel: 0F 1R 2B 3L 4U 5D
599
+ tp = equirect_facetype (h , w )
600
+
601
+ coor_x = np .empty ((h , w ), dtype = np .float32 )
602
+ coor_y = np .empty ((h , w ), dtype = np .float32 )
603
+ face_w2 = face_w / 2
604
+
605
+ # Middle band (front/right/back/left)
606
+ mask = tp < Face .UP
607
+ angles = u [mask ] - (np .pi / 2 * tp [mask ])
608
+ tan_angles = np .tan (angles )
609
+ cos_angles = np .cos (angles )
610
+ tan_v = np .tan (v [mask ])
611
+
612
+ coor_x [mask ] = face_w2 * tan_angles
613
+ coor_y [mask ] = - face_w2 * tan_v / cos_angles
614
+
615
+ mask = tp == Face .UP
616
+ c = face_w2 * np .tan (np .pi / 2 - v [mask ])
617
+ coor_x [mask ] = c * np .sin (u [mask ])
618
+ coor_y [mask ] = c * np .cos (u [mask ])
619
+
620
+ mask = tp == Face .DOWN
621
+ c = face_w2 * np .tan (np .pi / 2 - np .abs (v [mask ]))
622
+ coor_x [mask ] = c * np .sin (u [mask ])
623
+ coor_y [mask ] = - c * np .cos (u [mask ])
624
+
625
+ # Final renormalize
626
+ coor_x += face_w2
627
+ coor_y += face_w2
628
+ coor_x .clip (0 , face_w , out = coor_x )
629
+ coor_y .clip (0 , face_w , out = coor_y )
630
+
631
+ return cls (tp , coor_x , coor_y , order , face_w , face_w )
632
+
514
633
515
634
def cube_h2list (cube_h : NDArray [DType ]) -> list [NDArray [DType ]]:
516
635
"""Split an image into a list of 6 faces."""
0 commit comments