Skip to content

Commit 4d37f59

Browse files
authored
Merge pull request #40 from swell-d/swell-d-patch-1
caching speeds up batch processing
2 parents de8c6c8 + d278d1b commit 4d37f59

File tree

4 files changed

+129
-63
lines changed

4 files changed

+129
-63
lines changed

py360convert/c2e.py

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@
77
CubeFaceSampler,
88
CubeFormat,
99
DType,
10-
Face,
1110
InterpolationMode,
1211
cube_dice2list,
1312
cube_dict2list,
1413
cube_h2list,
15-
equirect_facetype,
16-
equirect_uvgrid,
1714
mode_to_order,
1815
)
1916

@@ -127,43 +124,9 @@ def c2e(
127124
raise ValueError("Cubemap faces must be square.")
128125
face_w = cube_faces.shape[2]
129126

130-
u, v = equirect_uvgrid(h, w)
131-
132-
# Get face id to each pixel: 0F 1R 2B 3L 4U 5D
133-
tp = equirect_facetype(h, w)
134-
135-
coor_x = np.empty((h, w), dtype=np.float32)
136-
coor_y = np.empty((h, w), dtype=np.float32)
137-
face_w2 = face_w / 2
138-
139-
# Middle band (front/right/back/left)
140-
mask = tp < Face.UP
141-
angles = u[mask] - (np.pi / 2 * tp[mask])
142-
tan_angles = np.tan(angles)
143-
cos_angles = np.cos(angles)
144-
tan_v = np.tan(v[mask])
145-
146-
coor_x[mask] = face_w2 * tan_angles
147-
coor_y[mask] = -face_w2 * tan_v / cos_angles
148-
149-
mask = tp == Face.UP
150-
c = face_w2 * np.tan(np.pi / 2 - v[mask])
151-
coor_x[mask] = c * np.sin(u[mask])
152-
coor_y[mask] = c * np.cos(u[mask])
153-
154-
mask = tp == Face.DOWN
155-
c = face_w2 * np.tan(np.pi / 2 - np.abs(v[mask]))
156-
coor_x[mask] = c * np.sin(u[mask])
157-
coor_y[mask] = -c * np.cos(u[mask])
158-
159-
# Final renormalize
160-
coor_x += face_w2
161-
coor_y += face_w2
162-
coor_x.clip(0, face_w, out=coor_x)
163-
coor_y.clip(0, face_w, out=coor_y)
127+
sampler = CubeFaceSampler.from_equirec(face_w, h, w, order)
164128

165129
equirec = np.empty((h, w, cube_faces.shape[3]), dtype=cube_faces[0].dtype)
166-
sampler = CubeFaceSampler(tp, coor_x, coor_y, order, face_w, face_w)
167130
for i in range(cube_faces.shape[3]):
168131
equirec[..., i] = sampler(cube_faces[..., i])
169132

py360convert/e2c.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212
cube_h2dict,
1313
cube_h2list,
1414
mode_to_order,
15-
uv2coor,
16-
xyz2uv,
17-
xyzcube,
1815
)
1916

2017

@@ -79,12 +76,7 @@ def e2c(
7976

8077
h, w = e_img.shape[:2]
8178
order = mode_to_order(mode)
82-
83-
xyz = xyzcube(face_w)
84-
u, v = xyz2uv(xyz)
85-
coor_x, coor_y = uv2coor(u, v, h, w)
86-
87-
sampler = EquirecSampler(coor_x, coor_y, order)
79+
sampler = EquirecSampler.from_cubemap(face_w, h, w, order)
8880
cubemap = np.stack(
8981
[sampler(e_img[..., i]) for i in range(e_img.shape[2])],
9082
axis=-1,

py360convert/e2p.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@
99
EquirecSampler,
1010
InterpolationMode,
1111
mode_to_order,
12-
uv2coor,
13-
xyz2uv,
14-
xyzpers,
1512
)
1613

1714

@@ -59,21 +56,16 @@ def e2p(
5956
h, w = e_img.shape[:2]
6057

6158
if isinstance(fov_deg, Real):
62-
h_fov = v_fov = np.deg2rad(float(fov_deg))
59+
h_fov = v_fov = float(np.deg2rad(float(fov_deg)))
6360
else:
6461
h_fov, v_fov = map(np.deg2rad, fov_deg)
6562

66-
in_rot = np.deg2rad(in_rot_deg)
67-
6863
order = mode_to_order(mode)
6964

70-
u = -u_deg * np.pi / 180
71-
v = v_deg * np.pi / 180
72-
xyz = xyzpers(h_fov, v_fov, u, v, out_hw, in_rot)
73-
u, v = xyz2uv(xyz)
74-
coor_x, coor_y = uv2coor(u, v, h, w)
75-
76-
sampler = EquirecSampler(coor_x, coor_y, order)
65+
u = -float(np.deg2rad(u_deg))
66+
v = float(np.deg2rad(v_deg))
67+
in_rot = float(np.deg2rad(in_rot_deg))
68+
sampler = EquirecSampler.from_perspective(h_fov, v_fov, u, v, in_rot, h, w, order)
7769
pers_img = np.stack([sampler(e_img[..., i]) for i in range(e_img.shape[2])], axis=-1)
7870

7971
return pers_img[..., 0] if squeeze else pers_img

py360convert/utils.py

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from collections.abc import Sequence
22
from enum import IntEnum
3+
from functools import lru_cache
34
from typing import Any, Literal, Optional, TypeVar, Union
45

56
import numpy as np
@@ -43,6 +44,7 @@
4344
"quintic",
4445
]
4546
DType = TypeVar("DType", bound=np.generic, covariant=True)
47+
_CACHE_SIZE = 8
4648

4749

4850
class Face(IntEnum):
@@ -85,6 +87,7 @@ def slice_chunk(index: int, width: int, offset=0):
8587
return slice(start, start + width)
8688

8789

90+
@lru_cache(_CACHE_SIZE)
8891
def xyzcube(face_w: int) -> NDArray[np.float32]:
8992
"""
9093
Return the xyz coordinates of the unit cube in [F R B L U D] format.
@@ -145,15 +148,23 @@ def face_slice(index):
145148
out[:, face_slice(Face.DOWN), Dim.Y] = -0.5
146149
out[:, face_slice(Face.DOWN), Dim.Z] = y
147150

151+
# Since we are using lru_cache, we want the return value to be immutable.
152+
out.setflags(write=False)
148153
return out
149154

150155

156+
@lru_cache(_CACHE_SIZE)
151157
def equirect_uvgrid(h: int, w: int) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
152158
u = np.linspace(-np.pi, np.pi, num=w, dtype=np.float32)
153159
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]
155165

156166

167+
@lru_cache(_CACHE_SIZE)
157168
def equirect_facetype(h: int, w: int) -> NDArray[np.int32]:
158169
"""Generate a 2D equirectangular segmentation image for each facetype.
159170
@@ -225,10 +236,15 @@ def equirect_facetype(h: int, w: int) -> NDArray[np.int32]:
225236
tp[:h3, s.stop :][mask[:, :remainder]] = Face.UP # pyright: ignore[reportPossiblyUnboundVariable]
226237
tp[-h3:, s.stop :][flip_mask[:, :remainder]] = Face.DOWN # pyright: ignore[reportPossiblyUnboundVariable]
227238

239+
# Since we are using lru_cache, we want the return value to be immutable.
240+
tp.setflags(write=False)
241+
228242
return tp
229243

230244

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]:
232248
out = np.ones((*out_hw, 3), np.float32)
233249

234250
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
240256
Ry = rotation_matrix(u, Dim.Y)
241257
Ri = rotation_matrix(in_rot, np.array([0, 0, 1.0]).dot(Rx).dot(Ry))
242258

243-
return out.dot(Rx).dot(Ry).dot(Ri)
259+
return out.dot(Rx).dot(Ry).dot(Ri).astype(np.float32)
244260

245261

246262
def xyz2uv(xyz: NDArray[DType]) -> tuple[NDArray[DType], NDArray[DType]]:
@@ -380,6 +396,56 @@ def _pad(self, img: NDArray[DType]) -> NDArray[DType]:
380396
padded[-1, :] = np.roll(img[[-1]], w // 2, 1)
381397
return padded
382398

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+
383449

384450
class CubeFaceSampler:
385451
"""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]:
511577

512578
return padded
513579

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+
514633

515634
def cube_h2list(cube_h: NDArray[DType]) -> list[NDArray[DType]]:
516635
"""Split an image into a list of 6 faces."""

0 commit comments

Comments
 (0)