From e1e191e7191ad0dcf1128b8015f47e07523e3e47 Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 11:23:21 -0500 Subject: [PATCH 01/14] fix c2e return dtype --- py360convert/c2e.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py360convert/c2e.py b/py360convert/c2e.py index 344d64b..d8d89ab 100644 --- a/py360convert/c2e.py +++ b/py360convert/c2e.py @@ -156,7 +156,7 @@ def c2e( coor_x_norm = (np.clip(coor_x, -0.5, 0.5) + 0.5) * face_w coor_y_norm = (np.clip(coor_y, -0.5, 0.5) + 0.5) * face_w - equirec = np.empty((h, w, cube_faces.shape[3])) + equirec = np.empty((h, w, cube_faces.shape[3]), dtype=cube_faces[0].dtype) for i in range(cube_faces.shape[3]): equirec[..., i] = sample_cubefaces(cube_faces[..., i], tp, coor_y_norm, coor_x_norm, order=order) From 2c095fa5a7f2dd1f2f922af510119344d68a4bd8 Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 11:35:59 -0500 Subject: [PATCH 02/14] heavily optimize equirect_facetype. --- py360convert/utils.py | 45 ++++++++++++++++++++++++++++--------------- tests/test_main.py | 1 + 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/py360convert/utils.py b/py360convert/utils.py index 4b52b95..716559e 100644 --- a/py360convert/utils.py +++ b/py360convert/utils.py @@ -75,8 +75,8 @@ def mode_to_order(mode: InterpolationMode) -> int: raise ValueError(f'Unknown mode "{mode}".') from None -def slice_chunk(index: int, width: int): - start = index * width +def slice_chunk(index: int, width: int, offset=0): + start = index * width + offset return slice(start, start + width) @@ -180,22 +180,37 @@ def equirect_facetype(h: int, w: int) -> NDArray[np.int32]: if w % 4: raise ValueError(f"w must be a multiple of 4. Got {w}.") - tp = np.roll(np.arange(4).repeat(w // 4)[None, :].repeat(h, 0), 3 * w // 8, 1) + # Create the pattern [2,3,3,0,0,1,1,2] + w4 = w // 4 + w8 = w // 8 + h3 = h // 3 + tp = np.empty((h, w), dtype=np.int32) + tp[:, :w8] = 2 + tp[:, w8 : w8 + w4] = 3 + tp[:, w8 + w4 : w8 + 2 * w4] = 0 + tp[:, w8 + 2 * w4 : w8 + 3 * w4] = 1 + tp[:, w8 + 3 * w4 :] = 2 # Prepare ceil mask - mask = np.zeros((h, w // 4), np.bool_) - idx = np.linspace(-np.pi, np.pi, w // 4) / 4 + idx = np.linspace(-np.pi, np.pi, w4) / 4 idx = np.round(h / 2 - np.arctan(np.cos(idx)) * h / np.pi).astype(np.int32) - - row_idx = np.arange(len(mask))[:, None] - mask[row_idx < idx[None, :]] = True - - mask = np.roll(np.tile(mask, (1, 4)), 3 * w // 8, 1) - - tp[mask] = Face.UP - tp[np.flip(mask, 0)] = Face.DOWN - - return tp.astype(np.int32) + # It'll never go past a third of the image, so only process that for optimization + mask = np.empty((h3, w4), np.bool_) + row_idx = np.arange(h3, dtype=np.int32)[:, None] + np.less(row_idx, idx[None], out=mask) + + flip_mask = np.flip(mask, 0) + tp[:h3, :w8][mask[:, w8:]] = Face.UP + tp[-h3:, :w8][flip_mask[:, w8:]] = Face.DOWN + for i in range(3): + s = slice_chunk(i, w4, w8) + tp[:h3, s][mask] = Face.UP + tp[-h3:, s][flip_mask] = Face.DOWN + remainder = w - s.stop # pyright: ignore[reportPossiblyUnboundVariable] + tp[:h3, s.stop :][mask[:, :remainder]] = Face.UP # pyright: ignore[reportPossiblyUnboundVariable] + tp[-h3:, s.stop :][flip_mask[:, :remainder]] = Face.DOWN # pyright: ignore[reportPossiblyUnboundVariable] + + return tp def xyzpers(h_fov: float, v_fov: float, u: float, v: float, out_hw: tuple[int, int], in_rot: float) -> NDArray: diff --git a/tests/test_main.py b/tests/test_main.py index aacff51..36de36a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -13,6 +13,7 @@ def diff(x, y): def test_c2e_dice(equirec_image, dice_image): equirec_actual = py360convert.c2e(dice_image, 512, 1024) + assert equirec_actual.dtype == dice_image.dtype equirec_diff = diff(equirec_image, equirec_actual) assert equirec_diff.mean() < AVG_DIFF_THRESH From c35dcf1ac56b37e045dac6b86885084c5cb935a2 Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 11:41:08 -0500 Subject: [PATCH 03/14] optimize equirect_uvgrid; don't stack to just split/squeeze. --- py360convert/c2e.py | 5 +---- py360convert/utils.py | 7 +++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/py360convert/c2e.py b/py360convert/c2e.py index d8d89ab..db4fa87 100644 --- a/py360convert/c2e.py +++ b/py360convert/c2e.py @@ -126,10 +126,7 @@ def c2e( raise ValueError("w must be a multiple of 8.") face_w = cubemap.shape[0] - uv = equirect_uvgrid(h, w) - u, v = np.split(uv, 2, axis=-1) - u = u[..., 0] - v = v[..., 0] + u, v = equirect_uvgrid(h, w) cube_faces = np.stack(np.split(cubemap, 6, 1), 0) # Get face id to each pixel: 0F 1R 2B 3L 4U 5D diff --git a/py360convert/utils.py b/py360convert/utils.py index 716559e..c2265e7 100644 --- a/py360convert/utils.py +++ b/py360convert/utils.py @@ -132,11 +132,10 @@ def face_slice(index): return out -def equirect_uvgrid(h: int, w: int) -> NDArray[np.float32]: +def equirect_uvgrid(h: int, w: int) -> tuple[NDArray[np.float32], NDArray[np.float32]]: u = np.linspace(-np.pi, np.pi, num=w, dtype=np.float32) - v = np.linspace(np.pi, -np.pi, num=h, dtype=np.float32) / 2 - - return np.stack(np.meshgrid(u, v), axis=-1) + v = np.linspace(np.pi / 2, -np.pi / 2, num=h, dtype=np.float32) + return np.meshgrid(u, v) # pyright: ignore[reportReturnType] def equirect_facetype(h: int, w: int) -> NDArray[np.int32]: From 6728191eb2981ff3afa6e7fcb15f6dcaddb76355 Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 11:54:04 -0500 Subject: [PATCH 04/14] optimize out going from list->horizon->list. --- py360convert/c2e.py | 26 ++++++++++++++------------ py360convert/utils.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/py360convert/c2e.py b/py360convert/c2e.py index db4fa87..f0ae991 100644 --- a/py360convert/c2e.py +++ b/py360convert/c2e.py @@ -8,7 +8,10 @@ DType, InterpolationMode, cube_dice2h, + cube_dice2list, cube_dict2h, + cube_dict2list, + cube_h2list, cube_list2h, equirect_facetype, equirect_uvgrid, @@ -74,6 +77,8 @@ def c2e( Equirectangular image. """ order = mode_to_order(mode) + if w % 8 != 0: + raise ValueError("w must be a multiple of 8.") if cube_format == "horizon": if not isinstance(cubemap, np.ndarray): @@ -83,17 +88,18 @@ def c2e( squeeze = True else: squeeze = False + cube_faces = cube_h2list(cubemap) elif cube_format == "list": if not isinstance(cubemap, list): raise TypeError('cubemap must be a list for cube_format="list"') if len({x.shape for x in cubemap}) != 1: raise ValueError("All cubemap elements must have same shape") if cubemap[0].ndim == 2: - cubemap = [x[..., None] for x in cubemap] + cube_faces = [x[..., None] for x in cubemap] squeeze = True else: + cube_faces = cubemap squeeze = False - cubemap = cube_list2h(cubemap) elif cube_format == "dict": if not isinstance(cubemap, dict): raise TypeError('cubemap must be a dict for cube_format="dict"') @@ -104,7 +110,7 @@ def c2e( squeeze = True else: squeeze = False - cubemap = cube_dict2h(cubemap) + cube_faces = cube_dict2list(cubemap) elif cube_format == "dice": if not isinstance(cubemap, np.ndarray): raise TypeError('cubemap must be a numpy array for cube_format="dice"') @@ -113,21 +119,17 @@ def c2e( squeeze = True else: squeeze = False - cubemap = cube_dice2h(cubemap) + cube_faces = cube_dice2list(cubemap) else: raise ValueError('Unknown cube_format "{cube_format}".') - if cubemap.ndim != 3: - raise ValueError(f"Cubemap must have 2 or 3 dimensions; got {cubemap.ndim}.") + cube_faces = np.stack(cube_faces) - if cubemap.shape[0] * 6 != cubemap.shape[1]: - raise ValueError("Cubemap's width must by 6x its height.") - if w % 8 != 0: - raise ValueError("w must be a multiple of 8.") - face_w = cubemap.shape[0] + if cube_faces.shape[1] != cube_faces.shape[2]: + raise ValueError("Cubemap faces must be square.") + face_w = cube_faces.shape[2] u, v = equirect_uvgrid(h, w) - cube_faces = np.stack(np.split(cubemap, 6, 1), 0) # Get face id to each pixel: 0F 1R 2B 3L 4U 5D tp = equirect_facetype(h, w) diff --git a/py360convert/utils.py b/py360convert/utils.py index c2265e7..c28a2c9 100644 --- a/py360convert/utils.py +++ b/py360convert/utils.py @@ -401,11 +401,15 @@ def cube_h2dict(cube_h: NDArray[DType]) -> dict[str, NDArray[DType]]: return dict(zip("FRBLUD", cube_h2list(cube_h))) -def cube_dict2h(cube_dict: dict[Any, NDArray[DType]], face_k: Optional[Sequence] = None) -> NDArray[DType]: +def cube_dict2list(cube_dict: dict[Any, NDArray[DType]], face_k: Optional[Sequence] = None) -> list[NDArray[DType]]: face_k = face_k or "FRBLUD" if len(face_k) != 6: raise ValueError(f"6 face_k keys must be provided to construct a cube; got {len(face_k)}.") - return cube_list2h([cube_dict[k] for k in face_k]) + return [cube_dict[k] for k in face_k] + + +def cube_dict2h(cube_dict: dict[Any, NDArray[DType]], face_k: Optional[Sequence] = None) -> NDArray[DType]: + return cube_list2h(cube_dict2list(cube_dict, face_k)) def cube_h2dice(cube_h: NDArray[DType]) -> NDArray[DType]: @@ -428,6 +432,27 @@ def cube_h2dice(cube_h: NDArray[DType]) -> NDArray[DType]: return cube_dice +def cube_dice2list(cube_dice: NDArray[DType]) -> list[NDArray[DType]]: + if cube_dice.shape[0] % 3 != 0: + raise ValueError("Dice image height must be a multiple of 3.") + w = cube_dice.shape[0] // 3 + if cube_dice.shape[1] != w * 4: + raise ValueError(f'Dice width must be 4 "faces" (4x{w}={4*w}) wide.') + # Order: F R B L U D + # ┌────┐ + # │ U │ + # ┌────┼────┼────┬────┐ + # │ L │ F │ R │ B │ + # └────┼────┼────┴────┘ + # │ D │ + # └────┘ + out = [] + sxy = [(1, 1), (2, 1), (3, 1), (0, 1), (1, 0), (1, 2)] + for sx, sy in sxy: + out.append(cube_dice[slice_chunk(sy, w), slice_chunk(sx, w)]) + return out + + def cube_dice2h(cube_dice: NDArray[DType]) -> NDArray[DType]: if cube_dice.shape[0] % 3 != 0: raise ValueError("Dice image height must be a multiple of 3.") From 237781ebf8eb20427c8c99ca2286ea84858167e5 Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 12:32:59 -0500 Subject: [PATCH 05/14] optimize c2e coordinate calculation --- py360convert/c2e.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/py360convert/c2e.py b/py360convert/c2e.py index f0ae991..d0afe41 100644 --- a/py360convert/c2e.py +++ b/py360convert/c2e.py @@ -6,13 +6,11 @@ from .utils import ( CubeFormat, DType, + Face, InterpolationMode, - cube_dice2h, cube_dice2list, - cube_dict2h, cube_dict2list, cube_h2list, - cube_list2h, equirect_facetype, equirect_uvgrid, mode_to_order, @@ -133,30 +131,39 @@ def c2e( # Get face id to each pixel: 0F 1R 2B 3L 4U 5D tp = equirect_facetype(h, w) - coor_x = np.zeros((h, w)) - coor_y = np.zeros((h, w)) - for i in range(4): - mask = tp == i - coor_x[mask] = 0.5 * np.tan(u[mask] - np.pi * i / 2) - coor_y[mask] = -0.5 * np.tan(v[mask]) / np.cos(u[mask] - np.pi * i / 2) + coor_x = np.empty((h, w)) + coor_y = np.empty((h, w)) + face_w2 = face_w / 2 - mask = tp == 4 - c = 0.5 * np.tan(np.pi / 2 - v[mask]) + # Middle band (front/right/back/left) + mask = tp < Face.UP + angles = u[mask] - (np.pi / 2 * tp[mask]) + tan_angles = np.tan(angles) + cos_angles = np.cos(angles) + tan_v = np.tan(v[mask]) + + coor_x[mask] = face_w2 * tan_angles + coor_y[mask] = -face_w2 * tan_v / cos_angles + + mask = tp == Face.UP + c = face_w2 * np.tan(np.pi / 2 - v[mask]) coor_x[mask] = c * np.sin(u[mask]) coor_y[mask] = c * np.cos(u[mask]) - mask = tp == 5 - c = 0.5 * np.tan(np.pi / 2 - np.abs(v[mask])) + mask = tp == Face.DOWN + c = face_w2 * np.tan(np.pi / 2 - np.abs(v[mask])) coor_x[mask] = c * np.sin(u[mask]) coor_y[mask] = -c * np.cos(u[mask]) # Final renormalize - coor_x_norm = (np.clip(coor_x, -0.5, 0.5) + 0.5) * face_w - coor_y_norm = (np.clip(coor_y, -0.5, 0.5) + 0.5) * face_w + coor_x += face_w2 + coor_y += face_w2 + coor_x.clip(0, face_w, out=coor_x) + coor_y.clip(0, face_w, out=coor_y) equirec = np.empty((h, w, cube_faces.shape[3]), dtype=cube_faces[0].dtype) for i in range(cube_faces.shape[3]): - equirec[..., i] = sample_cubefaces(cube_faces[..., i], tp, coor_y_norm, coor_x_norm, order=order) + equirec[..., i] = sample_cubefaces(cube_faces[..., i], tp, coor_y, coor_x, order=order) return equirec[..., 0] if squeeze else equirec From 8efd98700e45267b9faed20f0cc90d479157f959 Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 12:45:50 -0500 Subject: [PATCH 06/14] coordinates should be float32; don't initialize padding if we are going to be overwriting it. --- py360convert/c2e.py | 4 ++-- py360convert/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/py360convert/c2e.py b/py360convert/c2e.py index d0afe41..5828fdd 100644 --- a/py360convert/c2e.py +++ b/py360convert/c2e.py @@ -132,8 +132,8 @@ def c2e( # Get face id to each pixel: 0F 1R 2B 3L 4U 5D tp = equirect_facetype(h, w) - coor_x = np.empty((h, w)) - coor_y = np.empty((h, w)) + coor_x = np.empty((h, w), dtype=np.float32) + coor_y = np.empty((h, w), dtype=np.float32) face_w2 = face_w / 2 # Middle band (front/right/back/left) diff --git a/py360convert/utils.py b/py360convert/utils.py index c28a2c9..d0f9cc3 100644 --- a/py360convert/utils.py +++ b/py360convert/utils.py @@ -340,7 +340,7 @@ def sample_cubefaces( BELOW = (-1, slice(None)) LEFT = (slice(None), 0) RIGHT = (slice(None), -1) - padded = np.pad(cube_faces, ((0, 0), (1, 1), (1, 1)), mode="constant") + padded = np.pad(cube_faces, ((0, 0), (1, 1), (1, 1)), mode="empty") # Pad above/below padded[Face.FRONT][ABOVE] = padded[Face.UP, -2, :] From dce082217050cac4408fa69724a0725e8c5cde0b Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 13:43:08 -0500 Subject: [PATCH 07/14] Use much faster cv2.remap if available. --- py360convert/utils.py | 48 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/py360convert/utils.py b/py360convert/utils.py index d0f9cc3..50f6753 100644 --- a/py360convert/utils.py +++ b/py360convert/utils.py @@ -7,6 +7,12 @@ from scipy.ndimage import map_coordinates from scipy.spatial.transform import Rotation +try: + import cv2 +except ImportError: + cv2 = None + + _mode_to_order = { "nearest": 0, "linear": 1, @@ -318,6 +324,36 @@ def sample_equirec(e_img: NDArray[DType], coor_xy: NDArray, order: int) -> NDArr return map_coordinates(e_img, [coor_y, coor_x], order=order, mode="wrap")[..., 0] # pyright: ignore[reportReturnType] +def _cv2_remap(padded, tp, coor_y, coor_x, order): + """A much faster sampler; requires opencv (cv2) to be installed. + + WARNING: coor_y is updated in place (for performance reasons) + """ + if not cv2: + raise ValueError("opencv (cv2) must be installed to use _cv2_remap.") + + # Map interpolation modes + if order == 0: + interpolation = cv2.INTER_NEAREST + nninterpolation = True + elif order == 1: + interpolation = cv2.INTER_LINEAR + nninterpolation = False + elif order == 3: + interpolation = cv2.INTER_CUBIC + nninterpolation = False + else: + raise ValueError + + # Vertically concatenated the image and update y-coordinates so we can do a single remap operation. + h, w = padded.shape[-2:] + v_img = padded.reshape(-1, w) + coor_y += np.multiply(tp, h, dtype=np.float32) + map_1, map_2 = cv2.convertMaps(coor_x, coor_y, cv2.CV_16SC2, nninterpolation=nninterpolation) + out = cv2.remap(v_img, map_1, map_2, interpolation=interpolation) + return out + + def sample_cubefaces( cube_faces: NDArray[DType], tp: NDArray, coor_y: NDArray, coor_x: NDArray, order: int ) -> NDArray[DType]: @@ -326,7 +362,7 @@ def sample_cubefaces( Parameters ---------- cube_faces: numpy.ndarray - (6, H, W) Cube faces. + (6, S, S) Cube faces. tp: numpy.ndarray (H, W) facetype image from ``equirect_facetype``. coor_y: numpy.ndarray @@ -335,6 +371,11 @@ def sample_cubefaces( (H, W) X coordinates to sample. order: int The order of the spline interpolation. See ``scipy.ndimage.map_coordinates``. + + Returns + ------- + numpy.ndarray + (H, W) Sampled image. """ ABOVE = (0, slice(None)) BELOW = (-1, slice(None)) @@ -370,7 +411,10 @@ def sample_cubefaces( padded[Face.DOWN][LEFT] = padded[Face.LEFT, -2, ::-1] padded[Face.DOWN][RIGHT] = padded[Face.RIGHT, -2, :] - return map_coordinates(padded, [tp, coor_y + 1, coor_x + 1], order=order) # pyright: ignore[reportReturnType] + if cv2 and order in (0, 1, 3): + return _cv2_remap(padded, tp, coor_y + 1, coor_x + 1, order) + else: + return map_coordinates(padded, [tp, coor_y + 1, coor_x + 1], order=order) # pyright: ignore[reportReturnType] def cube_h2list(cube_h: NDArray[DType]) -> list[NDArray[DType]]: From d6ab73413dc0dff3870c7181739c6b32f8226a87 Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 14:39:30 -0500 Subject: [PATCH 08/14] convert sampler from functional to a class to intermediate computations can be re-used. --- py360convert/c2e.py | 5 +- py360convert/utils.py | 218 ++++++++++++++++++++++++------------------ pyproject.toml | 1 + 3 files changed, 131 insertions(+), 93 deletions(-) diff --git a/py360convert/c2e.py b/py360convert/c2e.py index 5828fdd..cab5b13 100644 --- a/py360convert/c2e.py +++ b/py360convert/c2e.py @@ -7,6 +7,7 @@ CubeFormat, DType, Face, + ImageSampler2d, InterpolationMode, cube_dice2list, cube_dict2list, @@ -14,7 +15,6 @@ equirect_facetype, equirect_uvgrid, mode_to_order, - sample_cubefaces, ) @@ -163,7 +163,8 @@ def c2e( coor_y.clip(0, face_w, out=coor_y) equirec = np.empty((h, w, cube_faces.shape[3]), dtype=cube_faces[0].dtype) + sampler = ImageSampler2d(tp, coor_x, coor_y, order, face_w, face_w) for i in range(cube_faces.shape[3]): - equirec[..., i] = sample_cubefaces(cube_faces[..., i], tp, coor_y, coor_x, order=order) + equirec[..., i] = sampler(cube_faces[..., i]) return equirec[..., 0] if squeeze else equirec diff --git a/py360convert/utils.py b/py360convert/utils.py index 50f6753..e740a1f 100644 --- a/py360convert/utils.py +++ b/py360convert/utils.py @@ -324,97 +324,133 @@ def sample_equirec(e_img: NDArray[DType], coor_xy: NDArray, order: int) -> NDArr return map_coordinates(e_img, [coor_y, coor_x], order=order, mode="wrap")[..., 0] # pyright: ignore[reportReturnType] -def _cv2_remap(padded, tp, coor_y, coor_x, order): - """A much faster sampler; requires opencv (cv2) to be installed. - - WARNING: coor_y is updated in place (for performance reasons) - """ - if not cv2: - raise ValueError("opencv (cv2) must be installed to use _cv2_remap.") - - # Map interpolation modes - if order == 0: - interpolation = cv2.INTER_NEAREST - nninterpolation = True - elif order == 1: - interpolation = cv2.INTER_LINEAR - nninterpolation = False - elif order == 3: - interpolation = cv2.INTER_CUBIC - nninterpolation = False - else: - raise ValueError - - # Vertically concatenated the image and update y-coordinates so we can do a single remap operation. - h, w = padded.shape[-2:] - v_img = padded.reshape(-1, w) - coor_y += np.multiply(tp, h, dtype=np.float32) - map_1, map_2 = cv2.convertMaps(coor_x, coor_y, cv2.CV_16SC2, nninterpolation=nninterpolation) - out = cv2.remap(v_img, map_1, map_2, interpolation=interpolation) - return out - - -def sample_cubefaces( - cube_faces: NDArray[DType], tp: NDArray, coor_y: NDArray, coor_x: NDArray, order: int -) -> NDArray[DType]: - """Sample cube faces. - - Parameters - ---------- - cube_faces: numpy.ndarray - (6, S, S) Cube faces. - tp: numpy.ndarray - (H, W) facetype image from ``equirect_facetype``. - coor_y: numpy.ndarray - (H, W) Y coordinates to sample. - coor_x: numpy.ndarray - (H, W) X coordinates to sample. - order: int - The order of the spline interpolation. See ``scipy.ndimage.map_coordinates``. - - Returns - ------- - numpy.ndarray - (H, W) Sampled image. - """ - ABOVE = (0, slice(None)) - BELOW = (-1, slice(None)) - LEFT = (slice(None), 0) - RIGHT = (slice(None), -1) - padded = np.pad(cube_faces, ((0, 0), (1, 1), (1, 1)), mode="empty") - - # Pad above/below - padded[Face.FRONT][ABOVE] = padded[Face.UP, -2, :] - padded[Face.FRONT][BELOW] = padded[Face.DOWN, 1, :] - padded[Face.RIGHT][ABOVE] = padded[Face.UP, ::-1, -2] - padded[Face.RIGHT][BELOW] = padded[Face.DOWN, :, -2] - padded[Face.BACK][ABOVE] = padded[Face.UP, 1, ::-1] - padded[Face.BACK][BELOW] = padded[Face.DOWN, -2, ::-1] - padded[Face.LEFT][ABOVE] = padded[Face.UP, :, 1] - padded[Face.LEFT][BELOW] = padded[Face.DOWN, ::-1, 1] - padded[Face.UP][ABOVE] = padded[Face.BACK, 1, ::-1] - padded[Face.UP][BELOW] = padded[Face.FRONT, 1, :] - padded[Face.DOWN][ABOVE] = padded[Face.FRONT, -2, :] - padded[Face.DOWN][BELOW] = padded[Face.BACK, -2, ::-1] - - # Pad left/right - padded[Face.FRONT][LEFT] = padded[Face.LEFT, :, -2] - padded[Face.FRONT][RIGHT] = padded[Face.RIGHT, :, 1] - padded[Face.RIGHT][LEFT] = padded[Face.FRONT, :, -2] - padded[Face.RIGHT][RIGHT] = padded[Face.BACK, :, 1] - padded[Face.BACK][LEFT] = padded[Face.RIGHT, :, -2] - padded[Face.BACK][RIGHT] = padded[Face.LEFT, :, 1] - padded[Face.LEFT][LEFT] = padded[Face.BACK, :, -2] - padded[Face.LEFT][RIGHT] = padded[Face.FRONT, :, 1] - padded[Face.UP][LEFT] = padded[Face.LEFT, 1, :] - padded[Face.UP][RIGHT] = padded[Face.RIGHT, 1, ::-1] - padded[Face.DOWN][LEFT] = padded[Face.LEFT, -2, ::-1] - padded[Face.DOWN][RIGHT] = padded[Face.RIGHT, -2, :] - - if cv2 and order in (0, 1, 3): - return _cv2_remap(padded, tp, coor_y + 1, coor_x + 1, order) - else: - return map_coordinates(padded, [tp, coor_y + 1, coor_x + 1], order=order) # pyright: ignore[reportReturnType] +class ImageSampler2d: + """Arranged as a class so coordinate computations can be re-used across multiple image interpolations.""" + + def __init__( + self, + tp: NDArray, + coor_x: NDArray, + coor_y: NDArray, + order: int, + h: int, + w: int, + ): + """Initializes sampler and performs pre-computations. + + Parameters + ---------- + tp: numpy.ndarray + (H, W) facetype image from ``equirect_facetype``. + coor_x: numpy.ndarray + (H, W) X coordinates to sample. + coor_y: numpy.ndarray + (H, W) Y coordinates to sample. + order: int + The order of the spline interpolation. See ``scipy.ndimage.map_coordinates``. + h: int + Expected input image height. + w: int + Expected input image width. + """ + # Add 1 to compensate for 1-pixel-surround padding. + coor_x = coor_x + 1 # Not done inplace on purpose. + coor_y = coor_y + 1 # Not done inplace on purpose. + + self._tp = tp + self._h = h + self._w = w + if cv2 and order in (0, 1, 3): + if order == 0: + self._order = cv2.INTER_NEAREST + nninterpolation = True + elif order == 1: + self._order = cv2.INTER_LINEAR + nninterpolation = False + elif order == 3: + self._order = cv2.INTER_CUBIC + nninterpolation = False + else: + raise NotImplementedError + + # The +2 comes from padding from self._pad. + coor_y += np.multiply(tp, h + 2, dtype=np.float32) + self._coor_x, self._coor_y = cv2.convertMaps( + coor_x, + coor_y, + cv2.CV_16SC2, + nninterpolation=nninterpolation, + ) + else: + self._coor_x = coor_x + self._coor_y = coor_y + self._order = order + + def __call__(self, cube_faces: NDArray[DType]) -> NDArray[DType]: + """Sample cube faces. + + Parameters + ---------- + cube_faces: numpy.ndarray + (6, S, S) Cube faces. + + Returns + ------- + numpy.ndarray + (H, W) Sampled image. + """ + h, w = cube_faces.shape[-2:] + if h != self._h: + raise ValueError("Input height {h} doesn't match expected height {self._h}.") + if w != self._w: + raise ValueError("Input width {w} doesn't match expected height {self._w}.") + + padded = self._pad(cube_faces) + if cv2 and self._order in (0, 1, 3): + w = padded.shape[-1] + v_img = padded.reshape(-1, w) + out = cv2.remap(v_img, self._coor_x, self._coor_y, interpolation=self._order) # pyright: ignore + else: + out = map_coordinates(padded, (self._tp, self._coor_y, self._coor_x), order=self._order) + return out # pyright: ignore[reportReturnType] + + def _pad(self, cube_faces: NDArray[DType]) -> NDArray[DType]: + """Adds 1 pixel of padding around each cube face.""" + ABOVE = (0, slice(None)) + BELOW = (-1, slice(None)) + LEFT = (slice(None), 0) + RIGHT = (slice(None), -1) + padded = np.pad(cube_faces, ((0, 0), (1, 1), (1, 1)), mode="empty") + + # Pad above/below + padded[Face.FRONT][ABOVE] = padded[Face.UP, -2, :] + padded[Face.FRONT][BELOW] = padded[Face.DOWN, 1, :] + padded[Face.RIGHT][ABOVE] = padded[Face.UP, ::-1, -2] + padded[Face.RIGHT][BELOW] = padded[Face.DOWN, :, -2] + padded[Face.BACK][ABOVE] = padded[Face.UP, 1, ::-1] + padded[Face.BACK][BELOW] = padded[Face.DOWN, -2, ::-1] + padded[Face.LEFT][ABOVE] = padded[Face.UP, :, 1] + padded[Face.LEFT][BELOW] = padded[Face.DOWN, ::-1, 1] + padded[Face.UP][ABOVE] = padded[Face.BACK, 1, ::-1] + padded[Face.UP][BELOW] = padded[Face.FRONT, 1, :] + padded[Face.DOWN][ABOVE] = padded[Face.FRONT, -2, :] + padded[Face.DOWN][BELOW] = padded[Face.BACK, -2, ::-1] + + # Pad left/right + padded[Face.FRONT][LEFT] = padded[Face.LEFT, :, -2] + padded[Face.FRONT][RIGHT] = padded[Face.RIGHT, :, 1] + padded[Face.RIGHT][LEFT] = padded[Face.FRONT, :, -2] + padded[Face.RIGHT][RIGHT] = padded[Face.BACK, :, 1] + padded[Face.BACK][LEFT] = padded[Face.RIGHT, :, -2] + padded[Face.BACK][RIGHT] = padded[Face.LEFT, :, 1] + padded[Face.LEFT][LEFT] = padded[Face.BACK, :, -2] + padded[Face.LEFT][RIGHT] = padded[Face.FRONT, :, 1] + padded[Face.UP][LEFT] = padded[Face.LEFT, 1, :] + padded[Face.UP][RIGHT] = padded[Face.RIGHT, 1, ::-1] + padded[Face.DOWN][LEFT] = padded[Face.LEFT, -2, ::-1] + padded[Face.DOWN][RIGHT] = padded[Face.RIGHT, -2, :] + + return padded def cube_h2list(cube_h: NDArray[DType]) -> list[NDArray[DType]]: diff --git a/pyproject.toml b/pyproject.toml index b0e1543..3181d09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,7 @@ ignore = [ "E501", "F401", "N806", + "PGH003", # Use specific rule codes when ignoring type issues "TRY003", # Avoid specifying messages outside exception class; overly strict, especially for ValueError ] From c5adf9233916fefb9985532b5207b0701907b94c Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 15:08:37 -0500 Subject: [PATCH 09/14] Update equirec sampler to also use cv2 and to cache coordinates. --- py360convert/c2e.py | 4 +-- py360convert/e2c.py | 7 ++-- py360convert/e2p.py | 7 ++-- py360convert/utils.py | 74 +++++++++++++++++++++++++++++++++++-------- 4 files changed, 71 insertions(+), 21 deletions(-) diff --git a/py360convert/c2e.py b/py360convert/c2e.py index cab5b13..e13da3d 100644 --- a/py360convert/c2e.py +++ b/py360convert/c2e.py @@ -4,10 +4,10 @@ from numpy.typing import NDArray from .utils import ( + CubeFaceSampler, CubeFormat, DType, Face, - ImageSampler2d, InterpolationMode, cube_dice2list, cube_dict2list, @@ -163,7 +163,7 @@ def c2e( coor_y.clip(0, face_w, out=coor_y) equirec = np.empty((h, w, cube_faces.shape[3]), dtype=cube_faces[0].dtype) - sampler = ImageSampler2d(tp, coor_x, coor_y, order, face_w, face_w) + sampler = CubeFaceSampler(tp, coor_x, coor_y, order, face_w, face_w) for i in range(cube_faces.shape[3]): equirec[..., i] = sampler(cube_faces[..., i]) diff --git a/py360convert/e2c.py b/py360convert/e2c.py index d291b39..369da1a 100644 --- a/py360convert/e2c.py +++ b/py360convert/e2c.py @@ -6,12 +6,12 @@ from .utils import ( CubeFormat, DType, + EquirecSampler, InterpolationMode, cube_h2dice, cube_h2dict, cube_h2list, mode_to_order, - sample_equirec, uv2coor, xyz2uv, xyzcube, @@ -82,10 +82,11 @@ def e2c( xyz = xyzcube(face_w) uv = xyz2uv(xyz) - coor_xy = uv2coor(uv, h, w) + coor_x, coor_y = uv2coor(uv, h, w) + sampler = EquirecSampler(coor_x, coor_y, order) cubemap = np.stack( - [sample_equirec(e_img[..., i], coor_xy, order=order) for i in range(e_img.shape[2])], + [sampler(e_img[..., i]) for i in range(e_img.shape[2])], axis=-1, dtype=e_img.dtype, ) diff --git a/py360convert/e2p.py b/py360convert/e2p.py index d619129..ff0d314 100644 --- a/py360convert/e2p.py +++ b/py360convert/e2p.py @@ -6,9 +6,9 @@ from .utils import ( DType, + EquirecSampler, InterpolationMode, mode_to_order, - sample_equirec, uv2coor, xyz2uv, xyzpers, @@ -71,8 +71,9 @@ def e2p( v = v_deg * np.pi / 180 xyz = xyzpers(h_fov, v_fov, u, v, out_hw, in_rot) uv = xyz2uv(xyz) - coor_xy = uv2coor(uv, h, w) + coor_x, coor_y = uv2coor(uv, h, w) - pers_img = np.stack([sample_equirec(e_img[..., i], coor_xy, order=order) for i in range(e_img.shape[2])], axis=-1) + sampler = EquirecSampler(coor_x, coor_y, order) + pers_img = np.stack([sampler(e_img[..., i]) for i in range(e_img.shape[2])], axis=-1) return pers_img[..., 0] if squeeze else pers_img diff --git a/py360convert/utils.py b/py360convert/utils.py index e740a1f..31c89f9 100644 --- a/py360convert/utils.py +++ b/py360convert/utils.py @@ -12,7 +12,6 @@ except ImportError: cv2 = None - _mode_to_order = { "nearest": 0, "linear": 1, @@ -273,7 +272,7 @@ def uv2unitxyz(uv: NDArray[DType]) -> NDArray[DType]: return np.concatenate([x, y, z], axis=-1, dtype=uv.dtype) -def uv2coor(uv: NDArray[DType], h: int, w: int) -> NDArray[DType]: +def uv2coor(uv: NDArray[DType], h: int, w: int) -> tuple[NDArray[DType], NDArray[DType]]: """Transform spherical(r, u, v) into equirectangular(x, y). Assume that u has range 2pi and v has range pi. @@ -304,8 +303,7 @@ def uv2coor(uv: NDArray[DType], h: int, w: int) -> NDArray[DType]: u, v = np.split(uv, 2, axis=-1) coor_x = (u / (2 * np.pi) + 0.5) * w - 0.5 # pyright: ignore[reportOperatorIssue] coor_y = (-v / np.pi + 0.5) * h - 0.5 # pyright: ignore[reportOperatorIssue] - out = np.concatenate([coor_x, coor_y], axis=-1, dtype=uv.dtype) - return out + return coor_x, coor_y def coor2uv(coorxy: NDArray[DType], h: int, w: int) -> NDArray[DType]: @@ -315,16 +313,64 @@ def coor2uv(coorxy: NDArray[DType], h: int, w: int) -> NDArray[DType]: return np.concatenate([u, v], axis=-1, dtype=coorxy.dtype) -def sample_equirec(e_img: NDArray[DType], coor_xy: NDArray, order: int) -> NDArray[DType]: - w = e_img.shape[1] - coor_x, coor_y = np.split(coor_xy, 2, axis=-1) - pad_u = np.roll(e_img[[0]], w // 2, 1) - pad_d = np.roll(e_img[[-1]], w // 2, 1) - e_img = np.concatenate([e_img, pad_d, pad_u], 0, dtype=e_img.dtype) - return map_coordinates(e_img, [coor_y, coor_x], order=order, mode="wrap")[..., 0] # pyright: ignore[reportReturnType] +class EquirecSampler: + def __init__( + self, + coor_x: NDArray, + coor_y: NDArray, + order: int, + ): + if cv2 and order in (0, 1, 3): + self._use_cv2 = True + if order == 0: + self._order = cv2.INTER_NEAREST + nninterpolation = True + elif order == 1: + self._order = cv2.INTER_LINEAR + nninterpolation = False + elif order == 3: + self._order = cv2.INTER_CUBIC + nninterpolation = False + else: + raise NotImplementedError + + # TODO: I think coor_y has an off-by-one due to the 1 pixel padding? + self._coor_x, self._coor_y = cv2.convertMaps( + coor_x, + coor_y, + cv2.CV_16SC2, + nninterpolation=nninterpolation, + ) + else: + self._use_cv2 = False + self._coor_x = coor_x + self._coor_y = coor_y + self._order = order + + def __call__(self, img: NDArray[DType]) -> NDArray[DType]: + padded = self._pad(img) + if self._use_cv2: + out = cv2.remap(padded, self._coor_x, self._coor_y, interpolation=self._order) # pyright: ignore + else: + out = map_coordinates( + padded, + (self._coor_y, self._coor_x), + order=self._order, + mode="wrap", + )[..., 0] + + return out # pyright: ignore[reportReturnType] + + def _pad(self, img: NDArray[DType]) -> NDArray[DType]: + """Adds 1 pixel of padding above/below image.""" + w = img.shape[1] + pad_u = np.roll(img[[0]], w // 2, 1) + pad_d = np.roll(img[[-1]], w // 2, 1) + img = np.concatenate([img, pad_d, pad_u], 0, dtype=img.dtype) + return img -class ImageSampler2d: +class CubeFaceSampler: """Arranged as a class so coordinate computations can be re-used across multiple image interpolations.""" def __init__( @@ -361,6 +407,7 @@ def __init__( self._h = h self._w = w if cv2 and order in (0, 1, 3): + self._use_cv2 = True if order == 0: self._order = cv2.INTER_NEAREST nninterpolation = True @@ -382,6 +429,7 @@ def __init__( nninterpolation=nninterpolation, ) else: + self._use_cv2 = False self._coor_x = coor_x self._coor_y = coor_y self._order = order @@ -406,7 +454,7 @@ def __call__(self, cube_faces: NDArray[DType]) -> NDArray[DType]: raise ValueError("Input width {w} doesn't match expected height {self._w}.") padded = self._pad(cube_faces) - if cv2 and self._order in (0, 1, 3): + if self._use_cv2: w = padded.shape[-1] v_img = padded.reshape(-1, w) out = cv2.remap(v_img, self._coor_x, self._coor_y, interpolation=self._order) # pyright: ignore From db0ab36556c18b2d509e071a9a16c2f3ddd808fb Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 15:16:55 -0500 Subject: [PATCH 10/14] optimize xyzcube. --- py360convert/utils.py | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/py360convert/utils.py b/py360convert/utils.py index 31c89f9..4e6a2ec 100644 --- a/py360convert/utils.py +++ b/py360convert/utils.py @@ -91,48 +91,59 @@ def xyzcube(face_w: int) -> NDArray[np.float32]: Parameters ---------- - face_w: int - Specify the length of each face of the cubemap. + face_w: int + Specify the length of each face of the cubemap. Returns ------- - out: ndarray - An array object with dimension (face_w, face_w * 6, 3) - which store the each face of numalized cube coordinates. - The cube is centered at the origin so that each face k - in out has range [-0.5, 0.5] x [-0.5, 0.5]. - + out: ndarray + An array object with dimension (face_w, face_w * 6, 3) + which store the each face of numalized cube coordinates. + The cube is centered at the origin so that each face k + in out has range [-0.5, 0.5] x [-0.5, 0.5]. """ - out = np.zeros((face_w, face_w * 6, 3), np.float32) + out = np.empty((face_w, face_w * 6, 3), np.float32) + + # Create coordinates once and reuse rng = np.linspace(-0.5, 0.5, num=face_w, dtype=np.float32) - grid = np.stack(np.meshgrid(rng, -rng), -1) + x, y = np.meshgrid(rng, -rng) + + # Pre-compute flips + x_flip = np.flip(x, 1) + y_flip = np.flip(y, 0) def face_slice(index): return slice_chunk(index, face_w) # Front face (z = 0.5) - out[:, face_slice(Face.FRONT), [Dim.X, Dim.Y]] = grid + out[:, face_slice(Face.FRONT), Dim.X] = x + out[:, face_slice(Face.FRONT), Dim.Y] = y out[:, face_slice(Face.FRONT), Dim.Z] = 0.5 # Right face (x = 0.5) - out[:, face_slice(Face.RIGHT), [Dim.Z, Dim.Y]] = np.flip(grid, axis=1) out[:, face_slice(Face.RIGHT), Dim.X] = 0.5 + out[:, face_slice(Face.RIGHT), Dim.Y] = y + out[:, face_slice(Face.RIGHT), Dim.Z] = x_flip # Back face (z = -0.5) - out[:, face_slice(Face.BACK), [Dim.X, Dim.Y]] = np.flip(grid, axis=1) + out[:, face_slice(Face.BACK), Dim.X] = x_flip + out[:, face_slice(Face.BACK), Dim.Y] = y out[:, face_slice(Face.BACK), Dim.Z] = -0.5 # Left face (x = -0.5) - out[:, face_slice(Face.LEFT), [Dim.Z, Dim.Y]] = grid out[:, face_slice(Face.LEFT), Dim.X] = -0.5 + out[:, face_slice(Face.LEFT), Dim.Y] = y + out[:, face_slice(Face.LEFT), Dim.Z] = x # Up face (y = 0.5) - out[:, face_slice(Face.UP), [Dim.X, Dim.Z]] = np.flip(grid, axis=0) + out[:, face_slice(Face.UP), Dim.X] = x out[:, face_slice(Face.UP), Dim.Y] = 0.5 + out[:, face_slice(Face.UP), Dim.Z] = y_flip # Down face (y = -0.5) - out[:, face_slice(Face.DOWN), [Dim.X, Dim.Z]] = grid + out[:, face_slice(Face.DOWN), Dim.X] = x out[:, face_slice(Face.DOWN), Dim.Y] = -0.5 + out[:, face_slice(Face.DOWN), Dim.Z] = y return out From 3ddc8790d4bff8d3cde3061869d3d2179bc45a40 Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 15:25:53 -0500 Subject: [PATCH 11/14] optimize xyz2uv --- py360convert/e2c.py | 4 ++-- py360convert/e2p.py | 4 ++-- py360convert/utils.py | 10 ++++------ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/py360convert/e2c.py b/py360convert/e2c.py index 369da1a..9ef68c0 100644 --- a/py360convert/e2c.py +++ b/py360convert/e2c.py @@ -81,8 +81,8 @@ def e2c( order = mode_to_order(mode) xyz = xyzcube(face_w) - uv = xyz2uv(xyz) - coor_x, coor_y = uv2coor(uv, h, w) + u, v = xyz2uv(xyz) + coor_x, coor_y = uv2coor(u, v, h, w) sampler = EquirecSampler(coor_x, coor_y, order) cubemap = np.stack( diff --git a/py360convert/e2p.py b/py360convert/e2p.py index ff0d314..fd7c852 100644 --- a/py360convert/e2p.py +++ b/py360convert/e2p.py @@ -70,8 +70,8 @@ def e2p( u = -u_deg * np.pi / 180 v = v_deg * np.pi / 180 xyz = xyzpers(h_fov, v_fov, u, v, out_hw, in_rot) - uv = xyz2uv(xyz) - coor_x, coor_y = uv2coor(uv, h, w) + u, v = xyz2uv(xyz) + coor_x, coor_y = uv2coor(u, v, h, w) sampler = EquirecSampler(coor_x, coor_y, order) pers_img = np.stack([sampler(e_img[..., i]) for i in range(e_img.shape[2])], axis=-1) diff --git a/py360convert/utils.py b/py360convert/utils.py index 4e6a2ec..534e5f2 100644 --- a/py360convert/utils.py +++ b/py360convert/utils.py @@ -243,7 +243,7 @@ def xyzpers(h_fov: float, v_fov: float, u: float, v: float, out_hw: tuple[int, i return out.dot(Rx).dot(Ry).dot(Ri) -def xyz2uv(xyz: NDArray[DType]) -> NDArray[DType]: +def xyz2uv(xyz: NDArray[DType]) -> tuple[NDArray[DType], NDArray[DType]]: """Transform cartesian (x,y,z) to spherical(r, u, v), and only outputs (u, v). Parameters @@ -268,10 +268,9 @@ def xyz2uv(xyz: NDArray[DType]) -> NDArray[DType]: """ x, y, z = np.split(xyz, 3, axis=-1) u = np.arctan2(x, z) - c = np.sqrt(np.square(x) + np.square(z)) + c = np.hypot(x, z) v = np.arctan2(y, c) - out = np.concatenate([u, v], axis=-1, dtype=xyz.dtype) - return out + return u, v def uv2unitxyz(uv: NDArray[DType]) -> NDArray[DType]: @@ -283,7 +282,7 @@ def uv2unitxyz(uv: NDArray[DType]) -> NDArray[DType]: return np.concatenate([x, y, z], axis=-1, dtype=uv.dtype) -def uv2coor(uv: NDArray[DType], h: int, w: int) -> tuple[NDArray[DType], NDArray[DType]]: +def uv2coor(u: NDArray[DType], v: NDArray[DType], h: int, w: int) -> tuple[NDArray[DType], NDArray[DType]]: """Transform spherical(r, u, v) into equirectangular(x, y). Assume that u has range 2pi and v has range pi. @@ -311,7 +310,6 @@ def uv2coor(uv: NDArray[DType], h: int, w: int) -> tuple[NDArray[DType], NDArray * coor_x is in [-0.5, w-0.5] * coor_y is in [-0.5, h-0.5] """ - u, v = np.split(uv, 2, axis=-1) coor_x = (u / (2 * np.pi) + 0.5) * w - 0.5 # pyright: ignore[reportOperatorIssue] coor_y = (-v / np.pi + 0.5) * h - 0.5 # pyright: ignore[reportOperatorIssue] return coor_x, coor_y From 0cee7fc441bd9c682f7ee52e3db0e78eebda3f51 Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 15:30:35 -0500 Subject: [PATCH 12/14] xyz2uv micro-optimization --- py360convert/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/py360convert/utils.py b/py360convert/utils.py index 534e5f2..c032ecc 100644 --- a/py360convert/utils.py +++ b/py360convert/utils.py @@ -266,7 +266,9 @@ def xyz2uv(xyz: NDArray[DType]) -> tuple[NDArray[DType], NDArray[DType]]: * v is in [-pi/2, pi/2] * any point i of output array is in [-pi, pi] x [-pi/2, pi/2]. """ - x, y, z = np.split(xyz, 3, axis=-1) + x = xyz[..., 0:1] # Keep dimensions but avoid copy + y = xyz[..., 1:2] + z = xyz[..., 2:3] u = np.arctan2(x, z) c = np.hypot(x, z) v = np.arctan2(y, c) From 06a70b02c5f7e3061dfd44dbb1d8a228ddadabe0 Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 15:33:13 -0500 Subject: [PATCH 13/14] update readme to refer to opencv acceleration. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5df84b6..68e9db5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # py360convert Features of this project: -- Conversion between cubemap and equirectangular +- Conversion between cubemap and equirectangular: ![](assets/teaser_convertion.png) -- Equirectangular to planar +- Conversion between Equirectangular and planar: ![](assets/teaser_2planar.png) -- Pure python implementation and depend only on [numpy](http://www.numpy.org/) and [scipy](https://www.scipy.org/) -- Vectorization implementation (in most of the place) - - `c2e` takes 300ms and `e2c` takes 160ms on 1.6 GHz Intel Core i5 CPU +- Pure python implementation and depend only on [numpy](http://www.numpy.org/) and [scipy](https://www.scipy.org/). +- Vectorization implementation: + - If opencv is installed, py360convert will automatically use it to accelerate computations (several times speedup). ## Install ``` From 3e039347cf2fe01f36272f369bd738ad681904d6 Mon Sep 17 00:00:00 2001 From: Brian Pugh Date: Sat, 21 Dec 2024 15:40:34 -0500 Subject: [PATCH 14/14] fix pyright warning about opencv; exercise opencv code in GA. --- .github/workflows/tests.yaml | 12 +++++++++++- py360convert/utils.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 6a62bfa..01f0e31 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -93,7 +93,17 @@ jobs: source ${{ env.ACTIVATE_PYTHON_VENV }} pre-commit run --show-diff-on-failure --color=always --all-files - - name: Run tests + - name: Run tests (standard install) + run: | + source ${{ env.ACTIVATE_PYTHON_VENV }} + python -m pytest + + - name: Install opencv + run: | + source ${{ env.ACTIVATE_PYTHON_VENV }} + pip install opencv-python + + - name: Run tests (with opencv) run: | source ${{ env.ACTIVATE_PYTHON_VENV }} python -m pytest diff --git a/py360convert/utils.py b/py360convert/utils.py index c032ecc..f3101ce 100644 --- a/py360convert/utils.py +++ b/py360convert/utils.py @@ -8,7 +8,7 @@ from scipy.spatial.transform import Rotation try: - import cv2 + import cv2 # pyright: ignore[reportMissingImports] except ImportError: cv2 = None