# Copyright 2026 Realsee. All rights reserved. # Licensed under the Apache License, Version 2.0. """ Shared I/O and preprocessing utilities for panoramic image data. These functions are used by both evaluation and training pipelines. """ import os os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1" import cv2 import numpy as np def read_image_cv2_360(path: str, rgb: bool = True, shape=(560, 280)) -> np.ndarray: """Read and resize a 360 panorama image. Args: path: Path to the image file. rgb: If True, convert BGR to RGB (default: True). shape: Target (width, height) tuple. Returns: Image as numpy array with shape (H, W, 3). """ img = cv2.imread(path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if img.shape[1] != shape[0]: img = cv2.resize(img, shape, interpolation=cv2.INTER_AREA) return img def read_depth_360(path: str, depth_scale=5000.0, shape=(560, 280)) -> np.ndarray: """Read and normalize a 360 depth map. Args: path: Path to the depth image file. depth_scale: Scale factor to convert raw depth to meters. shape: Target (width, height) tuple. Returns: Depth map as float32 numpy array with shape (H, W). """ d = cv2.imread(path, cv2.IMREAD_UNCHANGED) if d.shape[1] != shape[0]: d = cv2.resize(d, shape, interpolation=cv2.INTER_NEAREST) d = d.astype(np.float32) / depth_scale return d def random_rotate_theta(W=560, max_shift_percent=0.5): """Generate a random rotation angle for panorama augmentation. Args: W: Panorama width in pixels. max_shift_percent: Maximum horizontal shift as fraction of width. Returns: Rotation angle in radians. """ max_shift = int(W * max_shift_percent) shift_pixels = np.random.randint(-max_shift, max_shift + 1) theta = (shift_pixels * 2 * np.pi) / W return theta def rotate_y(theta): """Create a 3x3 rotation matrix around the Y-axis. Args: theta: Rotation angle in radians. Returns: 3x3 rotation matrix as float64 numpy array. """ cos_theta = np.cos(theta) sin_theta = np.sin(theta) return np.array( [[cos_theta, 0, -sin_theta], [0, 1, 0], [sin_theta, 0, cos_theta]], dtype=np.float64, ) def pano_depth_to_points(depth_map, pano_shape=(560, 280), crop=True, crop_ratio=0.15): """Convert a panorama depth map to 3D point cloud. Args: depth_map: 2D depth map (H, W) or flattened array. pano_shape: Original panorama (width, height) tuple. crop: Whether the depth map has been vertically cropped. crop_ratio: Crop ratio applied to top and bottom. Returns: Point cloud as numpy array with shape (N, 3). """ w, h = pano_shape if not crop: px = np.tile(np.arange(w), int(h)) py = np.arange(0, int(h)).repeat(w) else: px = np.tile(np.arange(w), int(h * (1 - 2 * crop_ratio))) py = np.arange(int(crop_ratio * h), int((1 - crop_ratio) * h)).repeat(w) dist = depth_map.reshape(-1) lat = (py / h - 0.5) * np.pi long = (px / w - 0.5) * np.pi * 2.0 y = dist * np.sin(lat) tmp = dist * np.cos(lat) x = tmp * np.sin(long) z = tmp * np.cos(long) point_map = np.concatenate([i.reshape(-1, 1) for i in (x, y, z)], axis=-1) return point_map # (h*w, 3) def crop_panorama(pano, crop_ratio=0.15): """Crop the top and bottom of a panorama by a given ratio. Args: pano: Input panorama array with shape (H, W, ...). crop_ratio: Fraction to crop from top and bottom. Returns: Cropped panorama. """ H, W = pano.shape[:2] crop_H_top = int(crop_ratio * H) crop_H_bottom = H - int(crop_ratio * H) crop_pano = pano[crop_H_top:crop_H_bottom, ...] return crop_pano def rotate_panorama(panorama, theta): """Horizontally rotate a panorama by shifting pixels. Args: panorama: Input panorama array with shape (H, W, ...). theta: Rotation angle in radians. Returns: Shifted panorama. """ H, W = panorama.shape[:2] shift_pixels = int((theta * W) / (2 * np.pi)) shifted = np.roll(panorama, shift_pixels, axis=1) return shifted