import cv2 import numpy as np class Perspective: r"""Convert perspective image to equirectangular image. Args: img_name (str or np.ndarray): The name of the image file or the image array. FOV (float): The field of view of the image in degrees. THETA (float): The left/right angle in degrees. PHI (float): The up/down angle in degrees. img_width (int): The width of the output equirectangular image. img_height (int): The height of the output equirectangular image. crop_bound (bool): Whether to crop the boundary area of the image proportionally. """ def __init__(self, img_name=None, FOV=None, THETA=None, PHI=None, img_width=512, img_height=512, crop_bound=False): # either img_name is provided, or img_width/img_height and img in function GetEquirec is provided self.crop_bound = crop_bound if img_name is not None: # Load the image if isinstance(img_name, str): self._img = cv2.imread(img_name, cv2.IMREAD_COLOR) elif isinstance(img_name, np.ndarray): self._img = img_name [self._height, self._width, _] = self._img.shape # Crop the boundary area of the image proportionally if self.crop_bound: self._img = self._img[int( self._height*0.05):int(self._height*0.95), int(self._width*0.05):int(self._width*0.95), :] [self._height, self._width, _] = self._img.shape else: self._img = None self._height = img_height self._width = img_width self.THETA = THETA self.PHI = PHI if self._width > self._height: self.wFOV = FOV self.hFOV = (float(self._height) / self._width) * FOV else: self.wFOV = (float(self._width) / self._height) * FOV self.hFOV = FOV self.w_len = np.tan(np.radians(self.wFOV / 2.0)) self.h_len = np.tan(np.radians(self.hFOV / 2.0)) def GetEquirec(self, height, width, img=None): # # THETA is left/right angle, PHI is up/down angle, both in degree # if self._img is None: self._img = img # Calculate the equirectangular coordinates x, y = np.meshgrid(np.linspace(-180, 180, width), np.linspace(90, -90, height)) # Convert spherical coordinates to Cartesian coordinates x_map = np.cos(np.radians(x)) * np.cos(np.radians(y)) y_map = np.sin(np.radians(x)) * np.cos(np.radians(y)) z_map = np.sin(np.radians(y)) # Stack the coordinates to form a 3D array xyz = np.stack((x_map, y_map, z_map), axis=2) # Reshape the coordinates to match the image dimensions y_axis = np.array([0.0, 1.0, 0.0], np.float32) z_axis = np.array([0.0, 0.0, 1.0], np.float32) # Calculate the rotation matrices [R1, _] = cv2.Rodrigues(z_axis * np.radians(self.THETA)) [R2, _] = cv2.Rodrigues(np.dot(R1, y_axis) * np.radians(-self.PHI)) # Invert rotations to transform from equirectangular to perspective R1 = np.linalg.inv(R1) R2 = np.linalg.inv(R2) # Apply rotations xyz = xyz.reshape([height * width, 3]).T xyz = np.dot(R2, xyz) xyz = np.dot(R1, xyz).T xyz = xyz.reshape([height, width, 3]) # Create mask for valid forward-facing points (x > 0) inverse_mask = np.where(xyz[:, :, 0] > 0, 1, 0) # Normalize coordinates by x-component (perspective division) xyz[:, :] = xyz[:, :] / \ np.repeat(xyz[:, :, 0][:, :, np.newaxis], 3, axis=2) # Map 3D points back to 2D perspective image coordinates lon_map = np.where( (-self.w_len < xyz[:, :, 1]) & (xyz[:, :, 1] < self.w_len) \ & (-self.h_len < xyz[:, :, 2]) & (xyz[:, :, 2] < self.h_len), (xyz[:, :, 1]+self.w_len)/2/self.w_len*self._width, 0) lat_map = np.where( (-self.w_len < xyz[:, :, 1]) & (xyz[:, :, 1] < self.w_len) \ & (-self.h_len < xyz[:, :, 2]) & (xyz[:, :, 2] < self.h_len), (-xyz[:, :, 2]+self.h_len) / 2/self.h_len*self._height, 0) mask = np.where( (-self.w_len < xyz[:, :, 1]) & (xyz[:, :, 1] < self.w_len) \ & (-self.h_len < xyz[:, :, 2]) & (xyz[:, :, 2] < self.h_len), 1, 0) # Remap the image using the longitude and latitude maps persp = cv2.remap(self._img, lon_map.astype(np.float32), lat_map.astype( np.float32), cv2.INTER_CUBIC, borderMode=cv2.BORDER_WRAP) # BORDER_CONSTANT) #)) # Apply the mask to the equirectangular image mask = mask * inverse_mask mask = np.repeat(mask[:, :, np.newaxis], 3, axis=2) persp = persp * mask return persp, mask