一、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
是宽高比,near
和far
是裁剪面的位置。
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
为透视图像的视场角度,theta
和phi
为旋转角度,width
和height
为输出透视图像的宽度和高度。
三、封装函数
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个数
这一步定义了两个列表,thetas
和phis
,分别表示水平(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}')
在这个循环中,对于每个THETA
和PHI
的组合,调用Equirectangular
类的GetPerspective
方法生成透视图像,并将其保存到磁盘。GetPerspective
方法接受视场角(FOV)、水平旋转角度(THETA)、垂直旋转角度(PHI)以及输出图像的尺寸(高度和宽度)作为参数。
2.5: 等待按键并关闭窗口
cv2.waitKey(0)
cv2.destroyAllWindows()
最后,cv2.waitKey(0)
使得程序等待用户按键,之后cv2.destroyAllWindows()
关闭所有OpenCV创建的窗口。这段代码通过遍历一系列预定义的角度组合,使用Equirectangular
类的GetPerspective
方法将等矩形投影图像转换为透视投影图像,并将结果保存为一系列图像文件。每个文件名包含了对应的THETA
和PHI
值,以便识别和查看不同视角下的图像效果。
五、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))
初始化THETA
和PHI
的值,这些值代表当前的视角参数。使用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
,它根据给定的THETA
和PHI
值构建图像的文件名。
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
在一个无限循环中显示图像,并根据用户按键更新THETA
和PHI
的值。按键操作如下:
- '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创建的窗口。这个脚本允许用户通过键盘操作来浏览不同视角下的透视图像,提供了一种交互式的方式来查看和比较不同视角的效果。