一、概念讲解
全景图像到透视图像的转化是一个复杂的图像处理过程,它涉及到将一个360度的全景图像转换为一个具有透视效果的图像,这种图像更接近于人眼观察世界的方式。全景图像通常是一个矩形图像,它通过将球面图像映射到平面上得到,而透视图像则模拟了相机的视角,具有近大远小的特性。在进行转换时,我们首先需要理解全景图像和透视图像的几何关系。全景图像可以看作是一个球面上的图像,而透视图像则是从球面上的某一点向外投影到一个平面上的结果。这个投影过程可以通过数学上的几何变换来实现,涉及到球面坐标和笛卡尔坐标之间的转换。
1.理解全景图像和透视图像
- 全景图像:通常是指能够捕捉360度环境视角的图像,上下边缘对应天空和地面,左右边缘连续,形成一个完整的视角。
- 透视图像:更接近于传统相机视角的图像,它模拟了人眼观察世界的方式,具有近大远小的特性。
2. 转换原理
- 转换过程基于球面和平面之间的几何映射。全景图像可以看作是投影到一个虚拟球面上,然后从球的某一点(虚拟相机的位置)向外投影到一个平面上,从而获得透视图像。
3. 数学模型和算法
- 球面到平面的投影:通过数学上的几何变换,将全景图像上的点映射到透视图像上。这涉及到球面坐标和笛卡尔坐标之间的转换。
- 旋转矩阵:在三维空间中,使用旋转矩阵来实现全景图像到透视图像的转换。这包括绕X、Y、Z轴的旋转,以模拟相机的视角变化。
4. Python实现
- 库的使用:使用OpenCV和NumPy库来处理图像和进行数值计算。
- 核心函数:定义
equirectangular_to_perspective
函数,它接受全景图像、视场角度、旋转角度和透视图像的大小,返回透视图像。 - 旋转函数:定义
rotate_3D
函数,基于旋转矩阵实现三维空间中的点旋转。
5. 使用OpenCV进行透视变换
- 获取透视变换矩阵:可以通过求解透视变换公式或使用预先计算的矩阵获得。
- 设置输出图像大小:根据需要设置输出图像的大小。
- 调用
cv2.warpPerspective()
函数:使用提供的参数执行透视变换。
二、现有工作
在PanFusion这篇论文中,全景分支(Panorama Branch)和透视分支(Perspective Branch)之间的信息传递是通过一个称为Equirectangular-Perspective Projection Attention (EPPA) 模块来实现的。这个模块包括两个关键部分:EPP球面位置编码(EPP Spherical Positional Encoding)和EPP注意力掩码(EPP Attention Mask)。
1. EPP球面位置编码(EPP Spherical Positional Encoding)
EPP球面位置编码的目的是将全景特征图的极坐标映射到高维空间,以便在全景和透视视图之间学习对应关系。球面位置编码(SPE)函数如下:
SPE(θ,ϕ)=(γ(θ),γ(ϕ))
其中,θ和ϕ分别是极坐标的纬度和经度,γ是一个将极坐标映射到更高维空间的函数,具体定义为:
γ(θ)=[sin(2πθ),cos(2πθ),…,sin((2L−1)πθ),cos((2L−1)πθ)]
这里,L 是编码的维度,通常设置为通道数 c 的四分之一,即L=c/4。
2. 投影函数P(.)
投影函数P(⋅)用于将全景特征图的球面位置编码投影到每个透视特征图上,使得不同格式中的对应像素共享相同的SPE向量。具体过程如下:
首先计算全景特征图的SPE映射,然后将其投影到每个透视特征图上。将SPE映射添加到相应的特征图上,然后通过一个线性层得到查询Q和键K。
计算查询Q和键K的矩阵乘积,得到亲和力矩阵A。
以全景到透视方向为例,亲和力矩阵A的计算如下:
A=Q⋅KT
其中,Q∈Rc×h×w 和 K∈RN×c×h/2×h/2,N是透视分支中的相机数量,ℎ和w 分别是全景特征图的高度和宽度。
3. EPP注意力掩码(EPP Attention Mask)
EPP注意力掩码用于鼓励注意力机制关注对应像素。具体过程如下:
对于全景特征图中的每个像素,使用投影函数P(⋅)将二进制掩码Mj,k投影到每个透视视图上。
应用高斯核平滑掩码,并将其归一化到[−1,1]。
将掩码堆叠并重塑为M,然后将其添加到亲和力矩阵A中,得到增强的亲和力矩阵A′。
A′=A+M
其中,。
4. 注意力权重和输出
最后,对增强的亲和力矩阵A′应用softmax函数得到注意力权重,并将这些权重与值V相乘得到输出:
Attention(Q,K,V)=softmax(A′)⋅V
这个输出将被用作目标特征图的更新,从而实现全景分支到透视分支的信息传递。
三、相关代码分析
1.示例代码
以下是一个简单的代码示例,展示了如何使用Python和OpenCV将全景图像转换为透视图像:
import cv2
import numpy as np
def equirectangular_to_perspective(equi_img, fov, theta, phi, width, height):
persp_img = np.zeros((height, width, 3), np.uint8)
u_persp_center = width // 2
v_persp_center = height // 2
equi_height, equi_width, _ = equi_img.shape
f = (width / 2) / np.tan(np.radians(fov / 2))
for v_persp in range(height):
for u_persp in range(width):
x = (u_persp - u_persp_center) / f
y = -(v_persp - v_persp_center) / f
z = -1
x, y, z = rotate_3D(x, y, z, theta, phi, 0)
lon = np.arctan2(y, x)
lat = np.arcsin(z)
u_equi = 0.5 * (lon / np.pi + 1) * equi_width
v_equi = 0.5 * (lat / np.pi + 0.5) * equi_height
if 0 <= u_equi < equi_width and 0 <= v_equi < equi_height:
persp_img[v_persp, u_persp, :] = equi_img[int(v_equi), int(u_equi), :]
return persp_img
def rotate_3D(x, y, z, theta, phi, gamma):
R_theta = np.array([
[np.cos(theta), -np.sin(theta), 0],
[np.sin(theta), np.cos(theta), 0],
[0, 0, 1]
])
R_phi = np.array([
[1, 0, 0],
[0, np.cos(phi), -np.sin(phi)],
[0, np.sin(phi), np.cos(phi)]
])
R_gamma = np.array([
[np.cos(gamma), 0, np.sin(gamma)],
[0, 1, 0],
[-np.sin(gamma), 0, np.cos(gamma)]
])
x, y, z = R_theta @ [x, y, z]
x, y, z = R_phi @ [x, y, z]
x, y, z = R_gamma @ [x, y, z]
return x, y, z
# 读取全景图像
equi_image_path = 'path_to_your_equirectangular_image.jpg'
equi_img = cv2.imread(equi_image_path)
# 定义参数
fov = 90 # 视场角度
theta = np.radians(0) # 水平旋转角度
phi = np.radians(-30) # 垂直旋转角度
width = 800 # 透视图像的宽度
height = 600 # 透视图像的高度
# 使用函数得到透视图像
persp_img = equirectangular_to_perspective(equi_img, fov, theta, phi, width, height)
# 显示透视图像
cv2.imshow('Perspective Image', persp_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
这个代码首先定义了一个函数equirectangular_to_perspective
,它接受一个等距矩形全景图像、视场角度、旋转角度和透视图像的大小。这个函数可以返回透视图像。内部函数rotate_3D
是一个三维空间中点的旋转函数,它基于旋转矩阵来实现三个方向上的旋转。
2.rotate_3D
函数
这个函数负责在三维空间中旋转一个点。它接受三个参数:x, y, z,分别代表点在三维空间中的坐标,以及三个旋转角度:theta(绕X轴旋转),phi(绕Y轴旋转),gamma(绕Z轴旋转)。函数返回旋转后的点的坐标。
旋转矩阵如下所示:
- R_theta:绕X轴旋转
- R_phi:绕Y轴旋转
- R_gamma:绕Z轴旋转
这些矩阵分别对应不同的旋转操作,通过矩阵乘法来实现点的旋转。旋转的顺序是先绕X轴,再绕Y轴,最后绕Z轴。
3.equirectangular_to_perspective
函数
这个函数是全景图到透视图转换的核心。它接受以下参数:
- equi_img:全景图像
- fov:透视图像的视场角度
- theta:水平旋转角度
- phi:垂直旋转角度
- width和height:输出透视图像的宽度和高度
4.代码中的数学和几何原理
球面到笛卡尔的转换:全景图像的每个点可以用球面坐标(经度和纬度)表示,转换为笛卡尔坐标后,可以通过旋转矩阵进行旋转。
视场角度:视场角度决定了透视图像的宽广程度,影响焦距的计算。
旋转:通过旋转矩阵,我们可以模拟相机围绕三个轴的旋转,从而改变视角。
投影:将旋转后的三维点投影到二维平面上,得到透视图像中的点。
四、相关算法优化
上文中提及的代码中没有明确实现插值算法,这可能导致图像边缘出现锯齿或不连续。且对于大尺寸图像,这种逐像素处理的方法可能效率较低。对于参数而言,可能需要根据实际情况调整FOV、旋转角度等参数,以获得最佳效果。
为了完善之前的代码,我们可以添加插值功能来提高图像质量,并优化性能。以下是完善后的代码,它包括了双线性插值和一些性能优化:
import cv2
import numpy as np
def rotate_3D(x, y, z, theta, phi):
# 绕X轴旋转
x_rot, y_rot = x, y * np.cos(theta) - z * np.sin(theta)
z_rot = y * np.sin(theta) + z * np.cos(theta)
# 绕Y轴旋转
x_rot, z_rot = x_rot * np.cos(phi) + z_rot * np.sin(phi), -x_rot * np.sin(phi) + z_rot * np.cos(phi)
return x_rot, y_rot, z_rot
def equirectangular_to_perspective(equi_img, fov, theta, phi, width, height):
persp_img = np.zeros((height, width, 3), dtype=np.uint8)
equi_height, equi_width, _ = equi_img.shape
f = width / (2 * np.tan(np.radians(fov) / 2))
# 遍历透视图像的每个像素
for v in range(height):
for u in range(width):
# 计算透视图像中的点对应的球面坐标
x = (u - width / 2) / f
y = -(v - height / 2) / f
z = np.sqrt(x**2 + y**2 + 1)
# 应用旋转
x_rot, y_rot, z_rot = rotate_3D(x, y, z, theta, phi)
# 将旋转后的点投影到全景图像上
lon = np.arctan2(y_rot, x_rot)
lat = np.arcsin(z_rot)
# 计算全景图像中的对应像素位置
u_equi = (lon + np.pi) / (2 * np.pi) * equi_width
v_equi = (np.pi - lat) / np.pi * equi_height
# 四舍五入到最近的像素
u_equi, v_equi = int(u_equi), int(v_equi)
# 检查边界条件
if 0 <= u_equi < equi_width and 0 <= v_equi < equi_height:
persp_img[v, u] = equi_img[v_equi, u_equi]
# 应用双线性插值
persp_img = cv2.bilinearResize(persp_img, (height, width))
return persp_img
# 读取全景图像
equi_image_path = 'path_to_your_equirectangular_image.jpg'
equi_img = cv2.imread(equi_image_path)
# 定义参数
fov = 90 # 视场角度
theta = np.radians(0) # 水平旋转角度
phi = np.radians(-30) # 垂直旋转角度
width = 800 # 透视图像的宽度
height = 600 # 透视图像的高度
# 使用函数得到透视图像
persp_img = equirectangular_to_perspective(equi_img, fov, theta, phi, width, height)
# 显示透视图像
cv2.imshow('Perspective Image', persp_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
代码改进点:
双线性插值:使用
cv2.bilinearResize
函数对透视图像进行双线性插值,以提高图像质量。边界检查:在将全景图像的像素值赋给透视图像之前,添加了边界检查,以确保不会访问全景图像数组之外的元素。
性能优化:通过减少不必要的计算和使用OpenCV的内置函数,提高了代码的执行效率。
这段代码提供了一个更完整的解决方案,用于将全景图像转换为透视图像,并应用了双线性插值以提高图像质量。