Bootstrap

Equirectangular to Perspective(E2P)算法详解(附代码)

一、E2P概述

        Equirectangular to Perspective(E2P)技术是一种将如下图的等矩形投影(Equirectangular projection)图像转换为透视投影(Perspective projection)图像的方法。这种技术在多个领域有着广泛的应用,比如虚拟现实(VR)、全景图像查看器以及360度视频播放等。

二、技术原理

1. 球面到笛卡尔的转换

        等矩形投影图像可以看作是将全景图像投影到一个虚拟球面上,然后从球的某一点(虚拟相机的位置)向外投影到一个平面上,从而获得透视图像。这个过程涉及到球面坐标和笛卡尔坐标之间的转换。

import numpy as np

def spherical_to_cartesian(lon, lat):
    # lon: 经度,lat: 纬度
    lon_rad = np.radians(lon)
    lat_rad = np.radians(lat)
    
    x = np.cos(lat_rad) * np.cos(lon_rad)
    y = np.cos(lat_rad) * np.sin(lon_rad)
    z = np.sin(lat_rad)
    
    return x, y, z

这个函数将球面坐标(经度和纬度)转换为笛卡尔坐标(X, Y, Z)。

2. 视场角度(FOV)

        视场角度决定了透视图像的宽广程度,它影响焦距的计算。焦距与视场角度和图像尺寸有关,决定了透视图像的视角范围。

def perspective_projection(fov, aspect, near, far):
    # fov: 视角(Field of View),即垂直方向的角度
    # aspect: 视口宽高比
    # near: 近裁剪面的位置
    # far: 远裁剪面的位置
    f = 1.0 / np.tan(np.radians(fov) / 2)
    return np.array([
        [f / aspect, 0, 0, 0],
        [0, f, 0, 0],
        [0, 0, -(far + near) / (far - near), -1],
        [0, 0, -(2 * far * near) / (far - near), 0]
    ])

这个函数计算透视投影矩阵,其中fov是视场角度,aspect是宽高比,nearfar是裁剪面的位置。

3. 旋转

在三维空间中,使用旋转矩阵来实现全景图像到透视图像的转换。这包括绕X、Y、Z轴的旋转,以模拟相机的视角变化。

def rotate_3D(x, y, z, theta, phi, gamma):
    # 旋转矩阵
    R_theta = np.array([
        [1, 0, 0],
        [0, np.cos(np.radians(theta)), -np.sin(np.radians(theta))],
        [0, np.sin(np.radians(theta)), np.cos(np.radians(theta))]
    ])
    R_phi = np.array([
        [np.cos(np.radians(phi)), 0, np.sin(np.radians(phi))],
        [0, 1, 0],
        [-np.sin(np.radians(phi)), 0, np.cos(np.radians(phi))]
    ])
    R_gamma = np.array([
        [np.cos(np.radians(gamma)), -np.sin(np.radians(gamma)), 0],
        [np.sin(np.radians(gamma)), np.cos(np.radians(gamma)), 0],
        [0, 0, 1]
    ])
    # 旋转点
    point = np.array([x, y, z])
    return np.dot(R_gamma, np.dot(R_phi, np.dot(R_theta, point)))

这个函数负责在三维空间中旋转一个点。

4. 投影

将旋转后的三维点投影到二维平面上,得到透视图像中的点。

def equirectangular_to_perspective(equi_img, fov, theta, phi, width, height):
    # 省略了部分代码,这里只提供核心逻辑
    # ...
    x, y, z = spherical_to_cartesian(theta, phi)
    # 应用旋转
    rotated_point = rotate_3D(x, y, z, theta, phi, 0)
    # 应用投影
    projected_point = perspective_projection(fov, width / height, 0.1, 100)
    # 应用到图像
    # ...
    return persp_img

这个函数是全景图到透视图转换的核心。它接受以下参数:equi_img为全景图像,fov为透视图像的视场角度,thetaphi为旋转角度,widthheight为输出透视图像的宽度和高度。

三、封装函数

 1.完整Equirec2Perspec函数

import os
import sys
import cv2
import numpy as np

def xyz2lonlat(xyz):
    atan2 = np.arctan2
    asin = np.arcsin

    norm = np.linalg.norm(xyz, axis=-1, keepdims=True)
    xyz_norm = xyz / norm
    x = xyz_norm[..., 0:1]
    y = xyz_norm[..., 1:2]
    z = xyz_norm[..., 2:]

    lon = atan2(x, z)
    lat = asin(y)
    lst = [lon, lat]

    out = np.concatenate(lst, axis=-1)
    return out

def lonlat2XY(lonlat, shape):
    X = (lonlat[..., 0:1] / (2 * np.pi) + 0.5) * (shape[1] - 1)
    Y = (lonlat[..., 1:] / (np.pi) + 0.5) * (shape[0] - 1)
    lst = [X, Y]
    out = np.concatenate(lst, axis=-1)

    return out 

class Equirectangular:
    def __init__(self, img_name):
        self._img = cv2.imread(img_name, cv2.IMREAD_COLOR)
        [self._height, self._width, _] = self._img.shape
        #cp = self._img.copy()  
        #w = self._width
        #self._img[:, :w/8, :] = cp[:, 7*w/8:, :]
        #self._img[:, w/8:, :] = cp[:, :7*w/8, :]
    

    def GetPerspective(self, FOV, THETA, PHI, height, width):
        #
        # THETA is left/right angle, PHI is up/down angle, both in degree
        #

        f = 0.5 * width * 1 / np.tan(0.5 * FOV / 180.0 * np.pi)
        cx = (width - 1) / 2.0
        cy = (height - 1) / 2.0
        K = np.array([
                [f, 0, cx],
                [0, f, cy],
                [0, 0,  1],
            ], np.float32)
        K_inv = np.linalg.inv(K)
        
        x = np.arange(width)
        y = np.arange(height)
        x, y = np.meshgrid(x, y)
        z = np.ones_like(x)
        xyz = np.concatenate([x[..., None], y[..., None], z[..., None]], axis=-1)
        xyz = xyz @ K_inv.T

        y_axis = np.array([0.0, 1.0, 0.0], np.float32)
        x_axis = np.array([1.0, 0.0, 0.0], np.float32)
        R1, _ = cv2.Rodrigues(y_axis * np.radians(THETA))
        R2, _ = cv2.Rodrigues(np.dot(R1, x_axis) * np.radians(PHI))
        R = R2 @ R1
        xyz = xyz @ R.T
        lonlat = xyz2lonlat(xyz) 
        XY = lonlat2XY(lonlat, shape=self._img.shape).astype(np.float32)
        persp = cv2.remap(self._img, XY[..., 0], XY[..., 1], cv2.INTER_CUBIC, borderMode=cv2.BORDER_WRAP)

        return persp

2.代码分步讲解

2.1  xyz2lonlat 函数

这个函数将三维空间中的点(以笛卡尔坐标表示)转换为球面坐标(经度和纬度)。

def xyz2lonlat(xyz):
    atan2 = np.arctan2
    asin = np.arcsin

    norm = np.linalg.norm(xyz, axis=-1, keepdims=True)  # 计算每个点的模长
    xyz_norm = xyz / norm  # 归一化xyz坐标,使其位于单位球面上
    x = xyz_norm[..., 0:1]  # x坐标
    y = xyz_norm[..., 1:2]  # y坐标
    z = xyz_norm[..., 2:]   # z坐标

    lon = atan2(x, z)  # 计算经度,使用atan2来避免atan2(x/z)的不确定性
    lat = asin(y)     # 计算纬度,使用asin(y)因为y是z轴的投影

    lst = [lon, lat]  # 将经度和纬度组合在一起
    out = np.concatenate(lst, axis=-1)  # 将经度和纬度合并为一个数组
    return out

2.2 lonlat2XY 函数

这个函数将球面坐标(经度和纬度)转换回等矩形投影图像中的像素坐标(X, Y)。

def lonlat2XY(lonlat, shape):
    X = (lonlat[..., 0:1] / (2 * np.pi) + 0.5) * (shape[1] - 1)  # 将经度转换为等矩形投影的X坐标
    Y = (lonlat[..., 1:] / (np.pi) + 0.5) * (shape[0] - 1)       # 将纬度转换为等矩形投影的Y坐标
    lst = [X, Y]
    out = np.concatenate(lst, axis=-1)  # 将X和Y坐标合并为一个数组
    return out

2.3 Equirectangular 类

这个类用于处理等矩形投影图像,并提供一个方法来生成透视投影图像。

class Equirectangular:
    def __init__(self, img_name):
        self._img = cv2.imread(img_name, cv2.IMREAD_COLOR)  # 读取等矩形投影图像
        [self._height, self._width, _] = self._img.shape  # 获取图像的高度和宽度

2.4 GetPerspective 方法

这个方法是 Equirectangular 类的一部分,用于生成透视投影图像。

def GetPerspective(self, FOV, THETA, PHI, height, width):
    f = 0.5 * width * 1 / np.tan(0.5 * FOV / 180.0 * np.pi)  # 计算焦距
    cx = (width - 1) / 2.0  # 透视图像的中心X坐标
    cy = (height - 1) / 2.0  # 透视图像的中心Y坐标
    K = np.array([
        [f, 0, cx],
        [0, f, cy],
        [0, 0, 1],
    ], np.float32)  # 内参矩阵
    K_inv = np.linalg.inv(K)  # 内参矩阵的逆矩阵

    x = np.arange(width)  # X坐标网格
    y = np.arange(height)  # Y坐标网格
    x, y = np.meshgrid(x, y)  # 创建X和Y坐标的网格
    z = np.ones_like(x)  # Z坐标,对于透视投影,我们假设Z=1
    xyz = np.concatenate([x[..., None], y[..., None], z[..., None]], axis=-1)  # 将X, Y, Z坐标合并
    xyz = xyz @ K_inv.T  # 应用内参矩阵的逆矩阵

    y_axis = np.array([0.0, 1.0, 0.0], np.float32)  # Y轴单位向量
    x_axis = np.array([1.0, 0.0, 0.0], np.float32)  # X轴单位向量
    R1, _ = cv2.Rodrigues(y_axis * np.radians(THETA))  # 绕Y轴旋转
    R2, _ = cv2.Rodrigues(np.dot(R1, x_axis) * np.radians(PHI))  # 绕X轴旋转
    R = R2 @ R1  # 组合旋转矩阵
    xyz = xyz @ R.T  # 应用旋转矩阵
    lonlat = xyz2lonlat(xyz)  # 将XYZ坐标转换为经度和纬度
    XY = lonlat2XY(lonlat, shape=self._img.shape).astype(np.float32)  # 将经度和纬度转换为等矩形投影的像素坐标
    persp = cv2.remap(self._img, XY[..., 0], XY[..., 1], cv2.INTER_CUBIC, borderMode=cv2.BORDER_WRAP)  # 应用重映射

    return persp

        这个方法首先计算了透视投影的内参矩阵和其逆矩阵,然后创建了一个坐标网格,并将其转换为三维空间中的点。接着,应用旋转矩阵来模拟相机的旋转,然后将这些点投影回等矩形投影图像的像素坐标。最后,使用 cv2.remap 函数来生成透视投影图像。

四、调用函数

1.完整调用函数

        这段代码实现了从等矩形投影图像到透视投影图像的转换,并保存了一系列不同视角下的透视图像。

import cv2
import numpy as np
from Equirec2Perspec import xyz2lonlat, lonlat2XY, Equirectangular

def main():
    img_name = 'input/image.jpg'  # 替换为你的图像文件名
    FOV = 90  # 视场角,单位度
    height = 1080  # 输出图像的高度
    width = 1920  # 输出图像的宽度

    equirectangular = Equirectangular(img_name)

    # 定义不同视角的THETA和PHI值
    thetas = [i for i in range(361)]  # 从0到360,包括0和360,共361个数
    phis = [j for j in range(-90, 91)]  # 从-90到90,包括-90和90,共181个数

    # 遍历不同的视角
    for THETA in thetas:
        for PHI in phis:
            persp_img = equirectangular.GetPerspective(FOV, THETA, PHI, height, width)
            # 构建输出文件名
            output_filename = f'output/perspective_image_theta{THETA}_phi{PHI}.jpg'
            cv2.imwrite(output_filename, persp_img)
            print(f'Saved: {output_filename}')

    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == '__main__':
    main()

2.代码分布讲解

2. 1 导入必要的库和模块

import cv2
import numpy as np
import sys
sys.path.append(r'C:\Users\ASUS\Desktop\paper\code\Equirec2Perspec-master\Equirec2Perspec.py')
from Equirec2Perspec import xyz2lonlat, lonlat2XY, Equirectangular

这一步导入了OpenCV库(用于图像处理)、NumPy库(用于数值计算)以及自定义的Equirec2Perspec模块,该模块包含了转换所需的函数和类。

2.2 初始化等矩形投影图像

img_name = 'input/image.jpg'
equirectangular = Equirectangular(img_name)

这里创建了一个Equirectangular类的实例,该实例读取等矩形投影图像,并存储其尺寸信息。

2.3 定义视角参数

thetas = [i for i in range(361)]  # 从0到360,包括0和360,共361个数
phis = [j for j in range(-90, 91)]  # 从-90到90,包括-90和90,共181个数

这一步定义了两个列表,thetasphis,分别表示水平(THETA)和垂直(PHI)旋转角度。这些角度将用于生成不同视角的透视图像。

2.4 遍历不同的视角并生成透视图像

for THETA in thetas:
    for PHI in phis:
        persp_img = equirectangular.GetPerspective(FOV, THETA, PHI, height, width)
        output_filename = f'output/perspective_image_theta{THETA}_phi{PHI}.jpg'
        cv2.imwrite(output_filename, persp_img)
        print(f'Saved: {output_filename}')

在这个循环中,对于每个THETAPHI的组合,调用Equirectangular类的GetPerspective方法生成透视图像,并将其保存到磁盘。GetPerspective方法接受视场角(FOV)、水平旋转角度(THETA)、垂直旋转角度(PHI)以及输出图像的尺寸(高度和宽度)作为参数。

2.5: 等待按键并关闭窗口

cv2.waitKey(0)
cv2.destroyAllWindows()

最后,cv2.waitKey(0)使得程序等待用户按键,之后cv2.destroyAllWindows()关闭所有OpenCV创建的窗口。这段代码通过遍历一系列预定义的角度组合,使用Equirectangular类的GetPerspective方法将等矩形投影图像转换为透视投影图像,并将结果保存为一系列图像文件。每个文件名包含了对应的THETAPHI值,以便识别和查看不同视角下的图像效果。

五、3D视角图实现

1.导入库和定义目录

import cv2
import os

# 定义图像所在的目录
image_dir = 'output'

这里导入了OpenCV库cv2用于图像处理,以及os库用于操作文件系统。同时定义了存储图像的目录变量image_dir

2.初始化变量和获取文件列表

# 初始化THETA和PHI的值
THETA = 0
PHI = 0

# 获取所有图像文件
image_files = sorted(os.listdir(image_dir))

初始化THETAPHI的值,这些值代表当前的视角参数。使用os.listdir获取指定目录下的所有文件,并对其进行排序。

3.筛选和排序图像文件

# 筛选出符合透视图像命名规则的文件
perspective_images = [img for img in image_files if 'perspective_image_theta' in img and img.endswith('.jpg')]

# 按照THETA值排序,确保图像顺序正确
perspective_images.sort(key=lambda x: int(x.split('theta')[1].split('_phi')[0]))

从文件列表中筛选出符合特定命名规则的透视图像文件,并按照THETA值对这些文件进行排序,确保它们按照THETA的顺序排列。

4.初始化图像索引

# 初始化图像索引
image_index = 0

初始化一个变量image_index,用于跟踪当前显示的图像在列表中的位置。

5.构建图像文件名函数

def get_image_filename(theta, phi):
    # 根据THETA和PHI值构建图像文件名
    return f'perspective_image_theta{theta}_phi{phi}.jpg'

定义一个函数get_image_filename,它根据给定的THETAPHI值构建图像的文件名。

6.显示图像函数

def display_images(images):
    global image_index  # 声明image_index为全局变量
    global THETA
    global PHI

    while True:
        # 构建当前图像的文件名
        current_image_name = get_image_filename(THETA, PHI)
        image_path = os.path.join(image_dir, current_image_name)

        # 读取图像
        image = cv2.imread(image_path)
        if image is not None:
            # 显示图像
            cv2.imshow('Perspective Image', image)
        else:
            print(f"Error: Unable to load image {current_image_name}")
            break

        # 等待键盘输入
        key = cv2.waitKey(0) & 0xFF
        if key == ord('a'):
            THETA = (THETA - 1) % 360
        elif key == ord('d'):
            THETA = (THETA + 1) % 360
        elif key == ord('w'):
            PHI = min(PHI + 1, 30)  # 假设PHI的最大值为30
        elif key == ord('s'):
            PHI = max(PHI - 1, -30)  # 假设PHI的最小值为-30
        elif key == ord('q'):
            break

        # 更新图像索引
        image_index = (image_index + 1) % len(images)

定义一个函数display_images,它使用cv2.imshow在一个无限循环中显示图像,并根据用户按键更新THETAPHI的值。按键操作如下:

  • 'a':减少THETA值,循环回到359。
  • 'd':增加THETA值,循环回到0。
  • 'w':增加PHI值,不超过30。
  • 's':减少PHI值,不小于-30。
  • 'q':退出循环。

7.调用显示函数和关闭窗口

# 调用函数展示图像
display_images(perspective_images)

# 关闭所有OpenCV窗口
cv2.destroyAllWindows()

最后,调用display_images函数来展示图像,并在所有图像展示完毕后或用户按下'q'键时关闭所有OpenCV创建的窗口。这个脚本允许用户通过键盘操作来浏览不同视角下的透视图像,提供了一种交互式的方式来查看和比较不同视角的效果。

;